From 751cba371e7dafdf51c86c8a650fd829f0b95621 Mon Sep 17 00:00:00 2001 From: Lee Smet Date: Thu, 4 Jun 2026 16:17:18 +0200 Subject: [PATCH 1/2] Fully configure TUN interface in FFI Android path The FFI's create_tun_fd only ran TUNSETIFF, but the downstream tun/android.rs assumes the fd is fully configured (matching the mobile/VpnService contract). Result: the interface had no IP, was administratively down, and could not carry traffic. Extend create_tun_fd to assign the node's IPv6 with the global subnet's prefix length (installs the on-link 400::/7 route), set MTU 1400 to match tun/android.rs's LINK_MTU, and bring the interface up via SIOCSIFFLAGS. --- mycelium/src/ffi/exports.rs | 27 ++++++-- mycelium/src/node_handle.rs | 124 ++++++++++++++++++++++++++++++++++-- 2 files changed, 140 insertions(+), 11 deletions(-) diff --git a/mycelium/src/ffi/exports.rs b/mycelium/src/ffi/exports.rs index 485dbce5..dd83ac6f 100644 --- a/mycelium/src/ffi/exports.rs +++ b/mycelium/src/ffi/exports.rs @@ -177,20 +177,33 @@ pub unsafe extern "C" fn mycelium_start( info!(peers = peers_strs.len(), "starting mycelium node"); + let node_key = crypto::SecretKey::from(cfg.priv_key); + #[cfg(target_os = "android")] - let tun_fd = match crate::node_handle::create_tun_fd(&tun_name) { - Ok(fd) => fd, - Err(e) => { - error!("Failed to create TUN fd: {e}"); - error::set(format!("failed to create tun fd: {e}")); - return std::ptr::null_mut(); + let tun_fd = { + // MTU matches `LINK_MTU` in `mycelium/src/tun/android.rs`; the prefix + // length matches what `tun/linux.rs` uses, so the kernel installs the + // on-link route for the global mycelium subnet. + let node_addr = crypto::PublicKey::from(&node_key).address(); + match crate::node_handle::create_tun_fd( + &tun_name, + node_addr, + crate::GLOBAL_SUBNET_PREFIX_LEN, + 1400, + ) { + Ok(fd) => fd, + Err(e) => { + error!("Failed to create TUN fd: {e}"); + error::set(format!("failed to create tun fd: {e}")); + return std::ptr::null_mut(); + } } }; #[cfg(any(target_os = "ios", all(target_os = "macos", feature = "mactunfd")))] let tun_fd = cfg.tun_fd; let config = Config { - node_key: crypto::SecretKey::from(cfg.priv_key), + node_key, peers: endpoints, no_tun: false, #[cfg(any( diff --git a/mycelium/src/node_handle.rs b/mycelium/src/node_handle.rs index 87907059..868bfd67 100644 --- a/mycelium/src/node_handle.rs +++ b/mycelium/src/node_handle.rs @@ -38,15 +38,30 @@ impl std::error::Error for NodeError {} // ── TUN setup (Android only) ──────────────────────────────────────────────── -/// On Android the `tun` crate expects an already-opened file descriptor. -/// This function opens `/dev/tun`, configures the interface with TUNSETIFF, -/// and returns the raw fd. Requires `CAP_NET_ADMIN`. +/// Open and fully configure a TUN file descriptor on Android. +/// +/// The `tun/android.rs` data-plane code expects an already-configured fd +/// (the same contract the `mobile/` crate satisfies via `VpnService.Builder`). +/// For the FFI path there is no VpnService doing that work, so this function +/// does it: opens `/dev/tun`, sets `IFF_TUN | IFF_NO_PI` via `TUNSETIFF`, +/// assigns `node_addr` with `prefix_len` (`/7` causes the kernel to install +/// the on-link route for the global mycelium subnet), sets the MTU, and +/// brings the interface up. +/// +/// Requires `CAP_NET_ADMIN`. #[cfg(target_os = "android")] -pub fn create_tun_fd(tun_name: &str) -> Result { +pub fn create_tun_fd( + tun_name: &str, + node_addr: std::net::Ipv6Addr, + prefix_len: u8, + mtu: i32, +) -> Result { const TUNSETIFF: libc::c_ulong = 0x400454ca; const IFF_TUN: libc::c_short = 0x0001; const IFF_NO_PI: libc::c_short = 0x1000; + // SAFETY: the path is a static NUL-terminated byte string; `open` has no + // other preconditions. let fd = unsafe { libc::open(b"/dev/tun\0".as_ptr() as *const libc::c_char, libc::O_RDWR) }; if fd < 0 { return Err(std::io::Error::last_os_error()); @@ -60,16 +75,117 @@ pub fn create_tun_fd(tun_name: &str) -> Result { ifr[16] = (flags & 0xff) as u8; ifr[17] = ((flags >> 8) & 0xff) as u8; + // SAFETY: `fd` is a valid file descriptor that we just opened. `ifr` is a + // 40-byte buffer matching the kernel's `struct ifreq` layout, with name + // bytes (offset 0..15), a NUL terminator (offset 15, left at zero), and + // `ifr_flags` (offset 16..18) populated; remaining bytes are zero, which + // is what TUNSETIFF expects for unused union members. let ret = unsafe { libc::ioctl(fd, TUNSETIFF as i32, ifr.as_ptr()) }; if ret < 0 { let err = std::io::Error::last_os_error(); + // SAFETY: `fd` is the open fd from the `open` call above; nothing else + // can reference it because it has not escaped this function. unsafe { libc::close(fd) }; return Err(err); } + if let Err(e) = configure_tun_interface(tun_name, node_addr, prefix_len, mtu) { + // SAFETY: `fd` is the open fd from the `open` call above; nothing else + // can reference it because it has not escaped this function. + unsafe { libc::close(fd) }; + return Err(e); + } + Ok(fd) } +/// Assign address, set MTU and bring the named interface up via an +/// `AF_INET6` control socket. `SIOCSIFADDR` for IPv6 requires `AF_INET6`. +#[cfg(target_os = "android")] +fn configure_tun_interface( + tun_name: &str, + node_addr: std::net::Ipv6Addr, + prefix_len: u8, + mtu: i32, +) -> std::io::Result<()> { + // SAFETY: `socket(2)` has no preconditions; the arguments are valid + // constants exported by libc. + let sock = unsafe { libc::socket(libc::AF_INET6, libc::SOCK_DGRAM, 0) }; + if sock < 0 { + return Err(std::io::Error::last_os_error()); + } + + let result = (|| -> std::io::Result<()> { + // SAFETY: `libc::ifreq` is a `repr(C)` aggregate of POD types and a + // C union; the all-zero bit pattern is a valid value of every + // variant (the union members are all integers or fixed-size byte + // arrays), and the kernel ignores fields not consumed by the + // specific ioctl request. + let mut ifr: libc::ifreq = unsafe { std::mem::zeroed() }; + let name_bytes = tun_name.as_bytes(); + let copy_len = name_bytes.len().min(libc::IFNAMSIZ - 1); + for (dst, &src) in ifr.ifr_name[..copy_len].iter_mut().zip(name_bytes) { + *dst = src as libc::c_char; + } + + // SAFETY: `sock` is a valid socket fd opened above; `ifr` is fully + // initialised with the interface name in `ifr_name` (NUL-terminated: + // we copied at most IFNAMSIZ-1 bytes into a zero-initialised buffer) + // — the layout expected by SIOCGIFINDEX. + if unsafe { libc::ioctl(sock, libc::SIOCGIFINDEX as _, &mut ifr) } < 0 { + return Err(std::io::Error::last_os_error()); + } + // SAFETY: SIOCGIFINDEX populates the `ifru_ifindex` union variant on + // success; the ioctl returned >= 0 above. + let ifindex = unsafe { ifr.ifr_ifru.ifru_ifindex }; + + let ifr6 = libc::in6_ifreq { + ifr6_addr: libc::in6_addr { + s6_addr: node_addr.octets(), + }, + ifr6_prefixlen: prefix_len as u32, + ifr6_ifindex: ifindex, + }; + // SAFETY: `sock` is a valid AF_INET6 socket fd (required for IPv6 + // SIOCSIFADDR); `ifr6` is a fully initialised `in6_ifreq` with the + // address, prefix length, and interface index expected by the ioctl. + if unsafe { libc::ioctl(sock, libc::SIOCSIFADDR as _, &ifr6) } < 0 { + return Err(std::io::Error::last_os_error()); + } + + ifr.ifr_ifru.ifru_mtu = mtu; + // SAFETY: `sock` is a valid socket fd; `ifr` has the interface name + // set in `ifr_name` and the MTU written into the `ifru_mtu` union + // variant — the layout SIOCSIFMTU expects. + if unsafe { libc::ioctl(sock, libc::SIOCSIFMTU as _, &ifr) } < 0 { + return Err(std::io::Error::last_os_error()); + } + + // SAFETY: `sock` is a valid socket fd; `ifr_name` is still set from + // the SIOCGIFINDEX call above (the kernel only writes the `ifr_ifru` + // union on success, leaving `ifr_name` intact). + if unsafe { libc::ioctl(sock, libc::SIOCGIFFLAGS as _, &mut ifr) } < 0 { + return Err(std::io::Error::last_os_error()); + } + // SAFETY: SIOCGIFFLAGS populates the `ifru_flags` union variant on + // success; the ioctl returned >= 0 above. + ifr.ifr_ifru.ifru_flags = unsafe { ifr.ifr_ifru.ifru_flags } | libc::IFF_UP as libc::c_short; + // SAFETY: `sock` is a valid socket fd; `ifr` has the interface name + // set and the updated flags written into the `ifru_flags` union + // variant. + if unsafe { libc::ioctl(sock, libc::SIOCSIFFLAGS as _, &ifr) } < 0 { + return Err(std::io::Error::last_os_error()); + } + + Ok(()) + })(); + + // SAFETY: `sock` is the fd from the `socket` call above; it has not + // escaped this function and is no longer used. + unsafe { libc::close(sock) }; + result +} + // ── NodeHandle ────────────────────────────────────────────────────────────── /// Upper bound on how long node teardown waits for the background Tokio From 67c281998f38147701ea25d09ee37e9b69446527 Mon Sep 17 00:00:00 2001 From: Lee Smet Date: Thu, 4 Jun 2026 17:23:22 +0200 Subject: [PATCH 2/2] Make regular linux tun code available on android Previously android always used the same code which was an issue for system services. Signed-off-by: Lee Smet --- mobile/Cargo.lock | 87 +++++++++-------- mobile/Cargo.toml | 3 + mycelium-tun/Cargo.toml | 2 +- mycelium-tun/src/lib.rs | 12 ++- mycelium/Cargo.toml | 7 ++ mycelium/src/ffi/exports.rs | 41 +++----- mycelium/src/lib.rs | 20 ++-- mycelium/src/node_handle.rs | 190 +----------------------------------- mycelium/src/tun.rs | 25 +++-- 9 files changed, 111 insertions(+), 276 deletions(-) diff --git a/mobile/Cargo.lock b/mobile/Cargo.lock index 212a5ab2..988e3c66 100644 --- a/mobile/Cargo.lock +++ b/mobile/Cargo.lock @@ -301,6 +301,15 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -431,9 +440,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -614,16 +623,16 @@ dependencies = [ [[package]] name = "dashmap" -version = "6.1.0" +version = "6.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" dependencies = [ "cfg-if", "crossbeam-utils", "hashbrown 0.14.5", "lock_api", "once_cell", - "parking_lot_core 0.9.10", + "parking_lot_core 0.9.12", ] [[package]] @@ -727,7 +736,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -917,7 +926,7 @@ dependencies = [ "libc", "log", "rustversion", - "windows-link 0.1.3", + "windows-link 0.2.1", "windows-result", ] @@ -1688,11 +1697,10 @@ checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] @@ -1825,7 +1833,7 @@ dependencies = [ [[package]] name = "mycelium" -version = "0.7.7" +version = "0.7.9" dependencies = [ "aes-gcm", "ahash 0.8.11", @@ -1849,7 +1857,7 @@ dependencies = [ "libc", "mycelium-tun", "netdev", - "nix 0.31.2", + "nix 0.31.3", "openssl", "quinn", "rand 0.10.1", @@ -1874,7 +1882,7 @@ dependencies = [ [[package]] name = "mycelium-api" -version = "0.7.7" +version = "0.7.9" dependencies = [ "async-trait", "axum", @@ -1890,7 +1898,7 @@ dependencies = [ [[package]] name = "mycelium-metrics" -version = "0.7.7" +version = "0.7.9" dependencies = [ "axum", "mycelium", @@ -1904,7 +1912,7 @@ name = "mycelium-tun" version = "0.1.0" dependencies = [ "libc", - "nix 0.31.2", + "nix 0.31.3", "tokio", "tracing", ] @@ -2013,9 +2021,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.31.2" +version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ "bitflags 2.9.0", "cfg-if", @@ -2178,9 +2186,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" dependencies = [ "critical-section", "portable-atomic", @@ -2194,9 +2202,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.79" +version = "0.10.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542" +checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" dependencies = [ "bitflags 2.9.0", "cfg-if", @@ -2228,9 +2236,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.115" +version = "0.9.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781" +checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" dependencies = [ "cc", "libc", @@ -2263,7 +2271,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", - "parking_lot_core 0.9.10", + "parking_lot_core 0.9.12", ] [[package]] @@ -2282,15 +2290,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall 0.5.11", "smallvec", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] @@ -2501,7 +2509,7 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.1", "rustls", - "socket2 0.5.9", + "socket2 0.6.3", "thiserror 2.0.12", "tokio", "tracing", @@ -2642,9 +2650,9 @@ checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "rcgen" -version = "0.14.7" +version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10b99e0098aa4082912d4c649628623db6aba77335e4f4569ff5083a6448b32e" +checksum = "57f6d249aad744e274e682777a50283a225a32705394ee6d5fcc01efa25e4055" dependencies = [ "pem", "ring", @@ -2877,7 +2885,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3241,10 +3249,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.2", + "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3355,9 +3363,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.2" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -3750,7 +3758,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ba782755fc073877e567c2253c0be48e4aa9a254c232d36d3985dfae0bd5205" dependencies = [ "libc", - "nix 0.31.2", + "nix 0.31.3", ] [[package]] @@ -3967,7 +3975,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4385,10 +4393,11 @@ checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" [[package]] name = "yasna" -version = "0.5.2" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" dependencies = [ + "bit-vec", "time", ] diff --git a/mobile/Cargo.toml b/mobile/Cargo.toml index 019cbe01..488a0b51 100644 --- a/mobile/Cargo.toml +++ b/mobile/Cargo.toml @@ -19,6 +19,9 @@ once_cell = "1.21.1" serde_json = "1.0" [target.'cfg(target_os = "android")'.dependencies] +# Android consumers (VpnService.Builder hands in a pre-configured fd) use the +# fd-handoff TUN path. This feature enables that path in mycelium. +mycelium = { path = "../mycelium", features = ["vendored-openssl", "androidtunfd"] } tracing-android = "0.2.0" [target.'cfg(target_os = "ios")'.dependencies] diff --git a/mycelium-tun/Cargo.toml b/mycelium-tun/Cargo.toml index 3f168dba..9c2df380 100644 --- a/mycelium-tun/Cargo.toml +++ b/mycelium-tun/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2024" license-file = "../LICENSE" -[target.'cfg(target_os = "linux")'.dependencies] +[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] tokio = { version = "1.52.3", default-features = false, features = ["net"] } nix = { version = "0.31.3", features = ["ioctl", "net"] } libc = "0.2" diff --git a/mycelium-tun/src/lib.rs b/mycelium-tun/src/lib.rs index f10cda64..45916917 100644 --- a/mycelium-tun/src/lib.rs +++ b/mycelium-tun/src/lib.rs @@ -1,13 +1,15 @@ //! Platform-specific TUN device implementations. //! -//! Currently only Linux is supported. On other platforms this crate is empty. +//! Linux and Android share the same implementation (Android is Linux + bionic +//! and exposes the same `/dev/net/tun` uAPI). On other platforms this crate +//! is empty. -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "android"))] mod checksum; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "android"))] mod linux; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "android"))] mod offload; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "android"))] pub use linux::{ReadHalf, Tun, WriteHalf}; diff --git a/mycelium/Cargo.toml b/mycelium/Cargo.toml index 5b7bd9d7..1eb05279 100644 --- a/mycelium/Cargo.toml +++ b/mycelium/Cargo.toml @@ -12,6 +12,11 @@ vendored-openssl = ["openssl/vendored"] mactunfd = [ "tun/appstore", ] #mactunfd is a flag to specify that macos should provide tun FD instead of tun name +# androidtunfd is the Android equivalent of mactunfd: when enabled the caller +# (typically an app using Android's VpnService) provides a pre-configured TUN +# file descriptor. Default Android builds (e.g. a `cargo ndk` system-service +# build) open and manage the TUN device themselves via the shared Linux path. +androidtunfd = [] # Build a C ABI surface (cdylib + staticlib + generated header) on top of the # core node. Used by external daemons that wrap libmycelium without depending # on Rust as a build dependency. @@ -97,6 +102,8 @@ wintun = "0.5.1" [target.'cfg(target_os = "android")'.dependencies] tun = { git = "https://github.com/LeeSmet/rust-tun", features = ["async"] } libc = "0.2.186" +nix = { version = "0.31.3", features = ["socket"] } +mycelium-tun = { path = "../mycelium-tun" } [target.'cfg(target_os = "ios")'.dependencies] tun = { git = "https://github.com/LeeSmet/rust-tun", features = ["async"] } diff --git a/mycelium/src/ffi/exports.rs b/mycelium/src/ffi/exports.rs index dd83ac6f..030e755c 100644 --- a/mycelium/src/ffi/exports.rs +++ b/mycelium/src/ffi/exports.rs @@ -169,7 +169,11 @@ pub unsafe extern "C" fn mycelium_start( Err(_) => return std::ptr::null_mut(), }; - #[cfg(not(any(target_os = "ios", all(target_os = "macos", feature = "mactunfd"))))] + #[cfg(not(any( + target_os = "ios", + all(target_os = "macos", feature = "mactunfd"), + all(target_os = "android", feature = "androidtunfd"), + )))] let tun_name = match cstr_to_str(cfg.tun_name, "tun_name") { Ok(s) => s.to_owned(), Err(_) => return std::ptr::null_mut(), @@ -177,45 +181,28 @@ pub unsafe extern "C" fn mycelium_start( info!(peers = peers_strs.len(), "starting mycelium node"); - let node_key = crypto::SecretKey::from(cfg.priv_key); - - #[cfg(target_os = "android")] - let tun_fd = { - // MTU matches `LINK_MTU` in `mycelium/src/tun/android.rs`; the prefix - // length matches what `tun/linux.rs` uses, so the kernel installs the - // on-link route for the global mycelium subnet. - let node_addr = crypto::PublicKey::from(&node_key).address(); - match crate::node_handle::create_tun_fd( - &tun_name, - node_addr, - crate::GLOBAL_SUBNET_PREFIX_LEN, - 1400, - ) { - Ok(fd) => fd, - Err(e) => { - error!("Failed to create TUN fd: {e}"); - error::set(format!("failed to create tun fd: {e}")); - return std::ptr::null_mut(); - } - } - }; - #[cfg(any(target_os = "ios", all(target_os = "macos", feature = "mactunfd")))] + #[cfg(any( + target_os = "ios", + all(target_os = "macos", feature = "mactunfd"), + all(target_os = "android", feature = "androidtunfd"), + ))] let tun_fd = cfg.tun_fd; let config = Config { - node_key, + node_key: crypto::SecretKey::from(cfg.priv_key), peers: endpoints, no_tun: false, #[cfg(any( target_os = "linux", all(target_os = "macos", not(feature = "mactunfd")), - target_os = "windows" + target_os = "windows", + all(target_os = "android", not(feature = "androidtunfd")), ))] tun_name, #[cfg(any( - target_os = "android", target_os = "ios", all(target_os = "macos", feature = "mactunfd"), + all(target_os = "android", feature = "androidtunfd"), ))] tun_fd: Some(tun_fd), tcp_listen_port: cfg.tcp_listen_port, diff --git a/mycelium/src/lib.rs b/mycelium/src/lib.rs index 661d02f1..38857f12 100644 --- a/mycelium/src/lib.rs +++ b/mycelium/src/lib.rs @@ -84,7 +84,8 @@ pub struct Config { #[cfg(any( target_os = "linux", all(target_os = "macos", not(feature = "mactunfd")), - target_os = "windows" + target_os = "windows", + all(target_os = "android", not(feature = "androidtunfd")), ))] pub tun_name: String, @@ -97,14 +98,16 @@ pub struct Config { /// Mark that's set on all packets that we send on the underlying network pub firewall_mark: Option, - // tun_fd is android, iOS, macos on appstore specific option - // We can't create TUN device from the Rust code in android, iOS, and macos on appstore. - // So, we create the TUN device on Kotlin(android) or Swift(iOS, macos) then pass - // the TUN's file descriptor to mycelium. + // tun_fd is the iOS / macos-appstore / android-VpnService option. + // We can't create the TUN device from Rust on iOS or macOS-appstore (the + // platform doesn't expose `/dev/net/tun` to the app). Android apps using + // `VpnService.Builder` are in the same situation — the framework hands + // back a ready fd. In these cases the TUN is created by Kotlin (android) + // or Swift (iOS, macOS) and the file descriptor is passed to mycelium. #[cfg(any( - target_os = "android", target_os = "ios", all(target_os = "macos", feature = "mactunfd"), + all(target_os = "android", feature = "androidtunfd"), ))] pub tun_fd: Option, @@ -281,7 +284,8 @@ where #[cfg(any( target_os = "linux", all(target_os = "macos", not(feature = "mactunfd")), - target_os = "windows" + target_os = "windows", + all(target_os = "android", not(feature = "androidtunfd")), ))] let tun_config = TunConfig { name: config.tun_name.clone(), @@ -291,9 +295,9 @@ where .expect("Static configured TUN route is valid; qed"), }; #[cfg(any( - target_os = "android", target_os = "ios", all(target_os = "macos", feature = "mactunfd"), + all(target_os = "android", feature = "androidtunfd"), ))] let tun_config = TunConfig { tun_fd: config.tun_fd.unwrap(), diff --git a/mycelium/src/node_handle.rs b/mycelium/src/node_handle.rs index 868bfd67..68a41535 100644 --- a/mycelium/src/node_handle.rs +++ b/mycelium/src/node_handle.rs @@ -36,156 +36,6 @@ impl std::fmt::Display for NodeError { impl std::error::Error for NodeError {} -// ── TUN setup (Android only) ──────────────────────────────────────────────── - -/// Open and fully configure a TUN file descriptor on Android. -/// -/// The `tun/android.rs` data-plane code expects an already-configured fd -/// (the same contract the `mobile/` crate satisfies via `VpnService.Builder`). -/// For the FFI path there is no VpnService doing that work, so this function -/// does it: opens `/dev/tun`, sets `IFF_TUN | IFF_NO_PI` via `TUNSETIFF`, -/// assigns `node_addr` with `prefix_len` (`/7` causes the kernel to install -/// the on-link route for the global mycelium subnet), sets the MTU, and -/// brings the interface up. -/// -/// Requires `CAP_NET_ADMIN`. -#[cfg(target_os = "android")] -pub fn create_tun_fd( - tun_name: &str, - node_addr: std::net::Ipv6Addr, - prefix_len: u8, - mtu: i32, -) -> Result { - const TUNSETIFF: libc::c_ulong = 0x400454ca; - const IFF_TUN: libc::c_short = 0x0001; - const IFF_NO_PI: libc::c_short = 0x1000; - - // SAFETY: the path is a static NUL-terminated byte string; `open` has no - // other preconditions. - let fd = unsafe { libc::open(b"/dev/tun\0".as_ptr() as *const libc::c_char, libc::O_RDWR) }; - if fd < 0 { - return Err(std::io::Error::last_os_error()); - } - - let mut ifr = [0u8; 40]; - let name_bytes = tun_name.as_bytes(); - let len = name_bytes.len().min(15); - ifr[..len].copy_from_slice(&name_bytes[..len]); - let flags: i16 = IFF_TUN | IFF_NO_PI; - ifr[16] = (flags & 0xff) as u8; - ifr[17] = ((flags >> 8) & 0xff) as u8; - - // SAFETY: `fd` is a valid file descriptor that we just opened. `ifr` is a - // 40-byte buffer matching the kernel's `struct ifreq` layout, with name - // bytes (offset 0..15), a NUL terminator (offset 15, left at zero), and - // `ifr_flags` (offset 16..18) populated; remaining bytes are zero, which - // is what TUNSETIFF expects for unused union members. - let ret = unsafe { libc::ioctl(fd, TUNSETIFF as i32, ifr.as_ptr()) }; - if ret < 0 { - let err = std::io::Error::last_os_error(); - // SAFETY: `fd` is the open fd from the `open` call above; nothing else - // can reference it because it has not escaped this function. - unsafe { libc::close(fd) }; - return Err(err); - } - - if let Err(e) = configure_tun_interface(tun_name, node_addr, prefix_len, mtu) { - // SAFETY: `fd` is the open fd from the `open` call above; nothing else - // can reference it because it has not escaped this function. - unsafe { libc::close(fd) }; - return Err(e); - } - - Ok(fd) -} - -/// Assign address, set MTU and bring the named interface up via an -/// `AF_INET6` control socket. `SIOCSIFADDR` for IPv6 requires `AF_INET6`. -#[cfg(target_os = "android")] -fn configure_tun_interface( - tun_name: &str, - node_addr: std::net::Ipv6Addr, - prefix_len: u8, - mtu: i32, -) -> std::io::Result<()> { - // SAFETY: `socket(2)` has no preconditions; the arguments are valid - // constants exported by libc. - let sock = unsafe { libc::socket(libc::AF_INET6, libc::SOCK_DGRAM, 0) }; - if sock < 0 { - return Err(std::io::Error::last_os_error()); - } - - let result = (|| -> std::io::Result<()> { - // SAFETY: `libc::ifreq` is a `repr(C)` aggregate of POD types and a - // C union; the all-zero bit pattern is a valid value of every - // variant (the union members are all integers or fixed-size byte - // arrays), and the kernel ignores fields not consumed by the - // specific ioctl request. - let mut ifr: libc::ifreq = unsafe { std::mem::zeroed() }; - let name_bytes = tun_name.as_bytes(); - let copy_len = name_bytes.len().min(libc::IFNAMSIZ - 1); - for (dst, &src) in ifr.ifr_name[..copy_len].iter_mut().zip(name_bytes) { - *dst = src as libc::c_char; - } - - // SAFETY: `sock` is a valid socket fd opened above; `ifr` is fully - // initialised with the interface name in `ifr_name` (NUL-terminated: - // we copied at most IFNAMSIZ-1 bytes into a zero-initialised buffer) - // — the layout expected by SIOCGIFINDEX. - if unsafe { libc::ioctl(sock, libc::SIOCGIFINDEX as _, &mut ifr) } < 0 { - return Err(std::io::Error::last_os_error()); - } - // SAFETY: SIOCGIFINDEX populates the `ifru_ifindex` union variant on - // success; the ioctl returned >= 0 above. - let ifindex = unsafe { ifr.ifr_ifru.ifru_ifindex }; - - let ifr6 = libc::in6_ifreq { - ifr6_addr: libc::in6_addr { - s6_addr: node_addr.octets(), - }, - ifr6_prefixlen: prefix_len as u32, - ifr6_ifindex: ifindex, - }; - // SAFETY: `sock` is a valid AF_INET6 socket fd (required for IPv6 - // SIOCSIFADDR); `ifr6` is a fully initialised `in6_ifreq` with the - // address, prefix length, and interface index expected by the ioctl. - if unsafe { libc::ioctl(sock, libc::SIOCSIFADDR as _, &ifr6) } < 0 { - return Err(std::io::Error::last_os_error()); - } - - ifr.ifr_ifru.ifru_mtu = mtu; - // SAFETY: `sock` is a valid socket fd; `ifr` has the interface name - // set in `ifr_name` and the MTU written into the `ifru_mtu` union - // variant — the layout SIOCSIFMTU expects. - if unsafe { libc::ioctl(sock, libc::SIOCSIFMTU as _, &ifr) } < 0 { - return Err(std::io::Error::last_os_error()); - } - - // SAFETY: `sock` is a valid socket fd; `ifr_name` is still set from - // the SIOCGIFINDEX call above (the kernel only writes the `ifr_ifru` - // union on success, leaving `ifr_name` intact). - if unsafe { libc::ioctl(sock, libc::SIOCGIFFLAGS as _, &mut ifr) } < 0 { - return Err(std::io::Error::last_os_error()); - } - // SAFETY: SIOCGIFFLAGS populates the `ifru_flags` union variant on - // success; the ioctl returned >= 0 above. - ifr.ifr_ifru.ifru_flags = unsafe { ifr.ifr_ifru.ifru_flags } | libc::IFF_UP as libc::c_short; - // SAFETY: `sock` is a valid socket fd; `ifr` has the interface name - // set and the updated flags written into the `ifru_flags` union - // variant. - if unsafe { libc::ioctl(sock, libc::SIOCSIFFLAGS as _, &ifr) } < 0 { - return Err(std::io::Error::last_os_error()); - } - - Ok(()) - })(); - - // SAFETY: `sock` is the fd from the `socket` call above; it has not - // escaped this function and is no longer used. - unsafe { libc::close(sock) }; - result -} - // ── NodeHandle ────────────────────────────────────────────────────────────── /// Upper bound on how long node teardown waits for the background Tokio @@ -206,14 +56,6 @@ pub struct NodeHandle { /// teardown is synchronous: once the join completes the runtime is gone /// and any TUN interface the node created has been removed. thread: Option>, - /// On Android the `tun` crate does not close the TUN file descriptor on - /// drop (it assumes the fd is owned by Android's `VpnService`). Mycelium - /// opens this fd itself via [`create_tun_fd`], so it owns it and must - /// close it during teardown — otherwise the kernel keeps the - /// non-persistent TUN interface alive. Closed in [`Drop`], after the - /// background runtime has been joined. - #[cfg(target_os = "android")] - tun_fd: Option, } impl NodeHandle { @@ -222,11 +64,6 @@ impl NodeHandle { /// Blocks the calling thread until the node is ready (or fails to start). /// The caller provides a fully constructed [`Config`]. pub fn start(config: Config) -> Result { - // On Android mycelium opens the TUN fd itself (see `create_tun_fd`), - // so the handle owns it and is responsible for closing it on drop. - #[cfg(target_os = "android")] - let tun_fd = config.tun_fd; - let (result_tx, result_rx) = std::sync::mpsc::sync_channel(1); let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>(); @@ -274,17 +111,8 @@ impl NodeHandle { Ok(Ok(pair)) => pair, other => { // The node failed to start. Wait for the background runtime - // to finish tearing down, then release the TUN fd we opened - // (on Android nothing else will close it). + // to finish tearing down before returning. let _ = thread.join(); - #[cfg(target_os = "android")] - if let Some(fd) = tun_fd { - if fd >= 0 { - // SAFETY: `fd` came from `create_tun_fd`; the runtime - // that used it has been joined, so it is unreferenced. - unsafe { libc::close(fd) }; - } - } return Err(match other { Ok(Err(e)) => e, _ => NodeError::ThreadPanic, @@ -297,8 +125,6 @@ impl NodeHandle { rt_handle, shutdown_tx: Some(shutdown_tx), thread: Some(thread), - #[cfg(target_os = "android")] - tun_fd, }) } @@ -349,20 +175,6 @@ impl Drop for NodeHandle { error!("node background thread panicked during shutdown: {e:?}"); } } - // On Android the `tun` crate does not close the TUN fd on drop (it - // assumes Android's `VpnService` owns it). Mycelium opened this fd - // itself via `create_tun_fd`, so it must close it here — after the - // join above guarantees the runtime, and the tun device using the - // fd, are gone. The interface is non-persistent, so closing the last - // fd makes the kernel remove it. - #[cfg(target_os = "android")] - if let Some(fd) = self.tun_fd.take() { - if fd >= 0 { - // SAFETY: `fd` came from `create_tun_fd`; the runtime that - // used it has been joined, so nothing else references it. - unsafe { libc::close(fd) }; - } - } } } diff --git a/mycelium/src/tun.rs b/mycelium/src/tun.rs index c4831f2c..5ccdbd1d 100644 --- a/mycelium/src/tun.rs +++ b/mycelium/src/tun.rs @@ -3,14 +3,16 @@ #[cfg(any( target_os = "linux", all(target_os = "macos", not(feature = "mactunfd")), - target_os = "windows" + target_os = "windows", + all(target_os = "android", not(feature = "androidtunfd")), ))] use crate::subnet::Subnet; #[cfg(any( target_os = "linux", all(target_os = "macos", not(feature = "mactunfd")), - target_os = "windows" + target_os = "windows", + all(target_os = "android", not(feature = "androidtunfd")), ))] pub struct TunConfig { pub name: String, @@ -19,17 +21,26 @@ pub struct TunConfig { } #[cfg(any( - target_os = "android", target_os = "ios", all(target_os = "macos", feature = "mactunfd"), + all(target_os = "android", feature = "androidtunfd"), ))] pub struct TunConfig { pub tun_fd: i32, } -#[cfg(target_os = "linux")] + +// Android without the `androidtunfd` feature uses the same Linux uAPI as +// `target_os = "linux"`, so it compiles `tun/linux.rs`. +#[cfg(any( + target_os = "linux", + all(target_os = "android", not(feature = "androidtunfd")), +))] mod linux; -#[cfg(target_os = "linux")] +#[cfg(any( + target_os = "linux", + all(target_os = "android", not(feature = "androidtunfd")), +))] pub use linux::new; #[cfg(all(target_os = "macos", not(feature = "mactunfd")))] @@ -43,9 +54,9 @@ mod windows; #[cfg(target_os = "windows")] pub use windows::new; -#[cfg(target_os = "android")] +#[cfg(all(target_os = "android", feature = "androidtunfd"))] mod android; -#[cfg(target_os = "android")] +#[cfg(all(target_os = "android", feature = "androidtunfd"))] pub use android::new; #[cfg(any(target_os = "ios", all(target_os = "macos", feature = "mactunfd")))]