crafter 0.3.2

Packet-level network interaction for Rust tools and agents.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
//! Typed ICMPv6 message-body model.
//!
//! ICMPv4 (`icmp/v4/`) classifies its messages by the `type` byte and dispatches
//! a type-specific interpretation of the four rest-of-header bytes (echo
//! identifier/sequence, RFC 4884 length, packet-too-big MTU, parameter-problem
//! pointer) plus any trailing typed body layers. ICMPv6 follows the same idiom:
//! the [`Icmpv6`](super::Icmpv6) header keeps its inline fields and proven
//! byte-exact serialization, and this module adds an explicit [`Icmpv6Body`]
//! enum that names the body family the `type` selects.
//!
//! Today the enum covers the existing surface — the RFC 4443 echo body, the four
//! RFC 4443 error bodies (destination-unreachable / packet-too-big /
//! time-exceeded / parameter-problem, each carrying a rest-of-header plus a
//! quoted packet), and an [`Icmpv6Body::Unknown`] catch-all for unrecognized
//! types whose rest-of-header stays raw. It is the first-class home into which
//! later steps slot the NDP, MLD, node-information, and extended-echo bodies
//! (see the `// extensible:` marker on the body classifier) without reshaping
//! the [`Icmpv6`](super::Icmpv6) header or its constructors.
//!
//! The enum is a *view* derived from the header's typed fields: it carries no
//! state the header does not already hold, so deriving it never changes emitted
//! bytes. `summary()` / `show()` render the body variant; serialization stays in
//! the header's `effective_rest_of_header` path.

use super::*;

