//! FIPS TUN Interface //! //! Manages the TUN device for sending and receiving IPv6 packets. //! The TUN interface presents FIPS addresses to the local system, //! allowing standard socket applications to communicate over the mesh. use crate::{FipsAddress, TunConfig}; use futures::TryStreamExt; use rtnetlink::{Handle, LinkUnspec, RouteMessageBuilder, new_connection}; use std::fs::File; use std::io::{Read, Write}; use std::net::Ipv6Addr; use std::os::unix::io::{AsRawFd, FromRawFd}; use std::sync::mpsc; use thiserror::Error; use tracing::{debug, error, info, trace}; use tun::Layer; /// Channel sender for packets to be written to TUN. pub type TunTx = mpsc::Sender>; /// Channel sender for outbound packets from TUN reader to Node. pub type TunOutboundTx = tokio::sync::mpsc::Sender>; /// Channel receiver for outbound packets (consumed by Node's RX loop). pub type TunOutboundRx = tokio::sync::mpsc::Receiver>; /// Errors that can occur with TUN operations. #[derive(Debug, Error)] pub enum TunError { #[error("failed to create TUN device: {0}")] Create(#[from] tun::Error), #[error("failed to configure TUN device: {0}")] Configure(String), #[error("netlink error: {0}")] Netlink(#[from] rtnetlink::Error), #[error("interface not found: {0}")] InterfaceNotFound(String), #[error("permission denied: {0}")] PermissionDenied(String), #[error("IPv6 is disabled (set net.ipv6.conf.all.disable_ipv6=0)")] Ipv6Disabled, } /// TUN device state. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TunState { /// TUN is disabled in configuration. Disabled, /// TUN is configured but not yet created. Configured, /// TUN device is active and ready. Active, /// TUN device failed to initialize. Failed, } impl std::fmt::Display for TunState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { TunState::Disabled => write!(f, "disabled"), TunState::Configured => write!(f, "configured"), TunState::Active => write!(f, "active"), TunState::Failed => write!(f, "failed"), } } } /// FIPS TUN device wrapper. pub struct TunDevice { device: tun::Device, name: String, mtu: u16, address: FipsAddress, } impl TunDevice { /// 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 { // Check if IPv6 is enabled if is_ipv6_disabled() { return Err(TunError::Ipv6Disabled); } let name = config.name(); let mtu = config.mtu(); // 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)] tun_config.name(name).layer(Layer::L3).mtu(mtu); let device = tun::create(&tun_config)?; // Configure address and bring up via netlink configure_interface(name, address.to_ipv6(), mtu).await?; Ok(Self { device, name: name.to_string(), mtu, address, }) } /// Get the device name. pub fn name(&self) -> &str { &self.name } /// Get the configured MTU. pub fn mtu(&self) -> u16 { self.mtu } /// Get the FIPS address assigned to this device. pub fn address(&self) -> &FipsAddress { &self.address } /// Get a reference to the underlying tun::Device. pub fn device(&self) -> &tun::Device { &self.device } /// Get a mutable reference to the underlying tun::Device. 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 } /// Create a TunWriter for this device. /// /// This duplicates the underlying file descriptor so that reads and writes /// can happen independently on separate threads. Returns the writer and /// a channel sender for submitting packets to be written. /// /// The max_mss parameter is used for TCP MSS clamping on inbound packets. pub fn create_writer(&self, max_mss: u16) -> Result<(TunWriter, TunTx), TunError> { let fd = self.device.as_raw_fd(); // Duplicate the file descriptor for writing let write_fd = unsafe { libc::dup(fd) }; if write_fd < 0 { return Err(TunError::Configure(format!( "failed to dup fd: {}", std::io::Error::last_os_error() ))); } let write_file = unsafe { File::from_raw_fd(write_fd) }; let (tx, rx) = mpsc::channel(); Ok(( TunWriter { file: write_file, rx, name: self.name.clone(), max_mss, }, tx, )) } } /// Writer thread for TUN device. /// /// Services a queue of outbound packets and writes them to the TUN device. /// Multiple producers can send packets via the TunTx channel. /// /// Also performs TCP MSS clamping on inbound SYN-ACK packets. pub struct TunWriter { file: File, rx: mpsc::Receiver>, name: String, max_mss: u16, } impl TunWriter { /// Run the writer loop. /// /// Blocks forever, reading packets from the channel and writing them /// to the TUN device. Returns when the channel is closed (all senders dropped). pub fn run(mut self) { use super::tcp_mss::clamp_tcp_mss; debug!(name = %self.name, max_mss = self.max_mss, "TUN writer starting"); for mut packet in self.rx { // Clamp TCP MSS on inbound SYN-ACK packets if clamp_tcp_mss(&mut packet, self.max_mss) { trace!( name = %self.name, max_mss = self.max_mss, "Clamped TCP MSS in inbound SYN-ACK packet" ); } if let Err(e) = self.file.write_all(&packet) { // "Bad address" is expected during shutdown when interface is deleted let err_str = e.to_string(); if err_str.contains("Bad address") { break; } error!(name = %self.name, error = %e, "TUN write error"); } else { trace!(name = %self.name, len = packet.len(), "TUN packet written"); } } } } /// TUN packet reader loop. /// /// Reads IPv6 packets from the TUN device. Packets destined for FIPS addresses /// (fd::/8) are forwarded to the Node via the outbound channel for session /// encapsulation and routing. Non-FIPS packets receive ICMPv6 Destination /// Unreachable responses. /// /// Also performs TCP MSS clamping on SYN packets to prevent oversized segments. /// /// This is designed to run in a dedicated thread since TUN reads are blocking. /// The loop exits when the TUN interface is deleted (EFAULT) or an unrecoverable /// error occurs. pub fn run_tun_reader( mut device: TunDevice, mtu: u16, our_addr: FipsAddress, tun_tx: TunTx, outbound_tx: TunOutboundTx, transport_mtu: u16, ) { use super::icmp::{ DestUnreachableCode, build_dest_unreachable, effective_ipv6_mtu, should_send_icmp_error, }; use super::tcp_mss::clamp_tcp_mss; let name = device.name().to_string(); let mut buf = vec![0u8; mtu as usize + 100]; // Extra space for headers // Calculate maximum safe TCP MSS from the effective IPv6 MTU const IPV6_HEADER: u16 = 40; const TCP_HEADER: u16 = 20; let effective_mtu = effective_ipv6_mtu(transport_mtu); let max_mss = effective_mtu .saturating_sub(IPV6_HEADER) .saturating_sub(TCP_HEADER); debug!( name = %name, tun_mtu = mtu, transport_mtu = transport_mtu, effective_mtu = effective_mtu, max_mss = max_mss, "TUN reader starting" ); loop { match device.read_packet(&mut buf) { Ok(n) if n > 0 => { let packet = &mut buf[..n]; log_ipv6_packet(packet); // Must be a valid IPv6 packet if packet.len() < 40 || packet[0] >> 4 != 6 { continue; } // Check if destination is a FIPS address (fd::/8 prefix) if packet[24] == crate::identity::FIPS_ADDRESS_PREFIX { // Clamp TCP MSS if this is a SYN packet if clamp_tcp_mss(packet, max_mss) { trace!( name = %name, max_mss = max_mss, "Clamped TCP MSS in SYN packet" ); } // Forward to Node for session encapsulation and routing if outbound_tx.blocking_send(packet.to_vec()).is_err() { break; // Channel closed, shutdown } } else { // Non-FIPS destination: send ICMPv6 Destination Unreachable if should_send_icmp_error(packet) && let Some(response) = build_dest_unreachable( packet, DestUnreachableCode::NoRoute, our_addr.to_ipv6(), ) { trace!( name = %name, len = response.len(), "Sending ICMPv6 Destination Unreachable (non-FIPS destination)" ); if tun_tx.send(response).is_err() { break; } } } } Ok(_) => { // Zero-length read, continue } Err(e) => { // "Bad address" (EFAULT) is expected during shutdown when interface is deleted let err_str = format!("{}", e); if !err_str.contains("Bad address") { error!(name = %name, error = %e, "TUN read error"); } break; } } } } /// Log basic information about an IPv6 packet at TRACE 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", }; trace!("TUN packet received:"); trace!(" src: {}", src); trace!(" dst: {}", dst); trace!(" protocol: {} ({})", protocol, next_header); trace!(" payload: {} bytes, hop_limit: {}", payload_len, hop_limit); } /// 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!("Shutting down TUN interface {}", name); delete_interface(name).await?; info!("TUN interface {} stopped", name); Ok(()) } impl std::fmt::Debug for TunDevice { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("TunDevice") .field("name", &self.name) .field("mtu", &self.mtu) .field("address", &self.address) .finish() } } /// 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> { 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?; handle.link().del(index).execute().await?; 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() .map_err(|e| TunError::Configure(format!("netlink connection failed: {}", e)))?; tokio::spawn(connection); // Get interface index let index = get_interface_index(&handle, name).await?; // Add IPv6 address with /128 prefix (point-to-point) handle .address() .add(index, std::net::IpAddr::V6(addr), 128) .execute() .await?; // Set MTU handle .link() .change(LinkUnspec::new_with_index(index).mtu(mtu as u32).build()) .execute() .await?; // Bring interface up handle .link() .change(LinkUnspec::new_with_index(index).up().build()) .execute() .await?; // Add route for fd00::/8 (FIPS address space) via this interface let fd_prefix: Ipv6Addr = "fd00::".parse().unwrap(); let route = RouteMessageBuilder::::new() .destination_prefix(fd_prefix, 8) .output_interface(index) .build(); handle .route() .add(route) .execute() .await .map_err(|e| TunError::Configure(format!("failed to add fd00::/8 route: {}", e)))?; // Add ip6 rule to ensure fd00::/8 uses the main table, preventing other // routing software (e.g. Tailscale) from intercepting FIPS traffic via // catch-all rules in auxiliary routing tables. let mut rule_req = handle .rule() .add() .v6() .destination_prefix(fd_prefix, 8) .table_id(254) .priority(5265); rule_req.message_mut().header.action = 1.into(); // FR_ACT_TO_TBL if let Err(e) = rule_req.execute().await { debug!("ip6 rule for fd00::/8 not added (may already exist): {e}"); } Ok(()) } /// Get the interface index by name. async fn get_interface_index(handle: &Handle, name: &str) -> Result { let mut links = handle.link().get().match_name(name.to_string()).execute(); if let Some(link) = links.try_next().await? { Ok(link.header.index) } else { Err(TunError::InterfaceNotFound(name.to_string())) } } /// Check if IPv6 is disabled system-wide. fn is_ipv6_disabled() -> bool { std::fs::read_to_string("/proc/sys/net/ipv6/conf/all/disable_ipv6") .map(|s| s.trim() == "1") .unwrap_or(false) } #[cfg(test)] mod tests { use super::*; #[test] fn test_tun_state_display() { assert_eq!(format!("{}", TunState::Disabled), "disabled"); assert_eq!(format!("{}", TunState::Active), "active"); } // Note: TUN device creation tests require elevated privileges // and are better suited for integration tests. }