From 0243feaa3dfd96445c82baa4e1e930e5ee78fd66 Mon Sep 17 00:00:00 2001 From: Lee Smet Date: Tue, 24 Feb 2026 11:22:46 +0100 Subject: [PATCH] Add custom tun implementation on linux Signed-off-by: Lee Smet --- Cargo.lock | 10 ++ Cargo.toml | 8 +- flake.nix | 2 + mycelium-tun/Cargo.toml | 10 ++ mycelium-tun/src/lib.rs | 9 ++ mycelium-tun/src/linux.rs | 242 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 mycelium-tun/Cargo.toml create mode 100644 mycelium-tun/src/lib.rs create mode 100644 mycelium-tun/src/linux.rs diff --git a/Cargo.lock b/Cargo.lock index b407b4a..15ad52e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1989,6 +1989,16 @@ dependencies = [ "tracing", ] +[[package]] +name = "mycelium-tun" +version = "0.1.0" +dependencies = [ + "libc", + "nix 0.31.2", + "tokio", + "tracing", +] + [[package]] name = "ndk-context" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index a9fb711..c323404 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,11 @@ [workspace] -members = ["mycelium", "mycelium-metrics", "mycelium-api", "mycelium-cli"] +members = [ + "mycelium", + "mycelium-metrics", + "mycelium-api", + "mycelium-cli", + "mycelium-tun", +] exclude = ["myceliumd", "myceliumd-private", "myceliumd-common", "mobile"] resolver = "2" diff --git a/flake.nix b/flake.nix index dc56c54..dfff9f8 100644 --- a/flake.nix +++ b/flake.nix @@ -41,6 +41,7 @@ ./mycelium-api ./mycelium-cli ./mycelium-metrics + ./mycelium-tun ./myceliumd ./myceliumd-common ./myceliumd-private @@ -88,6 +89,7 @@ ./mycelium-api ./mycelium-cli ./mycelium-metrics + ./mycelium-tun ./myceliumd ./myceliumd-common ./myceliumd-private diff --git a/mycelium-tun/Cargo.toml b/mycelium-tun/Cargo.toml new file mode 100644 index 0000000..10d20a1 --- /dev/null +++ b/mycelium-tun/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "mycelium-tun" +version = "0.1.0" +edition = "2021" + +[target.'cfg(target_os = "linux")'.dependencies] +tokio = { version = "1.49.0", default-features = false, features = ["net"] } +nix = { version = "0.31.1", features = ["ioctl", "net"] } +libc = "0.2" +tracing = "0.1" diff --git a/mycelium-tun/src/lib.rs b/mycelium-tun/src/lib.rs new file mode 100644 index 0000000..56d6496 --- /dev/null +++ b/mycelium-tun/src/lib.rs @@ -0,0 +1,9 @@ +//! Platform-specific TUN device implementations. +//! +//! Currently only Linux is supported. On other platforms this crate is empty. + +#[cfg(target_os = "linux")] +mod linux; + +#[cfg(target_os = "linux")] +pub use linux::Tun; diff --git a/mycelium-tun/src/linux.rs b/mycelium-tun/src/linux.rs new file mode 100644 index 0000000..68f751d --- /dev/null +++ b/mycelium-tun/src/linux.rs @@ -0,0 +1,242 @@ +//! Linux TUN device implementation using ioctls and tokio's AsyncFd. + +use std::io; +use std::net::Ipv6Addr; +use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd, OwnedFd}; + +use nix::net::if_::InterfaceFlags; +use tokio::io::unix::AsyncFd; +use tracing::debug; + +/// `TUNSETIFF` ioctl request code. Not exposed by libc or nix. +const TUNSETIFF: libc::c_ulong = 0x400454ca; + +nix::ioctl_write_ptr_bad!( + /// Configure a TUN device (set name and flags). + tunsetiff, + TUNSETIFF, + libc::ifreq +); + +nix::ioctl_write_ptr_bad!( + /// Set the MTU on a network interface. + siocsifmtu, + libc::SIOCSIFMTU as libc::c_ulong, + libc::ifreq +); + +nix::ioctl_read_bad!( + /// Get the index of a network interface by name. + siocgifindex, + libc::SIOCGIFINDEX as libc::c_ulong, + libc::ifreq +); + +nix::ioctl_write_ptr_bad!( + /// Set an address on a network interface. + siocsifaddr, + libc::SIOCSIFADDR as libc::c_ulong, + libc::in6_ifreq +); + +/// A Linux TUN device. +/// +/// The device is opened with `IFF_TUN | IFF_NO_PI` flags, meaning it operates at the IP layer +/// (layer 3) with no packet information header prepended. +/// +/// All read/write operations are async, backed by tokio's [`AsyncFd`]. +pub struct Tun { + fd: AsyncFd, + name: String, +} + +impl Tun { + /// Create a new TUN device. + /// + /// If `name` is empty, the kernel assigns a name (typically "tun0", "tun1", ...). The actual + /// assigned name can be retrieved with [`Tun::name`]. + /// + /// # Panics + /// + /// Panics if called outside of a tokio runtime context. + pub fn new(name: &str) -> io::Result { + let file = std::fs::OpenOptions::new() + .read(true) + .write(true) + .open("/dev/net/tun")?; + + let mut ifr = new_ifreq(); + + // Copy the requested name, leaving room for the NUL terminator. + for (dst, &src) in ifr.ifr_name[..libc::IFNAMSIZ - 1] + .iter_mut() + .zip(name.as_bytes()) + { + *dst = src as libc::c_char; + } + + let flags = InterfaceFlags::IFF_TUN | InterfaceFlags::IFF_NO_PI; + ifr.ifr_ifru.ifru_flags = flags.bits() as libc::c_short; + + // SAFETY: file is a valid fd, ifr is properly initialized. + unsafe { tunsetiff(file.as_raw_fd(), &ifr) }.map_err(io::Error::from)?; + + let actual_name = ifreq_name(&ifr)?; + debug!(name = %actual_name, "created TUN device"); + + // Transfer ownership of the fd from File to OwnedFd without closing it. + // SAFETY: raw_fd is valid, we just obtained it from file. + let owned_fd = unsafe { OwnedFd::from_raw_fd(file.into_raw_fd()) }; + let async_fd = AsyncFd::new(owned_fd)?; + + Ok(Tun { + fd: async_fd, + name: actual_name, + }) + } + + /// Returns the name of the TUN interface. + pub fn name(&self) -> &str { + &self.name + } + + /// Set the MTU on the TUN interface. + pub fn set_mtu(&self, mtu: u32) -> io::Result<()> { + let sock = ioctl_socket()?; + + let mut ifr = self.new_ifreq_with_name(); + ifr.ifr_ifru.ifru_mtu = mtu as libc::c_int; + + // SAFETY: sock is a valid fd, ifr is properly initialized. + unsafe { siocsifmtu(sock.as_raw_fd(), &ifr) }.map_err(io::Error::from)?; + + debug!(name = %self.name, mtu, "set TUN MTU"); + Ok(()) + } + + /// Set an IPv6 address and prefix length on the TUN interface. + pub fn set_addr(&self, addr: Ipv6Addr, prefix_len: u8) -> io::Result<()> { + let sock = ioctl_socket()?; + let if_index = self.interface_index()?; + + let ifr6 = libc::in6_ifreq { + ifr6_addr: libc::in6_addr { + s6_addr: addr.octets(), + }, + ifr6_prefixlen: prefix_len as u32, + ifr6_ifindex: if_index, + }; + + // SAFETY: sock is a valid AF_INET6 fd, ifr6 is properly initialized. + unsafe { siocsifaddr(sock.as_raw_fd(), &ifr6) }.map_err(io::Error::from)?; + + debug!(name = %self.name, %addr, prefix_len, "set TUN address"); + Ok(()) + } + + /// Get the kernel interface index for this TUN device. + fn interface_index(&self) -> io::Result { + let sock = ioctl_socket()?; + let mut ifr = self.new_ifreq_with_name(); + + // SAFETY: sock is a valid fd, ifr is properly initialized with the interface name. + unsafe { siocgifindex(sock.as_raw_fd(), &mut ifr) }.map_err(io::Error::from)?; + + // SAFETY: siocgifindex populates ifr_ifru.ifru_ifindex on success. + Ok(unsafe { ifr.ifr_ifru.ifru_ifindex }) + } + + /// Create an `ifreq` with this device's name already filled in. + fn new_ifreq_with_name(&self) -> libc::ifreq { + let mut ifr = new_ifreq(); + for (dst, &src) in ifr.ifr_name[..libc::IFNAMSIZ - 1] + .iter_mut() + .zip(self.name.as_bytes()) + { + *dst = src as libc::c_char; + } + ifr + } + + /// Read a packet from the TUN device into `buf`. + /// + /// Returns the number of bytes read. The caller should use `&buf[..n]` to access the packet + /// data. + pub async fn read(&self, buf: &mut [u8]) -> io::Result { + loop { + let mut guard = self.fd.readable().await?; + match guard.try_io(|inner| { + // SAFETY: inner is a valid fd, buf is valid for buf.len() bytes. + let n = unsafe { + libc::read(inner.as_raw_fd(), buf.as_mut_ptr().cast(), buf.len()) + }; + if n < 0 { + Err(io::Error::last_os_error()) + } else { + Ok(n as usize) + } + }) { + Ok(result) => return result, + Err(_would_block) => continue, + } + } + } + + /// Write a packet to the TUN device. + /// + /// Returns the number of bytes written. + pub async fn write(&self, buf: &[u8]) -> io::Result { + loop { + let mut guard = self.fd.writable().await?; + match guard.try_io(|inner| { + // SAFETY: inner is a valid fd, buf is valid for buf.len() bytes. + let n = unsafe { + libc::write(inner.as_raw_fd(), buf.as_ptr().cast(), buf.len()) + }; + if n < 0 { + Err(io::Error::last_os_error()) + } else { + Ok(n as usize) + } + }) { + Ok(result) => return result, + Err(_would_block) => continue, + } + } + } +} + +/// Create a zero-initialized `ifreq`. +fn new_ifreq() -> libc::ifreq { + // SAFETY: ifreq is a C struct where all-zeros is a valid representation. + unsafe { std::mem::zeroed() } +} + +/// Extract the interface name from an `ifreq` as a Rust `String`. +fn ifreq_name(ifr: &libc::ifreq) -> io::Result { + let nul_pos = ifr + .ifr_name + .iter() + .position(|&c| c == 0) + .unwrap_or(libc::IFNAMSIZ); + let name_bytes: Vec = ifr.ifr_name[..nul_pos] + .iter() + .map(|&c| c as u8) + .collect(); + String::from_utf8(name_bytes) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid interface name")) +} + +/// Create a temporary socket for ioctl operations. +/// +/// An IPv6 UDP socket is used since all ioctls in this module work with any socket type, and +/// `SIOCSIFADDR` for IPv6 addresses requires an `AF_INET6` socket. +fn ioctl_socket() -> io::Result { + // SAFETY: standard socket creation. + let fd = unsafe { libc::socket(libc::AF_INET6, libc::SOCK_DGRAM, 0) }; + if fd < 0 { + return Err(io::Error::last_os_error()); + } + // SAFETY: fd is valid, we just created it. + Ok(unsafe { OwnedFd::from_raw_fd(fd) }) +}