From cf62cff5f44060d0d00a6b37047a700d612b3c47 Mon Sep 17 00:00:00 2001 From: Arjen <18398758+Origami74@users.noreply.github.com> Date: Tue, 14 Jul 2026 14:40:43 +0200 Subject: [PATCH] feat: Android-ready core: target_os gating and app-owned TUN seam Make the FIPS core build and run as an embedded Android library. The host app owns the TUN (e.g. an Android VpnService) and FIPS performs no system-TUN or CAP_NET_ADMIN operations. Squashed from the following changes: - gate desktop transports/TUN by target_os, not features: a plain `cargo build` now compiles for every target with no flags. Ethernet (raw AF_PACKET / BPF) is gated to linux/macos, so Android (target_os = "android", not "linux") self-excludes it as Windows already did; real system-TUN ops are gated per linux/macos and Android gets a no-op stub; the ipi6_ifindex cast handles it being i32 on Android vs u32 on macOS. No Cargo features are introduced; desktop builds are unchanged. - app-owned TUN seam: Node::enable_app_owned_tun() lets an embedder that owns the TUN fd exchange IPv6 packet bytes with FIPS over channels instead of FIPS creating a system TUN device. It returns (app_outbound_tx, app_inbound_rx): the embedder pushes packets read from its fd into the outbound sender (app -> mesh) and pulls packets destined for its fd from the inbound receiver (mesh -> app). start() gates system-TUN creation on tun_tx being unset, so with the channels pre-installed it skips device creation and does no system-TUN ops; both directions reuse the existing inbound-shim and run_rx_loop wiring. Packets entering via app_outbound_tx bypass handle_tun_packet, so the embedder must push only fd00::/8-destined packets and clamp TCP MSS on outbound SYNs; the rustdoc and the IPv6-adapter design doc spell this out. - keep the android target warning-clean so the cross-compile check passes clippy -D warnings. - add an Android cross-compile CI check: cross-compile the library for aarch64-linux-android via cargo-ndk and run clippy -D warnings. Android ships as an embedded library (the host app owns the TUN), so there is no daemon binary to package; this is a check job, not a packaging one. - docs: list Android as a supported platform. Tests: app_owned_tun_seam_wires_channels covers the channel round-trip and the Active state; start_skips_system_tun_when_app_owned runs start() and asserts no named system device is created. --- .github/workflows/ci.yml | 42 +++++++++++++++++++ README.md | 22 +++++----- docs/design/fips-ipv6-adapter.md | 28 +++++++++++++ src/node/dataplane/connected_udp.rs | 8 ++-- src/node/lifecycle/mod.rs | 5 ++- src/node/mod.rs | 40 +++++++++++++++--- src/node/tests/unit.rs | 63 +++++++++++++++++++++++++++++ src/transport/mod.rs | 44 ++++++++++---------- src/upper/dns.rs | 2 +- src/upper/tun.rs | 33 ++++++++++++++- 10 files changed, 241 insertions(+), 46 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0b33deb..fce01a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,6 +88,48 @@ jobs: ${{ runner.os }}-cargo- - run: cargo clippy --all-targets --all-features -- -D warnings + # ─────────────────────────────────────────────────────────────────────────── + # Android cross-check + # + # FIPS runs on Android as an embedded library — the host app owns the TUN + # (an Android VpnService), so there are no daemon binaries to package, unlike + # the desktop targets. This job only cross-compiles the library for the + # android target to guard the android-only cfg paths (and the `not(android)` + # exclusions) from silently bit-rotting; nothing else in CI compiles them. + # cargo-ndk wires the NDK toolchain, which is required even for a check + # because `ring` compiles C at build time. + # ─────────────────────────────────────────────────────────────────────────── + android-check: + name: Android cross-check (aarch64) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Install Rust toolchain (+ Android target) + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + target: aarch64-linux-android + components: clippy + cache: false + rustflags: '' + - name: Cache Cargo registry + build + uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-android-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + - name: Install cargo-ndk + uses: taiki-e/install-action@v2 + with: + tool: cargo-ndk + - name: Clippy the library for Android + run: | + export ANDROID_NDK_HOME="${ANDROID_NDK_HOME:-$ANDROID_NDK_LATEST_HOME}" + cargo ndk -t arm64-v8a clippy --lib -- -D warnings + build: name: Build (${{ matrix.os }}) runs-on: ${{ matrix.os }} diff --git a/README.md b/README.md index 5c4527b..71b5563 100644 --- a/README.md +++ b/README.md @@ -112,17 +112,19 @@ tutorial progression starting at cargo build --release ``` -Requires Rust 1.94.1+ (edition 2024). Linux, macOS, and Windows are -supported; transport availability varies by platform. +Requires Rust 1.94.1+ (edition 2024). Linux, macOS, and Windows run as +standalone daemons; Android is supported as an embedded library (the host +app owns the TUN, e.g. a `VpnService`). Transport availability varies by +platform. -| Transport | Linux | macOS | Windows | OpenWrt | -|-----------|:-----:|:-----:|:-------:|:-------:| -| UDP | ✅ | ✅ | ✅ | ✅ | -| TCP | ✅ | ✅ | ✅ | ✅ | -| Ethernet | ✅ | ✅ | ❌ | ✅ | -| Tor | ✅ | ✅ | ✅ | ✅ | -| Nym | ✅ | ✅ | ✅ | ❌ | -| BLE | ✅ | ❌ | ❌ | ❌ | +| Transport | Linux | macOS | Windows | Android | OpenWrt | +|-----------|:-----:|:-----:|:-------:|:-------:|:-------:| +| UDP | ✅ | ✅ | ✅ | ✅ | ✅ | +| TCP | ✅ | ✅ | ✅ | ✅ | ✅ | +| Ethernet | ✅ | ✅ | ❌ | ❌ | ✅ | +| Tor | ✅ | ✅ | ✅ | ❌ | ✅ | +| Nym | ✅ | ✅ | ✅ | ❌ | ❌ | +| BLE | ✅ | ❌ | ❌ | ❌ | ❌ | On Linux, a source build requires `libclang` — the LAN gateway's nftables bindings are generated by `bindgen` at build time, which diff --git a/docs/design/fips-ipv6-adapter.md b/docs/design/fips-ipv6-adapter.md index 383559a..06ad35a 100644 --- a/docs/design/fips-ipv6-adapter.md +++ b/docs/design/fips-ipv6-adapter.md @@ -302,6 +302,34 @@ alternative — running under a dedicated unprivileged service account with the capability granted on the binary — see [../how-to/run-as-unprivileged-user.md](../how-to/run-as-unprivileged-user.md). +### App-Owned TUN (embedded hosts) + +On platforms where FIPS is embedded rather than run as a daemon — notably +Android, where the `VpnService` owns the TUN fd and the app has no +`CAP_NET_ADMIN` — FIPS does not create `fips0` itself. Instead the embedder owns +the fd and exchanges IPv6 packet bytes with FIPS over channels. + +`Node::enable_app_owned_tun()` sets this up. It is called after `Node::new` and +before `start()` (and before the node is moved into a background task), mirroring +`control_read_handle()`, and returns two app-side channel ends: + +- **app → mesh** — the embedder pushes IPv6 packets read from its fd into + `app_outbound_tx`. These are drained by `run_rx_loop` into `handle_tun_outbound` + and routed exactly as the Reader Thread's output would be. +- **mesh → app** — inbound mesh traffic on port 256 is reconstructed and written + to the node's `tun_tx` (the same sink the Writer Thread reads); the embedder + pulls from `app_inbound_rx` and writes to its fd. + +With the channels installed, `start()` skips system-TUN creation (it gates on +`tun_tx` being unset), so FIPS does no `CAP_NET_ADMIN` operations. + +Because packets enter via `app_outbound_tx` rather than the Reader Thread, they +**bypass `handle_tun_packet`** — the `fd00::/8` destination filter, the ICMPv6 +Destination Unreachable for off-mesh dests (see [Reader Thread](#reader-thread)), +and the [TUN-Side TCP MSS Clamping](#tun-side-tcp-mss-clamping). The embedder is +therefore responsible for routing only `fd00::/8` to its TUN (so only mesh-bound +packets arrive) and for clamping TCP MSS on outbound SYNs. + ## Implementation Status | Feature | Status | diff --git a/src/node/dataplane/connected_udp.rs b/src/node/dataplane/connected_udp.rs index fc9d851..0bd2335 100644 --- a/src/node/dataplane/connected_udp.rs +++ b/src/node/dataplane/connected_udp.rs @@ -45,12 +45,10 @@ impl Node { /// (e.g. only non-UDP transports). Enabled on Linux and macOS: /// both kernels route a matching peer 5-tuple to the connected /// socket when it shares the wildcard listen port via SO_REUSEPORT. + /// Only compiled on Linux/macOS — the sole caller (the rx_loop tick) is + /// gated the same way, so on other targets (android) there is nothing to do. + #[cfg(any(target_os = "linux", target_os = "macos"))] pub(in crate::node) async fn activate_connected_udp_sessions(&mut self) { - #[cfg(not(any(target_os = "linux", target_os = "macos")))] - { - // No-op on platforms without the connected-UDP fast path. - } - #[cfg(any(target_os = "linux", target_os = "macos"))] { if !connected_udp_enabled() { return; diff --git a/src/node/lifecycle/mod.rs b/src/node/lifecycle/mod.rs index b3f35fb..a8b929d 100644 --- a/src/node/lifecycle/mod.rs +++ b/src/node/lifecycle/mod.rs @@ -1312,7 +1312,10 @@ impl Node { // Singleton child booleans, with today's exact enable conditions. let nostr = self.config().node.rendezvous.nostr.enabled; let mdns = self.config().node.rendezvous.lan.enabled; - let tun = self.config().tun.enabled; + // No Tun child when the TUN is app-owned (the embedder pre-set + // `tun_tx` via `enable_app_owned_tun`) — FIPS does no system-TUN ops; + // the channels installed before `start` carry both directions. + let tun = self.config().tun.enabled && self.supervisor.tun_tx.is_none(); let dns = self.config().dns.enabled; // Worker-pool booleans + counts. Unix only — the workers issue diff --git a/src/node/mod.rs b/src/node/mod.rs index fb939b8..c740378 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -50,7 +50,7 @@ use crate::proto::lookup::{Lookup, LookupBackoff, LookupForwardRateLimiter}; use crate::proto::mmp::Mmp; use crate::proto::routing::{self, Router, RoutingErrorRateLimiter}; use crate::proto::stp::TreeState; -#[cfg(unix)] +#[cfg(any(target_os = "linux", target_os = "macos"))] use crate::transport::ethernet::EthernetTransport; use crate::transport::nym::NymTransport; use crate::transport::tcp::TcpTransport; @@ -62,7 +62,7 @@ use crate::transport::{ }; use crate::upper::hosts::HostMap; use crate::upper::icmp_rate_limit::IcmpRateLimiter; -use crate::upper::tun::{TunError, TunState, TunTx}; +use crate::upper::tun::{TunError, TunOutboundTx, TunState, TunTx}; use crate::utils::index::IndexAllocator; use crate::{Config, ConfigError, Identity, IdentityError, NodeAddr, PeerIdentity, TreeCoordinate}; use rand::Rng; @@ -873,7 +873,7 @@ impl Node { } // Create Ethernet transport instances (Unix only — requires raw sockets) - #[cfg(unix)] + #[cfg(any(target_os = "linux", target_os = "macos"))] { let eth_instances: Vec<_> = self .config() @@ -1007,7 +1007,7 @@ impl Node { &self, addr_str: &str, ) -> Result<(TransportId, TransportAddr), NodeError> { - #[cfg(unix)] + #[cfg(any(target_os = "linux", target_os = "macos"))] { let (iface, mac_str) = addr_str.split_once('/').ok_or_else(|| { NodeError::NoTransportForType(format!( @@ -1039,7 +1039,7 @@ impl Node { Ok((transport_id, TransportAddr::from_bytes(&mac))) } - #[cfg(not(unix))] + #[cfg(not(any(target_os = "linux", target_os = "macos")))] { Err(NodeError::NoTransportForType( "Ethernet transport is not supported on this platform".to_string(), @@ -2691,6 +2691,36 @@ impl Node { self.supervisor.tun_tx.as_ref() } + /// Set up an **app-owned TUN**: rather than FIPS creating a system TUN + /// device, the embedder (e.g. an Android `VpnService`) owns the fd and + /// exchanges IPv6 packet bytes with FIPS over the returned channels. Call + /// this after [`Node::new`] and **before** [`Self::start`] — and before + /// moving the node into a background task. + /// + /// Returns `(app_outbound_tx, app_inbound_rx)`: + /// - push IPv6 packets read from the app's TUN fd into `app_outbound_tx` + /// (app → mesh); FIPS routes them to the destination node. + /// - pull IPv6 packets destined for the app's TUN fd from `app_inbound_rx` + /// (mesh → app) and write them to the fd (`recv_timeout` for clean stop). + /// + /// With this set, [`Self::start`] skips system-TUN creation (it gates on + /// `tun_tx` being unset). Packets pushed into `app_outbound_tx` bypass the + /// system-TUN reader's `handle_tun_packet`, so the embedder must do what that + /// path otherwise would: push only `fd::/8`-destined IPv6 packets — FIPS no + /// longer filters the destination or emits ICMPv6 unreachable for off-mesh + /// dests — and clamp TCP MSS on outbound SYNs. + pub fn enable_app_owned_tun(&mut self) -> (TunOutboundTx, std::sync::mpsc::Receiver>) { + let tun_channel_size = self.config().node.buffers.tun_channel; + // app → mesh: the app pushes; `run_rx_loop` drains `tun_outbound_rx`. + let (outbound_tx, outbound_rx) = tokio::sync::mpsc::channel(tun_channel_size); + // mesh → app: the node writes inbound packets to `tun_tx`; the app pulls. + let (tun_tx, tun_rx) = std::sync::mpsc::channel(); + self.supervisor.tun_tx = Some(tun_tx); + self.supervisor.tun_outbound_rx = Some(outbound_rx); + self.tun_state = TunState::Active; + (outbound_tx, tun_rx) + } + // === Sending === /// Encrypt and send a link-layer message to an authenticated peer. diff --git a/src/node/tests/unit.rs b/src/node/tests/unit.rs index 150a0de..39e7262 100644 --- a/src/node/tests/unit.rs +++ b/src/node/tests/unit.rs @@ -2137,3 +2137,66 @@ async fn handle_msg1_admits_existing_peer_at_cap() { "rate limiter must rebalance after the (bypass-admitted) handler returns" ); } + +/// App-owned TUN seam: `enable_app_owned_tun` wires the embedder's packet +/// channels (an Android `VpnService` owns the fd) and marks the TUN active so +/// `start()` skips system-TUN creation. +#[test] +fn app_owned_tun_seam_wires_channels() { + let mut config = crate::Config::new(); + config.tun.enabled = true; + let mut node = make_node_with(config); + + let (outbound_tx, tun_rx) = node.enable_app_owned_tun(); + + // TUN is active and the inbound (mesh→app) sender is installed, so `start()` + // will skip `TunDevice::create` (it gates on `tun_tx.is_none()`). + assert_eq!(node.tun_state(), crate::upper::tun::TunState::Active); + assert!(node.tun_tx().is_some(), "inbound sender installed"); + + // mesh → app: a packet the node delivers to its `tun_tx` reaches the app's rx. + let pkt = vec![0x60u8, 0, 0, 0, 0, 0]; + node.tun_tx().unwrap().send(pkt.clone()).unwrap(); + assert_eq!( + tun_rx + .recv_timeout(std::time::Duration::from_millis(200)) + .unwrap(), + pkt, + "the app pulls the same bytes the node wrote", + ); + + // app → mesh: the returned sender is live (its matching rx is held by the node + // and drained by `run_rx_loop` → `handle_tun_outbound`). + assert!(outbound_tx.try_send(vec![0x60]).is_ok()); +} + +/// With an app-owned TUN configured, `start()` must NOT create a system TUN +/// device: it leaves `tun_name` unset (a real device records its interface name) +/// and keeps the TUN `Active` with the app-owned channels. +#[tokio::test] +async fn start_skips_system_tun_when_app_owned() { + // Mirror `make_healthy_node` (one loopback UDP transport so bring-up + // reaches `Full`), plus tun.enabled so the Tun child WOULD spawn if the + // app-owned gate failed. + let mut config = crate::Config::new(); + config.transports.udp = crate::config::TransportInstances::Single(crate::config::UdpConfig { + bind_addr: Some("127.0.0.1:0".to_string()), + ..Default::default() + }); + config.dns.enabled = false; + config.tun.enabled = true; + let mut node = make_node_with(config); + + let (_outbound_tx, _tun_rx) = node.enable_app_owned_tun(); + node.start().await.unwrap(); + + // No system device was created (that path records the interface name); the + // app-owned TUN stayed active. + assert!( + node.tun_name().is_none(), + "app-owned TUN must not create a named system device", + ); + assert_eq!(node.tun_state(), crate::upper::tun::TunState::Active); + + node.stop().await.unwrap(); +} diff --git a/src/transport/mod.rs b/src/transport/mod.rs index 31df5f1..b78d32b 100644 --- a/src/transport/mod.rs +++ b/src/transport/mod.rs @@ -12,7 +12,7 @@ pub mod tcp; pub mod tor; pub mod udp; -#[cfg(unix)] +#[cfg(any(target_os = "linux", target_os = "macos"))] pub mod ethernet; #[cfg(target_os = "linux")] @@ -20,7 +20,7 @@ pub mod ble; #[cfg(target_os = "linux")] use ble::DefaultBleTransport; -#[cfg(unix)] +#[cfg(any(target_os = "linux", target_os = "macos"))] use ethernet::EthernetTransport; #[cfg(test)] use loopback::LoopbackTransport; @@ -664,7 +664,7 @@ pub enum TransportHandle { /// UDP/IP transport. Udp(UdpTransport), /// Raw Ethernet transport. - #[cfg(unix)] + #[cfg(any(target_os = "linux", target_os = "macos"))] Ethernet(EthernetTransport), /// TCP/IP transport. Tcp(TcpTransport), @@ -685,7 +685,7 @@ impl TransportHandle { pub async fn start(&mut self) -> Result<(), TransportError> { match self { TransportHandle::Udp(t) => t.start_async().await, - #[cfg(unix)] + #[cfg(any(target_os = "linux", target_os = "macos"))] TransportHandle::Ethernet(t) => t.start_async().await, TransportHandle::Tcp(t) => t.start_async().await, TransportHandle::Tor(t) => t.start_async().await, @@ -701,7 +701,7 @@ impl TransportHandle { pub async fn stop(&mut self) -> Result<(), TransportError> { match self { TransportHandle::Udp(t) => t.stop_async().await, - #[cfg(unix)] + #[cfg(any(target_os = "linux", target_os = "macos"))] TransportHandle::Ethernet(t) => t.stop_async().await, TransportHandle::Tcp(t) => t.stop_async().await, TransportHandle::Tor(t) => t.stop_async().await, @@ -717,7 +717,7 @@ impl TransportHandle { pub async fn send(&self, addr: &TransportAddr, data: &[u8]) -> Result { match self { TransportHandle::Udp(t) => t.send_async(addr, data).await, - #[cfg(unix)] + #[cfg(any(target_os = "linux", target_os = "macos"))] TransportHandle::Ethernet(t) => t.send_async(addr, data).await, TransportHandle::Tcp(t) => t.send_async(addr, data).await, TransportHandle::Tor(t) => t.send_async(addr, data).await, @@ -733,7 +733,7 @@ impl TransportHandle { pub fn transport_id(&self) -> TransportId { match self { TransportHandle::Udp(t) => t.transport_id(), - #[cfg(unix)] + #[cfg(any(target_os = "linux", target_os = "macos"))] TransportHandle::Ethernet(t) => t.transport_id(), TransportHandle::Tcp(t) => t.transport_id(), TransportHandle::Tor(t) => t.transport_id(), @@ -749,7 +749,7 @@ impl TransportHandle { pub fn name(&self) -> Option<&str> { match self { TransportHandle::Udp(t) => t.name(), - #[cfg(unix)] + #[cfg(any(target_os = "linux", target_os = "macos"))] TransportHandle::Ethernet(t) => t.name(), TransportHandle::Tcp(t) => t.name(), TransportHandle::Tor(t) => t.name(), @@ -765,7 +765,7 @@ impl TransportHandle { pub fn transport_type(&self) -> &TransportType { match self { TransportHandle::Udp(t) => t.transport_type(), - #[cfg(unix)] + #[cfg(any(target_os = "linux", target_os = "macos"))] TransportHandle::Ethernet(t) => t.transport_type(), TransportHandle::Tcp(t) => t.transport_type(), TransportHandle::Tor(t) => t.transport_type(), @@ -781,7 +781,7 @@ impl TransportHandle { pub fn state(&self) -> TransportState { match self { TransportHandle::Udp(t) => t.state(), - #[cfg(unix)] + #[cfg(any(target_os = "linux", target_os = "macos"))] TransportHandle::Ethernet(t) => t.state(), TransportHandle::Tcp(t) => t.state(), TransportHandle::Tor(t) => t.state(), @@ -797,7 +797,7 @@ impl TransportHandle { pub fn mtu(&self) -> u16 { match self { TransportHandle::Udp(t) => t.mtu(), - #[cfg(unix)] + #[cfg(any(target_os = "linux", target_os = "macos"))] TransportHandle::Ethernet(t) => t.mtu(), TransportHandle::Tcp(t) => t.mtu(), TransportHandle::Tor(t) => t.mtu(), @@ -816,7 +816,7 @@ impl TransportHandle { pub fn link_mtu(&self, addr: &TransportAddr) -> u16 { match self { TransportHandle::Udp(t) => t.link_mtu(addr), - #[cfg(unix)] + #[cfg(any(target_os = "linux", target_os = "macos"))] TransportHandle::Ethernet(t) => t.link_mtu(addr), TransportHandle::Tcp(t) => t.link_mtu(addr), TransportHandle::Tor(t) => t.link_mtu(addr), @@ -832,7 +832,7 @@ impl TransportHandle { pub fn local_addr(&self) -> Option { match self { TransportHandle::Udp(t) => t.local_addr(), - #[cfg(unix)] + #[cfg(any(target_os = "linux", target_os = "macos"))] TransportHandle::Ethernet(_) => None, TransportHandle::Tcp(t) => t.local_addr(), TransportHandle::Tor(_) => None, @@ -848,7 +848,7 @@ impl TransportHandle { pub fn interface_name(&self) -> Option<&str> { match self { TransportHandle::Udp(_) => None, - #[cfg(unix)] + #[cfg(any(target_os = "linux", target_os = "macos"))] TransportHandle::Ethernet(t) => Some(t.interface_name()), TransportHandle::Tcp(_) => None, TransportHandle::Tor(_) => None, @@ -888,7 +888,7 @@ impl TransportHandle { pub fn discover(&self) -> Result, TransportError> { match self { TransportHandle::Udp(t) => t.discover(), - #[cfg(unix)] + #[cfg(any(target_os = "linux", target_os = "macos"))] TransportHandle::Ethernet(t) => t.discover(), TransportHandle::Tcp(t) => t.discover(), TransportHandle::Tor(t) => t.discover(), @@ -904,7 +904,7 @@ impl TransportHandle { pub fn auto_connect(&self) -> bool { match self { TransportHandle::Udp(t) => t.auto_connect(), - #[cfg(unix)] + #[cfg(any(target_os = "linux", target_os = "macos"))] TransportHandle::Ethernet(t) => t.auto_connect(), TransportHandle::Tcp(t) => t.auto_connect(), TransportHandle::Tor(t) => t.auto_connect(), @@ -920,7 +920,7 @@ impl TransportHandle { pub fn accept_connections(&self) -> bool { match self { TransportHandle::Udp(t) => t.accept_connections(), - #[cfg(unix)] + #[cfg(any(target_os = "linux", target_os = "macos"))] TransportHandle::Ethernet(t) => t.accept_connections(), TransportHandle::Tcp(t) => t.accept_connections(), TransportHandle::Tor(t) => t.accept_connections(), @@ -942,7 +942,7 @@ impl TransportHandle { pub async fn connect(&self, addr: &TransportAddr) -> Result<(), TransportError> { match self { TransportHandle::Udp(_) => Ok(()), // connectionless - #[cfg(unix)] + #[cfg(any(target_os = "linux", target_os = "macos"))] TransportHandle::Ethernet(_) => Ok(()), // connectionless TransportHandle::Tcp(t) => t.connect_async(addr).await, TransportHandle::Tor(t) => t.connect_async(addr).await, @@ -962,7 +962,7 @@ impl TransportHandle { pub fn connection_state(&self, addr: &TransportAddr) -> ConnectionState { match self { TransportHandle::Udp(_) => ConnectionState::Connected, - #[cfg(unix)] + #[cfg(any(target_os = "linux", target_os = "macos"))] TransportHandle::Ethernet(_) => ConnectionState::Connected, TransportHandle::Tcp(t) => t.connection_state_sync(addr), TransportHandle::Tor(t) => t.connection_state_sync(addr), @@ -981,7 +981,7 @@ impl TransportHandle { pub async fn close_connection(&self, addr: &TransportAddr) { match self { TransportHandle::Udp(t) => t.close_connection(addr), - #[cfg(unix)] + #[cfg(any(target_os = "linux", target_os = "macos"))] TransportHandle::Ethernet(t) => t.close_connection(addr), TransportHandle::Tcp(t) => t.close_connection_async(addr).await, TransportHandle::Tor(t) => t.close_connection_async(addr).await, @@ -1006,7 +1006,7 @@ impl TransportHandle { pub fn congestion(&self) -> TransportCongestion { match self { TransportHandle::Udp(t) => t.congestion(), - #[cfg(unix)] + #[cfg(any(target_os = "linux", target_os = "macos"))] TransportHandle::Ethernet(_) => TransportCongestion::default(), TransportHandle::Tcp(_) => TransportCongestion::default(), TransportHandle::Tor(_) => TransportCongestion::default(), @@ -1026,7 +1026,7 @@ impl TransportHandle { TransportHandle::Udp(t) => { serde_json::to_value(t.stats().snapshot()).unwrap_or_default() } - #[cfg(unix)] + #[cfg(any(target_os = "linux", target_os = "macos"))] TransportHandle::Ethernet(t) => { serde_json::to_value(t.stats().snapshot()).unwrap_or_default() } diff --git a/src/upper/dns.rs b/src/upper/dns.rs index e29d866..2fca5e7 100644 --- a/src/upper/dns.rs +++ b/src/upper/dns.rs @@ -301,7 +301,7 @@ fn extract_pktinfo_ifindex(msg: &libc::msghdr) -> Option { if cmsg.cmsg_level == libc::IPPROTO_IPV6 && cmsg.cmsg_type == libc::IPV6_PKTINFO { let data_ptr = unsafe { libc::CMSG_DATA(cmsg_ptr) } as *const libc::in6_pktinfo; let pktinfo: libc::in6_pktinfo = unsafe { std::ptr::read_unaligned(data_ptr) }; - return Some(pktinfo.ipi6_ifindex); + return Some(pktinfo.ipi6_ifindex as u32); } cmsg_ptr = unsafe { libc::CMSG_NXTHDR(msg, cmsg_ptr) }; } diff --git a/src/upper/tun.rs b/src/upper/tun.rs index bc302b8..3ad0f0e 100644 --- a/src/upper/tun.rs +++ b/src/upper/tun.rs @@ -31,7 +31,7 @@ use tracing::error; use tracing::{debug, trace}; #[cfg(windows)] use tracing::{error, warn}; -#[cfg(unix)] +#[cfg(any(target_os = "linux", target_os = "macos"))] use tun::Layer; /// Read-only handle to the per-destination path MTU map. Populated by @@ -234,7 +234,10 @@ impl TunDevice { } } - // Create the TUN device + // Create the TUN device. `mut` is only exercised on linux/macos, where + // the name/layer/mtu are set below; other unix targets (android) pass + // the default config through unchanged. + #[cfg_attr(not(any(target_os = "linux", target_os = "macos")), allow(unused_mut))] let mut tun_config = tun::Configuration::default(); // On macOS, utun devices get kernel-assigned names (utun0, utun1, ...), @@ -1224,6 +1227,32 @@ mod windows_tun { #[cfg(windows)] pub use windows_tun::{TunDevice, TunWriter, run_tun_reader, shutdown_tun_interface}; +// Android uses an app-owned TUN (the embedder owns the fd, e.g. an Android +// VpnService); FIPS never creates or configures a system TUN here. These no-op +// stubs stand in for the platform ops so the shared TunDevice code compiles. +#[cfg(target_os = "android")] +mod platform { + use super::TunError; + use std::net::Ipv6Addr; + + pub fn is_ipv6_disabled() -> bool { + false + } + pub async fn interface_exists(_name: &str) -> bool { + false + } + pub async fn delete_interface(_name: &str) -> Result<(), TunError> { + Ok(()) + } + pub async fn configure_interface( + _name: &str, + _addr: Ipv6Addr, + _mtu: u16, + ) -> Result<(), TunError> { + Ok(()) + } +} + #[cfg(target_os = "linux")] mod platform { use super::TunError;