mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-05 19:18:25 +00:00
The upper module holds the TUN adapter, the .fips DNS responder, ICMPv6 generation, TCP MSS clamping and the IPv6 shim codec: the host-side IPv6 plane. Name the directory for what it is, so host-networking code has one obvious home as more of it is gathered there. This is a pure directory move. No moved file changes, and a `pub use ipv6tun as upper;` alias in lib.rs keeps every crate::upper:: and fips::upper:: path resolving, so no consumer changes either. Tracing targets follow module paths, so fips::upper::* becomes fips::ipv6tun::*. Update the RUST_LOG example in the MTU diagnosis guide and note the renamed targets in the changelog.
752 lines
26 KiB
Rust
752 lines
26 KiB
Rust
//! 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;
|
|
|
|
/// FIPS base encapsulation overhead for DataPacket (excluding port payload).
|
|
///
|
|
/// Re-exported: the value is FMP plus FSP framing and is defined with the
|
|
/// protocol layers, in [`crate::proto::framing::FIPS_OVERHEAD`].
|
|
pub use crate::proto::framing::FIPS_OVERHEAD;
|
|
|
|
/// FIPS encapsulation overhead for compressed IPv6 shim traffic (port 256).
|
|
///
|
|
/// With port multiplexing (4 bytes) and IPv6 header compression (format byte +
|
|
/// 6 residual bytes, stripping 34 bytes of addresses/version/payload length),
|
|
/// the net overhead for IPv6 packets is 77 bytes.
|
|
///
|
|
/// ```text
|
|
/// Wire size = FIPS_OVERHEAD(106) + port_header(4) + format(1) + residual(6) + upper_payload
|
|
/// = 117 + (ipv6_len - 40)
|
|
/// = ipv6_len + 77
|
|
/// ```
|
|
pub const FIPS_IPV6_OVERHEAD: u16 = 77;
|
|
|
|
/// Smallest remote-supplied transport path MTU this node will act on.
|
|
///
|
|
/// Re-exported: the value is a protocol policy decision and is defined beside
|
|
/// the path-MTU state machine that owns it, in
|
|
/// [`crate::proto::mmp::MIN_ACTIONABLE_PATH_MTU`]. It is named from here
|
|
/// because every site that applies it — the MSS clamp, the lookup response and
|
|
/// the `MtuExceeded` signal — reaches it through this module.
|
|
pub use crate::proto::mmp::MIN_ACTIONABLE_PATH_MTU;
|
|
|
|
/// Smallest path MTU this node will act on when the claim arrives on the
|
|
/// unauthenticated reactive carrier, `MtuExceeded`.
|
|
///
|
|
/// Re-exported: a protocol policy decision defined beside
|
|
/// [`crate::proto::mmp::MIN_ACTIONABLE_PATH_MTU`], in
|
|
/// [`crate::proto::mmp::MIN_REACTIVE_PATH_MTU`].
|
|
pub use crate::proto::mmp::MIN_REACTIVE_PATH_MTU;
|
|
|
|
/// 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 IPv6 header compression.
|
|
pub fn effective_ipv6_mtu(transport_mtu: u16) -> u16 {
|
|
transport_mtu.saturating_sub(FIPS_IPV6_OVERHEAD)
|
|
}
|
|
|
|
/// Largest TCP segment size a FIPS-encapsulated path of `transport_mtu`
|
|
/// bytes on the wire admits: the effective inner IPv6 MTU less the 40-byte
|
|
/// IPv6 header and the 20-byte TCP header.
|
|
///
|
|
/// Zero means the path has no room for even one payload byte, so no TCP
|
|
/// segment fits and no clamp derived from it carries information. That is
|
|
/// the one condition the SYN-time clamp treats as unusable regardless of
|
|
/// where the MTU came from, and it is why the seed site warns; both read it
|
|
/// from here so they cannot disagree about where the cliff is.
|
|
pub fn mss_ceiling(transport_mtu: u16) -> u16 {
|
|
effective_ipv6_mtu(transport_mtu)
|
|
.saturating_sub(40)
|
|
.saturating_sub(20)
|
|
}
|
|
|
|
/// 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 a multicast destination 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;
|
|
}
|
|
|
|
// Extract destination address
|
|
let dst = Ipv6Addr::from(<[u8; 16]>::try_from(&packet[24..40]).unwrap());
|
|
|
|
// Don't send errors for multicast destination (first byte 0xff)
|
|
// e.g., ff02::2 (all-routers) from Router Solicitation
|
|
if dst.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<Vec<u8>> {
|
|
// 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)
|
|
}
|
|
|
|
/// 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: u32,
|
|
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, network byte order per RFC 4443 §3.2)
|
|
response[icmp_start + 4..icmp_start + 8].copy_from_slice(&mtu.to_be_bytes());
|
|
|
|
// === 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.
|
|
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;
|
|
sum += len & 0xffff;
|
|
|
|
// 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<u8> {
|
|
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_multicast_destination() {
|
|
let src = "fe80::1".parse().unwrap();
|
|
let dst = "ff02::2".parse().unwrap(); // all-routers multicast
|
|
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);
|
|
}
|
|
|
|
#[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 = 1070u32;
|
|
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 (32-bit field per RFC 4443 §3.2)
|
|
let reported_mtu = u32::from_be_bytes([
|
|
response[IPV6_HEADER_LEN + 4],
|
|
response[IPV6_HEADER_LEN + 5],
|
|
response[IPV6_HEADER_LEN + 6],
|
|
response[IPV6_HEADER_LEN + 7],
|
|
]);
|
|
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);
|
|
}
|
|
|
|
/// Verify that when the ICMP source is set to the original packet's
|
|
/// destination (the remote peer), the PTB is correctly formed.
|
|
///
|
|
/// This is the critical fix for the PMTUD blackhole: Linux ignores
|
|
/// ICMPv6 PTBs whose source matches a local address. By using the
|
|
/// remote peer's address as the ICMP source, the kernel sees the PTB
|
|
/// as coming from a "remote router" and honors it.
|
|
#[test]
|
|
fn test_build_packet_too_big_remote_source_for_pmtud() {
|
|
let local_addr: Ipv6Addr = "fd41::1".parse().unwrap();
|
|
let remote_addr: Ipv6Addr = "fddf::2".parse().unwrap();
|
|
let original = make_ipv6_packet(local_addr, remote_addr, 6, &[0u8; 1200]); // TCP
|
|
|
|
// Pass remote_addr as our_addr — this is what send_icmpv6_packet_too_big
|
|
// does after the fix (original packet's dst = remote peer).
|
|
let response = build_packet_too_big(&original, 1203, remote_addr);
|
|
assert!(response.is_some());
|
|
let response = response.unwrap();
|
|
|
|
// PTB source must be the remote peer (not local)
|
|
let ptb_src = Ipv6Addr::from(<[u8; 16]>::try_from(&response[8..24]).unwrap());
|
|
assert_eq!(
|
|
ptb_src, remote_addr,
|
|
"PTB source must be remote peer address"
|
|
);
|
|
|
|
// PTB destination must be the local sender (original src)
|
|
let ptb_dst = Ipv6Addr::from(<[u8; 16]>::try_from(&response[24..40]).unwrap());
|
|
assert_eq!(
|
|
ptb_dst, local_addr,
|
|
"PTB destination must be original sender"
|
|
);
|
|
|
|
// Verify ICMPv6 type/code
|
|
assert_eq!(response[IPV6_HEADER_LEN], 2); // Type = Packet Too Big
|
|
assert_eq!(response[IPV6_HEADER_LEN + 1], 0); // Code = 0
|
|
|
|
// Verify reported MTU
|
|
let reported_mtu = u32::from_be_bytes([
|
|
response[IPV6_HEADER_LEN + 4],
|
|
response[IPV6_HEADER_LEN + 5],
|
|
response[IPV6_HEADER_LEN + 6],
|
|
response[IPV6_HEADER_LEN + 7],
|
|
]);
|
|
assert_eq!(reported_mtu, 1203);
|
|
|
|
// Verify checksum is valid (recalculate and compare)
|
|
let stored_checksum =
|
|
u16::from_be_bytes([response[IPV6_HEADER_LEN + 2], response[IPV6_HEADER_LEN + 3]]);
|
|
let recomputed = icmpv6_checksum(&response[IPV6_HEADER_LEN..], &remote_addr, &local_addr);
|
|
assert_eq!(stored_checksum, recomputed, "ICMPv6 checksum must be valid");
|
|
}
|
|
}
|