mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
feat(node): expose the UDP transport fd for app-owned socket binding
The UDP transport opens one wildcard socket and selects the egress path per destination address. That assumes the host routes by destination alone — true on an ordinary Unix box, not true everywhere. On hosts that associate each socket with one "network" and steer traffic by that association, a peer reachable only over a secondary network is unreachable in a way FIPS cannot see or fix: the address is well-formed, the send succeeds, the peer receives our handshake and replies, and the reply is discarded by the host before it reaches our socket. The link just retries msg1 forever. Nothing in FIPS can correct that, because the correction is a socket option on a socket the transport keeps private. So hand the embedder the fd: let rx = node.enable_app_owned_udp_fd(); // before start() node.start().await?; let fd = rx.recv()?; // once the transport is up This follows the existing `enable_app_owned_tun` / `enable_app_owned_dns` contract — call after `Node::new` and before `start()`, get a channel back — and stays deliberately narrow: FIPS keeps owning the socket, and the fd carries no promise beyond "this is the transport's socket, now open". If no UDP transport is configured, or it fails to start, nothing is ever sent. Also plumbs `raw_fd()` through `TransportHandle` and `UdpTransport`. Unix only, since `RawFd` is a unix concept; non-UDP transports return None.
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -440,6 +440,13 @@ pub struct Node {
|
||||
/// DNS responder task handle.
|
||||
dns_task: Option<tokio::task::JoinHandle<()>>,
|
||||
|
||||
// === 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<std::sync::mpsc::Sender<std::os::unix::io::RawFd>>,
|
||||
|
||||
// === 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<std::os::unix::io::RawFd> {
|
||||
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.
|
||||
|
||||
@@ -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<std::os::unix::io::RawFd> {
|
||||
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 {
|
||||
|
||||
@@ -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<std::os::unix::io::RawFd> {
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user