Skip to main content

fips_core/upper/
icmp.rs

1//! ICMPv6 message handling for FIPS.
2//!
3//! Implements ICMPv6 error message generation per RFC 4443.
4//! Currently supports Destination Unreachable (Type 1) for
5//! packets that cannot be routed.
6
7use std::net::Ipv6Addr;
8
9/// ICMPv6 message types.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11#[repr(u8)]
12pub enum Icmpv6Type {
13    /// Destination Unreachable (error).
14    DestinationUnreachable = 1,
15    /// Packet Too Big (error).
16    PacketTooBig = 2,
17    /// Time Exceeded (error).
18    TimeExceeded = 3,
19    /// Parameter Problem (error).
20    ParameterProblem = 4,
21    /// Echo Request.
22    EchoRequest = 128,
23    /// Echo Reply.
24    EchoReply = 129,
25}
26
27/// ICMPv6 Destination Unreachable codes.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29#[repr(u8)]
30pub enum DestUnreachableCode {
31    /// No route to destination.
32    NoRoute = 0,
33    /// Communication administratively prohibited.
34    AdminProhibited = 1,
35    /// Beyond scope of source address.
36    BeyondScope = 2,
37    /// Address unreachable.
38    AddressUnreachable = 3,
39    /// Port unreachable.
40    PortUnreachable = 4,
41    /// Source address failed policy.
42    SourcePolicy = 5,
43    /// Reject route to destination.
44    RejectRoute = 6,
45}
46
47/// IPv6 header next-header value for ICMPv6.
48pub const IPPROTO_ICMPV6: u8 = 58;
49
50/// Minimum IPv6 MTU - ICMPv6 responses must not exceed this.
51const MIN_IPV6_MTU: usize = 1280;
52
53/// IPv6 header length.
54const IPV6_HEADER_LEN: usize = 40;
55
56/// ICMPv6 header length (type + code + checksum + unused/data).
57const ICMPV6_HEADER_LEN: usize = 8;
58
59/// Maximum original packet bytes to include in ICMPv6 error.
60const MAX_ORIGINAL_PACKET: usize = MIN_IPV6_MTU - IPV6_HEADER_LEN - ICMPV6_HEADER_LEN;
61
62/// FIPS base encapsulation overhead for DataPacket (excluding port payload).
63///
64/// This is the fixed overhead for a SessionDatagram carrying an FSP DataPacket,
65/// used by the send path's CP-flag guard to check whether piggybacked coords
66/// fit within the transport MTU. For IPv6 effective MTU calculations, use
67/// [`FIPS_IPV6_OVERHEAD`] which accounts for port multiplexing and header
68/// compression.
69///
70/// Breakdown (traced through the actual send path):
71///
72/// ```text
73/// FMP outer header (cleartext AAD)              16
74///   common prefix (4) + receiver_idx (4) + counter (8)
75/// FMP AEAD ciphertext:
76///   timestamp (4) + msg_type (1)                 5   [FMP inner header]
77///   ttl (1) + path_mtu (2) + src (16) + dst (16) 35  [SessionDatagram body]
78///   FSP header (4 prefix + 8 counter)            12   [cleartext AAD]
79///   FSP AEAD ciphertext:
80///     timestamp (4) + msg_type (1) + flags (1)    6   [FSP inner header]
81///     <application data>
82///     Poly1305 tag                               16   [FSP AEAD]
83/// FMP Poly1305 tag                              16   [FMP AEAD]
84///                                              ────
85///                                               106
86/// ```
87///
88/// Note: the FMP inner header msg_type byte IS the SessionDatagram msg_type
89/// byte (shared, not double-counted). The "35 bytes" is the SessionDatagram
90/// body after msg_type is consumed by the dispatch layer.
91pub const FIPS_OVERHEAD: u16 = 16 + 16 + 5 + 35 + 12 + 6 + 16; // 106 bytes
92
93/// FIPS encapsulation overhead for compressed IPv6 shim traffic (port 256).
94///
95/// With port multiplexing (4 bytes) and IPv6 header compression (format byte +
96/// 6 residual bytes, stripping 34 bytes of addresses/version/payload length),
97/// the net overhead for IPv6 packets is 77 bytes.
98///
99/// ```text
100/// Wire size = FIPS_OVERHEAD(106) + port_header(4) + format(1) + residual(6) + upper_payload
101///           = 117 + (ipv6_len - 40)
102///           = ipv6_len + 77
103/// ```
104pub const FIPS_IPV6_OVERHEAD: u16 = 77;
105
106/// Calculate the effective IPv6 MTU for FIPS-encapsulated traffic.
107///
108/// Given a transport MTU (e.g., UDP payload size), returns the maximum
109/// IPv6 packet size (including IPv6 header) that can be transmitted
110/// through the FIPS mesh after IPv6 header compression.
111pub fn effective_ipv6_mtu(transport_mtu: u16) -> u16 {
112    transport_mtu.saturating_sub(FIPS_IPV6_OVERHEAD)
113}
114
115/// Check if we should send an ICMPv6 error for this packet.
116///
117/// Returns false if the packet is:
118/// - Too short to be valid IPv6
119/// - Not IPv6
120/// - An ICMPv6 error message itself
121/// - Has a multicast source address
122/// - Has a multicast destination address
123/// - Has an unspecified source address (::)
124pub fn should_send_icmp_error(packet: &[u8]) -> bool {
125    // Must have at least an IPv6 header
126    if packet.len() < IPV6_HEADER_LEN {
127        return false;
128    }
129
130    // Must be IPv6
131    let version = packet[0] >> 4;
132    if version != 6 {
133        return false;
134    }
135
136    // Extract source address
137    let src = Ipv6Addr::from(<[u8; 16]>::try_from(&packet[8..24]).unwrap());
138
139    // Don't send errors for unspecified source
140    if src.is_unspecified() {
141        return false;
142    }
143
144    // Don't send errors for multicast source (first byte 0xff)
145    if src.octets()[0] == 0xff {
146        return false;
147    }
148
149    // Extract destination address
150    let dst = Ipv6Addr::from(<[u8; 16]>::try_from(&packet[24..40]).unwrap());
151
152    // Don't send errors for multicast destination (first byte 0xff)
153    // e.g., ff02::2 (all-routers) from Router Solicitation
154    if dst.octets()[0] == 0xff {
155        return false;
156    }
157
158    // Don't send errors for ICMPv6 error messages (types 0-127)
159    let next_header = packet[6];
160    if next_header == IPPROTO_ICMPV6 && packet.len() > IPV6_HEADER_LEN {
161        let icmp_type = packet[IPV6_HEADER_LEN];
162        // ICMPv6 error messages are types 0-127
163        if icmp_type < 128 {
164            return false;
165        }
166    }
167
168    true
169}
170
171/// Build an ICMPv6 Destination Unreachable response.
172///
173/// Takes the original packet that couldn't be delivered and returns
174/// a complete IPv6 packet containing the ICMPv6 error response.
175///
176/// Arguments:
177/// - `original_packet`: The packet that couldn't be routed
178/// - `code`: The specific unreachable reason
179/// - `our_addr`: Our FIPS address (source of the error)
180///
181/// Returns None if the original packet is invalid.
182pub fn build_dest_unreachable(
183    original_packet: &[u8],
184    code: DestUnreachableCode,
185    our_addr: Ipv6Addr,
186) -> Option<Vec<u8>> {
187    // Validate original packet
188    if original_packet.len() < IPV6_HEADER_LEN {
189        return None;
190    }
191
192    // Extract destination from original packet (becomes our destination)
193    let dest_addr = Ipv6Addr::from(<[u8; 16]>::try_from(&original_packet[8..24]).unwrap());
194
195    // Calculate how much of the original packet to include
196    let original_len = original_packet.len().min(MAX_ORIGINAL_PACKET);
197    let icmpv6_len = ICMPV6_HEADER_LEN + original_len;
198    let total_len = IPV6_HEADER_LEN + icmpv6_len;
199
200    let mut response = vec![0u8; total_len];
201
202    // === IPv6 Header ===
203    // Version (4) + Traffic Class (8) + Flow Label (20)
204    response[0] = 0x60; // Version 6, TC high bits = 0
205    // response[1..4] = 0 (TC low bits + flow label)
206
207    // Payload length (ICMPv6 header + body)
208    let payload_len = icmpv6_len as u16;
209    response[4..6].copy_from_slice(&payload_len.to_be_bytes());
210
211    // Next header = ICMPv6
212    response[6] = IPPROTO_ICMPV6;
213
214    // Hop limit
215    response[7] = 64;
216
217    // Source = our address
218    response[8..24].copy_from_slice(&our_addr.octets());
219
220    // Destination = original source
221    response[24..40].copy_from_slice(&dest_addr.octets());
222
223    // === ICMPv6 Header ===
224    let icmp_start = IPV6_HEADER_LEN;
225
226    // Type = Destination Unreachable
227    response[icmp_start] = Icmpv6Type::DestinationUnreachable as u8;
228
229    // Code
230    response[icmp_start + 1] = code as u8;
231
232    // Checksum placeholder (calculated below)
233    // response[icmp_start + 2..icmp_start + 4] = 0
234
235    // Unused (4 bytes of zeros for Dest Unreachable)
236    // response[icmp_start + 4..icmp_start + 8] = 0
237
238    // === ICMPv6 Body ===
239    // As much of original packet as fits
240    response[icmp_start + ICMPV6_HEADER_LEN..].copy_from_slice(&original_packet[..original_len]);
241
242    // Calculate checksum
243    let checksum = icmpv6_checksum(&response[icmp_start..], &our_addr, &dest_addr);
244    response[icmp_start + 2..icmp_start + 4].copy_from_slice(&checksum.to_be_bytes());
245
246    Some(response)
247}
248
249/// Build an ICMPv6 Packet Too Big response.
250///
251/// RFC 4443 Section 3.2: Packet Too Big Message
252///
253/// ## Wire Format
254/// ```text
255/// 0                   1                   2                   3
256/// 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
257/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
258/// |     Type=2    |     Code=0    |          Checksum             |
259/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
260/// |                             MTU                               |
261/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
262/// |                    As much of invoking packet                 |
263/// +               as possible without exceeding 1280              +
264/// |                                                               |
265/// ```
266///
267/// ## Parameters
268/// - `original_packet`: The oversized IPv6 packet that triggered this error
269/// - `mtu`: The MTU value to report (effective IPv6 MTU after FIPS overhead)
270/// - `our_addr`: Our FIPS IPv6 address (source of ICMP message)
271///
272/// ## Returns
273/// Complete IPv6 packet containing the ICMP Packet Too Big message,
274/// ready to be written to the TUN interface.
275pub fn build_packet_too_big(
276    original_packet: &[u8],
277    mtu: u32,
278    our_addr: Ipv6Addr,
279) -> Option<Vec<u8>> {
280    // Validate original packet
281    if original_packet.len() < IPV6_HEADER_LEN {
282        return None;
283    }
284
285    // Must be IPv6
286    let version = original_packet[0] >> 4;
287    if version != 6 {
288        return None;
289    }
290
291    // Extract source address from original packet (becomes ICMP destination)
292    let src_addr = Ipv6Addr::from(<[u8; 16]>::try_from(&original_packet[8..24]).unwrap());
293
294    // Don't send ICMP in response to:
295    // - Multicast sources (ff00::/8)
296    // - Unspecified source (::)
297    if src_addr.is_unspecified() || src_addr.octets()[0] == 0xff {
298        return None;
299    }
300
301    // Don't send ICMP in response to ICMP errors (avoid loops)
302    let next_header = original_packet[6];
303    if next_header == IPPROTO_ICMPV6 && original_packet.len() > IPV6_HEADER_LEN {
304        let icmp_type = original_packet[IPV6_HEADER_LEN];
305        // ICMPv6 error messages are types 0-127
306        if icmp_type < 128 {
307            return None;
308        }
309    }
310
311    // Calculate how much of the original packet to include
312    // RFC 4443: "as much of invoking packet as possible without exceeding 1280"
313    let original_len = original_packet.len().min(MAX_ORIGINAL_PACKET);
314    let icmpv6_len = ICMPV6_HEADER_LEN + original_len;
315    let total_len = IPV6_HEADER_LEN + icmpv6_len;
316
317    let mut response = vec![0u8; total_len];
318
319    // === IPv6 Header ===
320    // Version (4) + Traffic Class (8) + Flow Label (20)
321    response[0] = 0x60; // Version 6, TC high bits = 0
322    // response[1..4] = 0 (TC low bits + flow label)
323
324    // Payload length (ICMPv6 header + body)
325    let payload_len = icmpv6_len as u16;
326    response[4..6].copy_from_slice(&payload_len.to_be_bytes());
327
328    // Next header = ICMPv6
329    response[6] = IPPROTO_ICMPV6;
330
331    // Hop limit
332    response[7] = 64;
333
334    // Source = our address
335    response[8..24].copy_from_slice(&our_addr.octets());
336
337    // Destination = original source
338    response[24..40].copy_from_slice(&src_addr.octets());
339
340    // === ICMPv6 Header ===
341    let icmp_start = IPV6_HEADER_LEN;
342
343    // Type = Packet Too Big
344    response[icmp_start] = Icmpv6Type::PacketTooBig as u8;
345
346    // Code = 0 (always 0 for Packet Too Big)
347    response[icmp_start + 1] = 0;
348
349    // Checksum placeholder (calculated below)
350    // response[icmp_start + 2..icmp_start + 4] = 0
351
352    // MTU (4 bytes, network byte order per RFC 4443 §3.2)
353    response[icmp_start + 4..icmp_start + 8].copy_from_slice(&mtu.to_be_bytes());
354
355    // === ICMPv6 Body ===
356    // As much of original packet as fits
357    response[icmp_start + ICMPV6_HEADER_LEN..].copy_from_slice(&original_packet[..original_len]);
358
359    // Calculate checksum
360    let checksum = icmpv6_checksum(&response[icmp_start..], &our_addr, &src_addr);
361    response[icmp_start + 2..icmp_start + 4].copy_from_slice(&checksum.to_be_bytes());
362
363    Some(response)
364}
365
366/// Calculate ICMPv6 checksum per RFC 4443.
367///
368/// The checksum is calculated over a pseudo-header plus the ICMPv6 message.
369fn icmpv6_checksum(icmpv6_message: &[u8], src: &Ipv6Addr, dst: &Ipv6Addr) -> u16 {
370    let mut sum: u32 = 0;
371
372    // Pseudo-header: source address (16 bytes)
373    for chunk in src.octets().chunks(2) {
374        sum += u16::from_be_bytes([chunk[0], chunk[1]]) as u32;
375    }
376
377    // Pseudo-header: destination address (16 bytes)
378    for chunk in dst.octets().chunks(2) {
379        sum += u16::from_be_bytes([chunk[0], chunk[1]]) as u32;
380    }
381
382    // Pseudo-header: upper-layer packet length (4 bytes, as u32)
383    let len = icmpv6_message.len() as u32;
384    sum += len >> 16;
385    sum += len & 0xffff;
386
387    // Pseudo-header: next header (padded to 4 bytes)
388    sum += IPPROTO_ICMPV6 as u32;
389
390    // ICMPv6 message (with checksum field = 0)
391    let mut i = 0;
392    while i + 1 < icmpv6_message.len() {
393        // Skip the checksum field (bytes 2-3)
394        if i == 2 {
395            i += 2;
396            continue;
397        }
398        sum += u16::from_be_bytes([icmpv6_message[i], icmpv6_message[i + 1]]) as u32;
399        i += 2;
400    }
401
402    // Handle odd byte
403    if i < icmpv6_message.len() {
404        sum += (icmpv6_message[i] as u32) << 8;
405    }
406
407    // Fold 32-bit sum to 16 bits
408    while sum >> 16 != 0 {
409        sum = (sum & 0xffff) + (sum >> 16);
410    }
411
412    // One's complement
413    !(sum as u16)
414}
415
416#[cfg(test)]
417mod tests {
418    use super::*;
419
420    fn make_ipv6_packet(src: Ipv6Addr, dst: Ipv6Addr, next_header: u8, payload: &[u8]) -> Vec<u8> {
421        let mut packet = vec![0u8; IPV6_HEADER_LEN + payload.len()];
422
423        // Version + TC + Flow Label
424        packet[0] = 0x60;
425
426        // Payload length
427        let len = payload.len() as u16;
428        packet[4..6].copy_from_slice(&len.to_be_bytes());
429
430        // Next header
431        packet[6] = next_header;
432
433        // Hop limit
434        packet[7] = 64;
435
436        // Source
437        packet[8..24].copy_from_slice(&src.octets());
438
439        // Destination
440        packet[24..40].copy_from_slice(&dst.octets());
441
442        // Payload
443        packet[IPV6_HEADER_LEN..].copy_from_slice(payload);
444
445        packet
446    }
447
448    #[test]
449    fn test_should_send_error_valid_packet() {
450        let src = "fd00::1".parse().unwrap();
451        let dst = "fd00::2".parse().unwrap();
452        let packet = make_ipv6_packet(src, dst, 17, &[0u8; 8]); // UDP
453
454        assert!(should_send_icmp_error(&packet));
455    }
456
457    #[test]
458    fn test_should_not_send_error_unspecified_source() {
459        let src = Ipv6Addr::UNSPECIFIED;
460        let dst = "fd00::2".parse().unwrap();
461        let packet = make_ipv6_packet(src, dst, 17, &[0u8; 8]);
462
463        assert!(!should_send_icmp_error(&packet));
464    }
465
466    #[test]
467    fn test_should_not_send_error_multicast_source() {
468        let src = "ff02::1".parse().unwrap();
469        let dst = "fd00::2".parse().unwrap();
470        let packet = make_ipv6_packet(src, dst, 17, &[0u8; 8]);
471
472        assert!(!should_send_icmp_error(&packet));
473    }
474
475    #[test]
476    fn test_should_not_send_error_multicast_destination() {
477        let src = "fe80::1".parse().unwrap();
478        let dst = "ff02::2".parse().unwrap(); // all-routers multicast
479        let packet = make_ipv6_packet(src, dst, 17, &[0u8; 8]);
480
481        assert!(!should_send_icmp_error(&packet));
482    }
483
484    #[test]
485    fn test_should_not_send_error_for_icmp_error() {
486        let src = "fd00::1".parse().unwrap();
487        let dst = "fd00::2".parse().unwrap();
488        // ICMPv6 Destination Unreachable (type 1)
489        let icmp_payload = [1u8, 0, 0, 0, 0, 0, 0, 0];
490        let packet = make_ipv6_packet(src, dst, IPPROTO_ICMPV6, &icmp_payload);
491
492        assert!(!should_send_icmp_error(&packet));
493    }
494
495    #[test]
496    fn test_should_send_error_for_icmp_echo() {
497        let src = "fd00::1".parse().unwrap();
498        let dst = "fd00::2".parse().unwrap();
499        // ICMPv6 Echo Request (type 128) - informational, not error
500        let icmp_payload = [128u8, 0, 0, 0, 0, 0, 0, 0];
501        let packet = make_ipv6_packet(src, dst, IPPROTO_ICMPV6, &icmp_payload);
502
503        assert!(should_send_icmp_error(&packet));
504    }
505
506    #[test]
507    fn test_should_not_send_error_short_packet() {
508        let packet = vec![0u8; 20]; // Too short for IPv6
509        assert!(!should_send_icmp_error(&packet));
510    }
511
512    #[test]
513    fn test_build_dest_unreachable() {
514        let src: Ipv6Addr = "fd00::1".parse().unwrap();
515        let dst: Ipv6Addr = "fd00::2".parse().unwrap();
516        let original = make_ipv6_packet(src, dst, 17, &[0u8; 8]);
517
518        let our_addr: Ipv6Addr = "fd00::ffff".parse().unwrap();
519        let response = build_dest_unreachable(&original, DestUnreachableCode::NoRoute, our_addr);
520
521        assert!(response.is_some());
522        let response = response.unwrap();
523
524        // Check IPv6 header
525        assert_eq!(response[0] >> 4, 6); // Version
526        assert_eq!(response[6], IPPROTO_ICMPV6); // Next header
527
528        // Check source is our address
529        let resp_src = Ipv6Addr::from(<[u8; 16]>::try_from(&response[8..24]).unwrap());
530        assert_eq!(resp_src, our_addr);
531
532        // Check destination is original source
533        let resp_dst = Ipv6Addr::from(<[u8; 16]>::try_from(&response[24..40]).unwrap());
534        assert_eq!(resp_dst, src);
535
536        // Check ICMPv6 type and code
537        assert_eq!(response[IPV6_HEADER_LEN], 1); // Type = Dest Unreachable
538        assert_eq!(response[IPV6_HEADER_LEN + 1], 0); // Code = No Route
539    }
540
541    #[test]
542    fn test_build_dest_unreachable_invalid_input() {
543        let short_packet = vec![0u8; 20];
544        let our_addr: Ipv6Addr = "fd00::ffff".parse().unwrap();
545
546        let response =
547            build_dest_unreachable(&short_packet, DestUnreachableCode::NoRoute, our_addr);
548        assert!(response.is_none());
549    }
550
551    #[test]
552    fn test_build_dest_unreachable_truncates_large_packet() {
553        let src: Ipv6Addr = "fd00::1".parse().unwrap();
554        let dst: Ipv6Addr = "fd00::2".parse().unwrap();
555        // Large payload
556        let original = make_ipv6_packet(src, dst, 17, &[0u8; 2000]);
557
558        let our_addr: Ipv6Addr = "fd00::ffff".parse().unwrap();
559        let response = build_dest_unreachable(&original, DestUnreachableCode::NoRoute, our_addr);
560
561        assert!(response.is_some());
562        let response = response.unwrap();
563
564        // Response must not exceed minimum MTU
565        assert!(response.len() <= MIN_IPV6_MTU);
566    }
567
568    #[test]
569    fn test_build_packet_too_big() {
570        let src: Ipv6Addr = "fd00::1".parse().unwrap();
571        let dst: Ipv6Addr = "fd00::2".parse().unwrap();
572        let original = make_ipv6_packet(src, dst, 17, &[0u8; 1200]); // Large UDP packet
573
574        let our_addr: Ipv6Addr = "fd00::ffff".parse().unwrap();
575        let mtu = 1070u32;
576        let response = build_packet_too_big(&original, mtu, our_addr);
577
578        assert!(response.is_some());
579        let response = response.unwrap();
580
581        // Check IPv6 header
582        assert_eq!(response[0] >> 4, 6); // Version
583        assert_eq!(response[6], IPPROTO_ICMPV6); // Next header
584
585        // Check source is our address
586        let resp_src = Ipv6Addr::from(<[u8; 16]>::try_from(&response[8..24]).unwrap());
587        assert_eq!(resp_src, our_addr);
588
589        // Check destination is original source
590        let resp_dst = Ipv6Addr::from(<[u8; 16]>::try_from(&response[24..40]).unwrap());
591        assert_eq!(resp_dst, src);
592
593        // Check ICMPv6 type and code
594        assert_eq!(response[IPV6_HEADER_LEN], 2); // Type = Packet Too Big
595        assert_eq!(response[IPV6_HEADER_LEN + 1], 0); // Code = 0
596
597        // Check MTU value (32-bit field per RFC 4443 §3.2)
598        let reported_mtu = u32::from_be_bytes([
599            response[IPV6_HEADER_LEN + 4],
600            response[IPV6_HEADER_LEN + 5],
601            response[IPV6_HEADER_LEN + 6],
602            response[IPV6_HEADER_LEN + 7],
603        ]);
604        assert_eq!(reported_mtu, mtu);
605
606        // Response must not exceed minimum MTU
607        assert!(response.len() <= MIN_IPV6_MTU);
608    }
609
610    #[test]
611    fn test_build_packet_too_big_invalid_input() {
612        let short_packet = vec![0u8; 20];
613        let our_addr: Ipv6Addr = "fd00::ffff".parse().unwrap();
614
615        let response = build_packet_too_big(&short_packet, 1280, our_addr);
616        assert!(response.is_none());
617    }
618
619    #[test]
620    fn test_build_packet_too_big_multicast_source() {
621        let src: Ipv6Addr = "ff02::1".parse().unwrap(); // Multicast
622        let dst: Ipv6Addr = "fd00::2".parse().unwrap();
623        let original = make_ipv6_packet(src, dst, 17, &[0u8; 1200]);
624
625        let our_addr: Ipv6Addr = "fd00::ffff".parse().unwrap();
626        let response = build_packet_too_big(&original, 1280, our_addr);
627
628        // Should not send ICMP for multicast source
629        assert!(response.is_none());
630    }
631
632    #[test]
633    fn test_build_packet_too_big_unspecified_source() {
634        let src = Ipv6Addr::UNSPECIFIED;
635        let dst: Ipv6Addr = "fd00::2".parse().unwrap();
636        let original = make_ipv6_packet(src, dst, 17, &[0u8; 1200]);
637
638        let our_addr: Ipv6Addr = "fd00::ffff".parse().unwrap();
639        let response = build_packet_too_big(&original, 1280, our_addr);
640
641        // Should not send ICMP for unspecified source
642        assert!(response.is_none());
643    }
644
645    #[test]
646    fn test_build_packet_too_big_for_icmp_error() {
647        let src: Ipv6Addr = "fd00::1".parse().unwrap();
648        let dst: Ipv6Addr = "fd00::2".parse().unwrap();
649        // ICMPv6 Destination Unreachable (type 1) - an error message
650        let icmp_payload = [1u8, 0, 0, 0, 0, 0, 0, 0];
651        let original = make_ipv6_packet(src, dst, IPPROTO_ICMPV6, &icmp_payload);
652
653        let our_addr: Ipv6Addr = "fd00::ffff".parse().unwrap();
654        let response = build_packet_too_big(&original, 1280, our_addr);
655
656        // Should not send ICMP in response to ICMP error
657        assert!(response.is_none());
658    }
659
660    #[test]
661    fn test_build_packet_too_big_for_icmp_echo() {
662        let src: Ipv6Addr = "fd00::1".parse().unwrap();
663        let dst: Ipv6Addr = "fd00::2".parse().unwrap();
664        // ICMPv6 Echo Request (type 128) - informational, not error
665        let icmp_payload = [128u8, 0, 0, 0, 0, 0, 0, 0];
666        let original = make_ipv6_packet(src, dst, IPPROTO_ICMPV6, &icmp_payload);
667
668        let our_addr: Ipv6Addr = "fd00::ffff".parse().unwrap();
669        let response = build_packet_too_big(&original, 1280, our_addr);
670
671        // Should send ICMP for informational messages
672        assert!(response.is_some());
673    }
674
675    #[test]
676    fn test_build_packet_too_big_truncates_large_packet() {
677        let src: Ipv6Addr = "fd00::1".parse().unwrap();
678        let dst: Ipv6Addr = "fd00::2".parse().unwrap();
679        // Very large payload
680        let original = make_ipv6_packet(src, dst, 17, &[0u8; 2000]);
681
682        let our_addr: Ipv6Addr = "fd00::ffff".parse().unwrap();
683        let response = build_packet_too_big(&original, 1070, our_addr);
684
685        assert!(response.is_some());
686        let response = response.unwrap();
687
688        // Response must not exceed minimum MTU
689        assert!(response.len() <= MIN_IPV6_MTU);
690    }
691
692    /// Verify that when the ICMP source is set to the original packet's
693    /// destination (the remote peer), the PTB is correctly formed.
694    ///
695    /// This is the critical fix for the PMTUD blackhole: Linux ignores
696    /// ICMPv6 PTBs whose source matches a local address. By using the
697    /// remote peer's address as the ICMP source, the kernel sees the PTB
698    /// as coming from a "remote router" and honors it.
699    #[test]
700    fn test_build_packet_too_big_remote_source_for_pmtud() {
701        let local_addr: Ipv6Addr = "fd41::1".parse().unwrap();
702        let remote_addr: Ipv6Addr = "fddf::2".parse().unwrap();
703        let original = make_ipv6_packet(local_addr, remote_addr, 6, &[0u8; 1200]); // TCP
704
705        // Pass remote_addr as our_addr — this is what send_icmpv6_packet_too_big
706        // does after the fix (original packet's dst = remote peer).
707        let response = build_packet_too_big(&original, 1203, remote_addr);
708        assert!(response.is_some());
709        let response = response.unwrap();
710
711        // PTB source must be the remote peer (not local)
712        let ptb_src = Ipv6Addr::from(<[u8; 16]>::try_from(&response[8..24]).unwrap());
713        assert_eq!(
714            ptb_src, remote_addr,
715            "PTB source must be remote peer address"
716        );
717
718        // PTB destination must be the local sender (original src)
719        let ptb_dst = Ipv6Addr::from(<[u8; 16]>::try_from(&response[24..40]).unwrap());
720        assert_eq!(
721            ptb_dst, local_addr,
722            "PTB destination must be original sender"
723        );
724
725        // Verify ICMPv6 type/code
726        assert_eq!(response[IPV6_HEADER_LEN], 2); // Type = Packet Too Big
727        assert_eq!(response[IPV6_HEADER_LEN + 1], 0); // Code = 0
728
729        // Verify reported MTU
730        let reported_mtu = u32::from_be_bytes([
731            response[IPV6_HEADER_LEN + 4],
732            response[IPV6_HEADER_LEN + 5],
733            response[IPV6_HEADER_LEN + 6],
734            response[IPV6_HEADER_LEN + 7],
735        ]);
736        assert_eq!(reported_mtu, 1203);
737
738        // Verify checksum is valid (recalculate and compare)
739        let stored_checksum =
740            u16::from_be_bytes([response[IPV6_HEADER_LEN + 2], response[IPV6_HEADER_LEN + 3]]);
741        let recomputed = icmpv6_checksum(&response[IPV6_HEADER_LEN..], &remote_addr, &local_addr);
742        assert_eq!(stored_checksum, recomputed, "ICMPv6 checksum must be valid");
743    }
744}