/// The ICMPv6 message body selected by the [`Icmpv6`](super::Icmpv6) `type`
/// byte.
///
/// This is the typed-body seam that mirrors ICMPv4's type-dispatched body model.
/// It is derived from the [`Icmpv6`](super::Icmpv6) header (via
/// [`Icmpv6::body`](super::Icmpv6::body)); the header remains the source of
/// truth for the bytes on the wire. New ICMPv6 message families (NDP, MLD, node
/// information, extended echo) become new variants in later steps.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Icmpv6Body {
    /// RFC 4443 echo request (128) / echo reply (129): a 16-bit identifier and a
    /// 16-bit sequence number in the rest-of-header, followed by an opaque data
    /// payload carried as a trailing [`Raw`] layer.
    Echo {
        /// Echo identifier (first half of the rest-of-header).
        identifier: u16,
        /// Echo sequence number (second half of the rest-of-header).
        sequence_number: u16,
    },
    /// RFC 4443 error message (destination-unreachable / packet-too-big /
    /// time-exceeded / parameter-problem): a four-byte rest-of-header whose
    /// type-specific fields are reported here, followed by the quoted packet
    /// that triggered the error (carried as trailing layers).
    Error(Icmpv6ErrorBody),
    /// RFC 2710 section 3 MLDv1 Multicast Listener Query (130): the four
    /// rest-of-header bytes are the 16-bit Maximum Response Delay (milliseconds)
    /// and a 16-bit Reserved field; the 128-bit Multicast Address (zero for a
    /// General Query, the group for a Multicast-Address-Specific Query) rides in a
    /// trailing
    /// [`MulticastListenerMessage`](super::message::mld::MulticastListenerMessage)
    /// layer (the way an echo body's data rides in a trailing [`Raw`]).
    ///
    /// The MLDv1 Query shares type 130 with the MLDv2 Query (RFC 3810), which is
    /// distinguished by a longer body; this header-derived view is the same for
    /// both, so a later MLDv2 step refines the *trailing body*, not this arm.
    MulticastListenerQuery {
        /// Maximum Response Delay in milliseconds (RFC 2710 sec 3.3: the maximum
        /// delay a responding host may insert before its Report; meaningful only
        /// on a Query). This is the first half of the rest-of-header.
        max_response_delay: u16,
    },
    /// RFC 2710 section 3 MLDv1 Multicast Listener Report (131): the four
    /// rest-of-header bytes are the (zero, on a Report) Maximum Response Delay and
    /// Reserved fields; the 128-bit Multicast Address (the group the sender is
    /// listening to) rides in a trailing
    /// [`MulticastListenerMessage`](super::message::mld::MulticastListenerMessage)
    /// layer. The Multicast Address is in the trailing body, so it is read back
    /// from the decoded layer rather than from this header-derived view (which
    /// reports only the rest-of-header fields, mirroring how the NDP arms report
    /// only their header word and leave the addresses to the trailing body).
    MulticastListenerReport {
        /// The Maximum Response Delay field (RFC 2710 sec 3.3: sent as zero in a
        /// Report, preserved verbatim here so a non-zero value is visible). This
        /// is the first half of the rest-of-header.
        max_response_delay: u16,
    },
    /// RFC 2710 section 3 MLDv1 Multicast Listener Done (132): the four
    /// rest-of-header bytes are the (zero, on a Done) Maximum Response Delay and
    /// Reserved fields; the 128-bit Multicast Address (the group the sender is
    /// ceasing to listen to — the multicast analogue of an IGMP Leave) rides in a
    /// trailing
    /// [`MulticastListenerMessage`](super::message::mld::MulticastListenerMessage)
    /// layer, read back from the decoded layer rather than from this
    /// header-derived view.
    MulticastListenerDone {
        /// The Maximum Response Delay field (RFC 2710 sec 3.3: sent as zero in a
        /// Done, preserved verbatim here so a non-zero value is visible). This is
        /// the first half of the rest-of-header.
        max_response_delay: u16,
    },
    /// RFC 3810 section 5.2 MLDv2 Version 2 Multicast Listener Report (143): the
    /// four rest-of-header bytes are a 16-bit Reserved field and the 16-bit Nr of
    /// Mcast Address Records (M); the Multicast Address Records themselves ride in
    /// a trailing [`Mldv2Report`](super::message::mld::Mldv2Report) layer (the way
    /// an echo body's data rides in a trailing [`Raw`]). This header-derived view
    /// surfaces the record count from the rest-of-header; the records are read
    /// back from the decoded layer.
    Mldv2Report {
        /// The Nr of Mcast Address Records (M) field (RFC 3810 sec 5.2: the
        /// second half of the rest-of-header). The records themselves live in the
        /// trailing Mldv2Report layer.
        number_of_records: u16,
    },
    /// RFC 4861 section 4.1 Router Solicitation (133): the four rest-of-header
    /// bytes are an unused, send-as-zero Reserved field; the message's NDP
    /// options ride in a trailing
    /// [`RouterSolicitation`](super::message::ndp::RouterSolicitation) layer (the
    /// way an echo body's data rides in a trailing [`Raw`]). This variant is the
    /// first NDP body; Router/Neighbor Advertisement, Neighbor Solicitation, and
    /// Redirect (133–137) join it in later steps.
    RouterSolicitation {
        /// The 32-bit Reserved field from the rest-of-header (RFC 4861 sec 4.1:
        /// sent as zero, preserved verbatim here so a non-zero value is visible).
        reserved: u32,
    },
    /// RFC 4861 section 4.2 Router Advertisement (134): the four rest-of-header
    /// bytes are the Cur Hop Limit, the M/O flags byte (with six Reserved bits),
    /// and the Router Lifetime; the Reachable-Time / Retrans-Timer words and the
    /// NDP options ride in a trailing
    /// [`RouterAdvertisement`](super::message::ndp::RouterAdvertisement) layer.
    /// RFC 4862 section 5.2 defines how the M and O flags drive host
    /// configuration.
    RouterAdvertisement {
        /// Cur Hop Limit (RFC 4861 sec 4.2: default hop limit for hosts using
        /// this router; 0 = unspecified).
        cur_hop_limit: u8,
        /// The M (Managed Address Configuration) flag — RFC 4861 sec 4.2 bit
        /// 0x80; RFC 4862 sec 5.2: select stateful (DHCPv6) address config.
        managed: bool,
        /// The O (Other Configuration) flag — RFC 4861 sec 4.2 bit 0x40;
        /// RFC 4862 sec 5.2: other configuration is available via DHCPv6.
        other: bool,
        /// The Default Router Preference (Prf) — RFC 4191 sec 2.2 bits 0x18 of
        /// the flags byte. RFC 4191 reassigned two of RFC 4861's send-as-zero
        /// Reserved bits to this 2-bit preference; it is decoded here rather than
        /// folded into [`reserved_flags`](Self::RouterAdvertisement::reserved_flags).
        preference: Prf,
        /// The remaining Reserved flag bits (RFC 4861 sec 4.2: send-as-zero),
        /// preserved verbatim so a non-zero value (or a later RFC 5175 "H"
        /// assignment at 0x20) is visible. This is the flags byte with M (0x80),
        /// O (0x40), and the RFC 4191 Prf bits (0x18) masked out — i.e. the bits
        /// 0x27 (the 0x20 "H" bit and the low three reserved bits).
        reserved_flags: u8,
        /// Router Lifetime in seconds (RFC 4861 sec 4.2: how long this router is
        /// a default router; 0 = not a default router).
        router_lifetime: u16,
    },
    /// RFC 4861 section 4.3 Neighbor Solicitation (135): the four rest-of-header
    /// bytes are an unused, send-as-zero Reserved field; the 128-bit Target
    /// Address (the IPv6 address being resolved) and the NDP options ride in a
    /// trailing
    /// [`NeighborSolicitation`](super::message::ndp::NeighborSolicitation) layer.
    /// Neighbor Solicitation is the IPv6 analogue of ARP "who-has" and, when sent
    /// from the unspecified source, a Duplicate Address Detection probe.
    NeighborSolicitation {
        /// The 32-bit Reserved field from the rest-of-header (RFC 4861 sec 4.3:
        /// sent as zero, preserved verbatim here so a non-zero value is visible).
        reserved: u32,
    },
    /// RFC 4861 section 4.4 Neighbor Advertisement (136): the four rest-of-header
    /// bytes are the R (Router) / S (Solicited) / O (Override) flags in the three
    /// most-significant bits plus 29 Reserved bits; the 128-bit Target Address (the
    /// address whose link-layer address is being reported) and the NDP options ride
    /// in a trailing
    /// [`NeighborAdvertisement`](super::message::ndp::NeighborAdvertisement) layer.
    /// Neighbor Advertisement is the IPv6 analogue of ARP "is-at" and answers a
    /// Neighbor Solicitation.
    NeighborAdvertisement {
        /// The R (Router) flag — RFC 4861 sec 4.4 bit 0x80000000: the sender is a
        /// router.
        router: bool,
        /// The S (Solicited) flag — RFC 4861 sec 4.4 bit 0x40000000: the
        /// advertisement was sent in response to a Neighbor Solicitation.
        solicited: bool,
        /// The O (Override) flag — RFC 4861 sec 4.4 bit 0x20000000: the
        /// advertisement should override an existing cache entry.
        override_flag: bool,
        /// The 29 Reserved bits (RFC 4861 sec 4.4: send-as-zero), preserved
        /// verbatim so a non-zero value is visible. These are the low 29 bits of
        /// the 32-bit flags word.
        reserved: u32,
    },
    /// RFC 4861 section 4.5 Redirect (137): the four rest-of-header bytes are an
    /// unused, send-as-zero Reserved field; the 128-bit Target Address (a better
    /// first hop), the 128-bit Destination Address (the destination being
    /// redirected), and the NDP options (commonly a Target Link-Layer Address and
    /// a Redirected Header carrying the packet that triggered the Redirect) ride in
    /// a trailing [`Redirect`](super::message::ndp::Redirect) layer. Redirect is
    /// the IPv6 analogue of the ICMPv4 Redirect: a router tells a host of a better
    /// first hop for a destination.
    Redirect {
        /// The 32-bit Reserved field from the rest-of-header (RFC 4861 sec 4.5:
        /// sent as zero, preserved verbatim here so a non-zero value is visible).
        reserved: u32,
    },
    /// RFC 4620 section 4 Node Information Query (139), **experimental**: the four
    /// rest-of-header bytes are the 16-bit Qtype (the question being asked) and the
    /// 16-bit Qtype-dependent Flags field; the 64-bit Nonce and the variable Data
    /// (the Subject of the Query) ride in a trailing
    /// [`NodeInformation`](super::message::node_info::NodeInformation) layer (the
    /// way an echo body's data rides in a trailing [`Raw`]). RFC 4620 is an
    /// Experimental RFC; this header-derived view surfaces the Qtype / Flags, and
    /// the Nonce / Data are read back from the decoded layer.
    NodeInformationQuery {
        /// The Qtype field (RFC 4620 sec 4: `0` NOOP, `2` Node Name, `3` Node
        /// Addresses, `4` IPv4 Addresses). This is the first half of the
        /// rest-of-header.
        qtype: u16,
        /// The Qtype-dependent Flags field (RFC 4620 sec 4), preserved verbatim so
        /// any flag combination — including reserved bits — is visible. This is
        /// the second half of the rest-of-header.
        flags: u16,
    },
    /// RFC 4620 section 4 Node Information Response (140), **experimental**: the
    /// four rest-of-header bytes are the 16-bit Qtype (echoed from the Query) and
    /// the 16-bit Qtype-dependent Flags field; the 64-bit Nonce (copied from the
    /// Query) and the variable Data (the answer) ride in a trailing
    /// [`NodeInformation`](super::message::node_info::NodeInformation) layer.
    /// RFC 4620 is an Experimental RFC; the ICMPv6 Code (0 success, 1 refused, 2
    /// unknown Qtype) distinguishes the result, and the Nonce / Data are read back
    /// from the decoded layer.
    NodeInformationResponse {
        /// The Qtype field echoed from the Query (RFC 4620 sec 4). This is the
        /// first half of the rest-of-header.
        qtype: u16,
        /// The Qtype-dependent Flags field (RFC 4620 sec 4), preserved verbatim.
        /// This is the second half of the rest-of-header.
        flags: u16,
    },
    /// RFC 8335 section 3 Extended Echo Request (160): the rest-of-header is a
    /// 16-bit Identifier, an 8-bit Sequence Number, and a flag byte whose
    /// rightmost bit is the L (Local) bit (the probed interface resides on a proxy
    /// node). The RFC 4884 ICMP Extension Structure naming the probed interface
    /// rides in trailing [`IcmpExtension`] / [`IcmpExtensionInterfaceId`] layers
    /// (the way an echo body's data rides in a trailing [`Raw`]).
    ExtendedEchoRequest {
        /// Echo Identifier (first half of the rest-of-header).
        identifier: u16,
        /// Echo Sequence Number (byte 2 of the rest-of-header; 8 bits).
        sequence_number: u8,
        /// The L (Local) bit (RFC 8335 sec 3: the rightmost bit of the flag
        /// byte) — the probed interface is on a proxy node.
        local: bool,
        /// The seven Reserved bits of the flag byte (RFC 8335 sec 3:
        /// send-as-zero), preserved verbatim so a non-zero value is visible.
        reserved_flags: u8,
    },
    /// RFC 8335 section 3 Extended Echo Reply (161): the rest-of-header is a
    /// 16-bit Identifier, an 8-bit Sequence Number, and a flag byte packing the
    /// State (top 3 bits), 2 Reserved bits, and the A (Active), 4 (IPv4), and 6
    /// (IPv6) status bits. The reply's `Code` (0-4) reports the query result. The
    /// reply carries no body of its own.
    ExtendedEchoReply {
        /// Echo Identifier (first half of the rest-of-header).
        identifier: u16,
        /// Echo Sequence Number (byte 2 of the rest-of-header; 8 bits).
        sequence_number: u8,
        /// The State field (RFC 8335 sec 3: the top 3 bits of the flag byte; the
        /// ARP/Neighbor Cache state of the probed entry).
        state: u8,
        /// The A (Active) status bit (RFC 8335 sec 3: 0x04 of the flag byte).
        active: bool,
        /// The 4 (IPv4) status bit (RFC 8335 sec 3: 0x02 of the flag byte).
        ipv4: bool,
        /// The 6 (IPv6) status bit (RFC 8335 sec 3: 0x01 of the flag byte).
        ipv6: bool,
    },
    /// Any ICMPv6 `type` not yet modeled with a typed body. The four
    /// rest-of-header bytes are preserved verbatim and any trailing bytes stay a
    /// [`Raw`] payload, so unknown messages round-trip unchanged.
    Unknown {
        /// The raw `type` byte, kept so callers can inspect the unrecognized
        /// message without re-reading the header.
        icmp_type: u8,
        /// The four rest-of-header bytes, preserved exactly.
        rest_of_header: [u8; 4],
    },
}

