diff --git a/src/node/lifecycle.rs b/src/node/lifecycle.rs index 08d8c73..8c07145 100644 --- a/src/node/lifecycle.rs +++ b/src/node/lifecycle.rs @@ -1242,6 +1242,12 @@ impl Node { match handle.start().await { Ok(()) => { + #[cfg(unix)] + if transport_type == "udp" + && let (Some(tx), Some(fd)) = (&self.udp_bind_tx, handle.raw_fd()) + { + let _ = tx.send(fd); + } self.transports.insert(transport_id, handle); } Err(e) => { diff --git a/src/node/mod.rs b/src/node/mod.rs index 04ec487..340fdcb 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -440,6 +440,13 @@ pub struct Node { /// DNS responder task handle. dns_task: Option>, + // === App-owned UDP socket binding === + /// Fires once with the UDP transport's raw fd right after it starts, so an + /// embedder can apply host-specific socket options (e.g. pinning it to one + /// of several OS networks). See [`Self::enable_app_owned_udp_fd`]. + #[cfg(unix)] + udp_bind_tx: Option>, + // === Index-Based Session Dispatch === /// Allocator for session indices. index_allocator: IndexAllocator, @@ -699,6 +706,8 @@ impl Node { tun_shutdown_fd: None, dns_identity_rx: None, dns_task: None, + #[cfg(unix)] + udp_bind_tx: None, index_allocator: IndexAllocator::new(), peers_by_index: HashMap::new(), pending_outbound: HashMap::new(), @@ -860,6 +869,8 @@ impl Node { tun_shutdown_fd: None, dns_identity_rx: None, dns_task: None, + #[cfg(unix)] + udp_bind_tx: None, index_allocator: IndexAllocator::new(), peers_by_index: HashMap::new(), pending_outbound: HashMap::new(), @@ -2930,6 +2941,34 @@ impl Node { identity_tx } + /// Set up an **app-owned UDP socket binding**: the returned receiver gets + /// the UDP transport's raw socket fd once, right after the transport opens + /// inside [`Self::start`], so an embedder can apply a socket option FIPS + /// itself has no way to choose — in particular pinning the socket to one + /// of several networks the host OS offers. + /// + /// FIPS opens a single wildcard UDP socket and picks the egress path per + /// destination address, which assumes the OS routes by destination alone. + /// Some hosts don't: where the OS binds each socket to one "network" and + /// steers replies by that association, a peer reachable only over a + /// secondary network (a link-local address on an interface that is not + /// the default route) can receive our handshake and have its reply + /// discarded before it reaches the socket. The fd is the only handle that + /// lets the embedder correct this, and it is otherwise private to the + /// transport. + /// + /// Call after [`Node::new`] and before [`Self::start`], like + /// [`Self::enable_app_owned_tun`]. Nothing is ever sent if no UDP + /// transport is configured or it fails to start. + #[cfg(unix)] + pub fn enable_app_owned_udp_fd( + &mut self, + ) -> std::sync::mpsc::Receiver { + let (tx, rx) = std::sync::mpsc::channel(); + self.udp_bind_tx = Some(tx); + rx + } + // === Sending === /// Encrypt and send a link-layer message to an authenticated peer. diff --git a/src/transport/mod.rs b/src/transport/mod.rs index b5da1b0..2b0ea92 100644 --- a/src/transport/mod.rs +++ b/src/transport/mod.rs @@ -1095,6 +1095,24 @@ impl TransportHandle { } } + /// Raw fd of the bound socket (UDP only, returns None for other + /// transports). See [`crate::transport::udp::UdpTransport::raw_fd`]. + #[cfg(unix)] + pub fn raw_fd(&self) -> Option { + match self { + TransportHandle::Udp(t) => t.raw_fd(), + #[cfg(any(target_os = "linux", target_os = "macos"))] + TransportHandle::Ethernet(_) => None, + TransportHandle::Tcp(_) => None, + TransportHandle::Tor(_) => None, + TransportHandle::Nym(_) => None, + #[cfg(ble_available)] + TransportHandle::Ble(_) => None, + #[cfg(test)] + TransportHandle::Loopback(_) => None, + } + } + /// Get the interface name (Ethernet only, returns None for other transports). pub fn interface_name(&self) -> Option<&str> { match self { diff --git a/src/transport/udp/mod.rs b/src/transport/udp/mod.rs index 3728fb9..7a500c1 100644 --- a/src/transport/udp/mod.rs +++ b/src/transport/udp/mod.rs @@ -89,6 +89,17 @@ impl UdpTransport { self.local_addr } + /// Raw fd of the bound socket, so an embedder can apply socket options + /// this transport does not manage itself — notably binding it to one of + /// several networks a host OS offers, when destination-based routing alone + /// does not reach a peer (see [`crate::Node::enable_app_owned_udp_fd`]). + /// `None` before start. + #[cfg(unix)] + pub fn raw_fd(&self) -> Option { + use std::os::unix::io::AsRawFd; + self.socket.as_ref().map(|s| s.as_raw_fd()) + } + /// Configured recv buffer size — used when opening per-peer /// `ConnectedPeerSocket`s so they get the same buffer ceiling as /// the wildcard listen socket.