mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
feat: implement ICMP Packet Too Big and TCP MSS clamping for MTU handling
Add dual-approach MTU handling to prevent TCP connections from hanging when packets exceed the transport MTU after FIPS encapsulation. ICMPv6 Packet Too Big: - Generate RFC 4443 PTB messages for oversized packets at TUN outbound - Inject back via TUN for local delivery to the application - Per-source rate limiting (100ms interval, 10s entry expiry) - MTU check in handle_tun_outbound before session encapsulation TCP MSS Clamping: - Intercept SYN packets in run_tun_reader() (outbound) - Intercept SYN-ACK packets in TunWriter (inbound) - Clamp MSS option to fit within effective MTU (transport - 127 overhead) - Recalculate TCP checksum after modification Code organization: - ICMP, TCP MSS, and rate limiter modules in upper/ alongside existing protocol-specific packet handling (dns.rs, tun.rs) - Shared FIPS_OVERHEAD constant (127 bytes) and effective_ipv6_mtu() function in upper/icmp.rs - Node::effective_ipv6_mtu() delegates to the shared function - run_tun_reader() accepts actual transport MTU from config Example config corrections: - UDP transport MTU set to 1472 across all configs (correct max UDP payload for standard Ethernet: 1500 - 20 IPv4 - 8 UDP) - Startup logging of effective MTU and max MSS values
This commit is contained in:
committed by
Johnathan Corgan
parent
5f1c6c2c7c
commit
852f561fa0
@@ -232,7 +232,7 @@ dns:
|
||||
transports:
|
||||
udp:
|
||||
bind_addr: "0.0.0.0:4000"
|
||||
mtu: 1280
|
||||
mtu: 1197
|
||||
|
||||
peers:
|
||||
- npub: "npub1tdwa4vjrjl33pcjdpf2t4p027nl86xrx24g4d3avg4vwvayr3g8qhd84le"
|
||||
@@ -320,7 +320,7 @@ dns:
|
||||
transports:
|
||||
udp:
|
||||
bind_addr: "0.0.0.0:4000"
|
||||
mtu: 1280
|
||||
mtu: 1197
|
||||
|
||||
peers: []
|
||||
```
|
||||
|
||||
@@ -2,10 +2,19 @@ FROM debian:bookworm-slim
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
iproute2 iputils-ping dnsutils && \
|
||||
iproute2 iputils-ping dnsutils openssh-client openssh-server && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Setup SSH server with no authentication (test only!)
|
||||
RUN mkdir -p /var/run/sshd && \
|
||||
ssh-keygen -A && \
|
||||
sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config && \
|
||||
sed -i 's/#PermitEmptyPasswords no/PermitEmptyPasswords yes/' /etc/ssh/sshd_config && \
|
||||
sed -i 's/UsePAM yes/UsePAM no/' /etc/ssh/sshd_config && \
|
||||
passwd -d root
|
||||
|
||||
COPY fips /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/fips
|
||||
|
||||
ENTRYPOINT ["fips", "--config", "/etc/fips/fips.yaml"]
|
||||
# Start SSH server in background, then run FIPS
|
||||
ENTRYPOINT ["/bin/bash", "-c", "/usr/sbin/sshd && exec fips --config /etc/fips/fips.yaml"]
|
||||
|
||||
@@ -20,7 +20,7 @@ dns:
|
||||
transports:
|
||||
udp:
|
||||
bind_addr: "0.0.0.0:4000"
|
||||
mtu: 1280
|
||||
mtu: 1472
|
||||
|
||||
peers:
|
||||
- npub: "npub1tdwa4vjrjl33pcjdpf2t4p027nl86xrx24g4d3avg4vwvayr3g8qhd84le"
|
||||
|
||||
@@ -20,7 +20,7 @@ dns:
|
||||
transports:
|
||||
udp:
|
||||
bind_addr: "0.0.0.0:4000"
|
||||
mtu: 1280
|
||||
mtu: 1472
|
||||
|
||||
peers:
|
||||
- npub: "npub1sjlh2c3x9w7kjsqg2ay080n2lff2uvt325vpan33ke34rn8l5jcqawh57m"
|
||||
|
||||
@@ -20,7 +20,7 @@ dns:
|
||||
transports:
|
||||
udp:
|
||||
bind_addr: "0.0.0.0:4000"
|
||||
mtu: 1280
|
||||
mtu: 1472
|
||||
|
||||
peers:
|
||||
- npub: "npub1tdwa4vjrjl33pcjdpf2t4p027nl86xrx24g4d3avg4vwvayr3g8qhd84le"
|
||||
|
||||
@@ -20,7 +20,7 @@ dns:
|
||||
transports:
|
||||
udp:
|
||||
bind_addr: "0.0.0.0:4000"
|
||||
mtu: 1280
|
||||
mtu: 1472
|
||||
|
||||
peers:
|
||||
- npub: "npub1cld9yay0u24davpu6c35l4vldrhzvaq66pcqtg9a0j2cnjrn9rtsxx2pe6"
|
||||
|
||||
@@ -20,7 +20,7 @@ dns:
|
||||
transports:
|
||||
udp:
|
||||
bind_addr: "0.0.0.0:4000"
|
||||
mtu: 1280
|
||||
mtu: 1472
|
||||
|
||||
peers:
|
||||
- npub: "npub1n9lpnv0592cc2ps6nm0ca3qls642vx7yjsv35rkxqzj2vgds52sqgpverl"
|
||||
|
||||
@@ -20,7 +20,7 @@ dns:
|
||||
transports:
|
||||
udp:
|
||||
bind_addr: "0.0.0.0:4000"
|
||||
mtu: 1280
|
||||
mtu: 1472
|
||||
|
||||
peers:
|
||||
- npub: "npub1n9lpnv0592cc2ps6nm0ca3qls642vx7yjsv35rkxqzj2vgds52sqgpverl"
|
||||
|
||||
@@ -20,7 +20,7 @@ dns:
|
||||
transports:
|
||||
udp:
|
||||
bind_addr: "0.0.0.0:4000"
|
||||
mtu: 1280
|
||||
mtu: 1472
|
||||
|
||||
peers:
|
||||
- npub: "npub1cld9yay0u24davpu6c35l4vldrhzvaq66pcqtg9a0j2cnjrn9rtsxx2pe6"
|
||||
|
||||
@@ -20,7 +20,7 @@ dns:
|
||||
transports:
|
||||
udp:
|
||||
bind_addr: "0.0.0.0:4000"
|
||||
mtu: 1280
|
||||
mtu: 1472
|
||||
|
||||
peers:
|
||||
- npub: "npub1tdwa4vjrjl33pcjdpf2t4p027nl86xrx24g4d3avg4vwvayr3g8qhd84le"
|
||||
|
||||
@@ -20,7 +20,7 @@ dns:
|
||||
transports:
|
||||
udp:
|
||||
bind_addr: "0.0.0.0:4000"
|
||||
mtu: 1280
|
||||
mtu: 1472
|
||||
|
||||
peers:
|
||||
- npub: "npub1sjlh2c3x9w7kjsqg2ay080n2lff2uvt325vpan33ke34rn8l5jcqawh57m"
|
||||
|
||||
@@ -20,7 +20,7 @@ dns:
|
||||
transports:
|
||||
udp:
|
||||
bind_addr: "0.0.0.0:4000"
|
||||
mtu: 1280
|
||||
mtu: 1472
|
||||
|
||||
peers:
|
||||
- npub: "npub1sjlh2c3x9w7kjsqg2ay080n2lff2uvt325vpan33ke34rn8l5jcqawh57m"
|
||||
|
||||
@@ -16,7 +16,7 @@ x-fips-common: &fips-common
|
||||
- net.ipv6.conf.all.disable_ipv6=0
|
||||
restart: "no"
|
||||
environment:
|
||||
- RUST_LOG=info
|
||||
- RUST_LOG=trace
|
||||
|
||||
services:
|
||||
# ── Mesh topology ──────────────────────────────────────────────
|
||||
|
||||
@@ -522,12 +522,23 @@ impl Node {
|
||||
/// Extracts the destination FipsAddress, looks up the NodeAddr and PublicKey
|
||||
/// from the identity cache, and either sends through an established session
|
||||
/// or initiates a new one (queuing the packet until established).
|
||||
///
|
||||
/// Also performs MTU checking: if the packet (plus FIPS overhead) exceeds
|
||||
/// the transport MTU, an ICMP Packet Too Big message is sent back to the
|
||||
/// source and the packet is dropped.
|
||||
pub(in crate::node) async fn handle_tun_outbound(&mut self, ipv6_packet: Vec<u8>) {
|
||||
// Validate IPv6 header
|
||||
if ipv6_packet.len() < 40 || ipv6_packet[0] >> 4 != 6 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if packet will fit after FIPS encapsulation
|
||||
let effective_mtu = self.effective_ipv6_mtu() as usize;
|
||||
if ipv6_packet.len() > effective_mtu {
|
||||
self.send_icmpv6_packet_too_big(&ipv6_packet, effective_mtu as u16);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract destination FipsAddress prefix (IPv6 dest bytes 1-15)
|
||||
// IPv6 header: bytes 24-39 are dest addr, so prefix = bytes 25-39
|
||||
let mut prefix = [0u8; 15];
|
||||
@@ -586,6 +597,47 @@ impl Node {
|
||||
}
|
||||
}
|
||||
|
||||
/// Send ICMPv6 Packet Too Big back through TUN.
|
||||
///
|
||||
/// Rate-limited per source address to prevent ICMP floods from
|
||||
/// misconfigured applications sending repeated oversized packets.
|
||||
pub(in crate::node) fn send_icmpv6_packet_too_big(&mut self, original_packet: &[u8], mtu: u16) {
|
||||
use crate::upper::icmp::build_packet_too_big;
|
||||
use crate::FipsAddress;
|
||||
use std::net::Ipv6Addr;
|
||||
|
||||
// Extract source address for rate limiting
|
||||
if original_packet.len() < 40 {
|
||||
return;
|
||||
}
|
||||
let src_addr = Ipv6Addr::from(<[u8; 16]>::try_from(&original_packet[8..24]).unwrap());
|
||||
|
||||
// Rate limit ICMP PTB messages per source
|
||||
if !self.icmp_rate_limiter.should_send(src_addr) {
|
||||
debug!(
|
||||
src = %src_addr,
|
||||
"Rate limiting ICMP Packet Too Big"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let our_ipv6 = FipsAddress::from_node_addr(self.node_addr()).to_ipv6();
|
||||
if let Some(response) = build_packet_too_big(original_packet, mtu, our_ipv6)
|
||||
&& let Some(tun_tx) = &self.tun_tx
|
||||
{
|
||||
debug!(
|
||||
src = %src_addr,
|
||||
dst = %our_ipv6,
|
||||
packet_size = original_packet.len(),
|
||||
reported_mtu = mtu,
|
||||
"Sending ICMP Packet Too Big (ICMP src={}, dst={})",
|
||||
our_ipv6,
|
||||
src_addr
|
||||
);
|
||||
let _ = tun_tx.send(response);
|
||||
}
|
||||
}
|
||||
|
||||
/// Queue a packet while waiting for session establishment.
|
||||
fn queue_pending_packet(&mut self, dest_addr: NodeAddr, packet: Vec<u8>) {
|
||||
// Reject if we already have too many pending destinations
|
||||
|
||||
+10
-2
@@ -277,8 +277,15 @@ impl Node {
|
||||
info!(" address: {}", device.address());
|
||||
info!(" mtu: {}", mtu);
|
||||
|
||||
// Calculate max MSS for TCP clamping
|
||||
let effective_mtu = self.effective_ipv6_mtu();
|
||||
let max_mss = effective_mtu.saturating_sub(40).saturating_sub(20); // IPv6 + TCP headers
|
||||
|
||||
info!("effective MTU: {} bytes", effective_mtu);
|
||||
info!(" max TCP MSS: {} bytes", max_mss);
|
||||
|
||||
// Create writer (dups the fd for independent write access)
|
||||
let (writer, tun_tx) = device.create_writer()?;
|
||||
let (writer, tun_tx) = device.create_writer(max_mss)?;
|
||||
|
||||
// Spawn writer thread
|
||||
let writer_handle = thread::spawn(move || {
|
||||
@@ -293,8 +300,9 @@ impl Node {
|
||||
let (outbound_tx, outbound_rx) = tokio::sync::mpsc::channel(tun_channel_size);
|
||||
|
||||
// Spawn reader thread
|
||||
let transport_mtu = self.transport_mtu();
|
||||
let reader_handle = thread::spawn(move || {
|
||||
run_tun_reader(device, mtu, our_addr, reader_tun_tx, outbound_tx);
|
||||
run_tun_reader(device, mtu, our_addr, reader_tun_tx, outbound_tx, transport_mtu);
|
||||
});
|
||||
|
||||
self.tun_state = TunState::Active;
|
||||
|
||||
@@ -26,6 +26,7 @@ use crate::transport::{
|
||||
};
|
||||
use crate::transport::udp::UdpTransport;
|
||||
use crate::tree::TreeState;
|
||||
use crate::upper::icmp_rate_limit::IcmpRateLimiter;
|
||||
use crate::upper::tun::{TunError, TunOutboundRx, TunState, TunTx};
|
||||
use self::wire::build_encrypted;
|
||||
use crate::{Config, ConfigError, Identity, IdentityError, NodeAddr};
|
||||
@@ -313,6 +314,8 @@ pub struct Node {
|
||||
// === Rate Limiting ===
|
||||
/// Rate limiter for msg1 processing (DoS protection).
|
||||
msg1_rate_limiter: HandshakeRateLimiter,
|
||||
/// Rate limiter for ICMP Packet Too Big messages.
|
||||
icmp_rate_limiter: IcmpRateLimiter,
|
||||
|
||||
// === Tree Announce Timing ===
|
||||
/// Last time we refreshed our root announcement (Unix seconds).
|
||||
@@ -406,6 +409,7 @@ impl Node {
|
||||
peers_by_index: HashMap::new(),
|
||||
pending_outbound: HashMap::new(),
|
||||
msg1_rate_limiter,
|
||||
icmp_rate_limiter: IcmpRateLimiter::new(),
|
||||
last_root_refresh_secs: 0,
|
||||
retry_pending: HashMap::new(),
|
||||
})
|
||||
@@ -483,6 +487,7 @@ impl Node {
|
||||
peers_by_index: HashMap::new(),
|
||||
pending_outbound: HashMap::new(),
|
||||
msg1_rate_limiter,
|
||||
icmp_rate_limiter: IcmpRateLimiter::new(),
|
||||
last_root_refresh_secs: 0,
|
||||
retry_pending: HashMap::new(),
|
||||
}
|
||||
@@ -558,6 +563,29 @@ impl Node {
|
||||
&self.config
|
||||
}
|
||||
|
||||
/// Calculate the effective IPv6 MTU that can be sent over FIPS.
|
||||
///
|
||||
/// Delegates to `upper::icmp::effective_ipv6_mtu()` with this node's
|
||||
/// transport MTU. Returns the maximum IPv6 packet size (including
|
||||
/// IPv6 header) that can be transmitted through the FIPS mesh.
|
||||
pub fn effective_ipv6_mtu(&self) -> u16 {
|
||||
crate::upper::icmp::effective_ipv6_mtu(self.transport_mtu())
|
||||
}
|
||||
|
||||
/// Get the transport MTU from configuration.
|
||||
///
|
||||
/// Returns the MTU of the first configured UDP transport, or 1280
|
||||
/// (IPv6 minimum) as fallback.
|
||||
pub fn transport_mtu(&self) -> u16 {
|
||||
self.config
|
||||
.transports
|
||||
.udp
|
||||
.iter()
|
||||
.next()
|
||||
.map(|(_, cfg)| cfg.mtu())
|
||||
.unwrap_or(1280)
|
||||
}
|
||||
|
||||
// === State ===
|
||||
|
||||
/// Get the node state.
|
||||
|
||||
@@ -59,6 +59,25 @@ 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;
|
||||
|
||||
/// Total FIPS encapsulation overhead (worst case).
|
||||
///
|
||||
/// Breakdown:
|
||||
/// - Noise encryption tag: 16 bytes
|
||||
/// - Encrypted frame header: 13 bytes
|
||||
/// - SessionDatagram header: 34 bytes
|
||||
/// - DataPacket header: 4 bytes
|
||||
/// - Coordinates (worst case): ~60 bytes (2 coords with depth 3 each)
|
||||
pub const FIPS_OVERHEAD: u16 = 16 + 13 + 34 + 4 + 60; // 127 bytes
|
||||
|
||||
/// Calculate the effective IPv6 MTU for FIPS-encapsulated traffic.
|
||||
///
|
||||
/// Given a transport MTU (e.g., UDP payload size), returns the maximum
|
||||
/// IPv6 packet size (including IPv6 header) that can be transmitted
|
||||
/// through the FIPS mesh after encapsulation overhead.
|
||||
pub fn effective_ipv6_mtu(transport_mtu: u16) -> u16 {
|
||||
transport_mtu.saturating_sub(FIPS_OVERHEAD)
|
||||
}
|
||||
|
||||
/// Check if we should send an ICMPv6 error for this packet.
|
||||
///
|
||||
/// Returns false if the packet is:
|
||||
@@ -193,6 +212,124 @@ pub fn build_dest_unreachable(
|
||||
Some(response)
|
||||
}
|
||||
|
||||
/// Build an ICMPv6 Packet Too Big response.
|
||||
///
|
||||
/// RFC 4443 Section 3.2: Packet Too Big Message
|
||||
///
|
||||
/// ## Wire Format
|
||||
/// ```text
|
||||
/// 0 1 2 3
|
||||
/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/// | Type=2 | Code=0 | Checksum |
|
||||
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/// | MTU |
|
||||
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/// | As much of invoking packet |
|
||||
/// + as possible without exceeding 1280 +
|
||||
/// | |
|
||||
/// ```
|
||||
///
|
||||
/// ## Parameters
|
||||
/// - `original_packet`: The oversized IPv6 packet that triggered this error
|
||||
/// - `mtu`: The MTU value to report (effective IPv6 MTU after FIPS overhead)
|
||||
/// - `our_addr`: Our FIPS IPv6 address (source of ICMP message)
|
||||
///
|
||||
/// ## Returns
|
||||
/// Complete IPv6 packet containing the ICMP Packet Too Big message,
|
||||
/// ready to be written to the TUN interface.
|
||||
pub fn build_packet_too_big(
|
||||
original_packet: &[u8],
|
||||
mtu: u16,
|
||||
our_addr: Ipv6Addr,
|
||||
) -> Option<Vec<u8>> {
|
||||
// Validate original packet
|
||||
if original_packet.len() < IPV6_HEADER_LEN {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Must be IPv6
|
||||
let version = original_packet[0] >> 4;
|
||||
if version != 6 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Extract source address from original packet (becomes ICMP destination)
|
||||
let src_addr = Ipv6Addr::from(<[u8; 16]>::try_from(&original_packet[8..24]).unwrap());
|
||||
|
||||
// Don't send ICMP in response to:
|
||||
// - Multicast sources (ff00::/8)
|
||||
// - Unspecified source (::)
|
||||
if src_addr.is_unspecified() || src_addr.octets()[0] == 0xff {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Don't send ICMP in response to ICMP errors (avoid loops)
|
||||
let next_header = original_packet[6];
|
||||
if next_header == IPPROTO_ICMPV6 && original_packet.len() > IPV6_HEADER_LEN {
|
||||
let icmp_type = original_packet[IPV6_HEADER_LEN];
|
||||
// ICMPv6 error messages are types 0-127
|
||||
if icmp_type < 128 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate how much of the original packet to include
|
||||
// RFC 4443: "as much of invoking packet as possible without exceeding 1280"
|
||||
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(&src_addr.octets());
|
||||
|
||||
// === ICMPv6 Header ===
|
||||
let icmp_start = IPV6_HEADER_LEN;
|
||||
|
||||
// Type = Packet Too Big
|
||||
response[icmp_start] = Icmpv6Type::PacketTooBig as u8;
|
||||
|
||||
// Code = 0 (always 0 for Packet Too Big)
|
||||
response[icmp_start + 1] = 0;
|
||||
|
||||
// Checksum placeholder (calculated below)
|
||||
// response[icmp_start + 2..icmp_start + 4] = 0
|
||||
|
||||
// MTU (4 bytes, but only first 2 bytes used, last 2 are unused/zero)
|
||||
response[icmp_start + 4..icmp_start + 6].copy_from_slice(&mtu.to_be_bytes());
|
||||
// response[icmp_start + 6..icmp_start + 8] = 0 (unused)
|
||||
|
||||
// === 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, &src_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.
|
||||
@@ -393,4 +530,126 @@ mod tests {
|
||||
// Response must not exceed minimum MTU
|
||||
assert!(response.len() <= MIN_IPV6_MTU);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_packet_too_big() {
|
||||
let src: Ipv6Addr = "fd00::1".parse().unwrap();
|
||||
let dst: Ipv6Addr = "fd00::2".parse().unwrap();
|
||||
let original = make_ipv6_packet(src, dst, 17, &[0u8; 1200]); // Large UDP packet
|
||||
|
||||
let our_addr: Ipv6Addr = "fd00::ffff".parse().unwrap();
|
||||
let mtu = 1070u16;
|
||||
let response = build_packet_too_big(&original, mtu, 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], 2); // Type = Packet Too Big
|
||||
assert_eq!(response[IPV6_HEADER_LEN + 1], 0); // Code = 0
|
||||
|
||||
// Check MTU value
|
||||
let reported_mtu = u16::from_be_bytes([
|
||||
response[IPV6_HEADER_LEN + 4],
|
||||
response[IPV6_HEADER_LEN + 5],
|
||||
]);
|
||||
assert_eq!(reported_mtu, mtu);
|
||||
|
||||
// Response must not exceed minimum MTU
|
||||
assert!(response.len() <= MIN_IPV6_MTU);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_packet_too_big_invalid_input() {
|
||||
let short_packet = vec![0u8; 20];
|
||||
let our_addr: Ipv6Addr = "fd00::ffff".parse().unwrap();
|
||||
|
||||
let response = build_packet_too_big(&short_packet, 1280, our_addr);
|
||||
assert!(response.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_packet_too_big_multicast_source() {
|
||||
let src: Ipv6Addr = "ff02::1".parse().unwrap(); // Multicast
|
||||
let dst: Ipv6Addr = "fd00::2".parse().unwrap();
|
||||
let original = make_ipv6_packet(src, dst, 17, &[0u8; 1200]);
|
||||
|
||||
let our_addr: Ipv6Addr = "fd00::ffff".parse().unwrap();
|
||||
let response = build_packet_too_big(&original, 1280, our_addr);
|
||||
|
||||
// Should not send ICMP for multicast source
|
||||
assert!(response.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_packet_too_big_unspecified_source() {
|
||||
let src = Ipv6Addr::UNSPECIFIED;
|
||||
let dst: Ipv6Addr = "fd00::2".parse().unwrap();
|
||||
let original = make_ipv6_packet(src, dst, 17, &[0u8; 1200]);
|
||||
|
||||
let our_addr: Ipv6Addr = "fd00::ffff".parse().unwrap();
|
||||
let response = build_packet_too_big(&original, 1280, our_addr);
|
||||
|
||||
// Should not send ICMP for unspecified source
|
||||
assert!(response.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_packet_too_big_for_icmp_error() {
|
||||
let src: Ipv6Addr = "fd00::1".parse().unwrap();
|
||||
let dst: Ipv6Addr = "fd00::2".parse().unwrap();
|
||||
// ICMPv6 Destination Unreachable (type 1) - an error message
|
||||
let icmp_payload = [1u8, 0, 0, 0, 0, 0, 0, 0];
|
||||
let original = make_ipv6_packet(src, dst, IPPROTO_ICMPV6, &icmp_payload);
|
||||
|
||||
let our_addr: Ipv6Addr = "fd00::ffff".parse().unwrap();
|
||||
let response = build_packet_too_big(&original, 1280, our_addr);
|
||||
|
||||
// Should not send ICMP in response to ICMP error
|
||||
assert!(response.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_packet_too_big_for_icmp_echo() {
|
||||
let src: Ipv6Addr = "fd00::1".parse().unwrap();
|
||||
let dst: Ipv6Addr = "fd00::2".parse().unwrap();
|
||||
// ICMPv6 Echo Request (type 128) - informational, not error
|
||||
let icmp_payload = [128u8, 0, 0, 0, 0, 0, 0, 0];
|
||||
let original = make_ipv6_packet(src, dst, IPPROTO_ICMPV6, &icmp_payload);
|
||||
|
||||
let our_addr: Ipv6Addr = "fd00::ffff".parse().unwrap();
|
||||
let response = build_packet_too_big(&original, 1280, our_addr);
|
||||
|
||||
// Should send ICMP for informational messages
|
||||
assert!(response.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_packet_too_big_truncates_large_packet() {
|
||||
let src: Ipv6Addr = "fd00::1".parse().unwrap();
|
||||
let dst: Ipv6Addr = "fd00::2".parse().unwrap();
|
||||
// Very large payload
|
||||
let original = make_ipv6_packet(src, dst, 17, &[0u8; 2000]);
|
||||
|
||||
let our_addr: Ipv6Addr = "fd00::ffff".parse().unwrap();
|
||||
let response = build_packet_too_big(&original, 1070, our_addr);
|
||||
|
||||
assert!(response.is_some());
|
||||
let response = response.unwrap();
|
||||
|
||||
// Response must not exceed minimum MTU
|
||||
assert!(response.len() <= MIN_IPV6_MTU);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
//! ICMP Packet Too Big rate limiting.
|
||||
//!
|
||||
//! Prevents ICMP flood from repeated oversized packets by rate-limiting
|
||||
//! ICMP Packet Too Big messages per source address.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::net::Ipv6Addr;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Rate limiter for ICMP Packet Too Big messages.
|
||||
///
|
||||
/// Tracks the last time an ICMP PTB was sent to each source address
|
||||
/// and enforces a minimum interval between messages to prevent floods.
|
||||
pub struct IcmpRateLimiter {
|
||||
/// Maps source IPv6 address to the last time we sent ICMP PTB to it.
|
||||
last_sent: HashMap<Ipv6Addr, Instant>,
|
||||
/// Minimum interval between ICMP messages to the same source.
|
||||
min_interval: Duration,
|
||||
/// Maximum age of entries before cleanup (prevents unbounded growth).
|
||||
max_age: Duration,
|
||||
}
|
||||
|
||||
impl IcmpRateLimiter {
|
||||
/// Create a new rate limiter.
|
||||
///
|
||||
/// Default: max 10 ICMP/sec per source (100ms interval).
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
last_sent: HashMap::new(),
|
||||
min_interval: Duration::from_millis(100),
|
||||
max_age: Duration::from_secs(10),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a rate limiter with custom interval.
|
||||
pub fn with_interval(min_interval: Duration) -> Self {
|
||||
Self {
|
||||
last_sent: HashMap::new(),
|
||||
min_interval,
|
||||
max_age: Duration::from_secs(10),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if we should send an ICMP PTB to this source address.
|
||||
///
|
||||
/// Returns true if enough time has passed since the last ICMP to this source,
|
||||
/// or if this is the first ICMP to this source.
|
||||
///
|
||||
/// If true is returned, the internal state is updated to record this send.
|
||||
pub fn should_send(&mut self, src_addr: Ipv6Addr) -> bool {
|
||||
let now = Instant::now();
|
||||
|
||||
// Check if we've sent to this source recently
|
||||
if let Some(&last) = self.last_sent.get(&src_addr)
|
||||
&& now.duration_since(last) < self.min_interval
|
||||
{
|
||||
return false; // Too soon, rate limit
|
||||
}
|
||||
|
||||
// Update last sent time
|
||||
self.last_sent.insert(src_addr, now);
|
||||
|
||||
// Cleanup old entries to prevent unbounded growth
|
||||
self.cleanup(now);
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Remove entries older than max_age.
|
||||
fn cleanup(&mut self, now: Instant) {
|
||||
self.last_sent.retain(|_, &mut last| {
|
||||
now.duration_since(last) < self.max_age
|
||||
});
|
||||
}
|
||||
|
||||
/// Get the number of tracked sources.
|
||||
#[cfg(test)]
|
||||
pub fn len(&self) -> usize {
|
||||
self.last_sent.len()
|
||||
}
|
||||
|
||||
/// Check if there are no tracked sources.
|
||||
#[cfg(test)]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.last_sent.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for IcmpRateLimiter {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::thread;
|
||||
|
||||
#[test]
|
||||
fn test_first_send_allowed() {
|
||||
let mut limiter = IcmpRateLimiter::new();
|
||||
let addr: Ipv6Addr = "fd00::1".parse().unwrap();
|
||||
|
||||
assert!(limiter.should_send(addr));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rapid_sends_rate_limited() {
|
||||
let mut limiter = IcmpRateLimiter::new();
|
||||
let addr: Ipv6Addr = "fd00::1".parse().unwrap();
|
||||
|
||||
// First send should succeed
|
||||
assert!(limiter.should_send(addr));
|
||||
|
||||
// Immediate second send should be rate limited
|
||||
assert!(!limiter.should_send(addr));
|
||||
assert!(!limiter.should_send(addr));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_sources_independent() {
|
||||
let mut limiter = IcmpRateLimiter::new();
|
||||
let addr1: Ipv6Addr = "fd00::1".parse().unwrap();
|
||||
let addr2: Ipv6Addr = "fd00::2".parse().unwrap();
|
||||
|
||||
// Both sources should be allowed independently
|
||||
assert!(limiter.should_send(addr1));
|
||||
assert!(limiter.should_send(addr2));
|
||||
|
||||
// But rapid resends to same source are limited
|
||||
assert!(!limiter.should_send(addr1));
|
||||
assert!(!limiter.should_send(addr2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_send_allowed_after_interval() {
|
||||
let mut limiter = IcmpRateLimiter::with_interval(Duration::from_millis(50));
|
||||
let addr: Ipv6Addr = "fd00::1".parse().unwrap();
|
||||
|
||||
// First send
|
||||
assert!(limiter.should_send(addr));
|
||||
|
||||
// Wait for interval to pass
|
||||
thread::sleep(Duration::from_millis(60));
|
||||
|
||||
// Second send should now be allowed
|
||||
assert!(limiter.should_send(addr));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cleanup_removes_old_entries() {
|
||||
let mut limiter = IcmpRateLimiter::new();
|
||||
let addr1: Ipv6Addr = "fd00::1".parse().unwrap();
|
||||
let addr2: Ipv6Addr = "fd00::2".parse().unwrap();
|
||||
|
||||
// Send to both addresses
|
||||
assert!(limiter.should_send(addr1));
|
||||
assert!(limiter.should_send(addr2));
|
||||
assert_eq!(limiter.len(), 2);
|
||||
|
||||
// Manually trigger cleanup with a future timestamp
|
||||
let future = Instant::now() + Duration::from_secs(11);
|
||||
limiter.cleanup(future);
|
||||
|
||||
// All entries should be cleaned up
|
||||
assert_eq!(limiter.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cleanup_preserves_recent_entries() {
|
||||
let mut limiter = IcmpRateLimiter::new();
|
||||
let addr: Ipv6Addr = "fd00::1".parse().unwrap();
|
||||
|
||||
assert!(limiter.should_send(addr));
|
||||
assert_eq!(limiter.len(), 1);
|
||||
|
||||
// Cleanup with current time shouldn't remove recent entry
|
||||
limiter.cleanup(Instant::now());
|
||||
assert_eq!(limiter.len(), 1);
|
||||
}
|
||||
}
|
||||
@@ -8,4 +8,6 @@
|
||||
pub mod config;
|
||||
pub mod dns;
|
||||
pub mod icmp;
|
||||
pub mod icmp_rate_limit;
|
||||
pub mod tcp_mss;
|
||||
pub mod tun;
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
//! TCP MSS (Maximum Segment Size) clamping for MTU handling.
|
||||
//!
|
||||
//! Intercepts TCP SYN packets and reduces the MSS option to ensure
|
||||
//! TCP segments fit within the FIPS effective MTU after encapsulation.
|
||||
|
||||
/// TCP header minimum length (without options).
|
||||
const TCP_HEADER_MIN_LEN: usize = 20;
|
||||
|
||||
/// TCP option kind for MSS.
|
||||
const TCP_OPT_MSS: u8 = 2;
|
||||
|
||||
/// TCP option length for MSS (kind + length + value).
|
||||
const TCP_OPT_MSS_LEN: u8 = 4;
|
||||
|
||||
/// TCP flags offset in header.
|
||||
const TCP_FLAGS_OFFSET: usize = 13;
|
||||
|
||||
/// TCP SYN flag bit.
|
||||
const TCP_FLAG_SYN: u8 = 0x02;
|
||||
|
||||
/// Check if a TCP packet is a SYN packet (has SYN flag set).
|
||||
fn is_tcp_syn(tcp_header: &[u8]) -> bool {
|
||||
if tcp_header.len() < TCP_HEADER_MIN_LEN {
|
||||
return false;
|
||||
}
|
||||
(tcp_header[TCP_FLAGS_OFFSET] & TCP_FLAG_SYN) != 0
|
||||
}
|
||||
|
||||
/// Get the TCP data offset (header length in 32-bit words).
|
||||
fn get_tcp_data_offset(tcp_header: &[u8]) -> usize {
|
||||
if tcp_header.len() < TCP_HEADER_MIN_LEN {
|
||||
return 0;
|
||||
}
|
||||
((tcp_header[12] >> 4) as usize) * 4
|
||||
}
|
||||
|
||||
/// Clamp TCP MSS in a SYN packet if needed.
|
||||
///
|
||||
/// Searches for the MSS option in TCP options and reduces it if it exceeds
|
||||
/// the maximum safe MSS for the given MTU.
|
||||
///
|
||||
/// Returns true if the packet was modified (MSS was clamped).
|
||||
pub fn clamp_tcp_mss(ipv6_packet: &mut [u8], max_mss: u16) -> bool {
|
||||
// Validate IPv6 header
|
||||
if ipv6_packet.len() < 40 || ipv6_packet[0] >> 4 != 6 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if next header is TCP (6)
|
||||
let next_header = ipv6_packet[6];
|
||||
if next_header != 6 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get TCP header start
|
||||
let tcp_start = 40;
|
||||
if ipv6_packet.len() < tcp_start + TCP_HEADER_MIN_LEN {
|
||||
return false;
|
||||
}
|
||||
|
||||
let tcp_header = &ipv6_packet[tcp_start..];
|
||||
|
||||
// Only process SYN packets
|
||||
if !is_tcp_syn(tcp_header) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get TCP header length
|
||||
let tcp_header_len = get_tcp_data_offset(tcp_header);
|
||||
if tcp_header_len < TCP_HEADER_MIN_LEN || tcp_header_len > tcp_header.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse TCP options
|
||||
let options_start = tcp_start + TCP_HEADER_MIN_LEN;
|
||||
let options_end = tcp_start + tcp_header_len;
|
||||
|
||||
if options_end > ipv6_packet.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut modified = false;
|
||||
let mut i = options_start;
|
||||
|
||||
while i < options_end {
|
||||
let kind = ipv6_packet[i];
|
||||
|
||||
// End of options
|
||||
if kind == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
// NOP (padding)
|
||||
if kind == 1 {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// All other options have length field
|
||||
if i + 1 >= options_end {
|
||||
break;
|
||||
}
|
||||
|
||||
let length = ipv6_packet[i + 1] as usize;
|
||||
if length < 2 || i + length > options_end {
|
||||
break;
|
||||
}
|
||||
|
||||
// Check for MSS option
|
||||
if kind == TCP_OPT_MSS && length == TCP_OPT_MSS_LEN as usize {
|
||||
// Read current MSS value
|
||||
let current_mss = u16::from_be_bytes([ipv6_packet[i + 2], ipv6_packet[i + 3]]);
|
||||
|
||||
// Clamp if needed
|
||||
if current_mss > max_mss {
|
||||
ipv6_packet[i + 2..i + 4].copy_from_slice(&max_mss.to_be_bytes());
|
||||
|
||||
// Recalculate TCP checksum
|
||||
recalculate_tcp_checksum(ipv6_packet, tcp_start);
|
||||
|
||||
modified = true;
|
||||
}
|
||||
break; // MSS option found, no need to continue
|
||||
}
|
||||
|
||||
i += length;
|
||||
}
|
||||
|
||||
modified
|
||||
}
|
||||
|
||||
/// Recalculate TCP checksum after modifying the packet.
|
||||
fn recalculate_tcp_checksum(ipv6_packet: &mut [u8], tcp_start: usize) {
|
||||
// Zero out existing checksum
|
||||
ipv6_packet[tcp_start + 16] = 0;
|
||||
ipv6_packet[tcp_start + 17] = 0;
|
||||
|
||||
// Extract addresses
|
||||
let src = &ipv6_packet[8..24];
|
||||
let dst = &ipv6_packet[24..40];
|
||||
|
||||
// Get TCP segment length
|
||||
let payload_len = u16::from_be_bytes([ipv6_packet[4], ipv6_packet[5]]) as usize;
|
||||
let tcp_segment = &ipv6_packet[tcp_start..tcp_start + payload_len];
|
||||
|
||||
// Calculate checksum with pseudo-header
|
||||
let mut sum: u32 = 0;
|
||||
|
||||
// Pseudo-header: source address
|
||||
for chunk in src.chunks(2) {
|
||||
sum += u16::from_be_bytes([chunk[0], chunk[1]]) as u32;
|
||||
}
|
||||
|
||||
// Pseudo-header: destination address
|
||||
for chunk in dst.chunks(2) {
|
||||
sum += u16::from_be_bytes([chunk[0], chunk[1]]) as u32;
|
||||
}
|
||||
|
||||
// Pseudo-header: TCP length
|
||||
sum += payload_len as u32;
|
||||
|
||||
// Pseudo-header: next header (TCP = 6)
|
||||
sum += 6;
|
||||
|
||||
// TCP segment
|
||||
for chunk in tcp_segment.chunks(2) {
|
||||
let value = if chunk.len() == 2 {
|
||||
u16::from_be_bytes([chunk[0], chunk[1]])
|
||||
} else {
|
||||
u16::from_be_bytes([chunk[0], 0])
|
||||
};
|
||||
sum += value as u32;
|
||||
}
|
||||
|
||||
// Fold 32-bit sum to 16 bits
|
||||
while sum >> 16 != 0 {
|
||||
sum = (sum & 0xffff) + (sum >> 16);
|
||||
}
|
||||
|
||||
// One's complement
|
||||
let checksum = !sum as u16;
|
||||
ipv6_packet[tcp_start + 16..tcp_start + 18].copy_from_slice(&checksum.to_be_bytes());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_tcp_syn_packet(src: [u8; 16], dst: [u8; 16], mss: u16) -> Vec<u8> {
|
||||
let mut packet = vec![0u8; 40 + 40]; // IPv6 + TCP with options
|
||||
|
||||
// IPv6 header
|
||||
packet[0] = 0x60; // Version 6
|
||||
packet[4..6].copy_from_slice(&40u16.to_be_bytes()); // Payload length
|
||||
packet[6] = 6; // Next header = TCP
|
||||
packet[7] = 64; // Hop limit
|
||||
packet[8..24].copy_from_slice(&src);
|
||||
packet[24..40].copy_from_slice(&dst);
|
||||
|
||||
// TCP header
|
||||
let tcp_start = 40;
|
||||
packet[tcp_start..tcp_start + 2].copy_from_slice(&12345u16.to_be_bytes()); // Source port
|
||||
packet[tcp_start + 2..tcp_start + 4].copy_from_slice(&80u16.to_be_bytes()); // Dest port
|
||||
packet[tcp_start + 4..tcp_start + 8].copy_from_slice(&1000u32.to_be_bytes()); // Seq
|
||||
packet[tcp_start + 8..tcp_start + 12].copy_from_slice(&0u32.to_be_bytes()); // Ack
|
||||
packet[tcp_start + 12] = 0xa0; // Data offset = 10 (40 bytes header)
|
||||
packet[tcp_start + 13] = TCP_FLAG_SYN; // Flags = SYN
|
||||
packet[tcp_start + 14..tcp_start + 16].copy_from_slice(&8192u16.to_be_bytes()); // Window
|
||||
|
||||
// TCP options: MSS
|
||||
packet[tcp_start + 20] = TCP_OPT_MSS; // Kind
|
||||
packet[tcp_start + 21] = TCP_OPT_MSS_LEN; // Length
|
||||
packet[tcp_start + 22..tcp_start + 24].copy_from_slice(&mss.to_be_bytes()); // MSS value
|
||||
|
||||
// End of options
|
||||
packet[tcp_start + 24] = 0;
|
||||
|
||||
// Calculate checksum
|
||||
recalculate_tcp_checksum(&mut packet, tcp_start);
|
||||
|
||||
packet
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clamp_tcp_mss_reduces_large_mss() {
|
||||
let src = [0xfd, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1];
|
||||
let dst = [0xfd, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2];
|
||||
let mut packet = make_tcp_syn_packet(src, dst, 1460);
|
||||
|
||||
let modified = clamp_tcp_mss(&mut packet, 1200);
|
||||
|
||||
assert!(modified);
|
||||
|
||||
// Check MSS was clamped
|
||||
let tcp_start = 40;
|
||||
let mss = u16::from_be_bytes([packet[tcp_start + 22], packet[tcp_start + 23]]);
|
||||
assert_eq!(mss, 1200);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clamp_tcp_mss_leaves_small_mss_unchanged() {
|
||||
let src = [0xfd, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1];
|
||||
let dst = [0xfd, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2];
|
||||
let mut packet = make_tcp_syn_packet(src, dst, 1000);
|
||||
|
||||
let modified = clamp_tcp_mss(&mut packet, 1200);
|
||||
|
||||
assert!(!modified);
|
||||
|
||||
// Check MSS unchanged
|
||||
let tcp_start = 40;
|
||||
let mss = u16::from_be_bytes([packet[tcp_start + 22], packet[tcp_start + 23]]);
|
||||
assert_eq!(mss, 1000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clamp_tcp_mss_ignores_non_syn() {
|
||||
let src = [0xfd, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1];
|
||||
let dst = [0xfd, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2];
|
||||
let mut packet = make_tcp_syn_packet(src, dst, 1460);
|
||||
|
||||
// Clear SYN flag
|
||||
packet[40 + 13] = 0x10; // ACK only
|
||||
|
||||
let modified = clamp_tcp_mss(&mut packet, 1200);
|
||||
|
||||
assert!(!modified);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clamp_tcp_mss_ignores_non_tcp() {
|
||||
let mut packet = vec![0u8; 80];
|
||||
packet[0] = 0x60; // IPv6
|
||||
packet[6] = 17; // UDP, not TCP
|
||||
|
||||
let modified = clamp_tcp_mss(&mut packet, 1200);
|
||||
|
||||
assert!(!modified);
|
||||
}
|
||||
}
|
||||
+51
-6
@@ -167,7 +167,9 @@ impl TunDevice {
|
||||
/// 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> {
|
||||
///
|
||||
/// The max_mss parameter is used for TCP MSS clamping on inbound packets.
|
||||
pub fn create_writer(&self, max_mss: u16) -> Result<(TunWriter, TunTx), TunError> {
|
||||
let fd = self.device.as_raw_fd();
|
||||
|
||||
// Duplicate the file descriptor for writing
|
||||
@@ -187,6 +189,7 @@ impl TunDevice {
|
||||
file: write_file,
|
||||
rx,
|
||||
name: self.name.clone(),
|
||||
max_mss,
|
||||
},
|
||||
tx,
|
||||
))
|
||||
@@ -197,10 +200,13 @@ impl TunDevice {
|
||||
///
|
||||
/// Services a queue of outbound packets and writes them to the TUN device.
|
||||
/// Multiple producers can send packets via the TunTx channel.
|
||||
///
|
||||
/// Also performs TCP MSS clamping on inbound SYN-ACK packets.
|
||||
pub struct TunWriter {
|
||||
file: File,
|
||||
rx: mpsc::Receiver<Vec<u8>>,
|
||||
name: String,
|
||||
max_mss: u16,
|
||||
}
|
||||
|
||||
impl TunWriter {
|
||||
@@ -209,9 +215,20 @@ impl TunWriter {
|
||||
/// 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) {
|
||||
debug!(name = %self.name, "TUN writer starting");
|
||||
use super::tcp_mss::clamp_tcp_mss;
|
||||
|
||||
debug!(name = %self.name, max_mss = self.max_mss, "TUN writer starting");
|
||||
|
||||
for mut packet in self.rx {
|
||||
// Clamp TCP MSS on inbound SYN-ACK packets
|
||||
if clamp_tcp_mss(&mut packet, self.max_mss) {
|
||||
debug!(
|
||||
name = %self.name,
|
||||
max_mss = self.max_mss,
|
||||
"Clamped TCP MSS in inbound SYN-ACK packet"
|
||||
);
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -233,6 +250,8 @@ impl TunWriter {
|
||||
/// encapsulation and routing. Non-FIPS packets receive ICMPv6 Destination
|
||||
/// Unreachable responses.
|
||||
///
|
||||
/// Also performs TCP MSS clamping on SYN packets to prevent oversized segments.
|
||||
///
|
||||
/// This is designed to run in a dedicated thread since TUN reads are blocking.
|
||||
/// The loop exits when the TUN interface is deleted (EFAULT) or an unrecoverable
|
||||
/// error occurs.
|
||||
@@ -242,18 +261,35 @@ pub fn run_tun_reader(
|
||||
our_addr: FipsAddress,
|
||||
tun_tx: TunTx,
|
||||
outbound_tx: TunOutboundTx,
|
||||
transport_mtu: u16,
|
||||
) {
|
||||
use super::icmp::{build_dest_unreachable, should_send_icmp_error, DestUnreachableCode};
|
||||
use super::icmp::{build_dest_unreachable, effective_ipv6_mtu, should_send_icmp_error, DestUnreachableCode};
|
||||
use super::tcp_mss::clamp_tcp_mss;
|
||||
|
||||
let name = device.name().to_string();
|
||||
let mut buf = vec![0u8; mtu as usize + 100]; // Extra space for headers
|
||||
|
||||
debug!(name = %name, "TUN reader starting");
|
||||
// Calculate maximum safe TCP MSS from the effective IPv6 MTU
|
||||
const IPV6_HEADER: u16 = 40;
|
||||
const TCP_HEADER: u16 = 20;
|
||||
let effective_mtu = effective_ipv6_mtu(transport_mtu);
|
||||
let max_mss = effective_mtu
|
||||
.saturating_sub(IPV6_HEADER)
|
||||
.saturating_sub(TCP_HEADER);
|
||||
|
||||
debug!(
|
||||
name = %name,
|
||||
tun_mtu = mtu,
|
||||
transport_mtu = transport_mtu,
|
||||
effective_mtu = effective_mtu,
|
||||
max_mss = max_mss,
|
||||
"TUN reader starting"
|
||||
);
|
||||
|
||||
loop {
|
||||
match device.read_packet(&mut buf) {
|
||||
Ok(n) if n > 0 => {
|
||||
let packet = &buf[..n];
|
||||
let packet = &mut buf[..n];
|
||||
log_ipv6_packet(packet);
|
||||
|
||||
// Must be a valid IPv6 packet
|
||||
@@ -263,6 +299,15 @@ pub fn run_tun_reader(
|
||||
|
||||
// Check if destination is a FIPS address (fd::/8 prefix)
|
||||
if packet[24] == crate::identity::FIPS_ADDRESS_PREFIX {
|
||||
// Clamp TCP MSS if this is a SYN packet
|
||||
if clamp_tcp_mss(packet, max_mss) {
|
||||
debug!(
|
||||
name = %name,
|
||||
max_mss = max_mss,
|
||||
"Clamped TCP MSS in SYN packet"
|
||||
);
|
||||
}
|
||||
|
||||
// Forward to Node for session encapsulation and routing
|
||||
if outbound_tx.blocking_send(packet.to_vec()).is_err() {
|
||||
break; // Channel closed, shutdown
|
||||
|
||||
Reference in New Issue
Block a user