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.
This commit is contained in:
Johnathan Corgan
2026-09-24 14:45:51 +00:00
parent 787dbb06d9
commit 1eb0e8a34e
18 changed files with 509 additions and 374 deletions
+7 -3
View File
@@ -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.
+402
View File
@@ -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<String>,
/// TUN packet sender channel.
pub(crate) tun_tx: Option<TunTx>,
/// Receiver for outbound packets from the TUN reader.
pub(crate) tun_outbound_rx: Option<TunOutboundRx>,
/// TUN reader thread handle.
pub(crate) tun_reader_handle: Option<JoinHandle<()>>,
/// TUN writer thread handle.
pub(crate) tun_writer_handle: Option<JoinHandle<()>>,
/// 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<std::os::unix::io::RawFd>,
/// Receiver for resolved identities from the DNS responder.
pub(crate) dns_identity_rx: Option<DnsIdentityRx>,
/// DNS responder task handle.
pub(crate) dns_task: Option<tokio::task::JoinHandle<()>>,
/// 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<SocketAddr>,
}
/// 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<Sender<Child>>,
}
/// 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<TunDevice> {
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<u32> {
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<Sender<Child>>,
) -> 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::<IpAddr>() {
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();
}
}
+1
View File
@@ -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;
+4 -2
View File
@@ -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);
+2 -2
View File
@@ -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,
)
+31 -285
View File
@@ -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<Output = ()>,
tx: Option<tokio::sync::mpsc::Sender<Child>>,
@@ -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<Child>>,
@@ -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::<std::net::IpAddr>() {
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<u32> {
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<Child> {
let mut up: Vec<Child> = 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
+6 -35
View File
@@ -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<PacketTx>,
/// TUN packet sender channel.
pub(in crate::node) tun_tx: Option<TunTx>,
/// Receiver for outbound packets from the TUN reader.
pub(in crate::node) tun_outbound_rx: Option<TunOutboundRx>,
/// TUN reader thread handle.
pub(in crate::node) tun_reader_handle: Option<JoinHandle<()>>,
/// TUN writer thread handle.
pub(in crate::node) tun_writer_handle: Option<JoinHandle<()>>,
/// 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<std::os::unix::io::RawFd>,
/// Receiver for resolved identities from the DNS responder.
pub(in crate::node) dns_identity_rx: Option<crate::upper::dns::DnsIdentityRx>,
/// DNS responder task handle.
pub(in crate::node) dns_task: Option<tokio::task::JoinHandle<()>>,
/// 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<std::net::SocketAddr>,
/// 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(),
+14 -11
View File
@@ -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<String>,
/// 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<std::net::SocketAddr> {
self.supervisor.dns_local_addr
self.supervisor.ipv6tun.dns_local_addr
}
/// A handle that wakes the medium-change detector (`node.netmon.*`) now
+1 -1
View File
@@ -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::<Vec<u8>>();
node.supervisor.tun_tx = Some(tun_tx);
node.install_tun(tun_tx);
// Build a target identity (the unreachable destination).
let target_identity = Identity::generate();
+19 -19
View File
@@ -637,7 +637,7 @@ async fn test_session_100_nodes() {
let mut tun_receivers: Vec<mpsc::Receiver<Vec<u8>>> = 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]
+4 -3
View File
@@ -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));
}
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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
+2 -1
View File
@@ -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).
+4 -1
View File
@@ -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:
+4 -3
View File
@@ -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)
+5 -5
View File
@@ -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