diff --git a/Cargo.lock b/Cargo.lock index 290e4fe..07b10cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -273,6 +273,7 @@ dependencies = [ "dirs", "futures", "hex", + "libc", "rand", "rtnetlink", "secp256k1", diff --git a/Cargo.toml b/Cargo.toml index 1b985ef..a25b225 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ hex = "0.4" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } tun = { version = "0.7", features = ["async"] } +libc = "0.2" rtnetlink = "0.14" tokio = { version = "1", features = ["rt", "macros", "signal", "sync"] } futures = "0.3" diff --git a/src/bin/fips.rs b/src/bin/fips.rs index ae561ce..0a34c48 100644 --- a/src/bin/fips.rs +++ b/src/bin/fips.rs @@ -2,21 +2,46 @@ //! //! Loads configuration and creates the top-level node instance. -use fips::{log_ipv6_packet, shutdown_tun_interface, Config, Node, TunDevice}; -use tracing::{error, info, warn, Level}; +use fips::{ + build_dest_unreachable, log_ipv6_packet, should_send_icmp_error, shutdown_tun_interface, + Config, DestUnreachableCode, FipsAddress, Node, TunDevice, TunTx, +}; +use tracing::{debug, 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. +/// Reads packets from the TUN device, logs them, and sends ICMPv6 +/// Destination Unreachable responses for packets we can't route. +/// /// This runs in a separate thread since TUN reads are blocking. -fn run_tun_reader(mut device: TunDevice, mtu: u16) { +fn run_tun_reader(mut device: TunDevice, mtu: u16, our_addr: FipsAddress, tun_tx: TunTx) { 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]); + let packet = &buf[..n]; + log_ipv6_packet(packet); + + // Currently no routing capability - send ICMPv6 Destination Unreachable + // for all packets that qualify for an error response + if should_send_icmp_error(packet) { + if let Some(response) = build_dest_unreachable( + packet, + DestUnreachableCode::NoRoute, + our_addr.to_ipv6(), + ) { + debug!( + len = response.len(), + "Sending ICMPv6 Destination Unreachable" + ); + if tun_tx.send(response).is_err() { + info!("TUN writer channel closed, reader stopping"); + break; + } + } + } } Ok(_) => { // Zero-length read, continue @@ -124,8 +149,11 @@ async fn main() { match output { Ok(out) => { if out.status.success() { - info!("ip link show {}:\n{}", device.name(), - String::from_utf8_lossy(&out.stdout)); + info!( + "ip link show {}:\n{}", + device.name(), + String::from_utf8_lossy(&out.stdout) + ); } } Err(e) => { @@ -145,14 +173,31 @@ async fn main() { info!("FIPS initialized successfully"); - // Spawn TUN reader task if TUN is active + // Spawn TUN reader and writer threads 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"); + let our_addr = *tun_device.address(); + // Create writer (dups the fd for independent write access) + let (writer, tun_tx) = match tun_device.create_writer() { + Ok(w) => w, + Err(e) => { + error!("Failed to create TUN writer: {}", e); + std::process::exit(1); + } + }; + + info!(mtu, name = %name, "Starting TUN reader and writer"); + + // Spawn writer thread std::thread::spawn(move || { - run_tun_reader(tun_device, mtu); + writer.run(); + }); + + // Spawn reader thread + std::thread::spawn(move || { + run_tun_reader(tun_device, mtu, our_addr, tun_tx); }); Some(name) diff --git a/src/icmp.rs b/src/icmp.rs new file mode 100644 index 0000000..4e3c944 --- /dev/null +++ b/src/icmp.rs @@ -0,0 +1,377 @@ +//! ICMPv6 message handling for FIPS. +//! +//! Implements ICMPv6 error message generation per RFC 4443. +//! Currently supports Destination Unreachable (Type 1) for +//! packets that cannot be routed. + +use std::net::Ipv6Addr; + +/// ICMPv6 message types. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum Icmpv6Type { + /// Destination Unreachable (error). + DestinationUnreachable = 1, + /// Packet Too Big (error). + PacketTooBig = 2, + /// Time Exceeded (error). + TimeExceeded = 3, + /// Parameter Problem (error). + ParameterProblem = 4, + /// Echo Request. + EchoRequest = 128, + /// Echo Reply. + EchoReply = 129, +} + +/// ICMPv6 Destination Unreachable codes. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum DestUnreachableCode { + /// No route to destination. + NoRoute = 0, + /// Communication administratively prohibited. + AdminProhibited = 1, + /// Beyond scope of source address. + BeyondScope = 2, + /// Address unreachable. + AddressUnreachable = 3, + /// Port unreachable. + PortUnreachable = 4, + /// Source address failed policy. + SourcePolicy = 5, + /// Reject route to destination. + RejectRoute = 6, +} + +/// IPv6 header next-header value for ICMPv6. +pub const IPPROTO_ICMPV6: u8 = 58; + +/// Minimum IPv6 MTU - ICMPv6 responses must not exceed this. +const MIN_IPV6_MTU: usize = 1280; + +/// IPv6 header length. +const IPV6_HEADER_LEN: usize = 40; + +/// ICMPv6 header length (type + code + checksum + unused/data). +const ICMPV6_HEADER_LEN: usize = 8; + +/// Maximum original packet bytes to include in ICMPv6 error. +const MAX_ORIGINAL_PACKET: usize = MIN_IPV6_MTU - IPV6_HEADER_LEN - ICMPV6_HEADER_LEN; + +/// Check if we should send an ICMPv6 error for this packet. +/// +/// Returns false if the packet is: +/// - Too short to be valid IPv6 +/// - Not IPv6 +/// - An ICMPv6 error message itself +/// - Has a multicast source address +/// - Has an unspecified source address (::) +pub fn should_send_icmp_error(packet: &[u8]) -> bool { + // Must have at least an IPv6 header + if packet.len() < IPV6_HEADER_LEN { + return false; + } + + // Must be IPv6 + let version = packet[0] >> 4; + if version != 6 { + return false; + } + + // Extract source address + let src = Ipv6Addr::from(<[u8; 16]>::try_from(&packet[8..24]).unwrap()); + + // Don't send errors for unspecified source + if src.is_unspecified() { + return false; + } + + // Don't send errors for multicast source (first byte 0xff) + if src.octets()[0] == 0xff { + return false; + } + + // Don't send errors for ICMPv6 error messages (types 0-127) + let next_header = packet[6]; + if next_header == IPPROTO_ICMPV6 && packet.len() > IPV6_HEADER_LEN { + let icmp_type = packet[IPV6_HEADER_LEN]; + // ICMPv6 error messages are types 0-127 + if icmp_type < 128 { + return false; + } + } + + true +} + +/// Build an ICMPv6 Destination Unreachable response. +/// +/// Takes the original packet that couldn't be delivered and returns +/// a complete IPv6 packet containing the ICMPv6 error response. +/// +/// Arguments: +/// - `original_packet`: The packet that couldn't be routed +/// - `code`: The specific unreachable reason +/// - `our_addr`: Our FIPS address (source of the error) +/// +/// Returns None if the original packet is invalid. +pub fn build_dest_unreachable( + original_packet: &[u8], + code: DestUnreachableCode, + our_addr: Ipv6Addr, +) -> Option> { + // Validate original packet + if original_packet.len() < IPV6_HEADER_LEN { + return None; + } + + // Extract destination from original packet (becomes our destination) + let dest_addr = Ipv6Addr::from(<[u8; 16]>::try_from(&original_packet[8..24]).unwrap()); + + // Calculate how much of the original packet to include + let original_len = original_packet.len().min(MAX_ORIGINAL_PACKET); + let icmpv6_len = ICMPV6_HEADER_LEN + original_len; + let total_len = IPV6_HEADER_LEN + icmpv6_len; + + let mut response = vec![0u8; total_len]; + + // === IPv6 Header === + // Version (4) + Traffic Class (8) + Flow Label (20) + response[0] = 0x60; // Version 6, TC high bits = 0 + // response[1..4] = 0 (TC low bits + flow label) + + // Payload length (ICMPv6 header + body) + let payload_len = icmpv6_len as u16; + response[4..6].copy_from_slice(&payload_len.to_be_bytes()); + + // Next header = ICMPv6 + response[6] = IPPROTO_ICMPV6; + + // Hop limit + response[7] = 64; + + // Source = our address + response[8..24].copy_from_slice(&our_addr.octets()); + + // Destination = original source + response[24..40].copy_from_slice(&dest_addr.octets()); + + // === ICMPv6 Header === + let icmp_start = IPV6_HEADER_LEN; + + // Type = Destination Unreachable + response[icmp_start] = Icmpv6Type::DestinationUnreachable as u8; + + // Code + response[icmp_start + 1] = code as u8; + + // Checksum placeholder (calculated below) + // response[icmp_start + 2..icmp_start + 4] = 0 + + // Unused (4 bytes of zeros for Dest Unreachable) + // response[icmp_start + 4..icmp_start + 8] = 0 + + // === ICMPv6 Body === + // As much of original packet as fits + response[icmp_start + ICMPV6_HEADER_LEN..].copy_from_slice(&original_packet[..original_len]); + + // Calculate checksum + let checksum = icmpv6_checksum(&response[icmp_start..], &our_addr, &dest_addr); + response[icmp_start + 2..icmp_start + 4].copy_from_slice(&checksum.to_be_bytes()); + + Some(response) +} + +/// Calculate ICMPv6 checksum per RFC 4443. +/// +/// The checksum is calculated over a pseudo-header plus the ICMPv6 message. +fn icmpv6_checksum(icmpv6_message: &[u8], src: &Ipv6Addr, dst: &Ipv6Addr) -> u16 { + let mut sum: u32 = 0; + + // Pseudo-header: source address (16 bytes) + for chunk in src.octets().chunks(2) { + sum += u16::from_be_bytes([chunk[0], chunk[1]]) as u32; + } + + // Pseudo-header: destination address (16 bytes) + for chunk in dst.octets().chunks(2) { + sum += u16::from_be_bytes([chunk[0], chunk[1]]) as u32; + } + + // Pseudo-header: upper-layer packet length (4 bytes, as u32) + let len = icmpv6_message.len() as u32; + sum += (len >> 16) as u32; + sum += (len & 0xffff) as u32; + + // Pseudo-header: next header (padded to 4 bytes) + sum += IPPROTO_ICMPV6 as u32; + + // ICMPv6 message (with checksum field = 0) + let mut i = 0; + while i + 1 < icmpv6_message.len() { + // Skip the checksum field (bytes 2-3) + if i == 2 { + i += 2; + continue; + } + sum += u16::from_be_bytes([icmpv6_message[i], icmpv6_message[i + 1]]) as u32; + i += 2; + } + + // Handle odd byte + if i < icmpv6_message.len() { + sum += (icmpv6_message[i] as u32) << 8; + } + + // Fold 32-bit sum to 16 bits + while sum >> 16 != 0 { + sum = (sum & 0xffff) + (sum >> 16); + } + + // One's complement + !(sum as u16) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_ipv6_packet(src: Ipv6Addr, dst: Ipv6Addr, next_header: u8, payload: &[u8]) -> Vec { + let mut packet = vec![0u8; IPV6_HEADER_LEN + payload.len()]; + + // Version + TC + Flow Label + packet[0] = 0x60; + + // Payload length + let len = payload.len() as u16; + packet[4..6].copy_from_slice(&len.to_be_bytes()); + + // Next header + packet[6] = next_header; + + // Hop limit + packet[7] = 64; + + // Source + packet[8..24].copy_from_slice(&src.octets()); + + // Destination + packet[24..40].copy_from_slice(&dst.octets()); + + // Payload + packet[IPV6_HEADER_LEN..].copy_from_slice(payload); + + packet + } + + #[test] + fn test_should_send_error_valid_packet() { + let src = "fd00::1".parse().unwrap(); + let dst = "fd00::2".parse().unwrap(); + let packet = make_ipv6_packet(src, dst, 17, &[0u8; 8]); // UDP + + assert!(should_send_icmp_error(&packet)); + } + + #[test] + fn test_should_not_send_error_unspecified_source() { + let src = Ipv6Addr::UNSPECIFIED; + let dst = "fd00::2".parse().unwrap(); + let packet = make_ipv6_packet(src, dst, 17, &[0u8; 8]); + + assert!(!should_send_icmp_error(&packet)); + } + + #[test] + fn test_should_not_send_error_multicast_source() { + let src = "ff02::1".parse().unwrap(); + let dst = "fd00::2".parse().unwrap(); + let packet = make_ipv6_packet(src, dst, 17, &[0u8; 8]); + + assert!(!should_send_icmp_error(&packet)); + } + + #[test] + fn test_should_not_send_error_for_icmp_error() { + let src = "fd00::1".parse().unwrap(); + let dst = "fd00::2".parse().unwrap(); + // ICMPv6 Destination Unreachable (type 1) + let icmp_payload = [1u8, 0, 0, 0, 0, 0, 0, 0]; + let packet = make_ipv6_packet(src, dst, IPPROTO_ICMPV6, &icmp_payload); + + assert!(!should_send_icmp_error(&packet)); + } + + #[test] + fn test_should_send_error_for_icmp_echo() { + let src = "fd00::1".parse().unwrap(); + let dst = "fd00::2".parse().unwrap(); + // ICMPv6 Echo Request (type 128) - informational, not error + let icmp_payload = [128u8, 0, 0, 0, 0, 0, 0, 0]; + let packet = make_ipv6_packet(src, dst, IPPROTO_ICMPV6, &icmp_payload); + + assert!(should_send_icmp_error(&packet)); + } + + #[test] + fn test_should_not_send_error_short_packet() { + let packet = vec![0u8; 20]; // Too short for IPv6 + assert!(!should_send_icmp_error(&packet)); + } + + #[test] + fn test_build_dest_unreachable() { + let src: Ipv6Addr = "fd00::1".parse().unwrap(); + let dst: Ipv6Addr = "fd00::2".parse().unwrap(); + let original = make_ipv6_packet(src, dst, 17, &[0u8; 8]); + + let our_addr: Ipv6Addr = "fd00::ffff".parse().unwrap(); + let response = build_dest_unreachable(&original, DestUnreachableCode::NoRoute, our_addr); + + assert!(response.is_some()); + let response = response.unwrap(); + + // Check IPv6 header + assert_eq!(response[0] >> 4, 6); // Version + assert_eq!(response[6], IPPROTO_ICMPV6); // Next header + + // Check source is our address + let resp_src = Ipv6Addr::from(<[u8; 16]>::try_from(&response[8..24]).unwrap()); + assert_eq!(resp_src, our_addr); + + // Check destination is original source + let resp_dst = Ipv6Addr::from(<[u8; 16]>::try_from(&response[24..40]).unwrap()); + assert_eq!(resp_dst, src); + + // Check ICMPv6 type and code + assert_eq!(response[IPV6_HEADER_LEN], 1); // Type = Dest Unreachable + assert_eq!(response[IPV6_HEADER_LEN + 1], 0); // Code = No Route + } + + #[test] + fn test_build_dest_unreachable_invalid_input() { + let short_packet = vec![0u8; 20]; + let our_addr: Ipv6Addr = "fd00::ffff".parse().unwrap(); + + let response = build_dest_unreachable(&short_packet, DestUnreachableCode::NoRoute, our_addr); + assert!(response.is_none()); + } + + #[test] + fn test_build_dest_unreachable_truncates_large_packet() { + let src: Ipv6Addr = "fd00::1".parse().unwrap(); + let dst: Ipv6Addr = "fd00::2".parse().unwrap(); + // Large payload + let original = make_ipv6_packet(src, dst, 17, &[0u8; 2000]); + + let our_addr: Ipv6Addr = "fd00::ffff".parse().unwrap(); + let response = build_dest_unreachable(&original, DestUnreachableCode::NoRoute, our_addr); + + assert!(response.is_some()); + let response = response.unwrap(); + + // Response must not exceed minimum MTU + assert!(response.len() <= MIN_IPV6_MTU); + } +} diff --git a/src/lib.rs b/src/lib.rs index 4d021a9..fb7739b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ pub mod bloom; pub mod cache; pub mod config; +pub mod icmp; pub mod identity; pub mod node; pub mod peer; @@ -52,4 +53,7 @@ pub use peer::{Peer, PeerError, PeerState, UpstreamPeer}; pub use node::{Node, NodeError, NodeState}; // Re-export TUN types -pub use tun::{log_ipv6_packet, shutdown_tun_interface, TunDevice, TunError, TunState}; +pub use tun::{log_ipv6_packet, shutdown_tun_interface, TunDevice, TunError, TunState, TunTx, TunWriter}; + +// Re-export ICMPv6 types +pub use icmp::{build_dest_unreachable, should_send_icmp_error, DestUnreachableCode, Icmpv6Type}; diff --git a/src/tun.rs b/src/tun.rs index 89ee0d1..bae7b4a 100644 --- a/src/tun.rs +++ b/src/tun.rs @@ -7,12 +7,18 @@ use crate::{FipsAddress, TunConfig}; use futures::TryStreamExt; use rtnetlink::{new_connection, Handle}; -use std::io::Read; +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, info}; +use tracing::{debug, error, info}; use tun::Layer; +/// Channel sender for packets to be written to TUN. +pub type TunTx = mpsc::Sender>; + /// Errors that can occur with TUN operations. #[derive(Debug, Error)] pub enum TunError { @@ -150,6 +156,72 @@ impl TunDevice { 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. + pub fn create_writer(&self) -> 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(), + }, + 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. +pub struct TunWriter { + file: File, + rx: mpsc::Receiver>, + name: String, +} + +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) { + info!(name = %self.name, "TUN writer starting"); + + for packet in self.rx { + 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") { + info!(name = %self.name, "TUN interface deleted, writer stopping"); + break; + } + error!(name = %self.name, error = %e, "TUN write error"); + } else { + debug!(name = %self.name, len = packet.len(), "TUN packet written"); + } + } + + info!(name = %self.name, "TUN writer stopped"); + } } /// Log basic information about an IPv6 packet at DEBUG level.