315 lines
9.9 KiB
Rust
315 lines
9.9 KiB
Rust
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<u8>),
|
|
}
|
|
|
|
/// 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<Vec<u8>> {
|
|
// 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"),
|
|
}
|
|
}
|
|
}
|