mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
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:
Generated
+11
@@ -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
@@ -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
@@ -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
@@ -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};
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user