/// The typed fields an RFC 4443 error message exposes in its rest-of-header.
///
/// Every error type carries the same four rest-of-header bytes; the meaning of
/// those bytes is type-specific, so the relevant interpretation is surfaced per
/// variant while the raw bytes stay available through the header's
/// `rest_of_header_value`.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Icmpv6ErrorBody {
    /// Destination unreachable (1): the rest-of-header is unused (sent as zero).
    DestinationUnreachable,
    /// Packet too big (2): the rest-of-header is the 32-bit next-hop MTU.
    PacketTooBig {
        /// Next-hop MTU advertised by the message.
        mtu: u32,
    },
    /// Time exceeded (3): the rest-of-header is unused (sent as zero).
    TimeExceeded,
    /// Parameter problem (4): the rest-of-header is a 32-bit pointer to the
    /// offending octet in the original packet.
    ParameterProblem {
        /// Offset of the octet that triggered the error.
        pointer: u32,
    },
}

impl Icmpv6Body {
    /// Classify the body an [`Icmpv6`](super::Icmpv6) header carries, reading the
    /// type-specific fields the header already holds.
    pub(crate) fn from_header(header: &Icmpv6) -> Self {
        let icmp_type = header.icmp_type_value();
        let rest_of_header = header.rest_of_header_value();
        // extensible: NDP (133-137), MLD (130-132, 143), node information
        // (139/140), and extended echo (160/161) gain their own arms here in
        // later steps. Router Solicitation (133) is classified below; the
        // remaining NDP types and the other families still fall through to
        // `Unknown`, preserving the raw rest-of-header so the messages still
        // round-trip.
        match icmp_type {
            ICMPV6_ECHO_REQUEST | ICMPV6_ECHO_REPLY => {
                let identifier = u16::from_be_bytes([rest_of_header[0], rest_of_header[1]]);
                let sequence_number = u16::from_be_bytes([rest_of_header[2], rest_of_header[3]]);
                Icmpv6Body::Echo {
                    identifier,
                    sequence_number,
                }
            }
            ICMPV6_DESTINATION_UNREACHABLE => {
                Icmpv6Body::Error(Icmpv6ErrorBody::DestinationUnreachable)
            }
            ICMPV6_PACKET_TOO_BIG => Icmpv6Body::Error(Icmpv6ErrorBody::PacketTooBig {
                mtu: u32::from_be_bytes(rest_of_header),
            }),
            ICMPV6_TIME_EXCEEDED => Icmpv6Body::Error(Icmpv6ErrorBody::TimeExceeded),
            ICMPV6_PARAMETER_PROBLEM => Icmpv6Body::Error(Icmpv6ErrorBody::ParameterProblem {
                pointer: u32::from_be_bytes(rest_of_header),
            }),
            ICMPV6_MULTICAST_LISTENER_QUERY => Icmpv6Body::MulticastListenerQuery {
                // RFC 2710 sec 3.3: the first half of the rest-of-header is the
                // 16-bit Maximum Response Delay (the second half is Reserved). The
                // Multicast Address lives in the trailing MulticastListenerMessage
                // layer, not in this header-derived view.
                max_response_delay: u16::from_be_bytes([rest_of_header[0], rest_of_header[1]]),
            },
            ICMPV6_MULTICAST_LISTENER_REPORT => Icmpv6Body::MulticastListenerReport {
                // RFC 2710 sec 3.3: the Maximum Response Delay is sent as zero in a
                // Report but is preserved verbatim. The Multicast Address (the
                // group the sender listens to) lives in the trailing
                // MulticastListenerMessage layer.
                max_response_delay: u16::from_be_bytes([rest_of_header[0], rest_of_header[1]]),
            },
            ICMPV6_MULTICAST_LISTENER_DONE => Icmpv6Body::MulticastListenerDone {
                // RFC 2710 sec 3.3: the Maximum Response Delay is sent as zero in a
                // Done but is preserved verbatim. The Multicast Address (the group
                // the sender is ceasing to listen to) lives in the trailing
                // MulticastListenerMessage layer.
                max_response_delay: u16::from_be_bytes([rest_of_header[0], rest_of_header[1]]),
            },
            ICMPV6_MLDV2_REPORT => Icmpv6Body::Mldv2Report {
                // RFC 3810 sec 5.2: the rest-of-header is a 16-bit Reserved field
                // (bytes 0..2) followed by the 16-bit Nr of Mcast Address Records
                // (bytes 2..4). The records live in the trailing Mldv2Report
                // layer, not in this header-derived view.
                number_of_records: u16::from_be_bytes([rest_of_header[2], rest_of_header[3]]),
            },
            ICMPV6_ROUTER_SOLICITATION => Icmpv6Body::RouterSolicitation {
                // RFC 4861 sec 4.1: the rest-of-header is the 32-bit Reserved
                // field. The options live in the trailing RouterSolicitation
                // layer, not in this header-derived view.
                reserved: u32::from_be_bytes(rest_of_header),
            },
            ICMPV6_ROUTER_ADVERTISEMENT => {
                // RFC 4861 sec 4.2: rest-of-header = Cur Hop Limit (byte 0),
                // flags byte (byte 1: 0x80 M, 0x40 O, low six bits Reserved),
                // Router Lifetime (bytes 2..4). RFC 4191 sec 2.2 reassigns bits
                // 0x18 of the flags byte to the Default Router Preference (Prf);
                // it is decoded here, and the remaining bits (0x27) stay in
                // reserved_flags. The Reachable-Time / Retrans-Timer words and
                // options live in the trailing RouterAdvertisement layer, not in
                // this header-derived view.
                let flags = rest_of_header[1];
                Icmpv6Body::RouterAdvertisement {
                    cur_hop_limit: rest_of_header[0],
                    managed: flags & ICMPV6_RA_FLAG_MANAGED != 0,
                    other: flags & ICMPV6_RA_FLAG_OTHER != 0,
                    preference: Prf::from_flag_byte(flags),
                    reserved_flags: flags & ICMPV6_RA_FLAGS_RESERVED & !NDP_PRF_MASK,
                    router_lifetime: u16::from_be_bytes([rest_of_header[2], rest_of_header[3]]),
                }
            }
            ICMPV6_NEIGHBOR_SOLICITATION => Icmpv6Body::NeighborSolicitation {
                // RFC 4861 sec 4.3: the rest-of-header is the 32-bit Reserved
                // field. The Target Address and options live in the trailing
                // NeighborSolicitation layer, not in this header-derived view.
                reserved: u32::from_be_bytes(rest_of_header),
            },
            ICMPV6_NEIGHBOR_ADVERTISEMENT => {
                // RFC 4861 sec 4.4: rest-of-header = the 32-bit flags word. The
                // three most-significant bits are R (0x80000000), S (0x40000000),
                // O (0x20000000); the low 29 bits are Reserved (preserved). The
                // Target Address and options live in the trailing
                // NeighborAdvertisement layer, not in this header-derived view.
                let flags = rest_of_header[0];
                Icmpv6Body::NeighborAdvertisement {
                    router: flags & ICMPV6_NA_FLAG_ROUTER != 0,
                    solicited: flags & ICMPV6_NA_FLAG_SOLICITED != 0,
                    override_flag: flags & ICMPV6_NA_FLAG_OVERRIDE != 0,
                    reserved: u32::from_be_bytes(rest_of_header) & ICMPV6_NA_FLAGS_RESERVED,
                }
            }
            ICMPV6_REDIRECT => Icmpv6Body::Redirect {
                // RFC 4861 sec 4.5: the rest-of-header is the 32-bit Reserved
                // field. The Target Address, Destination Address, and options
                // live in the trailing Redirect layer, not in this
                // header-derived view.
                reserved: u32::from_be_bytes(rest_of_header),
            },
            ICMPV6_NODE_INFORMATION_QUERY => Icmpv6Body::NodeInformationQuery {
                // RFC 4620 sec 4 (experimental): the rest-of-header is the 16-bit
                // Qtype (bytes 0..2) and the 16-bit Flags (bytes 2..4). The Nonce
                // and Data live in the trailing NodeInformation layer, not in this
                // header-derived view.
                qtype: u16::from_be_bytes([rest_of_header[0], rest_of_header[1]]),
                flags: u16::from_be_bytes([rest_of_header[2], rest_of_header[3]]),
            },
            ICMPV6_NODE_INFORMATION_RESPONSE => Icmpv6Body::NodeInformationResponse {
                // RFC 4620 sec 4 (experimental): same rest-of-header layout as the
                // Query — the Qtype (echoed) and the Flags. The Nonce and Data
                // (the answer) live in the trailing NodeInformation layer.
                qtype: u16::from_be_bytes([rest_of_header[0], rest_of_header[1]]),
                flags: u16::from_be_bytes([rest_of_header[2], rest_of_header[3]]),
            },
            ICMPV6_EXTENDED_ECHO_REQUEST => {
                // RFC 8335 sec 3: identifier (bytes 0..2), 8-bit sequence number
                // (byte 2), flag byte (byte 3) with the L-bit at 0x01 and the
                // upper seven bits Reserved. The RFC 4884 extension structure
                // rides in trailing layers, not in this header-derived view.
                let flags = rest_of_header[3];
                Icmpv6Body::ExtendedEchoRequest {
                    identifier: u16::from_be_bytes([rest_of_header[0], rest_of_header[1]]),
                    sequence_number: rest_of_header[2],
                    local: flags & ICMPV6_EXTENDED_ECHO_REQUEST_L_BIT != 0,
                    reserved_flags: flags & !ICMPV6_EXTENDED_ECHO_REQUEST_L_BIT,
                }
            }
            ICMPV6_EXTENDED_ECHO_REPLY => {
                // RFC 8335 sec 3: identifier (bytes 0..2), 8-bit sequence number
                // (byte 2), flag byte (byte 3) packing State (top 3 bits), 2
                // Reserved bits, and A (0x04) / 4 (0x02) / 6 (0x01) status bits.
                let flags = rest_of_header[3];
                Icmpv6Body::ExtendedEchoReply {
                    identifier: u16::from_be_bytes([rest_of_header[0], rest_of_header[1]]),
                    sequence_number: rest_of_header[2],
                    state: (flags >> 5) & 0x07,
                    active: flags & ICMPV6_EXTENDED_ECHO_REPLY_ACTIVE != 0,
                    ipv4: flags & ICMPV6_EXTENDED_ECHO_REPLY_IPV4 != 0,
                    ipv6: flags & ICMPV6_EXTENDED_ECHO_REPLY_IPV6 != 0,
                }
            }
            _ => Icmpv6Body::Unknown {
                icmp_type,
                rest_of_header,
            },
        }
    }

