//! 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). /// /// This is the fixed overhead for a SessionDatagram carrying an FSP DataPacket, /// used by the send path's CP-flag guard to check whether piggybacked coords /// fit within the transport MTU. For IPv6 effective MTU calculations, use /// [`FIPS_IPV6_OVERHEAD`] which accounts for port multiplexing and header /// compression. /// /// Breakdown (traced through the actual send path): /// /// ```text /// FMP outer header (cleartext AAD) 16 /// common prefix (4) + receiver_idx (4) + counter (8) /// FMP AEAD ciphertext: /// timestamp (4) + msg_type (1) 5 [FMP inner header] /// ttl (1) + path_mtu (2) + src (16) + dst (16) 35 [SessionDatagram body] /// FSP header (4 prefix + 8 counter) 12 [cleartext AAD] /// FSP AEAD ciphertext: /// timestamp (4) + msg_type (1) + flags (1) 6 [FSP inner header] /// /// Poly1305 tag 16 [FSP AEAD] /// FMP Poly1305 tag 16 [FMP AEAD] /// ──── /// 106 /// ``` /// /// Note: the FMP inner header msg_type byte IS the SessionDatagram msg_type /// byte (shared, not double-counted). The "35 bytes" is the SessionDatagram /// body after msg_type is consumed by the dispatch layer. pub const FIPS_OVERHEAD: u16 = 16 + 16 + 5 + 35 + 12 + 6 + 16; // 106 bytes /// 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; /// 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) } /// 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> { // 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> { // 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 { 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"); } }