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
This commit is contained in:
Johnathan Corgan
2026-01-30 04:57:35 +00:00
parent bfdc532058
commit db8aa5825d
6 changed files with 194 additions and 12 deletions
Generated
+11
View File
@@ -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",
+1 -1
View File
@@ -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]
+67 -3
View File
@@ -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");
}
+1 -1
View File
@@ -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};
+8
View File
@@ -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<TunDevice> {
self.tun_device.take()
}
/// Initialize the TUN interface.
///
/// Creates and configures the TUN device based on the node's configuration.
+106 -7
View File
@@ -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<Self, TunError> {
@@ -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<usize, TunError> {
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()