    /// Stable short label for the body family (`"echo"`, `"error"`,
    /// `"unknown"`), for inspection and dispatch in agent code.
    pub fn label(&self) -> &'static str {
        match self {
            Icmpv6Body::Echo { .. } => "echo",
            Icmpv6Body::Error(_) => "error",
            Icmpv6Body::MulticastListenerQuery { .. } => "multicast-listener-query",
            Icmpv6Body::MulticastListenerReport { .. } => "multicast-listener-report",
            Icmpv6Body::MulticastListenerDone { .. } => "multicast-listener-done",
            Icmpv6Body::Mldv2Report { .. } => "mldv2-report",
            Icmpv6Body::RouterSolicitation { .. } => "router-solicitation",
            Icmpv6Body::RouterAdvertisement { .. } => "router-advertisement",
            Icmpv6Body::NeighborSolicitation { .. } => "neighbor-solicitation",
            Icmpv6Body::NeighborAdvertisement { .. } => "neighbor-advertisement",
            Icmpv6Body::Redirect { .. } => "redirect",
            Icmpv6Body::NodeInformationQuery { .. } => "node-information-query",
            Icmpv6Body::NodeInformationResponse { .. } => "node-information-response",
            Icmpv6Body::ExtendedEchoRequest { .. } => "extended-echo-request",
            Icmpv6Body::ExtendedEchoReply { .. } => "extended-echo-reply",
            Icmpv6Body::Unknown { .. } => "unknown",
        }
    }

    /// A compact, body-specific description of the typed fields, for `show()`.
    pub(crate) fn detail(&self) -> String {
        match self {
            Icmpv6Body::Echo {
                identifier,
                sequence_number,
            } => format!("echo(id=0x{identifier:04x}, seq={sequence_number})"),
            Icmpv6Body::Error(error) => match error {
                Icmpv6ErrorBody::DestinationUnreachable => {
                    "error(destination-unreachable)".to_string()
                }
                Icmpv6ErrorBody::PacketTooBig { mtu } => {
                    format!("error(packet-too-big, mtu={mtu})")
                }
                Icmpv6ErrorBody::TimeExceeded => "error(time-exceeded)".to_string(),
                Icmpv6ErrorBody::ParameterProblem { pointer } => {
                    format!("error(parameter-problem, pointer={pointer})")
                }
            },
            Icmpv6Body::MulticastListenerQuery { max_response_delay } => {
                format!("multicast-listener-query(max_response_delay={max_response_delay})")
            }
            Icmpv6Body::MulticastListenerReport { max_response_delay } => {
                format!("multicast-listener-report(max_response_delay={max_response_delay})")
            }
            Icmpv6Body::MulticastListenerDone { max_response_delay } => {
                format!("multicast-listener-done(max_response_delay={max_response_delay})")
            }
            Icmpv6Body::Mldv2Report { number_of_records } => {
                format!("mldv2-report(records={number_of_records})")
            }
            Icmpv6Body::RouterSolicitation { reserved } => {
                format!("router-solicitation(reserved=0x{reserved:08x})")
            }
            Icmpv6Body::RouterAdvertisement {
                cur_hop_limit,
                managed,
                other,
                preference,
                reserved_flags,
                router_lifetime,
            } => format!(
                "router-advertisement(cur_hop_limit={cur_hop_limit}, M={managed}, O={other}, \
                 prf={preference:?}, reserved_flags=0x{reserved_flags:02x}, \
                 router_lifetime={router_lifetime})"
            ),
            Icmpv6Body::NeighborSolicitation { reserved } => {
                format!("neighbor-solicitation(reserved=0x{reserved:08x})")
            }
            Icmpv6Body::NeighborAdvertisement {
                router,
                solicited,
                override_flag,
                reserved,
            } => format!(
                "neighbor-advertisement(R={router}, S={solicited}, O={override_flag}, \
                 reserved=0x{reserved:08x})"
            ),
            Icmpv6Body::Redirect { reserved } => {
                format!("redirect(reserved=0x{reserved:08x})")
            }
            Icmpv6Body::NodeInformationQuery { qtype, flags } => {
                format!("node-information-query(qtype={qtype}, flags=0x{flags:04x})")
            }
            Icmpv6Body::NodeInformationResponse { qtype, flags } => {
                format!("node-information-response(qtype={qtype}, flags=0x{flags:04x})")
            }
            Icmpv6Body::ExtendedEchoRequest {
                identifier,
                sequence_number,
                local,
                reserved_flags,
            } => format!(
                "extended-echo-request(id=0x{identifier:04x}, seq={sequence_number}, L={local}, \
                 reserved_flags=0x{reserved_flags:02x})"
            ),
            Icmpv6Body::ExtendedEchoReply {
                identifier,
                sequence_number,
                state,
                active,
                ipv4,
                ipv6,
            } => format!(
                "extended-echo-reply(id=0x{identifier:04x}, seq={sequence_number}, state={state}, \
                 A={active}, 4={ipv4}, 6={ipv6})"
            ),
            Icmpv6Body::Unknown {
                icmp_type,
                rest_of_header,
            } => format!(
                "unknown(type={icmp_type}, rest_of_header={})",
                hex_bytes(rest_of_header)
            ),
        }
    }
}