From 1eb0e8a34e4b19760f5de9eee7a093fca2d4447d Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Thu, 24 Sep 2026 14:45:51 +0000 Subject: [PATCH] Move the TUN and DNS child start and stop bodies into ipv6tun Bringing the TUN device and the .fips DNS responder up and taking them down is host-side work, but the bodies sat inline in the node's supervisor arms, and their handles were eight loose fields on the supervisor. Move the bodies to ipv6tun::lifecycle and gather the handles into one Handles struct there, held by the supervisor. The supervisor arms, their order and the child-exit reporting are unchanged; each arm now calls into ipv6tun. The TUN start is two calls so the node can refresh its MSS ceiling between them, exactly where it did before: open_tun creates and logs the device, then spawn_tun creates the macOS/FreeBSD shutdown pipe and starts the writer and reader threads. A failure to create the device still continues without a TUN, and a pipe or writer failure still fails the node's start. stop_tun and stop_dns carry the teardown unchanged, including the shutdown-pipe write that wakes the reader on macOS and FreeBSD. The TUN device name moves into Handles as well, so the teardown up-set can ask ipv6tun whether each child is up. A TUN counts as up when it has a device name, not when it has a sender, so an app-owned TUN still produces no TUN teardown; DNS counts as up while its task handle exists. Node::tun_name, tun_tx, dns_local_addr and enable_app_owned_tun keep their behaviour and now read or write the handles. Node::mesh_ifindex had no caller left outside a test and is replaced by the same method on Handles. Tests install a TUN sender through a test-only Node::install_tun. The moved log lines now log under fips::ipv6tun::lifecycle instead of fips::node::lifecycle. Add that target to the NAT harness and its trace overlay, and to the harnesses that relied on fips::node=debug, and note the rename in the changelog. --- CHANGELOG.md | 10 +- src/ipv6tun/lifecycle.rs | 402 +++++++++++++++++++++++ src/ipv6tun/mod.rs | 1 + src/node/dataplane/rx_loop.rs | 6 +- src/node/handlers/session.rs | 4 +- src/node/lifecycle/mod.rs | 316 ++---------------- src/node/lifecycle/supervisor.rs | 41 +-- src/node/mod.rs | 25 +- src/node/tests/discovery.rs | 2 +- src/node/tests/session.rs | 38 +-- src/node/tests/unit.rs | 7 +- testing/acl-allowlist/docker-compose.yml | 2 +- testing/firewall/docker-compose.yml | 2 +- testing/iface-binding/docker-compose.yml | 2 +- testing/mesh-lab/README.md | 3 +- testing/mesh-lab/compose-trace-nat.yml | 5 +- testing/mesh-lab/run-loop.sh | 7 +- testing/nat/docker-compose.yml | 10 +- 18 files changed, 509 insertions(+), 374 deletions(-) create mode 100644 src/ipv6tun/lifecycle.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 95989d04..3f2ec425 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -336,9 +336,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ICMPv6 Packet Too Big debug lines ("Sending ICMP Packet Too Big", "Rate limiting ICMP Packet Too Big") log as `fips::ipv6tun::icmp` rather than `fips::node::handlers::session`, so a `fips::node=debug` filter no longer - shows them. An existing `RUST_LOG` filter naming an old target still parses - and simply stops matching, so the symptom is missing log lines rather than an - error. + shows them. The TUN and DNS start and stop lines ("TUN device active", + "effective MTU", "max TCP MSS", "Shutting down TUN interface", "DNS responder + started", "DNS responder stopped" and their failure warnings) log as + `fips::ipv6tun::lifecycle` rather than `fips::node::lifecycle`, so a filter + on `fips::node::lifecycle` or `fips::node` no longer selects them. An + existing `RUST_LOG` filter naming an old target still parses and simply + stops matching, so the symptom is missing log lines rather than an error. Update `RUST_LOG` filters, journal-watch recipes and any log-scraping alert accordingly. The library path `fips::upper` still resolves. diff --git a/src/ipv6tun/lifecycle.rs b/src/ipv6tun/lifecycle.rs new file mode 100644 index 00000000..9ff225b3 --- /dev/null +++ b/src/ipv6tun/lifecycle.rs @@ -0,0 +1,402 @@ +//! Start and stop of the TUN and DNS children. +//! +//! The node's supervisor decides when each child starts and stops, and in +//! what order. The bodies that bring the TUN device and the `.fips` DNS +//! responder up and take them down live here, together with the handles +//! they leave behind for the node to drive and, later, to tear down. + +use std::net::{IpAddr, SocketAddr}; +use std::path::PathBuf; +use std::thread::{self, JoinHandle}; + +use tokio::sync::mpsc::Sender; +use tracing::{debug, info, warn}; + +use super::config::{DnsConfig, TunConfig}; +use super::dns::{DnsIdentityRx, bind_dns_socket, lookup_mesh_ifindex, run_dns_responder}; +use super::tun::{ + MssCeiling, TunDevice, TunError, TunOutboundRx, TunTx, run_tun_reader, shutdown_tun_interface, +}; +use crate::FipsAddress; +use crate::config::PeerConfig; +use crate::hosts::{DEFAULT_HOSTS_PATH, HostMap, HostMapReloader}; +use crate::node::lifecycle::supervisor::Child; +use crate::node::lifecycle::{report_exit, report_thread}; +use crate::node::path_mtu::PathMtuLookup; + +/// Runtime handles of the TUN and DNS children, held by the node's +/// supervisor. +/// +/// Every field is empty until its child starts. The TUN channels are the +/// exception: an embedder that owns the TUN installs them before start +/// through `Node::enable_app_owned_tun`, without a device name. +#[derive(Default)] +pub(crate) struct Handles { + /// Kernel name of the TUN device this node created, used to delete or + /// down it at teardown. Set only while a system TUN is up; an app-owned + /// TUN leaves it unset. + pub(crate) tun_name: Option, + /// TUN packet sender channel. + pub(crate) tun_tx: Option, + /// Receiver for outbound packets from the TUN reader. + pub(crate) tun_outbound_rx: Option, + /// TUN reader thread handle. + pub(crate) tun_reader_handle: Option>, + /// TUN writer thread handle. + pub(crate) tun_writer_handle: Option>, + /// Shutdown pipe: writing to this fd unblocks the TUN reader thread on + /// macOS and FreeBSD. On Linux, deleting the interface via netlink + /// serves the same purpose. + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + pub(crate) tun_shutdown_fd: Option, + + /// Receiver for resolved identities from the DNS responder. + pub(crate) dns_identity_rx: Option, + /// DNS responder task handle. + pub(crate) dns_task: Option>, + /// Address the DNS responder actually bound, read back from the socket + /// after `bind` so a port-0 config resolves to the assigned port. `Some` + /// only while the responder is up; published to embedders through + /// [`Node::dns_local_addr`](crate::Node::dns_local_addr). + pub(crate) dns_local_addr: Option, +} + +/// What the TUN threads take from the node when they start. +pub(crate) struct TunThreads { + /// Shared TCP MSS ceiling, already refreshed by the node. Both threads + /// read it per packet from then on. + pub(crate) ceiling: MssCeiling, + /// The node's effective IPv6 MTU at start, logged with the ceiling. + pub(crate) effective: u16, + /// Per-destination path MTU, read by the clamp in both threads. + pub(crate) path_mtu: PathMtuLookup, + /// Capacity of the host-to-mesh channel (`node.buffers.tun_channel`). + pub(crate) channel: usize, + /// Sender each TUN thread reports `Child::Tun` on when it exits. + pub(crate) exit_tx: Option>, +} + +/// Create the TUN device, logging it, or log the failure and return `None`. +/// +/// A failure here is not fatal to the node: it continues without a TUN. +pub(crate) async fn open_tun(config: &TunConfig, address: FipsAddress) -> Option { + match TunDevice::create(config, address).await { + Ok(device) => { + info!("TUN device active:"); + info!(" name: {}", device.name()); + info!(" address: {}", device.address()); + info!(" mtu: {}", device.mtu()); + Some(device) + } + Err(e) => { + warn!(error = %e, "Failed to initialize TUN, continuing without it"); + None + } + } +} + +impl Handles { + /// Whether the TUN child is up. + /// + /// Follows the device name, not the TUN sender: an app-owned TUN + /// installs the sender but creates no device, so there is no TUN child + /// to tear down. + pub(crate) fn tun_up(&self) -> bool { + self.tun_name.is_some() + } + + /// Whether the DNS child is up: its responder task handle exists. + pub(crate) fn dns_up(&self) -> bool { + self.dns_task.is_some() + } + + /// Resolve the index of the mesh TUN device this node actually created. + /// + /// Reads the device name recorded when the TUN was brought up, which is + /// the kernel's name rather than the configured one. Returns `None` when + /// no TUN is up, which disables the DNS responder's mesh-interface + /// filter: with no mesh interface there is no mesh exposure to defend. + /// An app-owned TUN also leaves the name unset, so the filter stays off + /// there even though a mesh interface exists. + pub(crate) fn mesh_ifindex(&self) -> Option { + self.tun_name.as_deref().and_then(lookup_mesh_ifindex) + } + + /// Start the TUN reader and writer threads on an opened device and keep + /// their handles. + /// + /// An error here (the macOS/FreeBSD shutdown pipe, or duplicating the + /// device fd for the writer) is fatal to the node's start, unlike a + /// failure to create the device. + pub(crate) fn spawn_tun( + &mut self, + device: TunDevice, + threads: TunThreads, + ) -> Result<(), TunError> { + let TunThreads { + ceiling: max_mss, + effective: effective_mtu, + path_mtu: path_mtu_lookup, + channel: tun_channel_size, + exit_tx, + } = threads; + let mtu = device.mtu(); + let name = device.name().to_string(); + let our_addr = *device.address(); + + info!("effective MTU: {} bytes", effective_mtu); + debug!( + " max TCP MSS: {} bytes", + max_mss.load(std::sync::atomic::Ordering::Relaxed) + ); + + // On macOS and FreeBSD, create a shutdown pipe. Writing to it + // unblocks the reader thread's select() loop without closing + // the TUN fd (which would cause a double-close when TunDevice + // drops). Linux instead unblocks the reader by deleting the + // interface; on macOS/FreeBSD downing the interface does not + // wake a blocked read. + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + let (shutdown_read_fd, shutdown_write_fd) = { + let mut fds = [0i32; 2]; + if unsafe { libc::pipe(fds.as_mut_ptr()) } < 0 { + return Err(TunError::Configure("failed to create shutdown pipe".into())); + } + (fds[0], fds[1]) + }; + + // Create writer (dups the fd for independent write access). + // Pass path_mtu_lookup so inbound SYN-ACK clamp can read + // per-destination path MTU learned via discovery. + let (writer, tun_tx) = device.create_writer(max_mss.clone(), path_mtu_lookup.clone())?; + + // Spawn writer thread. On exit, including a panic, + // it self-reports `Child::Tun` (sync context → + // `blocking_send`); TUN is one compound child, so + // both threads reporting is fine (the FSM de-dups + // via `up.remove`). + let writer_child_tx = exit_tx.clone(); + let writer_handle = thread::spawn(move || { + report_thread(Child::Tun, move || writer.run(), writer_child_tx.as_ref()); + }); + + // Clone tun_tx for the reader + let reader_tun_tx = tun_tx.clone(); + + // Create outbound channel for TUN reader → Node + let (outbound_tx, outbound_rx) = tokio::sync::mpsc::channel(tun_channel_size); + + // Spawn reader thread. Like the writer, it + // self-reports `Child::Tun` on exit or panic (sync + // context → `blocking_send`). Exactly one cfg + // variant compiles, so the exit sender is moved + // into that closure. + let reader_child_tx = exit_tx; + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + let reader_handle = thread::spawn(move || { + report_thread( + Child::Tun, + move || { + run_tun_reader( + device, + mtu, + our_addr, + reader_tun_tx, + outbound_tx, + max_mss, + path_mtu_lookup, + shutdown_read_fd, + ) + }, + reader_child_tx.as_ref(), + ); + }); + #[cfg(not(any(target_os = "macos", target_os = "freebsd")))] + let reader_handle = thread::spawn(move || { + report_thread( + Child::Tun, + move || { + run_tun_reader( + device, + mtu, + our_addr, + reader_tun_tx, + outbound_tx, + max_mss, + path_mtu_lookup, + ) + }, + reader_child_tx.as_ref(), + ); + }); + + self.tun_name = Some(name); + self.tun_tx = Some(tun_tx); + self.tun_outbound_rx = Some(outbound_rx); + self.tun_reader_handle = Some(reader_handle); + self.tun_writer_handle = Some(writer_handle); + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + { + self.tun_shutdown_fd = Some(shutdown_write_fd); + } + Ok(()) + } + + /// Stop the TUN child: close the writer's channel, delete or down the + /// interface, wake the reader, and join both threads. + /// + /// Returns whether there was a TUN child to stop. With no device name + /// (never started, or app-owned) it does nothing, and an app-owned + /// sender is left in place. + pub(crate) async fn stop_tun(&mut self) -> bool { + let Some(name) = self.tun_name.take() else { + return false; + }; + info!(name = %name, "Shutting down TUN interface"); + + // Drop the tun_tx to signal the writer to stop + self.tun_tx.take(); + + // Delete the interface (on Linux, causes reader to get + // EFAULT; on macOS/FreeBSD this downs it — the kernel + // destroys the device once the reader closes the fd). + if let Err(e) = shutdown_tun_interface(&name).await { + warn!(name = %name, error = %e, "Failed to shutdown TUN interface"); + } + + // On macOS and FreeBSD, signal the reader thread to exit by + // writing to the shutdown pipe. The reader's select() will + // wake up and break. + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + if let Some(fd) = self.tun_shutdown_fd.take() { + unsafe { + libc::write(fd, b"x".as_ptr() as *const libc::c_void, 1); + libc::close(fd); + } + } + + // Wait for threads to finish + if let Some(handle) = self.tun_reader_handle.take() { + let _ = handle.join(); + } + if let Some(handle) = self.tun_writer_handle.take() { + let _ = handle.join(); + } + true + } + + /// Start the `.fips` DNS responder and keep its handles, returning + /// whether it came up. A failure is logged and is not fatal to the node. + /// + /// `peers` seeds the responder's own hosts map, which it reloads from + /// the hosts file on its own; `channel` is the capacity of the identity + /// channel back to the node (`node.buffers.dns_channel`). Reads the TUN + /// device name for the mesh-interface filter, so the TUN child, when + /// enabled, starts first. + pub(crate) fn start_dns( + &mut self, + config: &DnsConfig, + peers: &[PeerConfig], + channel: usize, + exit_tx: Option>, + ) -> bool { + // Initialize DNS responder (independent of TUN). + // + // Default bind_addr is "::1" (IPv6 loopback). The shipped + // fips-dns-setup configures systemd-resolved via a global + // /etc/systemd/resolved.conf.d/fips.conf drop-in pointing at + // [::1]:5354, which sidesteps a Linux IPV6_PKTINFO behaviour + // where self-destined traffic to fips0's address is attributed + // to fips0 in PKTINFO and gets silently dropped by the + // mesh-interface filter in src/ipv6tun/dns.rs. + // + // For mesh-reachable resolution (rare), set bind_addr: "::" + // in fips.yaml. The mesh-interface filter remains active to + // prevent hosts-file alias enumeration in that mode. + // `IPV6_V6ONLY=0` is set explicitly so IPv4 clients on + // 127.0.0.1 still reach us regardless of kernel sysctl + // defaults — but only when bind is on a wildcard / IPv6 path. + let addr_str = config.bind_addr(); + match addr_str.parse::() { + Ok(ip) => { + let bind = SocketAddr::new(ip, config.port()); + match bind_dns_socket(bind) { + Ok(socket) => { + // Read the bound address back off the socket + // rather than reusing `bind`: a port-0 config + // resolves to the kernel-assigned port here, + // and this is the address an embedder that + // proxies queries to us has to dial. + let local_addr = socket.local_addr().unwrap_or(bind); + let (identity_tx, identity_rx) = tokio::sync::mpsc::channel(channel); + let dns_ttl = config.ttl(); + let base_hosts = HostMap::from_peer_configs(peers); + let hosts_path = PathBuf::from(DEFAULT_HOSTS_PATH); + let reloader = HostMapReloader::new(base_hosts, hosts_path); + // Resolve the TUN ifindex so the responder can + // drop queries arriving on the mesh interface + // Without this, the `::` bind exposes the + // hosts file's alias space to any mesh peer. + // The name comes from the device the TUN + // path actually created, not the configured + // one: macOS and FreeBSD assign utunN/tunN + // of their own choosing and the configured + // name resolves to nothing there, which left + // the filter permanently off. + let mesh_ifindex = self.mesh_ifindex(); + if self.tun_name.is_some() && mesh_ifindex.is_none() { + warn!( + device = ?self.tun_name, + "Mesh interface index unresolved; DNS mesh filter disabled" + ); + } + info!( + bind = %local_addr, + hosts = reloader.hosts().len(), + mesh_ifindex = ?mesh_ifindex, + "DNS responder started for .fips domain (auto-reload enabled)" + ); + // Self-report on exit so the supervisor FSM + // routes health when the DNS task dies at + // runtime. The responder never returns, so + // in practice that is a panic. On a + // deliberate stop the task is `.abort()`ed, + // which drops the report with it; even if + // one fired, the FSM ignores it outside + // `Running`. + let handle = tokio::spawn(report_exit( + Child::Dns, + run_dns_responder(socket, identity_tx, dns_ttl, reloader, mesh_ifindex), + exit_tx, + )); + self.dns_identity_rx = Some(identity_rx); + self.dns_task = Some(handle); + self.dns_local_addr = Some(local_addr); + true + } + Err(e) => { + warn!(bind = %bind, error = %e, "Failed to start DNS responder"); + false + } + } + } + Err(e) => { + warn!(addr = %addr_str, error = %e, "Invalid dns.bind_addr; DNS responder not started"); + false + } + } + } + + /// Stop the DNS responder and retract its published address. + pub(crate) fn stop_dns(&mut self) { + // Stop DNS responder + if let Some(handle) = self.dns_task.take() { + handle.abort(); + debug!("DNS responder stopped"); + } + // Retract the published address in the same step that kills + // the listener, so an embedder polling `dns_local_addr()` + // never dials a socket that is already gone. + self.dns_local_addr.take(); + } +} diff --git a/src/ipv6tun/mod.rs b/src/ipv6tun/mod.rs index 47379156..9060595c 100644 --- a/src/ipv6tun/mod.rs +++ b/src/ipv6tun/mod.rs @@ -10,6 +10,7 @@ pub mod dns; pub mod icmp; pub mod icmp_rate_limit; pub mod ipv6_shim; +pub(crate) mod lifecycle; pub(crate) mod outbound; pub mod tcp_mss; pub mod tun; diff --git a/src/node/dataplane/rx_loop.rs b/src/node/dataplane/rx_loop.rs index bb26d78a..1c986237 100644 --- a/src/node/dataplane/rx_loop.rs +++ b/src/node/dataplane/rx_loop.rs @@ -79,7 +79,8 @@ impl Node { // Take the TUN outbound receiver, or create a dummy channel that never // produces messages (when TUN is disabled). Holding the sender prevents // the channel from closing. - let (mut tun_outbound_rx, _tun_guard) = match self.supervisor.tun_outbound_rx.take() { + let (mut tun_outbound_rx, _tun_guard) = match self.supervisor.ipv6tun.tun_outbound_rx.take() + { Some(rx) => (rx, None), None => { let (tx, rx) = tokio::sync::mpsc::channel(1); @@ -89,7 +90,8 @@ impl Node { // Take the DNS identity receiver, or create a dummy channel (when DNS // is disabled). Same pattern as TUN outbound. - let (mut dns_identity_rx, _dns_guard) = match self.supervisor.dns_identity_rx.take() { + let (mut dns_identity_rx, _dns_guard) = match self.supervisor.ipv6tun.dns_identity_rx.take() + { Some(rx) => (rx, None), None => { let (tx, rx) = tokio::sync::mpsc::channel(1); diff --git a/src/node/handlers/session.rs b/src/node/handlers/session.rs index 022d7b9b..b299f224 100644 --- a/src/node/handlers/session.rs +++ b/src/node/handlers/session.rs @@ -462,7 +462,7 @@ impl Node { mark_ipv6_ecn_ce(&mut packet); self.metrics().congestion.ce_received.inc(); } - if let Some(tun_tx) = &self.supervisor.tun_tx { + if let Some(tun_tx) = &self.supervisor.ipv6tun.tun_tx { if let Err(e) = tun_tx.send(packet) { debug!(error = %e, "Failed to deliver decompressed IPv6 packet to TUN"); } @@ -3131,7 +3131,7 @@ impl Node { pub(in crate::node) fn host_icmp(&mut self) -> IcmpContext<'_> { let our_ipv6 = crate::FipsAddress::from_node_addr(self.node_addr()).to_ipv6(); IcmpContext::new( - self.supervisor.tun_tx.as_ref(), + self.supervisor.ipv6tun.tun_tx.as_ref(), our_ipv6, &mut self.icmp_rate_limiter, ) diff --git a/src/node/lifecycle/mod.rs b/src/node/lifecycle/mod.rs index b8d20ec9..6db9235a 100644 --- a/src/node/lifecycle/mod.rs +++ b/src/node/lifecycle/mod.rs @@ -11,6 +11,7 @@ use super::peering::reconcile::{ use super::peering::retry::MAX_RETRY_CONNECTIONS_PER_TICK; use crate::config::{ConnectPolicy, PeerAddress, PeerConfig}; +use crate::ipv6tun::lifecycle::{TunThreads, open_tun}; use crate::node::acl::PeerAclContext; use crate::node::dataplane::PeerActionCtx; use crate::nostr::{BootstrapEvent, NostrRendezvous}; @@ -19,11 +20,10 @@ use crate::peer::machine::{HandshakeCrypto, PeerEvent, PeerMachine}; use crate::proto::fmp::wire::build_msg1; use crate::proto::fmp::{Disconnect, DisconnectReason}; use crate::transport::{Link, LinkDirection, LinkId, TransportAddr, TransportId, packet_channel}; -use crate::upper::tun::{TunDevice, TunState, run_tun_reader, shutdown_tun_interface}; +use crate::upper::tun::TunState; use crate::{NodeAddr, PeerIdentity}; use std::collections::{HashMap, HashSet}; use std::net::SocketAddr; -use std::thread; use std::time::Duration; use tracing::{debug, error, info, warn}; @@ -44,7 +44,7 @@ fn socket_addr_families_compatible(local: SocketAddr, remote: SocketAddr) -> boo /// A panic would otherwise unwind past the report and leave the node healthy /// with the child gone. Aborting the task still reports nothing: the abort /// drops this whole future, so a deliberate stop does not read as a death. -pub(in crate::node) async fn report_exit( +pub(crate) async fn report_exit( child: Child, body: impl std::future::Future, tx: Option>, @@ -66,7 +66,7 @@ pub(in crate::node) async fn report_exit( /// Run a supervised child's thread body, then report the child's exit on `tx`, /// whether the body returned or panicked. -pub(in crate::node) fn report_thread( +pub(crate) fn report_thread( child: Child, body: impl FnOnce(), tx: Option<&tokio::sync::mpsc::Sender>, @@ -1473,7 +1473,7 @@ impl Node { // 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 tun = self.config().tun.enabled && self.supervisor.ipv6tun.tun_tx.is_none(); let dns = self.config().dns.enabled; // Worker-pool booleans + counts. Unix only — the workers issue @@ -1706,244 +1706,43 @@ impl Node { // Initialize TUN interface after transports and peers are // ready. let address = *self.identity().address(); - match TunDevice::create(&self.config().tun, address).await { - Ok(device) => { - let mtu = device.mtu(); - let name = device.name().to_string(); - let our_addr = *device.address(); - - info!("TUN device active:"); - info!(" name: {}", name); - info!(" address: {}", device.address()); - info!(" mtu: {}", mtu); - + match open_tun(&self.config().tun, address).await { + Some(device) => { // Seed the shared MSS ceiling from whatever is bound // right now. Both TUN threads read it live from here // on, so a transport binding or unbinding later moves // the clamp instead of leaving it at this instant's // value — see `crate::upper::tun::MssCeiling`. self.refresh_tun_mss_ceiling(); - let max_mss = self.tun_mss_ceiling.clone(); - let effective_mtu = self.effective_ipv6_mtu(); - - info!("effective MTU: {} bytes", effective_mtu); - debug!( - " max TCP MSS: {} bytes", - max_mss.load(std::sync::atomic::Ordering::Relaxed) - ); - - // On macOS and FreeBSD, create a shutdown pipe. Writing to it - // unblocks the reader thread's select() loop without closing - // the TUN fd (which would cause a double-close when TunDevice - // drops). Linux instead unblocks the reader by deleting the - // interface; on macOS/FreeBSD downing the interface does not - // wake a blocked read. - #[cfg(any(target_os = "macos", target_os = "freebsd"))] - let (shutdown_read_fd, shutdown_write_fd) = { - let mut fds = [0i32; 2]; - if unsafe { libc::pipe(fds.as_mut_ptr()) } < 0 { - return Err(NodeError::Tun( - crate::upper::tun::TunError::Configure( - "failed to create shutdown pipe".into(), - ), - )); - } - (fds[0], fds[1]) + let threads = TunThreads { + ceiling: self.tun_mss_ceiling.clone(), + effective: self.effective_ipv6_mtu(), + path_mtu: self.path_mtu_lookup.clone(), + channel: self.config().node.buffers.tun_channel, + exit_tx: self.child_exit_tx.clone(), }; - - // Create writer (dups the fd for independent write access). - // Pass path_mtu_lookup so inbound SYN-ACK clamp can read - // per-destination path MTU learned via discovery. - let (writer, tun_tx) = device - .create_writer(max_mss.clone(), self.path_mtu_lookup.clone())?; - - // Spawn writer thread. On exit, including a panic, - // it self-reports `Child::Tun` (sync context → - // `blocking_send`); TUN is one compound child, so - // both threads reporting is fine (the FSM de-dups - // via `up.remove`). - let writer_child_tx = self.child_exit_tx.clone(); - let writer_handle = thread::spawn(move || { - report_thread( - Child::Tun, - move || writer.run(), - writer_child_tx.as_ref(), - ); - }); - - // Clone tun_tx for the reader - let reader_tun_tx = tun_tx.clone(); - - // Create outbound channel for TUN reader → Node - let tun_channel_size = self.config().node.buffers.tun_channel; - let (outbound_tx, outbound_rx) = - tokio::sync::mpsc::channel(tun_channel_size); - - // Spawn reader thread. Like the writer, it - // self-reports `Child::Tun` on exit or panic (sync - // context → `blocking_send`). Exactly one cfg - // variant compiles, so the single clone is moved - // into that closure. - let path_mtu_lookup = self.path_mtu_lookup.clone(); - let reader_child_tx = self.child_exit_tx.clone(); - #[cfg(any(target_os = "macos", target_os = "freebsd"))] - let reader_handle = thread::spawn(move || { - report_thread( - Child::Tun, - move || { - run_tun_reader( - device, - mtu, - our_addr, - reader_tun_tx, - outbound_tx, - max_mss, - path_mtu_lookup, - shutdown_read_fd, - ) - }, - reader_child_tx.as_ref(), - ); - }); - #[cfg(not(any(target_os = "macos", target_os = "freebsd")))] - let reader_handle = thread::spawn(move || { - report_thread( - Child::Tun, - move || { - run_tun_reader( - device, - mtu, - our_addr, - reader_tun_tx, - outbound_tx, - max_mss, - path_mtu_lookup, - ) - }, - reader_child_tx.as_ref(), - ); - }); - + self.supervisor.ipv6tun.spawn_tun(device, threads)?; self.tun_state = TunState::Active; - self.tun_name = Some(name); - self.supervisor.tun_tx = Some(tun_tx); - self.supervisor.tun_outbound_rx = Some(outbound_rx); - self.supervisor.tun_reader_handle = Some(reader_handle); - self.supervisor.tun_writer_handle = Some(writer_handle); - #[cfg(any(target_os = "macos", target_os = "freebsd"))] - { - self.supervisor.tun_shutdown_fd = Some(shutdown_write_fd); - } Event::SubstrateUp { child } } - Err(e) => { + None => { self.tun_state = TunState::Failed; - warn!(error = %e, "Failed to initialize TUN, continuing without it"); Event::SubstrateFailed { child } } } } Child::Dns => { - // Initialize DNS responder (independent of TUN). - // - // Default bind_addr is "::1" (IPv6 loopback). The shipped - // fips-dns-setup configures systemd-resolved via a global - // /etc/systemd/resolved.conf.d/fips.conf drop-in pointing at - // [::1]:5354, which sidesteps a Linux IPV6_PKTINFO behaviour - // where self-destined traffic to fips0's address is attributed - // to fips0 in PKTINFO and gets silently dropped by the - // mesh-interface filter in src/upper/dns.rs. - // - // For mesh-reachable resolution (rare), set bind_addr: "::" - // in fips.yaml. The mesh-interface filter remains active to - // prevent hosts-file alias enumeration in that mode. - // `IPV6_V6ONLY=0` is set explicitly so IPv4 clients on - // 127.0.0.1 still reach us regardless of kernel sysctl - // defaults — but only when bind is on a wildcard / IPv6 path. - let addr_str = self.config().dns.bind_addr(); - match addr_str.parse::() { - Ok(ip) => { - let bind = std::net::SocketAddr::new(ip, self.config().dns.port()); - match crate::ipv6tun::dns::bind_dns_socket(bind) { - Ok(socket) => { - // Read the bound address back off the socket - // rather than reusing `bind`: a port-0 config - // resolves to the kernel-assigned port here, - // and this is the address an embedder that - // proxies queries to us has to dial. - let local_addr = socket.local_addr().unwrap_or(bind); - let dns_channel_size = self.config().node.buffers.dns_channel; - let (identity_tx, identity_rx) = - tokio::sync::mpsc::channel(dns_channel_size); - let dns_ttl = self.config().dns.ttl(); - let base_hosts = - crate::upper::hosts::HostMap::from_peer_configs( - self.config().peers(), - ); - let hosts_path = std::path::PathBuf::from( - crate::upper::hosts::DEFAULT_HOSTS_PATH, - ); - let reloader = crate::upper::hosts::HostMapReloader::new( - base_hosts, hosts_path, - ); - // Resolve the TUN ifindex so the responder can - // drop queries arriving on the mesh interface - // Without this, the `::` bind exposes the - // hosts file's alias space to any mesh peer. - // The name comes from the device the TUN - // path actually created, not the configured - // one: macOS and FreeBSD assign utunN/tunN - // of their own choosing and the configured - // name resolves to nothing there, which left - // the filter permanently off. - let mesh_ifindex = self.mesh_ifindex(); - if self.tun_name.is_some() && mesh_ifindex.is_none() { - warn!( - device = ?self.tun_name, - "Mesh interface index unresolved; DNS mesh filter disabled" - ); - } - info!( - bind = %local_addr, - hosts = reloader.hosts().len(), - mesh_ifindex = ?mesh_ifindex, - "DNS responder started for .fips domain (auto-reload enabled)" - ); - // Self-report on exit so the supervisor FSM - // routes health when the DNS task dies at - // runtime. The responder never returns, so - // in practice that is a panic. On a - // deliberate stop the task is `.abort()`ed, - // which drops the report with it; even if - // one fired, the FSM ignores it outside - // `Running`. - let dns_child_tx = self.child_exit_tx.clone(); - let handle = tokio::spawn(report_exit( - Child::Dns, - crate::upper::dns::run_dns_responder( - socket, - identity_tx, - dns_ttl, - reloader, - mesh_ifindex, - ), - dns_child_tx, - )); - self.supervisor.dns_identity_rx = Some(identity_rx); - self.supervisor.dns_task = Some(handle); - self.supervisor.dns_local_addr = Some(local_addr); - Event::SubstrateUp { child } - } - Err(e) => { - warn!(bind = %bind, error = %e, "Failed to start DNS responder"); - Event::SubstrateFailed { child } - } - } - } - Err(e) => { - warn!(addr = %addr_str, error = %e, "Invalid dns.bind_addr; DNS responder not started"); - Event::SubstrateFailed { child } - } + let config = &self.context.config; + let up = self.supervisor.ipv6tun.start_dns( + &config.dns, + config.peers(), + config.node.buffers.dns_channel, + self.child_exit_tx.clone(), + ); + if up { + Event::SubstrateUp { child } + } else { + Event::SubstrateFailed { child } } } }; @@ -2072,20 +1871,6 @@ impl Node { Ok(()) } - /// Resolve the index of the mesh TUN device this node actually created. - /// - /// Reads the device name recorded when the TUN was brought up, which is - /// the kernel's name rather than the configured one. Returns `None` when - /// no TUN is up, which disables the DNS responder's mesh-interface - /// filter: with no mesh interface there is no mesh exposure to defend. - /// An app-owned TUN also leaves the name unset, so the filter stays off - /// there even though a mesh interface exists. - pub(crate) fn mesh_ifindex(&self) -> Option { - self.tun_name - .as_deref() - .and_then(crate::ipv6tun::dns::lookup_mesh_ifindex) - } - /// Stop the node. /// /// Shuts down TUN interface, stops I/O threads, and transitions to @@ -2161,15 +1946,7 @@ impl Node { match child { Child::Dns => { - // Stop DNS responder - if let Some(handle) = self.supervisor.dns_task.take() { - handle.abort(); - debug!("DNS responder stopped"); - } - // Retract the published address in the same step that kills - // the listener, so an embedder polling `dns_local_addr()` - // never dials a socket that is already gone. - self.supervisor.dns_local_addr.take(); + self.supervisor.ipv6tun.stop_dns(); } Child::Nostr => { // Stop Nostr overlay discovery background work and withdraw @@ -2209,38 +1986,7 @@ impl Node { } Child::Tun => { // Shutdown TUN interface - if let Some(name) = self.tun_name.take() { - info!(name = %name, "Shutting down TUN interface"); - - // Drop the tun_tx to signal the writer to stop - self.supervisor.tun_tx.take(); - - // Delete the interface (on Linux, causes reader to get - // EFAULT; on macOS/FreeBSD this downs it — the kernel - // destroys the device once the reader closes the fd). - if let Err(e) = shutdown_tun_interface(&name).await { - warn!(name = %name, error = %e, "Failed to shutdown TUN interface"); - } - - // On macOS and FreeBSD, signal the reader thread to exit by - // writing to the shutdown pipe. The reader's select() will - // wake up and break. - #[cfg(any(target_os = "macos", target_os = "freebsd"))] - if let Some(fd) = self.supervisor.tun_shutdown_fd.take() { - unsafe { - libc::write(fd, b"x".as_ptr() as *const libc::c_void, 1); - libc::close(fd); - } - } - - // Wait for threads to finish - if let Some(handle) = self.supervisor.tun_reader_handle.take() { - let _ = handle.join(); - } - if let Some(handle) = self.supervisor.tun_writer_handle.take() { - let _ = handle.join(); - } - + if self.supervisor.ipv6tun.stop_tun().await { self.tun_state = TunState::Disabled; } } @@ -2297,7 +2043,7 @@ impl Node { /// `Child::Dns`, which is what reaches this. pub(in crate::node) fn retract_child_publications(&mut self, child: Child) { if matches!(child, Child::Dns) { - self.supervisor.dns_local_addr.take(); + self.supervisor.ipv6tun.dns_local_addr.take(); } } @@ -2351,7 +2097,7 @@ impl Node { /// stops them. Shared by [`Self::stop`] and [`Self::enter_drain`]. fn reconstruct_supervised_up(&self) -> Vec { let mut up: Vec = Vec::new(); - if self.supervisor.dns_task.is_some() { + if self.supervisor.ipv6tun.dns_up() { up.push(Child::Dns); } if self.supervisor.nostr_rendezvous.engine().is_some() { @@ -2363,7 +2109,7 @@ impl Node { for id in self.transports.keys() { up.push(Child::Transport(*id)); } - if self.tun_name.is_some() { + if self.supervisor.ipv6tun.tun_up() { up.push(Child::Tun); } up diff --git a/src/node/lifecycle/supervisor.rs b/src/node/lifecycle/supervisor.rs index af960219..d79fd841 100644 --- a/src/node/lifecycle/supervisor.rs +++ b/src/node/lifecycle/supervisor.rs @@ -103,11 +103,9 @@ use std::collections::HashSet; use std::sync::Arc; -use std::thread::JoinHandle; use crate::node::NodeState; use crate::transport::{PacketTx, TransportId}; -use crate::upper::tun::{TunOutboundRx, TunTx}; /// A supervised substrate child. /// @@ -395,7 +393,7 @@ impl SupervisorFsm { /// A supervisor seeded directly into `Running` with a known up-set. /// /// The teardown driver (`stop()`) reconstructs the up-set from observed - /// runtime presence (`dns_task.is_some()`, transports keys, etc.) rather + /// runtime presence (`ipv6tun.dns_up()`, transports keys, etc.) rather /// than relying on a live machine persisted across start/stop, so that /// teardown ordering is authored here regardless of how the node reached /// `Running`. Feeding `Event::Stop` then yields the ordered `StopChild` @@ -811,29 +809,10 @@ pub(crate) struct Supervisor { /// Packet sender for transports. pub(in crate::node) packet_tx: Option, - /// TUN packet sender channel. - pub(in crate::node) tun_tx: Option, - /// Receiver for outbound packets from the TUN reader. - pub(in crate::node) tun_outbound_rx: Option, - /// TUN reader thread handle. - pub(in crate::node) tun_reader_handle: Option>, - /// TUN writer thread handle. - pub(in crate::node) tun_writer_handle: Option>, - /// Shutdown pipe: writing to this fd unblocks the TUN reader thread on - /// macOS and FreeBSD. On Linux, deleting the interface via netlink - /// serves the same purpose. - #[cfg(any(target_os = "macos", target_os = "freebsd"))] - pub(in crate::node) tun_shutdown_fd: Option, - - /// Receiver for resolved identities from the DNS responder. - pub(in crate::node) dns_identity_rx: Option, - /// DNS responder task handle. - pub(in crate::node) dns_task: Option>, - /// Address the DNS responder actually bound, read back from the socket - /// after `bind` so a port-0 config resolves to the assigned port. `Some` - /// only while the responder is up; published to embedders through - /// [`Node::dns_local_addr`](crate::Node::dns_local_addr). - pub(in crate::node) dns_local_addr: Option, + /// TUN and DNS child handles: the TUN device name, channels, reader and + /// writer threads and (macOS/FreeBSD) shutdown pipe, and the DNS + /// responder task, identity receiver and bound address. + pub(in crate::node) ipv6tun: crate::ipv6tun::lifecycle::Handles, /// Sender for each UDP listen socket the transport spawn binds — its raw /// fd and the instance name it was configured under — armed by @@ -892,15 +871,7 @@ impl Supervisor { Self { state: NodeState::Created, packet_tx: None, - tun_tx: None, - tun_outbound_rx: None, - tun_reader_handle: None, - tun_writer_handle: None, - #[cfg(any(target_os = "macos", target_os = "freebsd"))] - tun_shutdown_fd: None, - dns_identity_rx: None, - dns_task: None, - dns_local_addr: None, + ipv6tun: Default::default(), #[cfg(unix)] udp_fd_tx: None, nostr_rendezvous: crate::nostr::RendezvousDriver::default(), diff --git a/src/node/mod.rs b/src/node/mod.rs index 23a3d64f..5a22ccb8 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -15,7 +15,7 @@ pub(crate) mod decrypt_worker; #[cfg(unix)] pub(crate) mod encrypt_worker; mod handlers; -mod lifecycle; +pub(crate) mod lifecycle; pub(crate) mod metrics; pub(crate) mod netmon; pub use netmon::NetmonTrigger; @@ -606,8 +606,6 @@ pub struct Node { // === TUN Interface === /// TUN device state. tun_state: TunState, - /// TUN interface name (for cleanup). - tun_name: Option, /// Slot the embedder installs its BLE radio into, armed by /// [`Self::enable_app_owned_ble_radio`]. `None` unless armed. @@ -913,7 +911,6 @@ impl Node { crate::control::snapshot::NativeSnapshot::empty(), )), tun_state, - tun_name: None, #[cfg(all(ble_available, any(target_os = "android", test)))] ble_radio: None, index_allocator: IndexAllocator::new(), @@ -1087,7 +1084,6 @@ impl Node { crate::control::snapshot::NativeSnapshot::empty(), )), tun_state, - tun_name: None, #[cfg(all(ble_available, any(target_os = "android", test)))] ble_radio: None, index_allocator: IndexAllocator::new(), @@ -1936,7 +1932,7 @@ impl Node { estimated_mesh_size: self.estimated_mesh_size, state: self.supervisor.state, tun_state: self.tun_state, - tun_name: self.tun_name.clone(), + tun_name: self.supervisor.ipv6tun.tun_name.clone(), effective_ipv6_mtu: self.effective_ipv6_mtu(), connection_count: self.connection_count(), peer_count: self.peers.len(), @@ -2651,7 +2647,7 @@ impl Node { /// Get the TUN interface name, if active. pub fn tun_name(&self) -> Option<&str> { - self.tun_name.as_deref() + self.supervisor.ipv6tun.tun_name.as_deref() } // === Resource Limits === @@ -3532,7 +3528,14 @@ impl Node { /// /// Returns None if TUN is not active or the node hasn't been started. pub fn tun_tx(&self) -> Option<&TunTx> { - self.supervisor.tun_tx.as_ref() + self.supervisor.ipv6tun.tun_tx.as_ref() + } + + /// Install a TUN packet sender, standing in for the TUN writer, so a + /// test can read what the node delivers toward the host. + #[cfg(test)] + pub(crate) fn install_tun(&mut self, tun_tx: TunTx) { + self.supervisor.ipv6tun.tun_tx = Some(tun_tx); } /// Set up an **app-owned TUN**: rather than FIPS creating a system TUN @@ -3559,8 +3562,8 @@ impl Node { 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.supervisor.ipv6tun.tun_tx = Some(tun_tx); + self.supervisor.ipv6tun.tun_outbound_rx = Some(outbound_rx); self.tun_state = TunState::Active; (outbound_tx, tun_rx) } @@ -3733,7 +3736,7 @@ impl Node { /// Reading live node state from a backgrounded loop is a general gap, not /// one this accessor tries to close. pub fn dns_local_addr(&self) -> Option { - self.supervisor.dns_local_addr + self.supervisor.ipv6tun.dns_local_addr } /// A handle that wakes the medium-change detector (`node.netmon.*`) now diff --git a/src/node/tests/discovery.rs b/src/node/tests/discovery.rs index 2bf4b64c..88f9b6c3 100644 --- a/src/node/tests/discovery.rs +++ b/src/node/tests/discovery.rs @@ -1857,7 +1857,7 @@ async fn test_check_pending_lookups_default_sequence_unreachable() { // Inject a TUN sender so `send_icmpv6_dest_unreachable` is observable. let (tun_tx, tun_rx) = mpsc::channel::>(); - node.supervisor.tun_tx = Some(tun_tx); + node.install_tun(tun_tx); // Build a target identity (the unreachable destination). let target_identity = Identity::generate(); diff --git a/src/node/tests/session.rs b/src/node/tests/session.rs index f56f1d90..fe3e15f9 100644 --- a/src/node/tests/session.rs +++ b/src/node/tests/session.rs @@ -637,7 +637,7 @@ async fn test_session_100_nodes() { let mut tun_receivers: Vec>> = Vec::with_capacity(NUM_NODES); for tn in nodes.iter_mut() { let (tx, rx) = mpsc::channel(); - tn.node.supervisor.tun_tx = Some(tx); + tn.node.install_tun(tx); tun_receivers.push(rx); } @@ -1051,7 +1051,7 @@ async fn test_tun_outbound_established_session() { // Install TUN receiver on Node 1 let (tun_tx, tun_rx) = std::sync::mpsc::channel(); - nodes[1].node.supervisor.tun_tx = Some(tun_tx); + nodes[1].node.install_tun(tun_tx); // Build and inject an IPv6 packet let test_payload = b"data-plane-test-12345"; @@ -1133,7 +1133,7 @@ async fn rekey_cutover_preserves_data_plane() { // node 1's TUN receiver observes decoded plaintext. let (tun_tx, tun_rx) = std::sync::mpsc::channel(); - nodes[1].node.supervisor.tun_tx = Some(tun_tx); + nodes[1].node.install_tun(tun_tx); let src_fips = crate::FipsAddress::from_node_addr(&node0_addr); let dst_fips = crate::FipsAddress::from_node_addr(&node1_addr); @@ -1298,9 +1298,9 @@ async fn rekey_pair_with_held_msg2() -> HeldMsg2Pair { // Each node's TUN receiver observes the plaintext the other one sent. let (tun0_tx, tun0_rx) = std::sync::mpsc::channel(); - nodes[0].node.supervisor.tun_tx = Some(tun0_tx); + nodes[0].node.install_tun(tun0_tx); let (tun1_tx, tun1_rx) = std::sync::mpsc::channel(); - nodes[1].node.supervisor.tun_tx = Some(tun1_tx); + nodes[1].node.install_tun(tun1_tx); let fips0 = crate::FipsAddress::from_node_addr(&node0_addr); let fips1 = crate::FipsAddress::from_node_addr(&node1_addr); @@ -1823,7 +1823,7 @@ async fn test_tun_outbound_triggers_session_initiation() { // Install TUN receiver on Node 1 let (tun_tx, tun_rx) = std::sync::mpsc::channel(); - nodes[1].node.supervisor.tun_tx = Some(tun_tx); + nodes[1].node.install_tun(tun_tx); // Build and inject an IPv6 packet (identity cache populated at peer promotion) let test_payload = b"trigger-session-test"; @@ -1876,7 +1876,7 @@ async fn test_tun_outbound_unknown_destination() { // Install TUN receiver on Node 0 (for ICMPv6 response) let (tun_tx, tun_rx) = std::sync::mpsc::channel(); - nodes[0].node.supervisor.tun_tx = Some(tun_tx); + nodes[0].node.install_tun(tun_tx); let src_fips = crate::FipsAddress::from_node_addr(nodes[0].node.node_addr()); @@ -1928,7 +1928,7 @@ async fn test_tun_outbound_3node_forwarded() { // Install TUN receiver on Node 2 let (tun_tx, tun_rx) = std::sync::mpsc::channel(); - nodes[2].node.supervisor.tun_tx = Some(tun_tx); + nodes[2].node.install_tun(tun_tx); // Build and inject an IPv6 packet (triggers session initiation to Node 2) let test_payload = b"forwarded-data-plane"; @@ -1973,7 +1973,7 @@ async fn test_tun_outbound_pending_queue_flush() { // Install TUN receiver on Node 1 let (tun_tx, tun_rx) = std::sync::mpsc::channel(); - nodes[1].node.supervisor.tun_tx = Some(tun_tx); + nodes[1].node.install_tun(tun_tx); // Send 5 packets before any session exists let mut packets = Vec::new(); @@ -2624,7 +2624,7 @@ async fn test_tun_outbound_path_mtu_generates_ptb() { // Install TUN receiver on source node to capture ICMPv6 PTB let (tun_tx, tun_rx) = std::sync::mpsc::channel(); - nodes[0].node.supervisor.tun_tx = Some(tun_tx); + nodes[0].node.install_tun(tun_tx); // Build an IPv6 packet that fits local MTU but exceeds path MTU let reduced_ipv6_mtu = crate::upper::icmp::effective_ipv6_mtu(reduced_mtu) as usize; @@ -2681,7 +2681,7 @@ async fn test_tun_outbound_path_mtu_generates_ptb() { // Verify a packet that fits within path MTU passes through (no PTB) let (tun_tx2, tun_rx2) = std::sync::mpsc::channel(); - nodes[0].node.supervisor.tun_tx = Some(tun_tx2); + nodes[0].node.install_tun(tun_tx2); let fitting_payload = vec![0u8; reduced_ipv6_mtu - 41]; // fits within path MTU let fitting_packet = build_ipv6_packet(&src_fips, &dst_fips, &fitting_payload); assert!(fitting_packet.len() <= reduced_ipv6_mtu); @@ -2820,7 +2820,7 @@ async fn test_multihop_pmtud_heterogeneous_mtu() { // should check PathMtuState and generate ICMPv6 PTB on TUN instead // of forwarding. let (tun_tx2, tun_rx2) = std::sync::mpsc::channel(); - nodes[0].node.supervisor.tun_tx = Some(tun_tx2); + nodes[0].node.install_tun(tun_tx2); nodes[0].node.handle_tun_outbound(ipv6_packet.clone()).await; @@ -2864,7 +2864,7 @@ async fn test_multihop_pmtud_heterogeneous_mtu() { // Verify a fitting packet still passes through without PTB let (tun_tx3, tun_rx3) = std::sync::mpsc::channel(); - nodes[0].node.supervisor.tun_tx = Some(tun_tx3); + nodes[0].node.install_tun(tun_tx3); let fitting_payload = vec![0xCDu8; 600 - 40]; // 600-byte IPv6 packet, well within 694 let fitting_packet = build_ipv6_packet(&src_fips, &dst_fips, &fitting_payload); @@ -3188,7 +3188,7 @@ async fn test_forged_mtu_exceeded_of_zero_does_not_blackhole_the_session() { nodes[0].node.handle_mtu_exceeded(&reporter, &inner).await; let (tun_tx, tun_rx) = std::sync::mpsc::channel(); - nodes[0].node.supervisor.tun_tx = Some(tun_tx); + nodes[0].node.install_tun(tun_tx); let payload = vec![0u8; 560]; let ipv6_packet = build_ipv6_packet(&src_fips, &dst_fips, &payload); @@ -5377,9 +5377,9 @@ async fn a_lost_initial_msg3_is_resent_and_the_responder_completes_the_session() let node1_addr = *nodes[1].node.node_addr(); let (tun0_tx, tun0_rx) = std::sync::mpsc::channel(); - nodes[0].node.supervisor.tun_tx = Some(tun0_tx); + nodes[0].node.install_tun(tun0_tx); let (tun1_tx, tun1_rx) = std::sync::mpsc::channel(); - nodes[1].node.supervisor.tun_tx = Some(tun1_tx); + nodes[1].node.install_tun(tun1_tx); let fips0 = crate::FipsAddress::from_node_addr(&node0_addr); let fips1 = crate::FipsAddress::from_node_addr(&node1_addr); @@ -5432,7 +5432,7 @@ async fn an_initiator_stops_resending_msg3_once_a_responder_frame_authenticates( let node0_addr = *nodes[0].node.node_addr(); let node1_addr = *nodes[1].node.node_addr(); let (tun0_tx, tun0_rx) = std::sync::mpsc::channel(); - nodes[0].node.supervisor.tun_tx = Some(tun0_tx); + nodes[0].node.install_tun(tun0_tx); let fips0 = crate::FipsAddress::from_node_addr(&node0_addr); let fips1 = crate::FipsAddress::from_node_addr(&node1_addr); let interval_ms = nodes[0] @@ -5487,9 +5487,9 @@ async fn a_resent_msg3_reaching_an_established_responder_is_refused_and_the_sess let node0_addr = *nodes[0].node.node_addr(); let node1_addr = *nodes[1].node.node_addr(); let (tun0_tx, tun0_rx) = std::sync::mpsc::channel(); - nodes[0].node.supervisor.tun_tx = Some(tun0_tx); + nodes[0].node.install_tun(tun0_tx); let (tun1_tx, tun1_rx) = std::sync::mpsc::channel(); - nodes[1].node.supervisor.tun_tx = Some(tun1_tx); + nodes[1].node.install_tun(tun1_tx); let fips0 = crate::FipsAddress::from_node_addr(&node0_addr); let fips1 = crate::FipsAddress::from_node_addr(&node1_addr); let interval_ms = nodes[0] diff --git a/src/node/tests/unit.rs b/src/node/tests/unit.rs index 5f89bf4a..61e1efeb 100644 --- a/src/node/tests/unit.rs +++ b/src/node/tests/unit.rs @@ -3963,6 +3963,7 @@ async fn dns_responder_serves_a_proxying_embedder() { let identity = tokio::time::timeout( std::time::Duration::from_secs(2), node.supervisor + .ipv6tun .dns_identity_rx .as_mut() .expect("responder installed the identity receiver") @@ -4707,8 +4708,8 @@ fn mesh_filter_resolves_the_live_tun_device_rather_than_the_configured_name() { config.tun.name = Some("fips-absent-dev".to_string()); let mut node = Node::new(config).unwrap(); - assert_eq!(node.mesh_ifindex(), None); + assert_eq!(node.supervisor.ipv6tun.mesh_ifindex(), None); - node.tun_name = Some(loopback.to_string()); - assert_eq!(node.mesh_ifindex(), Some(expected)); + node.supervisor.ipv6tun.tun_name = Some(loopback.to_string()); + assert_eq!(node.supervisor.ipv6tun.mesh_ifindex(), Some(expected)); } diff --git a/testing/acl-allowlist/docker-compose.yml b/testing/acl-allowlist/docker-compose.yml index 0bee0f2e..63f58050 100644 --- a/testing/acl-allowlist/docker-compose.yml +++ b/testing/acl-allowlist/docker-compose.yml @@ -35,7 +35,7 @@ x-fips-common: &fips-common restart: "no" environment: - FIPS_TEST_MODE=default - - RUST_LOG=info,fips::node=debug,fips::ipv6tun::icmp=debug + - RUST_LOG=info,fips::node=debug,fips::ipv6tun::icmp=debug,fips::ipv6tun::lifecycle=debug volumes: - ../docker/resolv.conf:/etc/resolv.conf:ro diff --git a/testing/firewall/docker-compose.yml b/testing/firewall/docker-compose.yml index 13498b18..07deb44f 100644 --- a/testing/firewall/docker-compose.yml +++ b/testing/firewall/docker-compose.yml @@ -36,7 +36,7 @@ x-fips-common: &fips-common restart: "no" environment: - FIPS_TEST_MODE=default - - RUST_LOG=info,fips::node=debug,fips::ipv6tun::icmp=debug + - RUST_LOG=info,fips::node=debug,fips::ipv6tun::icmp=debug,fips::ipv6tun::lifecycle=debug services: service-a: diff --git a/testing/iface-binding/docker-compose.yml b/testing/iface-binding/docker-compose.yml index 1d631ca2..440585c8 100644 --- a/testing/iface-binding/docker-compose.yml +++ b/testing/iface-binding/docker-compose.yml @@ -33,7 +33,7 @@ x-fips-common: &fips-common # the daemon, which is precisely the workaround this mechanism retires. The # daemon must do its own waiting here or the suite proves nothing. - FIPS_TEST_MODE=default - - RUST_LOG=info,fips::transport::ethernet=debug,fips::node=debug,fips::ipv6tun::icmp=debug + - RUST_LOG=info,fips::transport::ethernet=debug,fips::node=debug,fips::ipv6tun::icmp=debug,fips::ipv6tun::lifecycle=debug networks: - ifb-net diff --git a/testing/mesh-lab/README.md b/testing/mesh-lab/README.md index 4887907a..15d3b020 100644 --- a/testing/mesh-lab/README.md +++ b/testing/mesh-lab/README.md @@ -102,7 +102,8 @@ rep does, set them in the invoking shell: `handshake`, `forwarding`, `session`, `encrypted`, `mmp` (via `compose-trace.yml`). - nat-lan — `fips::nostr`, `transport::udp`, - `node::lifecycle`, `handlers::handshake`, `dataplane::forwarding` + `node::lifecycle`, `ipv6tun::lifecycle`, `handlers::handshake`, + `dataplane::forwarding` (via `compose-trace-nat.yml`, picked up by `testing/nat/scripts/nat-test.sh` through the `FIPS_NAT_EXTRA_COMPOSE` env-var hook). diff --git a/testing/mesh-lab/compose-trace-nat.yml b/testing/mesh-lab/compose-trace-nat.yml index 70c6c700..13b5ac6b 100644 --- a/testing/mesh-lab/compose-trace-nat.yml +++ b/testing/mesh-lab/compose-trace-nat.yml @@ -8,6 +8,9 @@ # (where the punch packets flow) # - fips::node::lifecycle — daemon bootstrap, peer state # machine, adoption transitions +# - fips::ipv6tun::lifecycle — TUN and DNS child start/stop +# (formerly logged under +# node::lifecycle) # - fips::node::handlers::handshake — Noise handshake msg1/2/3, # cross-init tie-breaker # ("Ignoring established NAT @@ -33,7 +36,7 @@ # is set and the suite is nat-lan. x-trace-rust-log: &trace-rust-log - RUST_LOG: "info,fips::nostr=trace,fips::transport::udp=trace,fips::node::lifecycle=trace,fips::node::handlers::handshake=trace,fips::node::dataplane::forwarding=trace" + RUST_LOG: "info,fips::nostr=trace,fips::transport::udp=trace,fips::node::lifecycle=trace,fips::ipv6tun::lifecycle=trace,fips::node::handlers::handshake=trace,fips::node::dataplane::forwarding=trace" services: lan-a: diff --git a/testing/mesh-lab/run-loop.sh b/testing/mesh-lab/run-loop.sh index a4e0c987..cc6f5ca3 100755 --- a/testing/mesh-lab/run-loop.sh +++ b/testing/mesh-lab/run-loop.sh @@ -433,9 +433,10 @@ run_nat_lan() { # nat-test.sh at the nat-specific trace overlay via the # FIPS_NAT_EXTRA_COMPOSE env-var hook in nat-test.sh. The overlay # bumps RUST_LOG to trace on discovery::nostr, transport::udp, - # node::lifecycle, handlers::handshake, dataplane::forwarding — - # the modules covering the cross-init / adoption / handshake - # path that the NAT-traversal flake exhibits. Path is repo-relative. + # node::lifecycle, ipv6tun::lifecycle, handlers::handshake, + # dataplane::forwarding — the modules covering the cross-init / + # adoption / handshake path that the NAT-traversal flake exhibits. + # Path is repo-relative. local -a env_args=(FIPS_NAT_SKIP_FINAL_CLEANUP=1) if [ -n "${FIPS_MESH_LAB_TRACE:-}" ]; then env_args+=(FIPS_NAT_EXTRA_COMPOSE=testing/mesh-lab/compose-trace-nat.yml) diff --git a/testing/nat/docker-compose.yml b/testing/nat/docker-compose.yml index 93c2fb39..3189c471 100644 --- a/testing/nat/docker-compose.yml +++ b/testing/nat/docker-compose.yml @@ -27,7 +27,7 @@ x-fips-common: &fips-common - net.ipv6.conf.all.disable_ipv6=0 restart: "no" environment: - - RUST_LOG=info,fips::nostr=debug,fips::node::lifecycle=debug + - RUST_LOG=info,fips::nostr=debug,fips::node::lifecycle=debug,fips::ipv6tun::lifecycle=debug services: relay: @@ -134,7 +134,7 @@ services: entrypoint: - /usr/local/bin/nat-node-entrypoint.sh environment: - - RUST_LOG=info,fips::nostr=debug,fips::node::lifecycle=debug + - RUST_LOG=info,fips::nostr=debug,fips::node::lifecycle=debug,fips::ipv6tun::lifecycle=debug - DATA_IF=eth0 - ROUTE_SUBNET=${NAT_WAN_PREFIX:-172.31.254}.0/24 - ROUTE_VIA=172.31.1.254 @@ -160,7 +160,7 @@ services: entrypoint: - /usr/local/bin/nat-node-entrypoint.sh environment: - - RUST_LOG=info,fips::nostr=debug,fips::node::lifecycle=debug + - RUST_LOG=info,fips::nostr=debug,fips::node::lifecycle=debug,fips::ipv6tun::lifecycle=debug - DATA_IF=eth0 - ROUTE_SUBNET=${NAT_WAN_PREFIX:-172.31.254}.0/24 - ROUTE_VIA=172.31.2.254 @@ -186,7 +186,7 @@ services: entrypoint: - /usr/local/bin/nat-node-entrypoint.sh environment: - - RUST_LOG=info,fips::nostr=debug,fips::node::lifecycle=debug + - RUST_LOG=info,fips::nostr=debug,fips::node::lifecycle=debug,fips::ipv6tun::lifecycle=debug - DATA_IF=eth0 - ROUTE_SUBNET=${NAT_WAN_PREFIX:-172.31.254}.0/24 - ROUTE_VIA=172.31.1.254 @@ -212,7 +212,7 @@ services: entrypoint: - /usr/local/bin/nat-node-entrypoint.sh environment: - - RUST_LOG=info,fips::nostr=debug,fips::node::lifecycle=debug + - RUST_LOG=info,fips::nostr=debug,fips::node::lifecycle=debug,fips::ipv6tun::lifecycle=debug - DATA_IF=eth0 - ROUTE_SUBNET=${NAT_WAN_PREFIX:-172.31.254}.0/24 - ROUTE_VIA=172.31.2.254