use std::net::Ipv4Addr; /// Overhead breakdown for VPN tunnel encapsulation. #[derive(Debug, Clone)] pub struct TunnelOverhead { /// Outer IP header: 20 bytes (IPv4, no options). pub ip_header: u16, /// TCP header: typically 32 bytes (20 base + 12 for timestamps). pub tcp_header: u16, /// WebSocket framing: ~6 bytes (2 base + 4 mask from client). pub ws_framing: u16, /// VPN binary frame header: 5 bytes [type:1B][length:4B]. pub vpn_header: u16, /// Noise AEAD tag: 16 bytes (Poly1305). pub noise_tag: u16, } impl TunnelOverhead { /// Conservative default overhead estimate. pub fn default_overhead() -> Self { Self { ip_header: 20, tcp_header: 32, ws_framing: 6, vpn_header: 5, noise_tag: 16, } } /// Total encapsulation overhead in bytes. pub fn total(&self) -> u16 { self.ip_header + self.tcp_header + self.ws_framing + self.vpn_header + self.noise_tag } /// Compute effective TUN MTU given the underlying link MTU. pub fn effective_tun_mtu(&self, link_mtu: u16) -> u16 { link_mtu.saturating_sub(self.total()) } } /// MTU configuration for the VPN tunnel. #[derive(Debug, Clone)] pub struct MtuConfig { /// Underlying link MTU (typically 1500 for Ethernet). pub link_mtu: u16, /// Computed effective TUN MTU. pub effective_mtu: u16, /// Whether to generate ICMP too-big for oversized packets. pub send_icmp_too_big: bool, /// Counter: oversized packets encountered. pub oversized_packets: u64, /// Counter: ICMP too-big messages generated. pub icmp_too_big_sent: u64, } impl MtuConfig { /// Create a new MTU config from the underlying link MTU. pub fn new(link_mtu: u16) -> Self { let overhead = TunnelOverhead::default_overhead(); let effective = overhead.effective_tun_mtu(link_mtu); Self { link_mtu, effective_mtu: effective, send_icmp_too_big: true, oversized_packets: 0, icmp_too_big_sent: 0, } } /// Check if a packet exceeds the effective MTU. pub fn is_oversized(&self, packet_len: usize) -> bool { packet_len > self.effective_mtu as usize } } /// Action to take after checking MTU. pub enum MtuAction { /// Packet is within MTU, forward normally. Forward, /// Packet is oversized; contains the ICMP too-big message to write back into TUN. SendIcmpTooBig(Vec), } /// Check packet against MTU config and return the appropriate action. pub fn check_mtu(packet: &[u8], config: &MtuConfig) -> MtuAction { if !config.is_oversized(packet.len()) { return MtuAction::Forward; } if !config.send_icmp_too_big { return MtuAction::Forward; } match generate_icmp_too_big(packet, config.effective_mtu) { Some(icmp) => MtuAction::SendIcmpTooBig(icmp), None => MtuAction::Forward, } } /// Generate an ICMPv4 Destination Unreachable / Fragmentation Needed message. /// /// Per RFC 792: Type 3, Code 4, with next-hop MTU in bytes 6-7 (RFC 1191). /// Returns the complete IP + ICMP packet to write back into the TUN device. pub fn generate_icmp_too_big(original_packet: &[u8], next_hop_mtu: u16) -> Option> { // Need at least 20 bytes of original IP header if original_packet.len() < 20 { return None; } // Verify it's IPv4 if original_packet[0] >> 4 != 4 { return None; } // Parse source/dest from original IP header let src_ip = Ipv4Addr::new( original_packet[12], original_packet[13], original_packet[14], original_packet[15], ); let dst_ip = Ipv4Addr::new( original_packet[16], original_packet[17], original_packet[18], original_packet[19], ); // ICMP payload: IP header + first 8 bytes of original datagram (per RFC 792) let icmp_data_len = original_packet.len().min(28); // 20 IP header + 8 bytes let icmp_payload = &original_packet[..icmp_data_len]; // Build ICMP message: type(1) + code(1) + checksum(2) + unused(2) + next_hop_mtu(2) + data let mut icmp = Vec::with_capacity(8 + icmp_data_len); icmp.push(3); // Type: Destination Unreachable icmp.push(4); // Code: Fragmentation Needed and DF was Set icmp.push(0); // Checksum placeholder icmp.push(0); icmp.push(0); // Unused icmp.push(0); icmp.extend_from_slice(&next_hop_mtu.to_be_bytes()); icmp.extend_from_slice(icmp_payload); // Compute ICMP checksum let cksum = internet_checksum(&icmp); icmp[2] = (cksum >> 8) as u8; icmp[3] = (cksum & 0xff) as u8; // Build IP header (ICMP response: FROM tunnel gateway TO original source) let total_len = (20 + icmp.len()) as u16; let mut ip = Vec::with_capacity(total_len as usize); ip.push(0x45); // Version 4, IHL 5 ip.push(0x00); // DSCP/ECN ip.extend_from_slice(&total_len.to_be_bytes()); ip.extend_from_slice(&[0, 0]); // Identification ip.extend_from_slice(&[0x40, 0x00]); // Flags: Don't Fragment, Fragment Offset: 0 ip.push(64); // TTL ip.push(1); // Protocol: ICMP ip.extend_from_slice(&[0, 0]); // Header checksum placeholder ip.extend_from_slice(&dst_ip.octets()); // Source: tunnel endpoint (was dst) ip.extend_from_slice(&src_ip.octets()); // Destination: original source // Compute IP header checksum let ip_cksum = internet_checksum(&ip[..20]); ip[10] = (ip_cksum >> 8) as u8; ip[11] = (ip_cksum & 0xff) as u8; ip.extend_from_slice(&icmp); Some(ip) } /// Standard Internet checksum (RFC 1071). fn internet_checksum(data: &[u8]) -> u16 { let mut sum: u32 = 0; let mut i = 0; while i + 1 < data.len() { sum += u16::from_be_bytes([data[i], data[i + 1]]) as u32; i += 2; } if i < data.len() { sum += (data[i] as u32) << 8; } while sum >> 16 != 0 { sum = (sum & 0xFFFF) + (sum >> 16); } !sum as u16 } #[cfg(test)] mod tests { use super::*; #[test] fn default_overhead_total() { let oh = TunnelOverhead::default_overhead(); assert_eq!(oh.total(), 79); // 20+32+6+5+16 } #[test] fn effective_mtu_for_ethernet() { let oh = TunnelOverhead::default_overhead(); let mtu = oh.effective_tun_mtu(1500); assert_eq!(mtu, 1421); // 1500 - 79 } #[test] fn effective_mtu_saturates_at_zero() { let oh = TunnelOverhead::default_overhead(); let mtu = oh.effective_tun_mtu(50); // Less than overhead assert_eq!(mtu, 0); } #[test] fn mtu_config_default() { let config = MtuConfig::new(1500); assert_eq!(config.effective_mtu, 1421); assert_eq!(config.link_mtu, 1500); assert!(config.send_icmp_too_big); } #[test] fn is_oversized() { let config = MtuConfig::new(1500); assert!(!config.is_oversized(1421)); assert!(config.is_oversized(1422)); } #[test] fn icmp_too_big_generation() { // Craft a minimal IPv4 packet let mut original = vec![0u8; 28]; original[0] = 0x45; // version 4, IHL 5 original[2..4].copy_from_slice(&1500u16.to_be_bytes()); // total length original[9] = 6; // TCP original[12..16].copy_from_slice(&[10, 0, 0, 1]); // src IP original[16..20].copy_from_slice(&[10, 0, 0, 2]); // dst IP let icmp_pkt = generate_icmp_too_big(&original, 1421).unwrap(); // Verify it's a valid IPv4 packet assert_eq!(icmp_pkt[0] >> 4, 4); // IPv4 assert_eq!(icmp_pkt[9], 1); // ICMP protocol // Source should be original dst (10.0.0.2) assert_eq!(&icmp_pkt[12..16], &[10, 0, 0, 2]); // Destination should be original src (10.0.0.1) assert_eq!(&icmp_pkt[16..20], &[10, 0, 0, 1]); // ICMP type 3, code 4 assert_eq!(icmp_pkt[20], 3); assert_eq!(icmp_pkt[21], 4); // Next-hop MTU at ICMP bytes 6-7 (offset 26-27 in IP packet) let mtu = u16::from_be_bytes([icmp_pkt[26], icmp_pkt[27]]); assert_eq!(mtu, 1421); } #[test] fn icmp_too_big_rejects_short_packet() { let short = vec![0u8; 10]; assert!(generate_icmp_too_big(&short, 1421).is_none()); } #[test] fn icmp_too_big_rejects_non_ipv4() { let mut pkt = vec![0u8; 40]; pkt[0] = 0x60; // IPv6 assert!(generate_icmp_too_big(&pkt, 1421).is_none()); } #[test] fn icmp_checksum_valid() { let mut original = vec![0u8; 28]; original[0] = 0x45; original[2..4].copy_from_slice(&1500u16.to_be_bytes()); original[9] = 6; original[12..16].copy_from_slice(&[192, 168, 1, 100]); original[16..20].copy_from_slice(&[10, 8, 0, 1]); let icmp_pkt = generate_icmp_too_big(&original, 1420).unwrap(); // Verify IP header checksum let ip_cksum = internet_checksum(&icmp_pkt[..20]); assert_eq!(ip_cksum, 0, "IP header checksum should verify to 0"); // Verify ICMP checksum let icmp_cksum = internet_checksum(&icmp_pkt[20..]); assert_eq!(icmp_cksum, 0, "ICMP checksum should verify to 0"); } #[test] fn check_mtu_forward() { let config = MtuConfig::new(1500); let pkt = vec![0u8; 1421]; // Exactly at MTU assert!(matches!(check_mtu(&pkt, &config), MtuAction::Forward)); } #[test] fn check_mtu_oversized_generates_icmp() { let config = MtuConfig::new(1500); let mut pkt = vec![0u8; 1500]; pkt[0] = 0x45; // Valid IPv4 pkt[12..16].copy_from_slice(&[10, 0, 0, 1]); pkt[16..20].copy_from_slice(&[10, 0, 0, 2]); match check_mtu(&pkt, &config) { MtuAction::SendIcmpTooBig(icmp) => { assert_eq!(icmp[20], 3); // ICMP type assert_eq!(icmp[21], 4); // ICMP code } MtuAction::Forward => panic!("Expected SendIcmpTooBig"), } } }