mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-22 07:48:26 +00:00
Fix ICMPv6 Packet Too Big source address for PMTUD
The PTB source was set to the local FIPS address, which Linux ignores (loopback PTB security check). Change the source to the original packet's destination (the remote peer) so the kernel sees it as coming from a remote router and honors the PMTU update. Add test coverage for the PTB source address invariant in both the icmp unit tests and the node integration tests.
This commit is contained in:
@@ -1662,7 +1662,6 @@ impl Node {
|
||||
/// misconfigured applications sending repeated oversized packets.
|
||||
pub(in crate::node) fn send_icmpv6_packet_too_big(&mut self, original_packet: &[u8], mtu: u32) {
|
||||
use crate::upper::icmp::build_packet_too_big;
|
||||
use crate::FipsAddress;
|
||||
use std::net::Ipv6Addr;
|
||||
|
||||
// Extract source address for rate limiting
|
||||
@@ -1680,18 +1679,20 @@ impl Node {
|
||||
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)
|
||||
// Use the original packet's *destination* as the ICMP source so the
|
||||
// kernel sees the PTB coming from a remote router, not from itself.
|
||||
// Linux ignores PTBs whose source matches a local address, which
|
||||
// causes a PMTUD blackhole when both src and ICMP-src are local.
|
||||
let dest_addr = Ipv6Addr::from(<[u8; 16]>::try_from(&original_packet[24..40]).unwrap());
|
||||
if let Some(response) = build_packet_too_big(original_packet, mtu, dest_addr)
|
||||
&& let Some(tun_tx) = &self.tun_tx
|
||||
{
|
||||
debug!(
|
||||
src = %src_addr,
|
||||
dst = %our_ipv6,
|
||||
original_src = %src_addr,
|
||||
original_dst = %dest_addr,
|
||||
packet_size = original_packet.len(),
|
||||
reported_mtu = mtu,
|
||||
"Sending ICMP Packet Too Big (ICMP src={}, dst={})",
|
||||
our_ipv6,
|
||||
src_addr
|
||||
"Sending ICMP Packet Too Big"
|
||||
);
|
||||
let _ = tun_tx.send(response);
|
||||
}
|
||||
|
||||
@@ -1779,6 +1779,14 @@ async fn test_tun_outbound_path_mtu_generates_ptb() {
|
||||
assert_eq!(ptb[40], 2, "ICMPv6 type should be Packet Too Big (2)");
|
||||
assert_eq!(ptb[41], 0, "ICMPv6 code should be 0");
|
||||
|
||||
// Verify PTB source is the *remote peer* (original packet's destination),
|
||||
// NOT the local node. Linux ignores PTBs whose source matches a local
|
||||
// address, causing a PMTUD blackhole.
|
||||
let ptb_src = std::net::Ipv6Addr::from(<[u8; 16]>::try_from(&ptb[8..24]).unwrap());
|
||||
let ptb_dst = std::net::Ipv6Addr::from(<[u8; 16]>::try_from(&ptb[24..40]).unwrap());
|
||||
assert_eq!(ptb_src, dst_fips.to_ipv6(), "PTB source must be remote peer (original dst), not local node");
|
||||
assert_eq!(ptb_dst, src_fips.to_ipv6(), "PTB destination must be local node (original src)");
|
||||
|
||||
// Verify reported MTU (32-bit field at ICMPv6 header bytes 4-7)
|
||||
let reported_mtu = u32::from_be_bytes([ptb[44], ptb[45], ptb[46], ptb[47]]);
|
||||
assert_eq!(reported_mtu, reduced_ipv6_mtu as u32, "Reported MTU should match path IPv6 MTU");
|
||||
@@ -1904,6 +1912,14 @@ async fn test_multihop_pmtud_heterogeneous_mtu() {
|
||||
assert_eq!(ptb[40], 2, "ICMPv6 type should be Packet Too Big (2)");
|
||||
assert_eq!(ptb[41], 0, "ICMPv6 code should be 0");
|
||||
|
||||
// Verify PTB source is the *remote peer* (original packet's destination),
|
||||
// NOT the local node. Linux ignores PTBs whose source matches a local
|
||||
// address, causing a PMTUD blackhole.
|
||||
let ptb_src = std::net::Ipv6Addr::from(<[u8; 16]>::try_from(&ptb[8..24]).unwrap());
|
||||
let ptb_dst = std::net::Ipv6Addr::from(<[u8; 16]>::try_from(&ptb[24..40]).unwrap());
|
||||
assert_eq!(ptb_src, dst_fips.to_ipv6(), "PTB source must be remote peer (original dst), not local node");
|
||||
assert_eq!(ptb_dst, src_fips.to_ipv6(), "PTB destination must be local node (original src)");
|
||||
|
||||
// Verify reported MTU is the path MTU (not local MTU)
|
||||
let reported_mtu = u32::from_be_bytes([ptb[44], ptb[45], ptb[46], ptb[47]]);
|
||||
let expected_ipv6_mtu = crate::upper::icmp::effective_ipv6_mtu(path_mtu) as u32;
|
||||
|
||||
@@ -687,4 +687,57 @@ mod tests {
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user