From db8aa5825d01059ca6b0a7e0061153670c33c024 Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Fri, 30 Jan 2026 04:57:35 +0000 Subject: [PATCH] Session 33: TUN interface lifecycle management - Add TUN interface detection on startup, delete existing before create - Add proper interface deletion on shutdown via netlink - Add TUN packet reader in separate thread with DEBUG logging - Add graceful shutdown handling for expected "Bad address" error - Add take_tun_device() to Node for reader ownership transfer - Add shutdown_tun_interface() public function for cleanup by name - Add tokio signal feature for Ctrl+C handling --- Cargo.lock | 11 +++++ Cargo.toml | 2 +- src/bin/fips.rs | 70 ++++++++++++++++++++++++++++-- src/lib.rs | 2 +- src/node.rs | 8 ++++ src/tun.rs | 113 +++++++++++++++++++++++++++++++++++++++++++++--- 6 files changed, 194 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d230e03..290e4fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -892,6 +892,16 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "slab" version = "0.4.11" @@ -997,6 +1007,7 @@ dependencies = [ "libc", "mio", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.61.2", diff --git a/Cargo.toml b/Cargo.toml index cd825fd..1b985ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } tun = { version = "0.7", features = ["async"] } rtnetlink = "0.14" -tokio = { version = "1", features = ["rt", "macros"] } +tokio = { version = "1", features = ["rt", "macros", "signal", "sync"] } futures = "0.3" [dev-dependencies] diff --git a/src/bin/fips.rs b/src/bin/fips.rs index af07984..ae561ce 100644 --- a/src/bin/fips.rs +++ b/src/bin/fips.rs @@ -2,10 +2,39 @@ //! //! Loads configuration and creates the top-level node instance. -use fips::{Config, Node}; +use fips::{log_ipv6_packet, shutdown_tun_interface, Config, Node, TunDevice}; use tracing::{error, info, warn, Level}; use tracing_subscriber::{fmt, EnvFilter}; +/// TUN packet reader loop. +/// +/// Reads packets from the TUN device and logs them at DEBUG level. +/// This runs in a separate thread since TUN reads are blocking. +fn run_tun_reader(mut device: TunDevice, mtu: u16) { + let mut buf = vec![0u8; mtu as usize + 100]; // Extra space for headers + + loop { + match device.read_packet(&mut buf) { + Ok(n) if n > 0 => { + log_ipv6_packet(&buf[..n]); + } + Ok(_) => { + // Zero-length read, continue + } + Err(e) => { + // "Bad address" (EFAULT) is expected during shutdown when interface is deleted + let err_str = e.to_string(); + if err_str.contains("Bad address") { + info!("TUN interface deleted, reader stopping"); + } else { + error!("TUN read error: {}", e); + } + break; + } + } + } +} + #[tokio::main(flavor = "current_thread")] async fn main() { // Initialize logging @@ -116,6 +145,41 @@ async fn main() { info!("FIPS initialized successfully"); - // TODO: Start event loop, transports, etc. - info!("No transports configured, nothing to do"); + // Spawn TUN reader task if TUN is active + let tun_name = if let Some(tun_device) = node.take_tun_device() { + let mtu = tun_device.mtu(); + let name = tun_device.name().to_string(); + info!(mtu, name = %name, "Starting TUN packet reader"); + + std::thread::spawn(move || { + run_tun_reader(tun_device, mtu); + }); + + Some(name) + } else { + None + }; + + // TODO: Spawn additional event-driven tasks here: + // - Transport listeners/senders + // - Periodic timers (tree announcements, keepalives, etc.) + + info!("FIPS running, press Ctrl+C to exit"); + + match tokio::signal::ctrl_c().await { + Ok(()) => info!("Shutdown signal received"), + Err(e) => error!("Failed to listen for shutdown signal: {}", e), + } + + info!("FIPS shutting down"); + + // Shutdown TUN interface if active + if let Some(name) = tun_name { + info!(name = %name, "Shutting down TUN interface"); + if let Err(e) = shutdown_tun_interface(&name).await { + warn!("Failed to shutdown TUN interface: {}", e); + } + } + + info!("FIPS shutdown complete"); } diff --git a/src/lib.rs b/src/lib.rs index c2ff5c7..4d021a9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -52,4 +52,4 @@ pub use peer::{Peer, PeerError, PeerState, UpstreamPeer}; pub use node::{Node, NodeError, NodeState}; // Re-export TUN types -pub use tun::{TunDevice, TunError, TunState}; +pub use tun::{log_ipv6_packet, shutdown_tun_interface, TunDevice, TunError, TunState}; diff --git a/src/node.rs b/src/node.rs index 5f4066e..4d4899d 100644 --- a/src/node.rs +++ b/src/node.rs @@ -329,6 +329,14 @@ impl Node { self.tun_device.as_mut() } + /// Take ownership of the TUN device. + /// + /// This removes the TUN device from the node, transferring ownership + /// to the caller. Useful for moving the device into a reader task. + pub fn take_tun_device(&mut self) -> Option { + self.tun_device.take() + } + /// Initialize the TUN interface. /// /// Creates and configures the TUN device based on the node's configuration. diff --git a/src/tun.rs b/src/tun.rs index 709f950..89ee0d1 100644 --- a/src/tun.rs +++ b/src/tun.rs @@ -7,8 +7,10 @@ use crate::{FipsAddress, TunConfig}; use futures::TryStreamExt; use rtnetlink::{new_connection, Handle}; +use std::io::Read; use std::net::Ipv6Addr; use thiserror::Error; +use tracing::{debug, info}; use tun::Layer; /// Errors that can occur with TUN operations. @@ -66,7 +68,10 @@ pub struct TunDevice { } impl TunDevice { - /// Create and configure a new TUN device. + /// Create or open a TUN device. + /// + /// If the interface already exists, opens it and reconfigures it. + /// Otherwise, creates a new TUN device. /// /// This requires CAP_NET_ADMIN capability (run with sudo or setcap). pub async fn create(config: &TunConfig, address: FipsAddress) -> Result { @@ -78,7 +83,15 @@ impl TunDevice { let name = config.name(); let mtu = config.mtu(); - // Create the TUN device without address (we'll set it via netlink) + // Delete existing interface if present (TUN devices are exclusive) + if interface_exists(name).await { + info!(name, "Deleting existing TUN interface"); + if let Err(e) = delete_interface(name).await { + debug!(name, error = %e, "Failed to delete existing interface"); + } + } + + // Create the TUN device let mut tun_config = tun::Configuration::default(); #[allow(deprecated)] @@ -87,11 +100,7 @@ impl TunDevice { let device = tun::create(&tun_config)?; // Configure address and bring up via netlink - if let Err(e) = configure_interface(name, address.to_ipv6(), mtu).await { - // If netlink fails, the device was created but not configured. - // Drop will clean up the device. - return Err(e); - } + configure_interface(name, address.to_ipv6(), mtu).await?; Ok(Self { device, @@ -125,6 +134,71 @@ impl TunDevice { pub fn device_mut(&mut self) -> &mut tun::Device { &mut self.device } + + /// Read a packet from the TUN device. + /// + /// Returns the number of bytes read into the buffer, or an error. + /// The buffer should be at least MTU + header size (typically 1500+ bytes). + pub fn read_packet(&mut self, buf: &mut [u8]) -> Result { + self.device.read(buf).map_err(|e| TunError::Configure(format!("read failed: {}", e))) + } + + /// Shutdown and delete the TUN device. + /// + /// This deletes the interface entirely. + pub async fn shutdown(&self) -> Result<(), TunError> { + info!(name = %self.name, "Deleting TUN device"); + delete_interface(&self.name).await + } +} + +/// Log basic information about an IPv6 packet at DEBUG level. +pub fn log_ipv6_packet(packet: &[u8]) { + if packet.len() < 40 { + debug!(len = packet.len(), "Received undersized packet"); + return; + } + + let version = packet[0] >> 4; + if version != 6 { + debug!(version, len = packet.len(), "Received non-IPv6 packet"); + return; + } + + let payload_len = u16::from_be_bytes([packet[4], packet[5]]); + let next_header = packet[6]; + let hop_limit = packet[7]; + + let src = Ipv6Addr::from(<[u8; 16]>::try_from(&packet[8..24]).unwrap()); + let dst = Ipv6Addr::from(<[u8; 16]>::try_from(&packet[24..40]).unwrap()); + + let protocol = match next_header { + 6 => "TCP", + 17 => "UDP", + 58 => "ICMPv6", + _ => "other", + }; + + debug!( + %src, + %dst, + protocol, + next_header, + payload_len, + hop_limit, + total_len = packet.len(), + "TUN packet received" + ); +} + +/// Shutdown and delete a TUN interface by name. +/// +/// This deletes the interface, which will cause any blocking reads +/// to return an error. Use this for graceful shutdown when the TUN device +/// has been moved to another thread. +pub async fn shutdown_tun_interface(name: &str) -> Result<(), TunError> { + info!(name, "shutdown_tun_interface called"); + delete_interface(name).await } impl std::fmt::Debug for TunDevice { @@ -137,6 +211,31 @@ impl std::fmt::Debug for TunDevice { } } +/// Check if a network interface already exists. +async fn interface_exists(name: &str) -> bool { + let Ok((connection, handle, _)) = new_connection() else { + return false; + }; + tokio::spawn(connection); + + get_interface_index(&handle, name).await.is_ok() +} + +/// Delete a network interface by name. +async fn delete_interface(name: &str) -> Result<(), TunError> { + info!(name, "delete_interface: starting"); + let (connection, handle, _) = new_connection() + .map_err(|e| TunError::Configure(format!("netlink connection failed: {}", e)))?; + tokio::spawn(connection); + + let index = get_interface_index(&handle, name).await?; + info!(name, index, "delete_interface: got index, deleting"); + handle.link().del(index).execute().await?; + + info!(name, "delete_interface: done"); + Ok(()) +} + /// Configure a network interface with an IPv6 address via netlink. async fn configure_interface(name: &str, addr: Ipv6Addr, mtu: u16) -> Result<(), TunError> { let (connection, handle, _) = new_connection()