diff --git a/src/node/handlers/session.rs b/src/node/handlers/session.rs index 9ef1258..5944eb9 100644 --- a/src/node/handlers/session.rs +++ b/src/node/handlers/session.rs @@ -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); } diff --git a/src/node/tests/session.rs b/src/node/tests/session.rs index d3f3cf0..7d537da 100644 --- a/src/node/tests/session.rs +++ b/src/node/tests/session.rs @@ -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; diff --git a/src/upper/icmp.rs b/src/upper/icmp.rs index 0c6909d..5403ef1 100644 --- a/src/upper/icmp.rs +++ b/src/upper/icmp.rs @@ -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"); + } }