Add custom tun implementation on linux

Signed-off-by: Lee Smet <lee.smet@hotmail.com>
This commit is contained in:
Lee Smet
2026-02-24 11:22:46 +01:00
parent 518f287c40
commit 0243feaa3d
6 changed files with 280 additions and 1 deletions
Generated
+10
View File
@@ -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"
+7 -1
View File
@@ -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"
+2
View File
@@ -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
+10
View File
@@ -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"
+9
View File
@@ -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;
+242
View File
@@ -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<OwnedFd>,
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<Self> {
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<libc::c_int> {
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<usize> {
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<usize> {
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<String> {
let nul_pos = ifr
.ifr_name
.iter()
.position(|&c| c == 0)
.unwrap_or(libc::IFNAMSIZ);
let name_bytes: Vec<u8> = 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<OwnedFd> {
// 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) })
}