packet_formats/igmp/
messages.rs

1// Copyright 2019 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! Implementation of IGMP Messages.
6
7use core::borrow::Borrow as _;
8use core::ops::Deref;
9
10use net_types::ip::{Ip as _, Ipv4, Ipv4Addr};
11use net_types::{MulticastAddr, Witness as _};
12use packet::records::{ParsedRecord, RecordParseResult, Records, RecordsImpl, RecordsImplLayout};
13use packet::{BufferView, FragmentedByteSlice, InnerPacketBuilder, ParsablePacket, ParseMetadata};
14use zerocopy::byteorder::network_endian::U16;
15use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Ref, SplitByteSlice, Unaligned};
16
17use super::{
18    peek_message_type, IgmpMessage, IgmpNonEmptyBody, IgmpResponseTimeV2, IgmpResponseTimeV3,
19};
20use crate::error::{ParseError, UnrecognizedProtocolCode};
21use crate::gmp::{GmpReportGroupRecord, InvalidConstraintsError};
22use crate::igmp::{IgmpPacketBuilder, MessageType};
23
24create_protocol_enum!(
25    /// An IGMP message type.
26    #[allow(missing_docs)]
27    #[derive(PartialEq, Copy, Clone)]
28    pub enum IgmpMessageType: u8 {
29        MembershipQuery, 0x11, "Membership Query";
30        MembershipReportV1,0x12, "Membership Report V1";
31        MembershipReportV2,0x16, "Membership Report V2";
32        MembershipReportV3,0x22, "Membership Report V3";
33        LeaveGroup, 0x17, "Leave Group";
34    }
35);
36
37macro_rules! impl_igmp_simple_message_type {
38    ($type:ident, $code:tt, $fixed_header:ident) => {
39        impl<B> MessageType<B> for $type {
40            type FixedHeader = $fixed_header;
41            const TYPE: IgmpMessageType = IgmpMessageType::$code;
42            type MaxRespTime = ();
43            declare_no_body!();
44        }
45    };
46}
47
48macro_rules! declare_no_body {
49    () => {
50        type VariableBody = ();
51
52        fn parse_body<BV: BufferView<B>>(
53            _header: &Self::FixedHeader,
54            bytes: BV,
55        ) -> Result<Self::VariableBody, ParseError>
56        where
57            B: SplitByteSlice,
58        {
59            if bytes.len() != 0 {
60                Err(ParseError::NotExpected)
61            } else {
62                Ok(())
63            }
64        }
65
66        fn body_bytes(_body: &Self::VariableBody) -> &[u8]
67        where
68            B: SplitByteSlice,
69        {
70            &[]
71        }
72    };
73}
74
75/// IGMPv2 Membership Query message.
76///
77/// `IgmpMembershipQueryV2` implements `MessageType`, providing the intended
78/// behavior for IGMPv2 Membership Queries as defined in [RFC 2236].
79///
80/// There are two sub-types of Membership Query messages:
81/// - General Query, used to learn which groups have members on an
82///   attached network.
83/// - Group-Specific Query, used to learn if a particular group
84///   has any members on an attached network.
85///
86/// These two messages are differentiated by the Group Address, as
87/// described in [RFC 2236 section 1.4].
88///
89/// [RFC 2236]: https://tools.ietf.org/html/rfc2236
90/// [RFC 2236 section 1.4]: https://tools.ietf.org/html/rfc2236#section-1.4
91#[derive(Copy, Clone, Debug)]
92pub struct IgmpMembershipQueryV2;
93
94impl<B> MessageType<B> for IgmpMembershipQueryV2 {
95    type FixedHeader = Ipv4Addr;
96    type MaxRespTime = IgmpResponseTimeV2;
97    const TYPE: IgmpMessageType = IgmpMessageType::MembershipQuery;
98
99    declare_no_body!();
100}
101
102impl<B: SplitByteSlice> IgmpMessage<B, IgmpMembershipQueryV2> {
103    /// Returns true if this is an IGMPv1 query.
104    ///
105    /// Defined in [RFC 2236 section 4.1]:
106    ///
107    /// > The IGMPv1 router will send General Queries with the Max Response Time
108    /// > set to 0...
109    ///
110    /// [RFC 2236 section 4.1]:
111    ///     https://datatracker.ietf.org/doc/html/rfc2236#section-4
112    pub fn is_igmpv1_query(&self) -> bool {
113        self.prefix.max_resp_code == 0
114    }
115}
116
117/// Fixed information in IGMPv3 Membership Queries.
118///
119/// A `MembershipQueryData` struct represents the fixed data in IGMPv3
120/// Membership Queries.
121/// It is defined as the `FixedHeader` type for `IgmpMembershipQueryV3`.
122#[derive(Copy, Clone, Debug, IntoBytes, KnownLayout, FromBytes, Immutable, Unaligned)]
123#[repr(C)]
124pub struct MembershipQueryData {
125    group_address: Ipv4Addr,
126    sqrv: u8,
127    qqic: u8,
128    number_of_sources: U16,
129}
130
131impl MembershipQueryData {
132    const S_FLAG: u8 = (1 << 3);
133    const QRV_MSK: u8 = 0x07;
134
135    /// Returns the the number of sources.
136    pub fn number_of_sources(&self) -> u16 {
137        self.number_of_sources.get()
138    }
139
140    /// Returns the query's group address.
141    pub fn group_address(&self) -> Ipv4Addr {
142        self.group_address
143    }
144
145    /// Gets value of `S Flag`.
146    ///
147    /// Indicates to any receiving multicast routers that they are to suppress
148    /// the normal timer updates they perform upon hearing a Query. It does not,
149    /// however, suppress the querier election or the normal "host-side"
150    /// processing of a Query that a router may be required to perform as a
151    /// consequence of itself being a group member.
152    pub fn suppress_router_side_processing(&self) -> bool {
153        (self.sqrv & Self::S_FLAG) != 0
154    }
155
156    /// Gets querier robustness variable (QRV).
157    ///
158    /// If non-zero, the QRV field contains the *Robustness Variable* value
159    /// used by the querier, i.e., the sender of the Query.  If the querier's
160    /// *Robustness Variable* exceeds 7, the maximum value of the QRV field,
161    /// the QRV is set to zero.  Routers adopt the QRV value from the most
162    /// recently received Query as their own *Robustness Variable* value,
163    /// unless that most recently received QRV was zero, in which case the
164    /// receivers use the default *Robustness Variable* value specified in
165    /// [RFC 3376 section 8.1] (defined as `DEFAULT_QRV`) or a statically
166    /// configured value.
167    ///
168    /// [RFC 3376 section 8.1]: https://tools.ietf.org/html/rfc3376#section-8.1
169    pub fn querier_robustness_variable(&self) -> u8 {
170        self.sqrv & Self::QRV_MSK
171    }
172
173    /// Gets the querier's query interval (QQI).
174    ///
175    /// Multicast routers that are not the current querier adopt the QQI
176    /// value from the most recently received Query as their own *Query
177    /// Interval* value, unless that most recently received QQI was zero, in
178    /// which case the receiving routers use the default *Query Interval*
179    /// value specified in [RFC 3376 section 8.2], defined as
180    /// `DEFAULT_QUERY_INTERVAL`.
181    ///
182    /// [RFC 3376 section 8.2]: https://tools.ietf.org/html/rfc3376#section-8.2
183    pub fn querier_query_interval(&self) -> core::time::Duration {
184        // qqic is represented in a packed floating point format and interpreted
185        // as units of seconds.
186        Igmpv3QQIC::from(self.qqic).into()
187    }
188}
189
190/// QRV (Querier's Robustness Variable) defined
191/// in [RFC 3376 section 4.1.6].
192///
193/// Aliased to shared GMP implementation for convenience.
194///
195/// [RFC 3376 section 4.1.6]:
196///     https://datatracker.ietf.org/doc/html/rfc3376#section-4.1.6
197pub type Igmpv3QRV = crate::gmp::QRV;
198
199/// QQIC (Querier's Query Interval Code) defined in [RFC 3376 section 4.1.7].
200///
201/// Aliased to shared GMP implementation for convenience.
202///
203/// [RFC 3376 section 4.1.7]:
204///     https://datatracker.ietf.org/doc/html/rfc3376#section-4.1.7
205pub type Igmpv3QQIC = crate::gmp::QQIC;
206
207/// IGMPv3 Membership Query message.
208///
209/// `IgmpMembershipQueryV3` implements `MessageType`, providing the intended
210/// behavior for IGMPv3 Membership Queries as defined in
211/// [RFC 3376 section 4.1].
212///
213/// Membership Queries are sent by IP multicast routers to query the
214/// multicast reception state of neighboring interfaces.
215///
216/// [RFC 3376 section 4.1]: https://tools.ietf.org/html/rfc3376#section-4.1
217#[derive(Copy, Clone, Debug)]
218pub struct IgmpMembershipQueryV3;
219
220impl<B> IgmpNonEmptyBody for Ref<B, [Ipv4Addr]> {}
221
222impl<B> MessageType<B> for IgmpMembershipQueryV3 {
223    type FixedHeader = MembershipQueryData;
224    type VariableBody = Ref<B, [Ipv4Addr]>;
225    type MaxRespTime = IgmpResponseTimeV3;
226    const TYPE: IgmpMessageType = IgmpMessageType::MembershipQuery;
227
228    fn parse_body<BV: BufferView<B>>(
229        header: &Self::FixedHeader,
230        mut bytes: BV,
231    ) -> Result<Self::VariableBody, ParseError>
232    where
233        B: SplitByteSlice,
234    {
235        bytes
236            .take_slice_front::<Ipv4Addr>(header.number_of_sources() as usize)
237            .ok_or(ParseError::Format)
238    }
239
240    fn body_bytes(body: &Self::VariableBody) -> &[u8]
241    where
242        B: SplitByteSlice,
243    {
244        Ref::bytes(body)
245    }
246}
247
248impl<B: SplitByteSlice> IgmpMessage<B, IgmpMembershipQueryV3> {
249    /// Reinterprets this [`IgmpMembershipQueryV3`] message as an
250    /// [`IgmpMembershipQueryV2`] message.
251    ///
252    /// Given this crate parses the version separately, users desiring to
253    /// operate in IGMPv2 or IGMPv1 modes *SHOULD* reinterpret V3 queries as the
254    /// older version.
255    ///
256    /// See [RFC 3376 section 7.2.1] and [RFC 2236 section 2.5].
257    ///
258    /// [RFC 3376 section 7.2.1]:
259    ///     https://datatracker.ietf.org/doc/html/rfc3376#section-7.2.1
260    /// [RFC 2236 section 2.5]:
261    ///     https://datatracker.ietf.org/doc/html/rfc2236#section-2.5
262    pub fn as_v2_query(&self) -> IgmpMessage<&[u8], IgmpMembershipQueryV2> {
263        let Self { prefix, header, body: _ } = self;
264        // Unwraps are okay here, we know the sizes and alignments must fit.
265        let prefix = Ref::from_bytes(prefix.as_bytes()).unwrap();
266        let (header, _rest) = Ref::from_prefix(header.as_bytes()).unwrap();
267        IgmpMessage { prefix, header, body: () }
268    }
269}
270
271/// Fixed information in IGMPv3 Membership Reports.
272///
273/// A `MembershipReportV3Data` struct represents the fixed data in IGMPv3
274/// Membership Reports.
275/// It is defined as the `FixedHeader` type for `IgmpMembershipReportV3`.
276#[derive(Copy, Clone, Debug, IntoBytes, KnownLayout, FromBytes, Immutable, Unaligned)]
277#[repr(C)]
278pub struct MembershipReportV3Data {
279    _reserved: [u8; 2],
280    number_of_group_records: U16,
281}
282
283impl MembershipReportV3Data {
284    /// Returns the number of group records.
285    pub fn number_of_group_records(self) -> u16 {
286        self.number_of_group_records.get()
287    }
288}
289
290/// Group Record Types as defined in [RFC 3376 section 4.2.12]
291///
292/// Aliased to shared GMP implementation for convenience.
293///
294/// [RFC 3376 section 4.2.12]:
295///     https://tools.ietf.org/html/rfc3376#section-4.2.12
296pub type IgmpGroupRecordType = crate::gmp::GroupRecordType;
297
298/// Fixed information for IGMPv3 Membership Report's Group Records.
299///
300/// A `GroupRecordHeader` struct represents the fixed header of a Group Record
301/// in IGMPv3 Membership Report messages as defined in [RFC 3376 section 4.2.4].
302///
303/// The `GroupRecordHeader` is followed by a series of source IPv4 addresses.
304///
305/// [RFC 3376 section 4.2.4]: https://tools.ietf.org/html/rfc3376#section-4.2.4
306#[derive(Copy, Clone, Debug, IntoBytes, KnownLayout, FromBytes, Immutable, Unaligned)]
307#[repr(C)]
308pub struct GroupRecordHeader {
309    record_type: u8,
310    aux_data_len: u8,
311    number_of_sources: U16,
312    multicast_address: Ipv4Addr,
313}
314
315impl GroupRecordHeader {
316    /// Returns the number of sources.
317    pub fn number_of_sources(&self) -> u16 {
318        self.number_of_sources.get()
319    }
320
321    /// Returns the type of the record.
322    pub fn record_type(&self) -> Result<IgmpGroupRecordType, UnrecognizedProtocolCode<u8>> {
323        IgmpGroupRecordType::try_from(self.record_type)
324    }
325
326    /// Returns the multicast address.
327    pub fn multicast_addr(&self) -> &Ipv4Addr {
328        &self.multicast_address
329    }
330}
331
332/// Wire representation of an IGMPv3 Membership Report's Group Records.
333///
334/// A `GroupRecord` struct is composed of a fixed part provided by
335/// `GroupRecordHeader` and a variable part, which is a list of IPv4 addresses.
336///
337/// Each Group Record is a block of fields containing information
338/// pertaining to the sender's membership in a single multicast group on
339/// the interface from which the Report is sent.
340///
341/// The structure of a Group Record includes Auxiliary Data, as defined in
342/// [RFC 3376 section 4.2.10]. As the information in Auxiliary Data is supposed
343/// to be ignored, when parsing a `GroupRecord` struct from a buffer, the
344/// information in Auxiliary Data, if any, is discarded.
345///
346/// [RFC 3376 section 4.2.10]: https://tools.ietf.org/html/rfc3376#section-4.2.10
347pub struct GroupRecord<B> {
348    header: Ref<B, GroupRecordHeader>,
349    sources: Ref<B, [Ipv4Addr]>,
350}
351
352impl<B: SplitByteSlice> GroupRecord<B> {
353    /// Returns the group record header.
354    pub fn header(&self) -> &GroupRecordHeader {
355        self.header.deref()
356    }
357
358    /// Returns the group record's sources.
359    pub fn sources(&self) -> &[Ipv4Addr] {
360        self.sources.deref()
361    }
362}
363
364/// IGMPv3 Membership Report message.
365///
366/// `IgmpMembershipReportV3` implements `MessageType`, providing the intended
367/// behavior for IGMPv3 Membership Reports as defined in
368/// [RFC 3376 section 4.2].
369///
370/// Version 3 Membership Reports are sent by IP systems to report (to
371/// neighboring routers) the current multicast reception state, or
372/// changes in the multicast reception state, of their interfaces.
373///
374/// [RFC 3376 section 4.2]: https://tools.ietf.org/html/rfc3376#section-4.2
375#[derive(Copy, Clone, Debug)]
376pub struct IgmpMembershipReportV3;
377
378impl<B> IgmpNonEmptyBody for Records<B, IgmpMembershipReportV3> {}
379
380impl<B> MessageType<B> for IgmpMembershipReportV3 {
381    type FixedHeader = MembershipReportV3Data;
382    type VariableBody = Records<B, IgmpMembershipReportV3>;
383    type MaxRespTime = ();
384    const TYPE: IgmpMessageType = IgmpMessageType::MembershipReportV3;
385
386    fn parse_body<BV: BufferView<B>>(
387        header: &Self::FixedHeader,
388        bytes: BV,
389    ) -> Result<Self::VariableBody, ParseError>
390    where
391        B: SplitByteSlice,
392    {
393        Records::parse_with_context(bytes.into_rest(), header.number_of_group_records().into())
394    }
395
396    fn body_bytes(body: &Self::VariableBody) -> &[u8]
397    where
398        B: SplitByteSlice,
399    {
400        body.bytes()
401    }
402}
403
404impl RecordsImplLayout for IgmpMembershipReportV3 {
405    type Context = usize;
406    type Error = ParseError;
407}
408
409impl RecordsImpl for IgmpMembershipReportV3 {
410    type Record<'a> = GroupRecord<&'a [u8]>;
411
412    fn parse_with_context<'a, BV: BufferView<&'a [u8]>>(
413        data: &mut BV,
414        _ctx: &mut usize,
415    ) -> RecordParseResult<GroupRecord<&'a [u8]>, ParseError> {
416        let header = data
417            .take_obj_front::<GroupRecordHeader>()
418            .ok_or_else(debug_err_fn!(ParseError::Format, "Can't take group record header"))?;
419        let sources = data
420            .take_slice_front::<Ipv4Addr>(header.number_of_sources().into())
421            .ok_or_else(debug_err_fn!(ParseError::Format, "Can't group record sources"))?;
422        // every record may have aux_data_len 32-bit words at the end.
423        // we need to update our buffer view to reflect that.
424        let _ = data
425            .take_front(usize::from(header.aux_data_len) * 4)
426            .ok_or_else(debug_err_fn!(ParseError::Format, "Can't skip auxiliary data"))?;
427
428        Ok(ParsedRecord::Parsed(Self::Record { header, sources }))
429    }
430}
431
432/// IGMPv1 Membership Report message.
433///
434/// `IgmpMembershipReportV1` implements `MessageType`, providing the intended
435/// behavior for IGMPv1 Membership Reports as defined in [RFC 1112].
436///
437/// In a Host Membership Report message, the group address field (expressed in
438/// `FixedHeader`) holds the IP host group address of the group being reported.
439///
440/// Hosts respond to a Query by generating Host Membership Reports, reporting
441/// each host group to which they belong on the network interface from which the
442/// Query was received.
443///
444/// [RFC 1112]: https://tools.ietf.org/html/rfc1112
445#[derive(Debug)]
446pub struct IgmpMembershipReportV1;
447
448impl_igmp_simple_message_type!(IgmpMembershipReportV1, MembershipReportV1, Ipv4Addr);
449
450/// IGMPv2 Membership Report message.
451///
452/// `IgmpMembershipReportV2` implements `MessageType`, providing the intended
453/// behavior for IGMPv2 Membership Reports as defined in [RFC 2236].
454///
455/// In a Membership Report message, the group address field (expressed in
456/// `FixedHeader`) holds the IP multicast group address of the group being
457/// reported.
458///
459/// Hosts respond to a Query by generating Host Membership Reports, reporting
460/// each host group to which they belong on the network interface from which the
461/// Query was received.
462///
463/// [RFC 2236]: https://tools.ietf.org/html/rfc2236
464#[derive(Debug)]
465pub struct IgmpMembershipReportV2;
466
467impl_igmp_simple_message_type!(IgmpMembershipReportV2, MembershipReportV2, Ipv4Addr);
468
469/// IGMP Leave Group message.
470///
471/// `IgmpLeaveGroup` implements `MessageType`, providing the intended behavior
472/// for IGMP LeaveGroup as defined in [RFC 2236].
473///
474/// In a Leave Group message, the group address field (expressed in
475/// `FixedHeader`) holds the IP multicast group address of the group being
476/// left.
477///
478/// When a host leaves a multicast group, if it was the last host to reply to a
479/// Query with a Membership Report for that group, it SHOULD send a Leave Group
480/// message to the all-routers multicast group (224.0.0.2).
481///
482/// [RFC 2236]: https://tools.ietf.org/html/rfc2236
483#[derive(Debug)]
484pub struct IgmpLeaveGroup;
485
486impl_igmp_simple_message_type!(IgmpLeaveGroup, LeaveGroup, Ipv4Addr);
487
488/// An IGMP packet with a dynamic message type.
489///
490/// Each enum variant contains an `IgmpMessage` of
491/// the appropriate static type, making it easier to call `parse` without
492/// knowing the message type ahead of time while still getting the benefits of a
493/// statically-typed packet struct after parsing is complete.
494#[allow(missing_docs)]
495#[derive(Debug)]
496pub enum IgmpPacket<B: SplitByteSlice> {
497    MembershipQueryV2(IgmpMessage<B, IgmpMembershipQueryV2>),
498    MembershipQueryV3(IgmpMessage<B, IgmpMembershipQueryV3>),
499    MembershipReportV1(IgmpMessage<B, IgmpMembershipReportV1>),
500    MembershipReportV2(IgmpMessage<B, IgmpMembershipReportV2>),
501    MembershipReportV3(IgmpMessage<B, IgmpMembershipReportV3>),
502    LeaveGroup(IgmpMessage<B, IgmpLeaveGroup>),
503}
504
505impl<B: SplitByteSlice> ParsablePacket<B, ()> for IgmpPacket<B> {
506    type Error = ParseError;
507
508    fn parse_metadata(&self) -> ParseMetadata {
509        use self::IgmpPacket::*;
510        match self {
511            MembershipQueryV2(p) => p.parse_metadata(),
512            MembershipQueryV3(p) => p.parse_metadata(),
513            MembershipReportV1(p) => p.parse_metadata(),
514            MembershipReportV2(p) => p.parse_metadata(),
515            MembershipReportV3(p) => p.parse_metadata(),
516            LeaveGroup(p) => p.parse_metadata(),
517        }
518    }
519
520    fn parse<BV: BufferView<B>>(buffer: BV, args: ()) -> Result<Self, ParseError> {
521        macro_rules! mtch {
522            ($buffer:expr, $args:expr, $( ($code:ident, $long:tt) => $type:ty, $variant:ident )*) => {
523                match peek_message_type($buffer.as_ref())? {
524                    $( (IgmpMessageType::$code, $long) => {
525                        let packet = <IgmpMessage<B,$type> as ParsablePacket<_, _>>::parse($buffer, $args)?;
526                        IgmpPacket::$variant(packet)
527                    })*,
528                }
529            }
530        }
531
532        Ok(mtch!(
533            buffer,
534            args,
535            (MembershipQuery, false) => IgmpMembershipQueryV2, MembershipQueryV2
536            (MembershipQuery, true) => IgmpMembershipQueryV3, MembershipQueryV3
537            (MembershipReportV1, _) => IgmpMembershipReportV1, MembershipReportV1
538            (MembershipReportV2, _) => IgmpMembershipReportV2, MembershipReportV2
539            (MembershipReportV3, _) => IgmpMembershipReportV3, MembershipReportV3
540            (LeaveGroup, _) => IgmpLeaveGroup, LeaveGroup
541        ))
542    }
543}
544
545/// A builder for IGMPv3 membership queries.
546///
547/// This differs from [`IgmpPacketBuilder`] in that it implements
548/// [`InnerPacketBuilder`] directly, and provides a more ergonomic way of
549/// building queries than nesting builders.
550#[derive(Debug)]
551pub struct IgmpMembershipQueryV3Builder<I> {
552    max_resp_time: IgmpResponseTimeV3,
553    group_addr: Option<MulticastAddr<Ipv4Addr>>,
554    s_flag: bool,
555    qrv: Igmpv3QRV,
556    qqic: Igmpv3QQIC,
557    sources: I,
558}
559
560impl<I> IgmpMembershipQueryV3Builder<I> {
561    /// Creates a new [`IgmpMembershipQueryV3Builder`].
562    pub fn new(
563        max_resp_time: IgmpResponseTimeV3,
564        group_addr: Option<MulticastAddr<Ipv4Addr>>,
565        s_flag: bool,
566        qrv: Igmpv3QRV,
567        qqic: Igmpv3QQIC,
568        sources: I,
569    ) -> Self {
570        Self { max_resp_time, group_addr, s_flag, qrv, qqic, sources }
571    }
572
573    const HEADER_SIZE: usize = super::total_header_size::<MembershipQueryData>();
574}
575
576impl<I> InnerPacketBuilder for IgmpMembershipQueryV3Builder<I>
577where
578    I: Iterator<Item = Ipv4Addr> + Clone,
579{
580    fn bytes_len(&self) -> usize {
581        Self::HEADER_SIZE + self.sources.clone().count() * core::mem::size_of::<Ipv4Addr>()
582    }
583
584    fn serialize(&self, buf: &mut [u8]) {
585        use packet::BufferViewMut;
586
587        let Self { max_resp_time, group_addr, s_flag, qrv, qqic, sources } = self;
588        let (header, body) = buf.split_at_mut(Self::HEADER_SIZE);
589        let mut bytes = &mut body[..];
590        let mut bytes = &mut bytes;
591        // Serialize the body first, counting the records.
592        let mut count: u16 = 0;
593        for src in sources.clone() {
594            count = count.checked_add(1).expect("overflowed number of sources");
595            bytes.write_obj_front(&src).expect("too few bytes for source");
596        }
597        let builder = IgmpPacketBuilder::<&mut [u8], IgmpMembershipQueryV3>::new_with_resp_time(
598            MembershipQueryData {
599                group_address: group_addr
600                    .as_ref()
601                    .map(|a| a.get())
602                    .unwrap_or(Ipv4::UNSPECIFIED_ADDRESS),
603                sqrv: (u8::from(*s_flag) << 3) | (MembershipQueryData::QRV_MSK & u8::from(*qrv)),
604                qqic: (*qqic).into(),
605                number_of_sources: count.into(),
606            },
607            *max_resp_time,
608        );
609        builder.serialize_headers(header, FragmentedByteSlice::new(&mut [body][..]));
610    }
611}
612
613/// A builder for IGMPv3 membership reports.
614///
615/// This differs from [`IgmpPacketBuilder`] in that it implements
616/// [`InnerPacketBuilder`\ directly, and provides a more ergonomic way of
617/// building reports than nesting builders.
618#[derive(Debug)]
619pub struct IgmpMembershipReportV3Builder<I> {
620    groups: I,
621}
622
623impl<I> IgmpMembershipReportV3Builder<I> {
624    /// Creates a new [`IgmpMembershipReportV3Builder`].
625    pub fn new(groups: I) -> Self {
626        Self { groups }
627    }
628
629    const HEADER_SIZE: usize = super::total_header_size::<MembershipReportV3Data>();
630}
631
632impl<I> IgmpMembershipReportV3Builder<I>
633where
634    I: Iterator<Item: GmpReportGroupRecord<Ipv4Addr> + Clone> + Clone,
635{
636    /// Transform this builder into an iterator of builders with a given
637    /// `max_len` for each generated packet.
638    ///
639    /// `max_len` is the maximum length each builder yielded by the returned
640    /// iterator can have. The groups used to create this builder are split into
641    /// multiple reports in order to meet this length. Note that this length
642    /// does _not_ account for the IP header, but it accounts for the entire
643    /// IGMP packet.
644    ///
645    /// Returns `Err` if `max_len` is not large enough to meet minimal
646    /// constraints for each report.
647    pub fn with_len_limits(
648        self,
649        max_len: usize,
650    ) -> Result<
651        impl Iterator<
652            Item = IgmpMembershipReportV3Builder<
653                impl Iterator<Item: GmpReportGroupRecord<Ipv4Addr>> + Clone,
654            >,
655        >,
656        InvalidConstraintsError,
657    > {
658        let Self { groups } = self;
659        crate::gmp::group_record_split_iterator(
660            max_len.saturating_sub(Self::HEADER_SIZE),
661            core::mem::size_of::<GroupRecordHeader>(),
662            groups,
663        )
664        .map(|iter| iter.map(|groups| IgmpMembershipReportV3Builder { groups }))
665    }
666}
667
668impl<I> InnerPacketBuilder for IgmpMembershipReportV3Builder<I>
669where
670    I: Iterator<Item: GmpReportGroupRecord<Ipv4Addr>> + Clone,
671{
672    fn bytes_len(&self) -> usize {
673        Self::HEADER_SIZE
674            + self
675                .groups
676                .clone()
677                .map(|g| {
678                    core::mem::size_of::<GroupRecordHeader>()
679                        + g.sources().count() * core::mem::size_of::<Ipv4Addr>()
680                })
681                .sum::<usize>()
682    }
683
684    fn serialize(&self, buf: &mut [u8]) {
685        use packet::BufferViewMut;
686
687        let Self { groups } = self;
688        let (header, body) = buf.split_at_mut(Self::HEADER_SIZE);
689        let mut bytes = &mut body[..];
690        let mut bytes = &mut bytes;
691        // Serialize the body first, counting the records.
692        let mut count: u16 = 0;
693        for group in groups.clone() {
694            count = count.checked_add(1).expect("multicast groups count overflows");
695            let mut header = bytes
696                .take_obj_front_zero::<GroupRecordHeader>()
697                .expect("too few bytes for record header");
698            let GroupRecordHeader {
699                record_type,
700                aux_data_len,
701                number_of_sources,
702                multicast_address,
703            } = &mut *header;
704            *record_type = group.record_type().into();
705            *aux_data_len = 0;
706            *multicast_address = group.group().into();
707            let mut source_count: u16 = 0;
708            for src in group.sources() {
709                source_count = source_count.checked_add(1).expect("sources count overflows");
710                bytes.write_obj_front(src.borrow()).expect("too few bytes for source");
711            }
712            *number_of_sources = source_count.into();
713        }
714
715        let builder =
716            IgmpPacketBuilder::<&mut [u8], IgmpMembershipReportV3>::new(MembershipReportV3Data {
717                _reserved: [0, 0],
718                number_of_group_records: count.into(),
719            });
720        builder.serialize_headers(header, FragmentedByteSlice::new(&mut [body][..]));
721    }
722}
723
724#[cfg(test)]
725mod tests {
726    use core::fmt::Debug;
727    use core::time::Duration;
728
729    use packet::{ParseBuffer, Serializer};
730
731    use super::*;
732    use crate::igmp::testdata::*;
733    use crate::igmp::IgmpMaxRespCode;
734    use crate::ip::Ipv4Proto;
735    use crate::ipv4::options::Ipv4Option;
736    use crate::ipv4::{Ipv4Packet, Ipv4PacketBuilder, Ipv4PacketBuilderWithOptions};
737    use crate::testutil::set_logger_for_test;
738
739    const ALL_BUFFERS: [&[u8]; 6] = [
740        igmp_router_queries::v2::QUERY,
741        igmp_router_queries::v3::QUERY,
742        igmp_reports::v1::MEMBER_REPORT,
743        igmp_reports::v2::MEMBER_REPORT,
744        igmp_reports::v3::MEMBER_REPORT,
745        igmp_leave_group::LEAVE_GROUP,
746    ];
747
748    fn serialize_to_bytes<B: SplitByteSlice + Debug, M: MessageType<B> + Debug>(
749        igmp: &IgmpMessage<B, M>,
750    ) -> Vec<u8>
751    where
752        M::VariableBody: IgmpNonEmptyBody,
753    {
754        M::body_bytes(&igmp.body)
755            .into_serializer()
756            .encapsulate(igmp.builder())
757            .serialize_vec_outer()
758            .unwrap()
759            .as_ref()
760            .to_vec()
761    }
762
763    fn serialize_to_bytes_inner<
764        B: SplitByteSlice + Debug,
765        M: MessageType<B, VariableBody = ()> + Debug,
766    >(
767        igmp: &IgmpMessage<B, M>,
768    ) -> Vec<u8> {
769        igmp.builder().into_serializer().serialize_vec_outer().unwrap().as_ref().to_vec()
770    }
771
772    fn test_parse_and_serialize<
773        B: SplitByteSlice + Debug,
774        BV: BufferView<B>,
775        M: MessageType<B> + Debug,
776        F: FnOnce(&IgmpMessage<B, M>),
777    >(
778        req: BV,
779        check: F,
780    ) where
781        M::VariableBody: IgmpNonEmptyBody,
782    {
783        let orig_req = req.as_ref().to_owned();
784
785        let igmp = IgmpMessage::<_, M>::parse(req, ()).unwrap();
786        check(&igmp);
787
788        let data = serialize_to_bytes(&igmp);
789        assert_eq!(data, orig_req);
790    }
791
792    fn test_parse_and_serialize_inner<
793        M: for<'a> MessageType<&'a [u8], VariableBody = ()> + Debug,
794        F: for<'a> FnOnce(&IgmpMessage<&'a [u8], M>),
795    >(
796        mut req: &[u8],
797        check: F,
798    ) {
799        let orig_req = req;
800
801        let igmp = req.parse_with::<_, IgmpMessage<_, M>>(()).unwrap();
802        check(&igmp);
803
804        let data = serialize_to_bytes_inner(&igmp);
805        assert_eq!(&data[..], orig_req);
806    }
807
808    #[test]
809    fn membership_query_v2_parse_and_serialize() {
810        set_logger_for_test();
811        test_parse_and_serialize_inner::<IgmpMembershipQueryV2, _>(
812            igmp_router_queries::v2::QUERY,
813            |igmp| {
814                assert_eq!(
815                    *igmp.header,
816                    Ipv4Addr::new(igmp_router_queries::v2::HOST_GROUP_ADDRESS)
817                );
818                assert_eq!(igmp.prefix.max_resp_code, igmp_router_queries::v2::MAX_RESP_CODE);
819            },
820        );
821    }
822
823    #[test]
824    fn membership_query_v3_parse_and_serialize() {
825        set_logger_for_test();
826        let mut req = igmp_router_queries::v3::QUERY;
827        test_parse_and_serialize::<_, _, IgmpMembershipQueryV3, _>(&mut req, |igmp| {
828            assert_eq!(igmp.prefix.max_resp_code, igmp_router_queries::v3::MAX_RESP_CODE);
829            assert_eq!(
830                igmp.header.group_address,
831                Ipv4Addr::new(igmp_router_queries::v3::GROUP_ADDRESS)
832            );
833            assert_eq!(igmp.header.number_of_sources(), igmp_router_queries::v3::NUMBER_OF_SOURCES);
834            assert_eq!(
835                igmp.header.suppress_router_side_processing(),
836                igmp_router_queries::v3::SUPPRESS_ROUTER_SIDE
837            );
838            assert_eq!(igmp.header.querier_robustness_variable(), igmp_router_queries::v3::QRV);
839            assert_eq!(
840                igmp.header.querier_query_interval().as_secs() as u32,
841                igmp_router_queries::v3::QQIC_SECS
842            );
843            assert_eq!(igmp.body.len(), igmp_router_queries::v3::NUMBER_OF_SOURCES as usize);
844            assert_eq!(igmp.body[0], Ipv4Addr::new(igmp_router_queries::v3::SOURCE));
845
846            // When interpreted as a v2 query we should get the same values.
847            let v2 = igmp.as_v2_query();
848            assert_eq!(v2.prefix.max_resp_code, igmp_router_queries::v3::MAX_RESP_CODE);
849            assert_eq!(*(v2.header), Ipv4Addr::new(igmp_router_queries::v3::GROUP_ADDRESS));
850        });
851    }
852
853    #[test]
854    fn membership_report_v3_parse_and_serialize() {
855        use igmp_reports::v3::*;
856
857        set_logger_for_test();
858        let mut req = MEMBER_REPORT;
859        test_parse_and_serialize::<_, _, IgmpMembershipReportV3, _>(&mut req, |igmp| {
860            assert_eq!(igmp.header.number_of_group_records(), NUMBER_OF_RECORDS);
861            assert_eq!(igmp.prefix.max_resp_code, MAX_RESP_CODE);
862            let mut iter = igmp.body.iter();
863            // look at first group record:
864            let rec1 = iter.next().unwrap();
865            assert_eq!(rec1.header().number_of_sources(), NUMBER_OF_SOURCES_1);
866            assert_eq!(rec1.header().record_type, RECORD_TYPE_1);
867            assert_eq!(rec1.header().multicast_address, Ipv4Addr::new(MULTICAST_ADDR_1));
868            assert_eq!(rec1.header().record_type(), Ok(IgmpGroupRecordType::ModeIsInclude));
869            assert_eq!(rec1.sources().len(), NUMBER_OF_SOURCES_1 as usize);
870            assert_eq!(rec1.sources()[0], Ipv4Addr::new(SRC_1_1));
871            assert_eq!(rec1.sources()[1], Ipv4Addr::new(SRC_1_2));
872
873            // look at second group record:
874            let rec2 = iter.next().unwrap();
875            assert_eq!(rec2.header().number_of_sources(), NUMBER_OF_SOURCES_2);
876            assert_eq!(rec2.header().record_type, RECORD_TYPE_2);
877            assert_eq!(rec2.header().multicast_address, Ipv4Addr::new(MULTICAST_ADDR_2));
878            assert_eq!(rec2.header().record_type(), Ok(IgmpGroupRecordType::ModeIsExclude));
879            assert_eq!(rec2.sources().len(), NUMBER_OF_SOURCES_2 as usize);
880            assert_eq!(rec2.sources()[0], Ipv4Addr::new(SRC_2_1));
881
882            // assert that no other records came in:
883            assert_eq!(iter.next().is_none(), true);
884        });
885    }
886
887    #[test]
888    fn membership_query_v3_builder() {
889        set_logger_for_test();
890        let builder = IgmpMembershipQueryV3Builder::new(
891            IgmpResponseTimeV3::from_code(igmp_router_queries::v3::MAX_RESP_CODE),
892            Some(
893                MulticastAddr::new(Ipv4Addr::new(igmp_router_queries::v3::GROUP_ADDRESS)).unwrap(),
894            ),
895            igmp_router_queries::v3::SUPPRESS_ROUTER_SIDE,
896            Igmpv3QRV::new(igmp_router_queries::v3::QRV),
897            Igmpv3QQIC::new_exact(Duration::from_secs(igmp_router_queries::v3::QQIC_SECS.into()))
898                .unwrap(),
899            [Ipv4Addr::new(igmp_router_queries::v3::SOURCE)].into_iter(),
900        );
901        let serialized = builder.into_serializer().serialize_vec_outer().unwrap().unwrap_b();
902        assert_eq!(serialized.as_ref(), igmp_router_queries::v3::QUERY);
903    }
904
905    #[test]
906    fn membership_report_v3_builder() {
907        set_logger_for_test();
908        use igmp_reports::v3::*;
909        let builder = IgmpMembershipReportV3Builder::new(
910            [
911                (MULTICAST_ADDR_1, RECORD_TYPE_1, &[SRC_1_1, SRC_1_2][..]),
912                (MULTICAST_ADDR_2, RECORD_TYPE_2, &[SRC_2_1][..]),
913            ]
914            .into_iter()
915            .map(|(addr, rec_type, sources)| {
916                (
917                    MulticastAddr::new(Ipv4Addr::new(addr)).unwrap(),
918                    IgmpGroupRecordType::try_from(rec_type).unwrap(),
919                    sources.into_iter().copied().map(Ipv4Addr::new),
920                )
921            }),
922        );
923        let serialized = builder.into_serializer().serialize_vec_outer().unwrap().unwrap_b();
924        assert_eq!(serialized.as_ref(), MEMBER_REPORT);
925    }
926
927    // Test that our maximum sources accounting matches an equivalent example
928    // in RFC 3376 section 4.1.8:
929    //
930    //  For example, on an Ethernet with an MTU of 1500 octets, the IP header
931    //  including the Router Alert option consumes 24 octets, and the IGMP
932    //  fields up to including the Number of Sources (N) field consume 12
933    //  octets, leaving 1464 octets for source addresses, which limits the
934    //  number of source addresses to 366 (1464/4).
935    //
936    // This example is for queries, reports have 8 octets of IGMP headers + 8
937    // octets of group record header, hence (1500 - 24 - 16)/4 = 365.
938    #[test]
939    fn membership_report_v3_split_many_sources() {
940        use igmp_reports::v3::*;
941        use packet::PacketBuilder;
942        const ETH_MTU: usize = 1500;
943        const MAX_SOURCES: usize = 365;
944
945        let src = Ipv4Addr::new(SRC_1_1);
946        let ip_builder = Ipv4PacketBuilderWithOptions::new(
947            Ipv4PacketBuilder::new(src, src, 1, Ipv4Proto::Igmp),
948            &[Ipv4Option::RouterAlert { data: 0 }],
949        )
950        .unwrap();
951        let ip_header = ip_builder.constraints().header_len();
952
953        let src_ip = |i: usize| Ipv4Addr::new([10, 0, (i >> 8) as u8, i as u8]);
954        let group_addr = MulticastAddr::new(Ipv4Addr::new(MULTICAST_ADDR_1)).unwrap();
955        let reports = IgmpMembershipReportV3Builder::new(
956            [(
957                group_addr,
958                IgmpGroupRecordType::ModeIsInclude,
959                (0..MAX_SOURCES).into_iter().map(|i| src_ip(i)),
960            )]
961            .into_iter(),
962        )
963        .with_len_limits(ETH_MTU - ip_header)
964        .unwrap();
965
966        let mut reports = reports.map(|builder| {
967            builder
968                .into_serializer()
969                .encapsulate(ip_builder.clone())
970                .serialize_vec_outer()
971                .unwrap_or_else(|(err, _)| panic!("{err:?}"))
972                .unwrap_b()
973                .into_inner()
974        });
975        // We can generate a report at exactly ETH_MTU.
976        let serialized = reports.next().unwrap();
977        assert_eq!(serialized.len(), ETH_MTU);
978
979        let mut buffer = &serialized[..];
980        let _ip = buffer.parse_with::<_, Ipv4Packet<_>>(()).unwrap();
981        let igmp = buffer.parse::<IgmpMessage<_, IgmpMembershipReportV3>>().unwrap();
982        let mut groups = igmp.body.iter();
983        let group = groups.next().expect("has group");
984        assert_eq!(group.header.multicast_address, group_addr.get());
985        assert_eq!(usize::from(group.header.number_of_sources()), MAX_SOURCES);
986        assert_eq!(group.sources().len(), MAX_SOURCES);
987        for (i, addr) in group.sources().iter().enumerate() {
988            assert_eq!(*addr, src_ip(i));
989        }
990        assert_eq!(groups.next().map(|r| r.header.multicast_address), None);
991
992        // Only one report is generated.
993        assert_eq!(reports.next(), None);
994
995        let reports = IgmpMembershipReportV3Builder::new(
996            [(
997                group_addr,
998                IgmpGroupRecordType::ModeIsInclude,
999                core::iter::repeat(src).take(MAX_SOURCES + 1),
1000            )]
1001            .into_iter(),
1002        )
1003        .with_len_limits(ETH_MTU - ip_header)
1004        .unwrap();
1005        // 2 reports are generated with one extra source.
1006        assert_eq!(
1007            reports
1008                .map(|r| r.groups.map(|group| group.sources().count()).collect::<Vec<_>>())
1009                .collect::<Vec<_>>(),
1010            vec![vec![MAX_SOURCES], vec![1]]
1011        );
1012    }
1013
1014    // Like membership_report_v3_split_many_sources but we calculate how many
1015    // groups with no sources specified we can have in the same 1500 Ethernet
1016    // MTU.
1017    //
1018    // * 24 bytes for IPv4 header + router alert option.
1019    // * 8 bytes for IGMP header + report up to number of groups.
1020    // * 8 bytes per group with no sources.
1021    //
1022    // So we should be able to fit (1500 - 24 - 8)/8 = 183.5 groups. 183
1023    // groups result in a a 1496 byte-long message.
1024    #[test]
1025    fn membership_report_v3_split_many_groups() {
1026        use igmp_reports::v3::*;
1027        use packet::PacketBuilder;
1028
1029        const ETH_MTU: usize = 1500;
1030        const EXPECT_SERIALIZED: usize = 1496;
1031        const MAX_GROUPS: usize = 183;
1032
1033        let src = Ipv4Addr::new(SRC_1_1);
1034        let ip_builder = Ipv4PacketBuilderWithOptions::new(
1035            Ipv4PacketBuilder::new(src, src, 1, Ipv4Proto::Igmp),
1036            &[Ipv4Option::RouterAlert { data: 0 }],
1037        )
1038        .unwrap();
1039        let ip_header = ip_builder.constraints().header_len();
1040
1041        let group_ip = |i: usize| {
1042            MulticastAddr::new(Ipv4Addr::new([224, 0, (i >> 8) as u8, i as u8])).unwrap()
1043        };
1044        let reports = IgmpMembershipReportV3Builder::new((0..MAX_GROUPS).into_iter().map(|i| {
1045            (group_ip(i), IgmpGroupRecordType::ModeIsExclude, core::iter::empty::<Ipv4Addr>())
1046        }))
1047        .with_len_limits(ETH_MTU - ip_header)
1048        .unwrap();
1049
1050        let mut reports = reports.map(|builder| {
1051            builder
1052                .into_serializer()
1053                .encapsulate(ip_builder.clone())
1054                .serialize_vec_outer()
1055                .unwrap_or_else(|(err, _)| panic!("{err:?}"))
1056                .unwrap_b()
1057                .into_inner()
1058        });
1059        // We can generate a report at exactly ETH_MTU.
1060        let serialized = reports.next().unwrap();
1061        assert_eq!(serialized.len(), EXPECT_SERIALIZED);
1062
1063        let mut buffer = &serialized[..];
1064        let _ip = buffer.parse_with::<_, Ipv4Packet<_>>(()).unwrap();
1065        let igmp = buffer.parse::<IgmpMessage<_, IgmpMembershipReportV3>>().unwrap();
1066        assert_eq!(usize::from(igmp.header.number_of_group_records.get()), MAX_GROUPS);
1067        for (i, group) in igmp.body.iter().enumerate() {
1068            assert_eq!(group.header.multicast_address, group_ip(i).get());
1069            assert_eq!(group.header.number_of_sources.get(), 0);
1070        }
1071
1072        // Only one report is generated.
1073        assert_eq!(reports.next(), None);
1074
1075        let reports =
1076            IgmpMembershipReportV3Builder::new((0..MAX_GROUPS + 1).into_iter().map(|i| {
1077                (group_ip(i), IgmpGroupRecordType::ModeIsExclude, core::iter::empty::<Ipv4Addr>())
1078            }))
1079            .with_len_limits(ETH_MTU - ip_header)
1080            .unwrap();
1081        // 2 reports are generated with one extra source.
1082        assert_eq!(reports.map(|r| r.groups.count()).collect::<Vec<_>>(), vec![MAX_GROUPS, 1]);
1083    }
1084
1085    #[test]
1086    fn membership_report_v1_parse_and_serialize() {
1087        use igmp_reports::v1;
1088        set_logger_for_test();
1089        test_parse_and_serialize_inner::<IgmpMembershipReportV1, _>(v1::MEMBER_REPORT, |igmp| {
1090            assert_eq!(*igmp.header, Ipv4Addr::new(v1::GROUP_ADDRESS));
1091        });
1092    }
1093
1094    #[test]
1095    fn membership_report_v2_parse_and_serialize() {
1096        use igmp_reports::v2;
1097        set_logger_for_test();
1098        test_parse_and_serialize_inner::<IgmpMembershipReportV2, _>(v2::MEMBER_REPORT, |igmp| {
1099            assert_eq!(*igmp.header, Ipv4Addr::new(v2::GROUP_ADDRESS));
1100        });
1101    }
1102
1103    #[test]
1104    fn leave_group_parse_and_serialize() {
1105        set_logger_for_test();
1106        test_parse_and_serialize_inner::<IgmpLeaveGroup, _>(
1107            igmp_leave_group::LEAVE_GROUP,
1108            |igmp| {
1109                assert_eq!(*igmp.header, Ipv4Addr::new(igmp_leave_group::GROUP_ADDRESS));
1110            },
1111        );
1112    }
1113
1114    #[test]
1115    fn test_unknown_type() {
1116        let mut buff = igmp_invalid_buffers::UNKNOWN_TYPE.to_vec();
1117        let mut buff = buff.as_mut_slice();
1118        let packet = buff.parse_with::<_, IgmpPacket<_>>(());
1119        // we don't use expect_err here because IgmpPacket does not implement
1120        // core::fmt::Debug
1121        assert_eq!(packet.is_err(), true);
1122    }
1123
1124    #[test]
1125    fn test_full_parses() {
1126        let mut bufs = ALL_BUFFERS.to_vec();
1127        for buff in bufs.iter_mut() {
1128            let orig_req = &buff[..];
1129            let packet = buff.parse_with::<_, IgmpPacket<_>>(()).unwrap();
1130            let msg_type = match packet {
1131                IgmpPacket::MembershipQueryV2(p) => p.prefix.msg_type,
1132                IgmpPacket::MembershipQueryV3(p) => p.prefix.msg_type,
1133                IgmpPacket::MembershipReportV1(p) => p.prefix.msg_type,
1134                IgmpPacket::MembershipReportV2(p) => p.prefix.msg_type,
1135                IgmpPacket::MembershipReportV3(p) => p.prefix.msg_type,
1136                IgmpPacket::LeaveGroup(p) => p.prefix.msg_type,
1137            };
1138            assert_eq!(msg_type, orig_req[0]);
1139        }
1140    }
1141
1142    #[test]
1143    fn test_partial_parses() {
1144        // parsing a part of the buffer should always result in errors and
1145        // nothing panics.
1146        for buff in ALL_BUFFERS.iter() {
1147            for i in 0..buff.len() {
1148                let partial_buff = &mut &buff[0..i];
1149                let packet = partial_buff.parse_with::<_, IgmpPacket<_>>(());
1150                assert_eq!(packet.is_err(), true)
1151            }
1152        }
1153    }
1154
1155    // Asserts that a `Message` without `VariableBody` should have the same length
1156    // as the given ground truth packet.
1157    fn assert_message_length<Message: for<'a> MessageType<&'a [u8], VariableBody = ()>>(
1158        mut ground_truth: &[u8],
1159    ) {
1160        let ground_truth_len = ground_truth.len();
1161        let igmp = ground_truth.parse_with::<_, IgmpMessage<&[u8], Message>>(()).unwrap();
1162        let builder_len = igmp.builder().bytes_len();
1163        assert_eq!(builder_len, ground_truth_len);
1164    }
1165
1166    #[test]
1167    fn test_igmp_packet_length() {
1168        assert_message_length::<IgmpMembershipQueryV2>(igmp_router_queries::v2::QUERY);
1169        assert_message_length::<IgmpMembershipReportV1>(igmp_reports::v1::MEMBER_REPORT);
1170        assert_message_length::<IgmpMembershipReportV2>(igmp_reports::v2::MEMBER_REPORT);
1171        assert_message_length::<IgmpLeaveGroup>(igmp_leave_group::LEAVE_GROUP);
1172    }
1173}