netstack3_ip/gmp/
igmp.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//! Internet Group Management Protocol, Version 2 (IGMPv2).
6//!
7//! IGMPv2 is a communications protocol used by hosts and adjacent routers on
8//! IPv4 networks to establish multicast group memberships.
9
10use core::fmt::Debug;
11use core::time::Duration;
12
13use log::{debug, error};
14use net_declare::net_ip_v4;
15use net_types::ip::{AddrSubnet, Ip as _, Ipv4, Ipv4Addr};
16use net_types::{MulticastAddr, MulticastAddress as _, SpecifiedAddr, Witness};
17use netstack3_base::{
18    AnyDevice, Counter, DeviceIdContext, ErrorAndSerializer, HandleableTimer, Inspectable,
19    InspectableValue, Inspector, InspectorExt, Instant, InstantContext, Ipv4DeviceAddr,
20    ResourceCounterContext, WeakDeviceIdentifier,
21};
22use packet::{BufferMut, EmptyBuf, InnerPacketBuilder, PacketBuilder, Serializer};
23use packet_formats::error::ParseError;
24use packet_formats::igmp::messages::{
25    IgmpLeaveGroup, IgmpMembershipQueryV2, IgmpMembershipQueryV3, IgmpMembershipReportV1,
26    IgmpMembershipReportV2, IgmpMembershipReportV3Builder, IgmpPacket,
27};
28use packet_formats::igmp::{IgmpMessage, IgmpPacketBuilder, MessageType};
29use packet_formats::ip::{DscpAndEcn, Ipv4Proto};
30use packet_formats::ipv4::options::Ipv4Option;
31use packet_formats::ipv4::{
32    Ipv4OptionsTooLongError, Ipv4PacketBuilder, Ipv4PacketBuilderWithOptions,
33};
34use packet_formats::utils::NonZeroDuration;
35use thiserror::Error;
36use zerocopy::SplitByteSlice;
37
38use crate::internal::base::{IpDeviceMtuContext, IpLayerHandler, IpPacketDestination};
39use crate::internal::gmp::{
40    self, v2, GmpBindingsContext, GmpBindingsTypes, GmpContext, GmpContextInner, GmpEnabledGroup,
41    GmpGroupState, GmpMode, GmpState, GmpStateContext, GmpStateRef, GmpTimerId, GmpTypeLayout,
42    IpExt, MulticastGroupSet, NotAMemberErr,
43};
44use crate::internal::local_delivery::{IpHeaderInfo, LocalDeliveryPacketInfo};
45
46/// The destination address for all IGMPv3 reports.
47///
48/// Defined in [RFC 3376 section 4.2.14].
49///
50/// [RFC 3376 section 4.2.14]:
51///     https://datatracker.ietf.org/doc/html/rfc3376#section-4.2.14
52const ALL_IGMPV3_CAPABLE_ROUTERS: MulticastAddr<Ipv4Addr> =
53    unsafe { MulticastAddr::new_unchecked(net_ip_v4!("224.0.0.22")) };
54
55/// The bindings types for IGMP.
56pub trait IgmpBindingsTypes: GmpBindingsTypes {}
57impl<BT> IgmpBindingsTypes for BT where BT: GmpBindingsTypes {}
58
59/// The bindings execution context for IGMP.
60pub trait IgmpBindingsContext: GmpBindingsContext + 'static {}
61impl<BC> IgmpBindingsContext for BC where BC: GmpBindingsContext + 'static {}
62
63/// The IGMP mode controllable by the user.
64#[derive(Debug, Eq, PartialEq, Copy, Clone)]
65#[allow(missing_docs)]
66pub enum IgmpConfigMode {
67    V1,
68    V2,
69    V3,
70}
71
72/// A marker context for IGMP traits to allow for GMP test fakes.
73pub trait IgmpContextMarker {}
74
75/// Provides immutable access to IGMP state.
76pub trait IgmpStateContext<BT: IgmpBindingsTypes>:
77    DeviceIdContext<AnyDevice> + IgmpContextMarker
78{
79    /// Calls the function with an immutable reference to the device's IGMP
80    /// state.
81    fn with_igmp_state<
82        O,
83        F: FnOnce(
84            &MulticastGroupSet<Ipv4Addr, GmpGroupState<Ipv4, BT>>,
85            &GmpState<Ipv4, IgmpTypeLayout, BT>,
86        ) -> O,
87    >(
88        &mut self,
89        device: &Self::DeviceId,
90        cb: F,
91    ) -> O;
92}
93
94/// The inner execution context for IGMP capable of sending packets.
95pub trait IgmpSendContext<BT: IgmpBindingsTypes>:
96    DeviceIdContext<AnyDevice>
97    + IpLayerHandler<Ipv4, BT>
98    + IpDeviceMtuContext<Ipv4>
99    + ResourceCounterContext<Self::DeviceId, IgmpCounters>
100{
101    /// Gets an IP address and subnet associated with this device.
102    fn get_ip_addr_subnet(
103        &mut self,
104        device: &Self::DeviceId,
105    ) -> Option<AddrSubnet<Ipv4Addr, Ipv4DeviceAddr>>;
106}
107
108/// The execution context for the Internet Group Management Protocol (IGMP).
109pub trait IgmpContext<BT: IgmpBindingsTypes>:
110    DeviceIdContext<AnyDevice>
111    + IgmpContextMarker
112    + ResourceCounterContext<Self::DeviceId, IgmpCounters>
113{
114    /// The inner IGMP context capable of sending packets.
115    type SendContext<'a>: IgmpSendContext<BT, DeviceId = Self::DeviceId> + 'a;
116
117    /// Calls the function with a mutable reference to the device's IGMP state
118    /// and whether or not IGMP is enabled for the `device`.
119    fn with_igmp_state_mut<
120        O,
121        F: for<'a> FnOnce(Self::SendContext<'a>, GmpStateRef<'a, Ipv4, IgmpTypeLayout, BT>) -> O,
122    >(
123        &mut self,
124        device: &Self::DeviceId,
125        cb: F,
126    ) -> O;
127}
128
129/// A handler for incoming IGMP packets.
130///
131/// A blanket implementation is provided for all `C: IgmpContext`.
132pub trait IgmpPacketHandler<BC, DeviceId> {
133    /// Receive an IGMP message in an IP packet.
134    fn receive_igmp_packet<B: BufferMut, H: IpHeaderInfo<Ipv4>>(
135        &mut self,
136        bindings_ctx: &mut BC,
137        device: &DeviceId,
138        src_ip: Ipv4Addr,
139        dst_ip: SpecifiedAddr<Ipv4Addr>,
140        buffer: B,
141        info: &LocalDeliveryPacketInfo<Ipv4, H>,
142    );
143}
144
145impl<BC: IgmpBindingsContext, CC: IgmpContext<BC>> IgmpPacketHandler<BC, CC::DeviceId> for CC {
146    fn receive_igmp_packet<B: BufferMut, H: IpHeaderInfo<Ipv4>>(
147        &mut self,
148        bindings_ctx: &mut BC,
149        device: &CC::DeviceId,
150        _src_ip: Ipv4Addr,
151        dst_ip: SpecifiedAddr<Ipv4Addr>,
152        buffer: B,
153        info: &LocalDeliveryPacketInfo<Ipv4, H>,
154    ) {
155        receive_igmp_packet(self, bindings_ctx, device, dst_ip, buffer, info).unwrap_or_else(|e| {
156            debug!("Error occurred when handling IGMPv2 message: {}", e);
157        });
158    }
159}
160
161fn receive_igmp_packet<
162    B: BufferMut,
163    H: IpHeaderInfo<Ipv4>,
164    CC: IgmpContext<BC>,
165    BC: IgmpBindingsContext,
166>(
167    core_ctx: &mut CC,
168    bindings_ctx: &mut BC,
169    device: &CC::DeviceId,
170    dst_ip: SpecifiedAddr<Ipv4Addr>,
171    mut buffer: B,
172    info: &LocalDeliveryPacketInfo<Ipv4, H>,
173) -> Result<(), IgmpError> {
174    let LocalDeliveryPacketInfo { meta: _, header_info, marks: _ } = info;
175    let dst_ip = dst_ip.into_addr();
176    let ttl = header_info.hop_limit();
177
178    // All RFCs define messages as being sent with a TTL of 1.
179    //
180    // Rejecting messages with bad TTL is almost a violation of the Robustness
181    // Principle, but a packet with a different TTL is more likely to be
182    // malicious than a poor implementation. Note that the same rationale is
183    // applied to MLD.
184    //
185    // See RFC 1112 APPENDIX I, RFC 2236 section 2, and RFC 3376 section 4.
186    if ttl != IGMP_IP_TTL {
187        core_ctx.increment_both(device, |counters: &IgmpCounters| &counters.rx_err_bad_ttl);
188        return Err(IgmpError::BadTtl(ttl));
189    }
190
191    let packet = match buffer.parse_with::<_, IgmpPacket<&[u8]>>(()) {
192        Ok(packet) => packet,
193        Err(e) => {
194            core_ctx.increment_both(device, |counters: &IgmpCounters| &counters.rx_err_parse);
195            return Err(IgmpError::Parse(e));
196        }
197    };
198
199    match packet {
200        IgmpPacket::MembershipQueryV2(msg) => {
201            if msg.is_igmpv1_query() {
202                core_ctx
203                    .increment_both(device, |counters: &IgmpCounters| &counters.rx_igmpv1_query);
204            } else {
205                core_ctx
206                    .increment_both(device, |counters: &IgmpCounters| &counters.rx_igmpv2_query);
207            }
208            // From RFC 3376 section 9.1:
209            //
210            // Hosts SHOULD ignore v1, v2 or v3 General Queries sent to a
211            // multicast address other than 224.0.0.1, the all-systems address.
212            if msg.group_addr() == Ipv4::UNSPECIFIED_ADDRESS
213                && dst_ip.is_multicast()
214                && dst_ip != *Ipv4::ALL_SYSTEMS_MULTICAST_ADDRESS.as_ref()
215            {
216                core_ctx.increment_both(device, |counters: &IgmpCounters| {
217                    &counters.rx_err_rejected_general_query
218                });
219                return Err(IgmpError::RejectedGeneralQuery { dst_ip });
220            }
221            // From RFC 3376 section 9.1:
222            //
223            // Hosts SHOULD ignore v2 or v3 Queries without the Router-Alert
224            // option.
225            if !msg.is_igmpv1_query() && !header_info.router_alert() {
226                core_ctx.increment_both(device, |counters: &IgmpCounters| {
227                    &counters.rx_err_missing_router_alert_in_query
228                });
229                return Err(IgmpError::MissingRouterAlertInQuery);
230            }
231            gmp::v1::handle_query_message(core_ctx, bindings_ctx, device, &msg).map_err(Into::into)
232        }
233        IgmpPacket::MembershipQueryV3(msg) => {
234            core_ctx.increment_both(device, |counters: &IgmpCounters| &counters.rx_igmpv3_query);
235            // From RFC 3376 section 9.1:
236            //
237            // Hosts SHOULD ignore v1, v2 or v3 General Queries sent to a
238            // multicast address other than 224.0.0.1, the all-systems address.
239            if msg.header().group_address() == Ipv4::UNSPECIFIED_ADDRESS
240                && dst_ip.is_multicast()
241                && dst_ip != *Ipv4::ALL_SYSTEMS_MULTICAST_ADDRESS.as_ref()
242            {
243                core_ctx.increment_both(device, |counters: &IgmpCounters| {
244                    &counters.rx_err_rejected_general_query
245                });
246                return Err(IgmpError::RejectedGeneralQuery { dst_ip });
247            }
248            // From RFC 3376 section 9.1:
249            //
250            // Hosts SHOULD ignore v2 or v3 Queries without the Router-Alert
251            // option.
252            if !header_info.router_alert() {
253                core_ctx.increment_both(device, |counters: &IgmpCounters| {
254                    &counters.rx_err_missing_router_alert_in_query
255                });
256                return Err(IgmpError::MissingRouterAlertInQuery);
257            }
258
259            gmp::v2::handle_query_message(core_ctx, bindings_ctx, device, &msg).map_err(Into::into)
260        }
261        IgmpPacket::MembershipReportV1(msg) => {
262            core_ctx.increment_both(device, |counters: &IgmpCounters| &counters.rx_igmpv1_report);
263            let addr = msg.group_addr();
264            MulticastAddr::new(addr).map_or(Err(IgmpError::NotAMember { addr }), |group_addr| {
265                gmp::v1::handle_report_message(core_ctx, bindings_ctx, device, group_addr)
266                    .map_err(Into::into)
267            })
268        }
269        IgmpPacket::MembershipReportV2(msg) => {
270            core_ctx.increment_both(device, |counters: &IgmpCounters| &counters.rx_igmpv2_report);
271            let addr = msg.group_addr();
272            MulticastAddr::new(addr).map_or(Err(IgmpError::NotAMember { addr }), |group_addr| {
273                gmp::v1::handle_report_message(core_ctx, bindings_ctx, device, group_addr)
274                    .map_err(Into::into)
275            })
276        }
277        IgmpPacket::LeaveGroup(_) => {
278            core_ctx.increment_both(device, |counters: &IgmpCounters| &counters.rx_leave_group);
279            debug!("Hosts are not interested in Leave Group messages");
280            return Ok(());
281        }
282        IgmpPacket::MembershipReportV3(_) => {
283            core_ctx.increment_both(device, |counters: &IgmpCounters| &counters.rx_igmpv3_report);
284            debug!("Hosts are not interested in IGMPv3 report messages");
285            return Ok(());
286        }
287    }
288}
289
290impl<B: SplitByteSlice> gmp::v1::QueryMessage<Ipv4> for IgmpMessage<B, IgmpMembershipQueryV2> {
291    fn group_addr(&self) -> Ipv4Addr {
292        self.group_addr()
293    }
294
295    fn max_response_time(&self) -> Duration {
296        self.max_response_time().into()
297    }
298}
299
300impl<B: SplitByteSlice> gmp::v2::QueryMessage<Ipv4> for IgmpMessage<B, IgmpMembershipQueryV3> {
301    fn as_v1(&self) -> impl gmp::v1::QueryMessage<Ipv4> + '_ {
302        self.as_v2_query()
303    }
304
305    fn robustness_variable(&self) -> u8 {
306        self.header().querier_robustness_variable()
307    }
308
309    fn query_interval(&self) -> Duration {
310        self.header().querier_query_interval()
311    }
312
313    fn group_address(&self) -> Ipv4Addr {
314        self.header().group_address()
315    }
316
317    fn max_response_time(&self) -> Duration {
318        self.max_response_time().into()
319    }
320
321    fn sources(&self) -> impl Iterator<Item = Ipv4Addr> + '_ {
322        self.body().iter().copied()
323    }
324}
325
326/// The IGMPv1 compatibility mode.
327#[derive(Eq, PartialEq, Copy, Clone, Debug)]
328pub enum IgmpV1Mode<I: Instant> {
329    /// Forced IGMPv1 mode.
330    Forced,
331    /// IGMPv2 configuration in IGMPv1 compatibility.
332    ///
333    /// Sends v1 frames if `now` is before `until`.
334    V2Compat { until: I },
335    /// IGMPv3 configuration in IGMPv1 compatibility.
336    ///
337    /// Sends v1 frames if `now` is before `until`.
338    V3Compat { until: I },
339}
340
341/// The IGMP compatibility mode.
342#[derive(Eq, PartialEq, Copy, Clone, Debug, Default)]
343pub enum IgmpMode<I: Instant> {
344    /// Operating in IGMPv1 mode.
345    V1(IgmpV1Mode<I>),
346    /// Operating in IGMPv2 mode.
347    ///
348    /// If `compat` is `true` this is in compatibility mode and it'll eventually
349    /// exit back to `V3` (controlled by GMP).
350    V2 { compat: bool },
351    /// Operating in IGMPv3 mode.
352    #[default]
353    V3,
354}
355
356impl<I: Instant> From<IgmpMode<I>> for GmpMode {
357    fn from(value: IgmpMode<I>) -> Self {
358        match value {
359            IgmpMode::V1(v1) => {
360                let compat = match v1 {
361                    // GMP compat true means GMP expects to come back to GMPv2
362                    // after a timer, which is false if our configured mode is
363                    // IGMPv1 or IGMPv2.
364                    IgmpV1Mode::Forced | IgmpV1Mode::V2Compat { .. } => false,
365                    IgmpV1Mode::V3Compat { .. } => true,
366                };
367                Self::V1 { compat }
368            }
369            IgmpMode::V2 { compat } => Self::V1 { compat },
370            IgmpMode::V3 => Self::V2,
371        }
372    }
373}
374
375impl<I: Instant> IgmpMode<I> {
376    /// Returns `true` if we should send IGMPv1 messages at the current time
377    /// given by `bindings_ctx`.
378    fn should_send_v1<BC: InstantContext<Instant = I>>(&self, bindings_ctx: &mut BC) -> bool {
379        match self {
380            Self::V1(IgmpV1Mode::Forced) => true,
381            Self::V1(IgmpV1Mode::V2Compat { until } | IgmpV1Mode::V3Compat { until }) => {
382                bindings_ctx.now() < *until
383            }
384            Self::V2 { .. } | Self::V3 => false,
385        }
386    }
387}
388
389impl<I: Instant> InspectableValue for IgmpMode<I> {
390    fn record<X: Inspector>(&self, name: &str, inspector: &mut X) {
391        let v = match self {
392            IgmpMode::V1(IgmpV1Mode::Forced) => "IGMPv1",
393            IgmpMode::V1(IgmpV1Mode::V2Compat { .. }) => "IGMPv1(v2-compat)",
394            IgmpMode::V1(IgmpV1Mode::V3Compat { .. }) => "IGMPv1(v3-compat)",
395            IgmpMode::V2 { compat: true } => "IGMPv2(compat)",
396            IgmpMode::V2 { compat: false } => "IGMPv2",
397            IgmpMode::V3 => "IGMPv3",
398        };
399        inspector.record_str(name, v);
400    }
401}
402
403impl IpExt for Ipv4 {
404    type GmpProtoConfigMode = IgmpConfigMode;
405
406    fn should_perform_gmp(addr: MulticastAddr<Ipv4Addr>) -> bool {
407        // Per [RFC 2236 Section 6]:
408        //
409        //   The all-systems group (address 224.0.0.1) is handled as a special
410        //   case.  The host starts in Idle Member state for that group on every
411        //   interface, never transitions to another state, and never sends a
412        //   report for that group.
413        //
414        // We abide by this requirement by not executing [`Actions`] on these
415        // addresses. Executing [`Actions`] only produces externally-visible side
416        // effects, and is not required to maintain the correctness of the MLD state
417        // machines.
418        //
419        // [RFC 2236 Section 6]: https://datatracker.ietf.org/doc/html/rfc2236
420        addr != Ipv4::ALL_SYSTEMS_MULTICAST_ADDRESS
421    }
422}
423
424/// Uninstantiable type marking a [`GmpState`] as having IGMP types.
425pub enum IgmpTypeLayout {}
426
427impl<BT: IgmpBindingsTypes> GmpTypeLayout<Ipv4, BT> for IgmpTypeLayout {
428    type Config = IgmpConfig;
429    type ProtoMode = IgmpMode<BT::Instant>;
430}
431
432impl<BT: IgmpBindingsTypes, CC: IgmpStateContext<BT>> GmpStateContext<Ipv4, BT> for CC {
433    type TypeLayout = IgmpTypeLayout;
434    fn with_gmp_state<
435        O,
436        F: FnOnce(
437            &MulticastGroupSet<Ipv4Addr, GmpGroupState<Ipv4, BT>>,
438            &GmpState<Ipv4, IgmpTypeLayout, BT>,
439        ) -> O,
440    >(
441        &mut self,
442        device: &Self::DeviceId,
443        cb: F,
444    ) -> O {
445        self.with_igmp_state(device, cb)
446    }
447}
448
449impl<BC: IgmpBindingsContext, CC: IgmpContext<BC>> GmpContext<Ipv4, BC> for CC {
450    type Inner<'a> = CC::SendContext<'a>;
451    type TypeLayout = IgmpTypeLayout;
452
453    fn with_gmp_state_mut_and_ctx<
454        O,
455        F: FnOnce(Self::Inner<'_>, GmpStateRef<'_, Ipv4, IgmpTypeLayout, BC>) -> O,
456    >(
457        &mut self,
458        device: &Self::DeviceId,
459        cb: F,
460    ) -> O {
461        self.with_igmp_state_mut(device, cb)
462    }
463}
464
465impl<CC, BC> GmpContextInner<Ipv4, BC> for CC
466where
467    CC: IgmpSendContext<BC>,
468    BC: IgmpBindingsContext,
469{
470    type TypeLayout = IgmpTypeLayout;
471
472    fn send_message_v1(
473        &mut self,
474        bindings_ctx: &mut BC,
475        device: &Self::DeviceId,
476        cur_mode: &IgmpMode<BC::Instant>,
477        group_addr: GmpEnabledGroup<Ipv4Addr>,
478        msg_type: gmp::v1::GmpMessageType,
479    ) {
480        let group_addr = group_addr.into_multicast_addr();
481        let result = match msg_type {
482            gmp::v1::GmpMessageType::Report => {
483                if cur_mode.should_send_v1(bindings_ctx) {
484                    self.increment_both(device, |counters: &IgmpCounters| {
485                        &counters.tx_igmpv1_report
486                    });
487                    send_igmp_v2_message::<_, _, IgmpMembershipReportV1>(
488                        self,
489                        bindings_ctx,
490                        device,
491                        group_addr,
492                        group_addr,
493                        (),
494                    )
495                } else {
496                    self.increment_both(device, |counters: &IgmpCounters| {
497                        &counters.tx_igmpv2_report
498                    });
499                    send_igmp_v2_message::<_, _, IgmpMembershipReportV2>(
500                        self,
501                        bindings_ctx,
502                        device,
503                        group_addr,
504                        group_addr,
505                        (),
506                    )
507                }
508            }
509            gmp::v1::GmpMessageType::Leave => {
510                self.increment_both(device, |counters: &IgmpCounters| &counters.tx_leave_group);
511                send_igmp_v2_message::<_, _, IgmpLeaveGroup>(
512                    self,
513                    bindings_ctx,
514                    device,
515                    group_addr,
516                    Ipv4::ALL_ROUTERS_MULTICAST_ADDRESS,
517                    (),
518                )
519            }
520        };
521
522        match result {
523            Ok(()) => {}
524            Err(err) => {
525                self.increment_both(device, |counters: &IgmpCounters| &counters.tx_err);
526                debug!(
527                    "error sending IGMP message ({msg_type:?}) on device {device:?} for group \
528                {group_addr}: {err}",
529                )
530            }
531        }
532    }
533
534    fn send_report_v2(
535        &mut self,
536        bindings_ctx: &mut BC,
537        device: &Self::DeviceId,
538        groups: impl Iterator<Item: gmp::v2::VerifiedReportGroupRecord<Ipv4Addr> + Clone> + Clone,
539    ) {
540        let dst_ip = ALL_IGMPV3_CAPABLE_ROUTERS;
541        let mut header = new_ip_header_builder(self, device, dst_ip);
542        header.prefix_builder_mut().dscp_and_ecn(IGMPV3_DSCP_AND_ECN);
543        let avail_len =
544            usize::from(self.get_mtu(device)).saturating_sub(header.constraints().header_len());
545        let reports = match IgmpMembershipReportV3Builder::new(groups).with_len_limits(avail_len) {
546            Ok(msg) => msg,
547            Err(e) => {
548                // Warn here, we don't quite have a good global guarantee of
549                // minimal acceptable MTUs across both IPv4 and IPv6. This
550                // should effectively not happen though.
551                //
552                // TODO(https://fxbug.dev/383355972): Consider an assertion here
553                // instead.
554                error!("MTU too small to send IGMP reports: {e:?}");
555                self.increment_both(device, |counters: &IgmpCounters| &counters.tx_err);
556                return;
557            }
558        };
559        for report in reports {
560            self.increment_both(device, |counters: &IgmpCounters| &counters.tx_igmpv3_report);
561            let destination = IpPacketDestination::Multicast(dst_ip);
562            let ip_frame = report.into_serializer().encapsulate(header.clone());
563            IpLayerHandler::send_ip_frame(self, bindings_ctx, device, destination, ip_frame)
564                .unwrap_or_else(|ErrorAndSerializer { error, .. }| {
565                    self.increment_both(device, |counters: &IgmpCounters| &counters.tx_err);
566                    debug!("failed to send IGMPv3 report over {device:?}: {error:?}")
567                });
568        }
569    }
570
571    fn mode_update_from_v1_query<Q: gmp::v1::QueryMessage<Ipv4>>(
572        &mut self,
573        bindings_ctx: &mut BC,
574        query: &Q,
575        gmp_state: &GmpState<Ipv4, IgmpTypeLayout, BC>,
576        config: &IgmpConfig,
577    ) -> IgmpMode<BC::Instant> {
578        // IGMPv2 hosts should be compatible with routers that only speak
579        // IGMPv1. When an IGMPv2 host receives an IGMPv1 query (whose
580        // `MaxRespCode` is 0), it should set up a timer and only respond with
581        // IGMPv1 responses before the timer expires. Please refer to
582        // https://tools.ietf.org/html/rfc2236#section-4 for details.
583
584        // If this is an IGMPv2 query. Maintain any existing IGMPv1 mode
585        // or forced IGMPv2, otherwise enter IGMPv2 compat.
586        if query.max_response_time() != Duration::ZERO {
587            return match gmp_state.mode {
588                mode @ IgmpMode::V1(_) | mode @ IgmpMode::V2 { compat: false } => mode,
589                IgmpMode::V2 { compat: true } | IgmpMode::V3 => IgmpMode::V2 { compat: true },
590            };
591        }
592
593        // Otherwise this is an IGMPv1 query.
594        match gmp_state.mode {
595            mode @ IgmpMode::V1(IgmpV1Mode::Forced) => mode,
596            IgmpMode::V2 { compat: false } | IgmpMode::V1(IgmpV1Mode::V2Compat { .. }) => {
597                // From RFC 2236 section 4:
598                //
599                // This variable MUST be based upon whether or not an IGMPv1
600                // query was heard in the last Version 1 Router Present Timeout
601                // seconds, and MUST NOT be based upon the type of the last
602                // Query heard.
603                let duration = config.v1_router_present_timeout;
604                IgmpMode::V1(IgmpV1Mode::V2Compat {
605                    until: bindings_ctx.now().saturating_add(duration),
606                })
607            }
608            IgmpMode::V3
609            | IgmpMode::V2 { compat: true }
610            | IgmpMode::V1(IgmpV1Mode::V3Compat { .. }) => {
611                // From RFC 3376 section 4:
612                //
613                // The Group Compatibility Mode variable is based on whether an
614                // older version report was heard in the last Older Version Host
615                // Present Timeout seconds.
616                let duration =
617                    gmp_state.v2_proto.older_version_querier_present_timeout(config).get();
618                IgmpMode::V1(IgmpV1Mode::V3Compat {
619                    until: bindings_ctx.now().saturating_add(duration),
620                })
621            }
622        }
623    }
624
625    fn mode_to_config(mode: &IgmpMode<BC::Instant>) -> IgmpConfigMode {
626        match mode {
627            IgmpMode::V1(IgmpV1Mode::Forced) => IgmpConfigMode::V1,
628            IgmpMode::V1(IgmpV1Mode::V2Compat { .. }) | IgmpMode::V2 { compat: false } => {
629                IgmpConfigMode::V2
630            }
631            IgmpMode::V1(IgmpV1Mode::V3Compat { .. })
632            | IgmpMode::V2 { compat: true }
633            | IgmpMode::V3 => IgmpConfigMode::V3,
634        }
635    }
636
637    fn config_to_mode(
638        cur_mode: &IgmpMode<BC::Instant>,
639        config: IgmpConfigMode,
640    ) -> IgmpMode<BC::Instant> {
641        match config {
642            IgmpConfigMode::V1 => IgmpMode::V1(IgmpV1Mode::Forced),
643            IgmpConfigMode::V2 => match cur_mode {
644                IgmpMode::V1(IgmpV1Mode::V2Compat { .. }) => {
645                    // Switching from IGMPv2 to IGMPv2, copy current mode.
646                    *cur_mode
647                }
648                IgmpMode::V1(IgmpV1Mode::V3Compat { until }) => {
649                    // Switching from IGMPv3 to IGMPV2, copy the compatibility timeline.
650                    IgmpMode::V1(IgmpV1Mode::V2Compat { until: *until })
651                }
652                IgmpMode::V1(IgmpV1Mode::Forced) | IgmpMode::V2 { .. } | IgmpMode::V3 => {
653                    IgmpMode::V2 { compat: false }
654                }
655            },
656            IgmpConfigMode::V3 => {
657                match cur_mode {
658                    IgmpMode::V1(IgmpV1Mode::V2Compat { until }) => {
659                        // Switching from IGMPv2 to IGMPv3 just copy current mode.
660                        IgmpMode::V1(IgmpV1Mode::V3Compat { until: *until })
661                    }
662                    IgmpMode::V1(IgmpV1Mode::V3Compat { .. }) | IgmpMode::V2 { compat: true } => {
663                        // Switching from IGMPv3 to IGMPv3, copy current mode.
664                        *cur_mode
665                    }
666                    IgmpMode::V1(IgmpV1Mode::Forced)
667                    | IgmpMode::V2 { compat: false }
668                    | IgmpMode::V3 => IgmpMode::V3,
669                }
670            }
671        }
672    }
673
674    fn mode_on_disable(cur_mode: &IgmpMode<BC::Instant>) -> IgmpMode<BC::Instant> {
675        match cur_mode {
676            m @ IgmpMode::V1(IgmpV1Mode::Forced)
677            | m @ IgmpMode::V2 { compat: false }
678            | m @ IgmpMode::V3 => *m,
679            IgmpMode::V1(IgmpV1Mode::V2Compat { .. }) => IgmpMode::V2 { compat: false },
680            IgmpMode::V1(IgmpV1Mode::V3Compat { .. }) | IgmpMode::V2 { compat: true } => {
681                IgmpMode::V3
682            }
683        }
684    }
685
686    fn mode_on_exit_compat() -> IgmpMode<BC::Instant> {
687        IgmpMode::V3
688    }
689}
690
691#[derive(Debug, Error)]
692#[cfg_attr(test, derive(PartialEq))]
693pub(crate) enum IgmpError {
694    /// The host is trying to operate on an group address of which the host is
695    /// not a member.
696    #[error("the host has not already been a member of the address: {}", addr)]
697    NotAMember { addr: Ipv4Addr },
698    /// Failed to send an IGMP packet.
699    #[error("failed to send out an IGMP packet to address: {}", addr)]
700    SendFailure { addr: Ipv4Addr },
701    /// IGMP is disabled
702    #[error("IGMP is disabled on interface")]
703    Disabled,
704    #[error("failed to parse: {0}")]
705    Parse(ParseError),
706    #[error("message with incorrect ttl: {0}")]
707    BadTtl(u8),
708    #[error("rejected general query to {dst_ip}")]
709    RejectedGeneralQuery { dst_ip: Ipv4Addr },
710    #[error("router alert not present in query")]
711    MissingRouterAlertInQuery,
712}
713
714impl From<NotAMemberErr<Ipv4>> for IgmpError {
715    fn from(NotAMemberErr(addr): NotAMemberErr<Ipv4>) -> Self {
716        Self::NotAMember { addr }
717    }
718}
719
720impl From<v2::QueryError<Ipv4>> for IgmpError {
721    fn from(err: v2::QueryError<Ipv4>) -> Self {
722        match err {
723            v2::QueryError::NotAMember(addr) => Self::NotAMember { addr },
724            v2::QueryError::Disabled => Self::Disabled,
725        }
726    }
727}
728
729pub(crate) type IgmpResult<T> = Result<T, IgmpError>;
730
731/// An IGMP timer ID.
732#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
733pub struct IgmpTimerId<D: WeakDeviceIdentifier>(GmpTimerId<Ipv4, D>);
734
735impl<D: WeakDeviceIdentifier> IgmpTimerId<D> {
736    pub(crate) fn device_id(&self) -> &D {
737        let Self(inner) = self;
738        inner.device_id()
739    }
740
741    /// Creates a new [`IgmpTimerId`] for `device`.
742    #[cfg(any(test, feature = "testutils"))]
743    pub const fn new(device: D) -> Self {
744        Self(GmpTimerId::new(device))
745    }
746}
747
748impl<D: WeakDeviceIdentifier> From<GmpTimerId<Ipv4, D>> for IgmpTimerId<D> {
749    fn from(id: GmpTimerId<Ipv4, D>) -> IgmpTimerId<D> {
750        Self(id)
751    }
752}
753
754impl<BC: IgmpBindingsContext, CC: IgmpContext<BC>> HandleableTimer<CC, BC>
755    for IgmpTimerId<CC::WeakDeviceId>
756{
757    fn handle(self, core_ctx: &mut CC, bindings_ctx: &mut BC, _: BC::UniqueTimerId) {
758        let Self(gmp) = self;
759        gmp::handle_timer(core_ctx, bindings_ctx, gmp);
760    }
761}
762
763/// An iterator that generates the IP options for IGMP packets.
764///
765/// This allows us to write `new_ip_header_builder` easily without a big mess of
766/// static lifetimes.
767///
768/// IGMP messages require the Router Alert options. See [RFC 2236 section 2] ,
769/// [RFC 3376 section 4].
770///
771/// [RFC 2236 section 2]:
772///     https://datatracker.ietf.org/doc/html/rfc2236#section-2
773/// [RFC 3376 section 4]:
774///     https://datatracker.ietf.org/doc/html/rfc3376#section-4
775#[derive(Debug, Clone, Default)]
776struct IgmpIpOptions(bool);
777
778impl Iterator for IgmpIpOptions {
779    type Item = Ipv4Option<'static>;
780
781    fn next(&mut self) -> Option<Self::Item> {
782        let Self(yielded) = self;
783        if core::mem::replace(yielded, true) {
784            None
785        } else {
786            Some(Ipv4Option::RouterAlert { data: 0 })
787        }
788    }
789}
790
791/// The required IP TTL for IGMP messages.
792///
793/// See [RFC 2236 section 2] , [RFC 3376 section 4].
794///
795/// [RFC 2236 section 2]:
796///     https://datatracker.ietf.org/doc/html/rfc2236#section-2
797/// [RFC 3376 section 4]:
798///     https://datatracker.ietf.org/doc/html/rfc3376#section-4
799const IGMP_IP_TTL: u8 = 1;
800
801/// The required IP DSCP and ECN for IGMPv3 messages.
802///
803/// [RFC 3376 section 4] defines the IP TOS (now DSCP and ECN) as having the
804/// value 0xc0. So we construct the [`DscpAndEcn`] value from the value
805/// specified in the RFC.
806///
807/// [RFC 3376 section 4]:
808///     https://datatracker.ietf.org/doc/html/rfc3376#section-4
809const IGMPV3_DSCP_AND_ECN: DscpAndEcn = DscpAndEcn::new_with_raw(0xc0);
810
811fn new_ip_header_builder<BC: IgmpBindingsContext, CC: IgmpSendContext<BC>>(
812    core_ctx: &mut CC,
813    device: &CC::DeviceId,
814    dst_ip: MulticastAddr<Ipv4Addr>,
815) -> Ipv4PacketBuilderWithOptions<'static, IgmpIpOptions> {
816    // As per RFC 3376 section 4.2.13,
817    //
818    //   An IGMP report is sent with a valid IP source address for the
819    //   destination subnet. The 0.0.0.0 source address may be used by a system
820    //   that has not yet acquired an IP address.
821    //
822    // Note that RFC 3376 targets IGMPv3 but we could be running IGMPv2.
823    // However, we still allow sending IGMP packets with the unspecified source
824    // when no address is available so that IGMP snooping switches know to
825    // forward multicast packets to us before an address is available. See RFC
826    // 4541 for some details regarding considerations for IGMP/MLD snooping
827    // switches.
828    let src_ip =
829        core_ctx.get_ip_addr_subnet(device).map_or(Ipv4::UNSPECIFIED_ADDRESS, |a| a.addr().get());
830    Ipv4PacketBuilderWithOptions::new(
831        Ipv4PacketBuilder::new(src_ip, dst_ip, IGMP_IP_TTL, Ipv4Proto::Igmp),
832        IgmpIpOptions::default(),
833    )
834    .unwrap_or_else(|Ipv4OptionsTooLongError| unreachable!("router alert always fits"))
835}
836
837fn send_igmp_v2_message<BC: IgmpBindingsContext, CC: IgmpSendContext<BC>, M>(
838    core_ctx: &mut CC,
839    bindings_ctx: &mut BC,
840    device: &CC::DeviceId,
841    group_addr: MulticastAddr<Ipv4Addr>,
842    dst_ip: MulticastAddr<Ipv4Addr>,
843    max_resp_time: M::MaxRespTime,
844) -> IgmpResult<()>
845where
846    M: MessageType<EmptyBuf, FixedHeader = Ipv4Addr, VariableBody = ()>,
847{
848    let header = new_ip_header_builder(core_ctx, device, dst_ip);
849    let body =
850        IgmpPacketBuilder::<EmptyBuf, M>::new_with_resp_time(group_addr.get(), max_resp_time);
851    let body = body.into_serializer().encapsulate(header);
852    let destination = IpPacketDestination::Multicast(dst_ip);
853    IpLayerHandler::send_ip_frame(core_ctx, bindings_ctx, &device, destination, body)
854        .map_err(|_| IgmpError::SendFailure { addr: *group_addr })
855}
856
857#[derive(Debug)]
858pub struct IgmpConfig {
859    // When a host wants to send a report not because of a query, this value is
860    // used as the delay timer.
861    unsolicited_report_interval: Duration,
862    // When this option is true, the host can send a leave message even when it
863    // is not the last one in the multicast group.
864    send_leave_anyway: bool,
865    // Default timer value for Version 1 Router Present Timeout.
866    v1_router_present_timeout: Duration,
867}
868
869/// The default value for `unsolicited_report_interval` as per [RFC 2236 Section
870/// 8.10].
871///
872/// [RFC 2236 Section 8.10]: https://tools.ietf.org/html/rfc2236#section-8.10
873pub const IGMP_DEFAULT_UNSOLICITED_REPORT_INTERVAL: Duration = Duration::from_secs(10);
874/// The default value for `v1_router_present_timeout` as per [RFC 2236 Section
875/// 8.11].
876///
877/// [RFC 2236 Section 8.11]: https://tools.ietf.org/html/rfc2236#section-8.11
878const DEFAULT_V1_ROUTER_PRESENT_TIMEOUT: Duration = Duration::from_secs(400);
879/// The default value for the `MaxRespTime` if the query is a V1 query, whose
880/// `MaxRespTime` field is 0 in the packet. Please refer to [RFC 2236 Section
881/// 4].
882///
883/// [RFC 2236 Section 4]: https://tools.ietf.org/html/rfc2236#section-4
884const DEFAULT_V1_QUERY_MAX_RESP_TIME: NonZeroDuration =
885    NonZeroDuration::new(Duration::from_secs(10)).unwrap();
886
887impl Default for IgmpConfig {
888    fn default() -> Self {
889        IgmpConfig {
890            unsolicited_report_interval: IGMP_DEFAULT_UNSOLICITED_REPORT_INTERVAL,
891            send_leave_anyway: false,
892            v1_router_present_timeout: DEFAULT_V1_ROUTER_PRESENT_TIMEOUT,
893        }
894    }
895}
896
897impl gmp::v1::ProtocolConfig for IgmpConfig {
898    fn unsolicited_report_interval(&self) -> Duration {
899        self.unsolicited_report_interval
900    }
901
902    fn send_leave_anyway(&self) -> bool {
903        self.send_leave_anyway
904    }
905
906    fn get_max_resp_time(&self, resp_time: Duration) -> Option<NonZeroDuration> {
907        // As per RFC 2236 section 4,
908        //
909        //   An IGMPv2 host may be placed on a subnet where the Querier router
910        //   has not yet been upgraded to IGMPv2. The following requirements
911        //   apply:
912        //
913        //        The IGMPv1 router will send General Queries with the Max
914        //        Response Time set to 0.  This MUST be interpreted as a value
915        //        of 100 (10 seconds).
916        Some(NonZeroDuration::new(resp_time).unwrap_or(DEFAULT_V1_QUERY_MAX_RESP_TIME))
917    }
918}
919
920impl gmp::v2::ProtocolConfig for IgmpConfig {
921    fn query_response_interval(&self) -> NonZeroDuration {
922        gmp::v2::DEFAULT_QUERY_RESPONSE_INTERVAL
923    }
924
925    fn unsolicited_report_interval(&self) -> NonZeroDuration {
926        gmp::v2::DEFAULT_UNSOLICITED_REPORT_INTERVAL
927    }
928}
929
930/// Statistics about IGMP.
931///
932/// The counter type `C` is generic to facilitate testing.
933#[derive(Debug, Default)]
934#[cfg_attr(test, derive(PartialEq))]
935pub struct IgmpCounters<C = Counter> {
936    // Count of IGMPv1 queries received.
937    rx_igmpv1_query: C,
938    // Count of IGMPv2 queries received.
939    rx_igmpv2_query: C,
940    // Count of IGMPv3 queries received.
941    rx_igmpv3_query: C,
942    // Count of IGMPv1 reports received.
943    rx_igmpv1_report: C,
944    // Count of IGMPv2 reports received.
945    rx_igmpv2_report: C,
946    // Count of IGMPv3 reports received.
947    rx_igmpv3_report: C,
948    // Count of Leave Group messages received.
949    rx_leave_group: C,
950    // The count of received IGMP messages that could not be parsed.
951    rx_err_parse: C,
952    // Count of queries that were rejected because the Router Alert option was
953    // not present.
954    rx_err_missing_router_alert_in_query: C,
955    // Count of queries that were rejected because they weren't sent to the
956    // all-systems address.
957    rx_err_rejected_general_query: C,
958    // Count of received IGMP messages that were rejected because they had
959    // an invalid TTL.
960    rx_err_bad_ttl: C,
961    // Count of IGMPv1 reports sent.
962    tx_igmpv1_report: C,
963    // Count of IGMPv2 reports sent.
964    tx_igmpv2_report: C,
965    // Count of IGMPv3 reports sent.
966    tx_igmpv3_report: C,
967    // Count of Leave Group messages sent.
968    tx_leave_group: C,
969    // Count of IGMP messages that could not be sent.
970    tx_err: C,
971}
972
973impl Inspectable for IgmpCounters {
974    fn record<I: Inspector>(&self, inspector: &mut I) {
975        let Self {
976            rx_igmpv1_query,
977            rx_igmpv2_query,
978            rx_igmpv3_query,
979            rx_igmpv1_report,
980            rx_igmpv2_report,
981            rx_igmpv3_report,
982            rx_leave_group,
983            rx_err_parse,
984            rx_err_missing_router_alert_in_query,
985            rx_err_rejected_general_query,
986            rx_err_bad_ttl,
987            tx_igmpv1_report,
988            tx_igmpv2_report,
989            tx_igmpv3_report,
990            tx_leave_group,
991            tx_err,
992        } = self;
993        inspector.record_child("Rx", |inspector| {
994            inspector.record_counter("IGMPv1Query", rx_igmpv1_query);
995            inspector.record_counter("IGMPv2Query", rx_igmpv2_query);
996            inspector.record_counter("IGMPv3Query", rx_igmpv3_query);
997            inspector.record_counter("IGMPv1Report", rx_igmpv1_report);
998            inspector.record_counter("IGMPv2Report", rx_igmpv2_report);
999            inspector.record_counter("IGMPv3Report", rx_igmpv3_report);
1000            inspector.record_counter("LeaveGroup", rx_leave_group);
1001            inspector.record_child("Errors", |inspector| {
1002                inspector.record_counter("ParseFailed", rx_err_parse);
1003                inspector.record_counter(
1004                    "MissingRouterAlertInQuery",
1005                    rx_err_missing_router_alert_in_query,
1006                );
1007                inspector.record_counter("RejectedGeneralQuery", rx_err_rejected_general_query);
1008                inspector.record_counter("BadTTL", rx_err_bad_ttl);
1009            });
1010        });
1011        inspector.record_child("Tx", |inspector| {
1012            inspector.record_counter("IGMPv1Report", tx_igmpv1_report);
1013            inspector.record_counter("IGMPv2Report", tx_igmpv2_report);
1014            inspector.record_counter("IGMPv3Report", tx_igmpv3_report);
1015            inspector.record_counter("LeaveGroup", tx_leave_group);
1016            inspector.record_child("Errors", |inspector| {
1017                inspector.record_counter("SendFailed", tx_err);
1018            });
1019        })
1020    }
1021}
1022
1023#[cfg(test)]
1024mod tests {
1025    use core::cell::RefCell;
1026
1027    use alloc::rc::Rc;
1028    use alloc::vec;
1029    use alloc::vec::Vec;
1030    use assert_matches::assert_matches;
1031
1032    use net_types::ip::{Ip, Mtu};
1033    use netstack3_base::testutil::{
1034        assert_empty, run_with_many_seeds, FakeDeviceId, FakeTimerCtxExt, FakeWeakDeviceId,
1035        TestIpExt as _,
1036    };
1037    use netstack3_base::{
1038        CounterContext, CtxPair, Instant as _, IntoCoreTimerCtx, SendFrameContext as _,
1039    };
1040    use packet::serialize::Buf;
1041    use packet::{ParsablePacket as _, ParseBuffer};
1042    use packet_formats::gmp::GroupRecordType;
1043    use packet_formats::igmp::messages::{
1044        IgmpMembershipQueryV2, IgmpMembershipQueryV3Builder, Igmpv3QQIC, Igmpv3QRV,
1045    };
1046    use packet_formats::igmp::IgmpResponseTimeV3;
1047    use packet_formats::ipv4::{Ipv4Header, Ipv4Packet};
1048    use packet_formats::testutil::parse_ip_packet;
1049    use test_case::test_case;
1050
1051    use super::*;
1052    use crate::internal::base::{IpPacketDestination, IpSendFrameError, SendIpPacketMeta};
1053    use crate::internal::fragmentation::FragmentableIpSerializer;
1054    use crate::internal::gmp::{
1055        GmpEnabledGroup, GmpHandler as _, GmpState, GroupJoinResult, GroupLeaveResult,
1056    };
1057    use crate::testutil::FakeIpHeaderInfo;
1058
1059    /// Metadata for sending an IGMP packet.
1060    #[derive(Debug, PartialEq)]
1061    pub(crate) struct IgmpPacketMetadata<D> {
1062        pub(crate) device: D,
1063        pub(crate) dst_ip: MulticastAddr<Ipv4Addr>,
1064    }
1065
1066    impl<D> IgmpPacketMetadata<D> {
1067        fn new(device: D, dst_ip: MulticastAddr<Ipv4Addr>) -> IgmpPacketMetadata<D> {
1068            IgmpPacketMetadata { device, dst_ip }
1069        }
1070    }
1071
1072    /// A fake [`IgmpContext`] that stores the [`MulticastGroupSet`] and an
1073    /// optional IPv4 address and subnet that may be returned in calls to
1074    /// [`IgmpContext::get_ip_addr_subnet`].
1075    struct FakeIgmpCtx {
1076        igmp_enabled: bool,
1077        shared: Rc<RefCell<Shared>>,
1078        addr_subnet: Option<AddrSubnet<Ipv4Addr, Ipv4DeviceAddr>>,
1079        stack_wide_counters: IgmpCounters,
1080        device_specific_counters: IgmpCounters,
1081    }
1082
1083    /// The parts of `FakeIgmpCtx` that are behind a RefCell, mocking a lock.
1084    struct Shared {
1085        groups: MulticastGroupSet<Ipv4Addr, GmpGroupState<Ipv4, FakeBindingsCtx>>,
1086        gmp_state: GmpState<Ipv4, IgmpTypeLayout, FakeBindingsCtx>,
1087        config: IgmpConfig,
1088    }
1089
1090    impl FakeIgmpCtx {
1091        fn gmp_state(&mut self) -> &mut GmpState<Ipv4, IgmpTypeLayout, FakeBindingsCtx> {
1092            &mut Rc::get_mut(&mut self.shared).unwrap().get_mut().gmp_state
1093        }
1094
1095        fn gmp_state_and_config(
1096            &mut self,
1097        ) -> (&mut GmpState<Ipv4, IgmpTypeLayout, FakeBindingsCtx>, &mut IgmpConfig) {
1098            let shared = Rc::get_mut(&mut self.shared).unwrap().get_mut();
1099            (&mut shared.gmp_state, &mut shared.config)
1100        }
1101
1102        fn groups(
1103            &mut self,
1104        ) -> &mut MulticastGroupSet<Ipv4Addr, GmpGroupState<Ipv4, FakeBindingsCtx>> {
1105            &mut Rc::get_mut(&mut self.shared).unwrap().get_mut().groups
1106        }
1107    }
1108
1109    impl CounterContext<IgmpCounters> for FakeIgmpCtx {
1110        fn counters(&self) -> &IgmpCounters {
1111            &self.stack_wide_counters
1112        }
1113    }
1114
1115    impl ResourceCounterContext<FakeDeviceId, IgmpCounters> for FakeIgmpCtx {
1116        fn per_resource_counters<'a>(&'a self, _device_id: &'a FakeDeviceId) -> &'a IgmpCounters {
1117            &self.device_specific_counters
1118        }
1119    }
1120
1121    type FakeCtx = CtxPair<FakeCoreCtx, FakeBindingsCtx>;
1122
1123    type FakeCoreCtx = netstack3_base::testutil::FakeCoreCtx<
1124        FakeIgmpCtx,
1125        IgmpPacketMetadata<FakeDeviceId>,
1126        FakeDeviceId,
1127    >;
1128
1129    type FakeBindingsCtx = netstack3_base::testutil::FakeBindingsCtx<
1130        IgmpTimerId<FakeWeakDeviceId<FakeDeviceId>>,
1131        (),
1132        (),
1133        (),
1134    >;
1135
1136    impl IgmpContextMarker for FakeCoreCtx {}
1137
1138    impl IgmpStateContext<FakeBindingsCtx> for FakeCoreCtx {
1139        fn with_igmp_state<
1140            O,
1141            F: FnOnce(
1142                &MulticastGroupSet<Ipv4Addr, GmpGroupState<Ipv4, FakeBindingsCtx>>,
1143                &GmpState<Ipv4, IgmpTypeLayout, FakeBindingsCtx>,
1144            ) -> O,
1145        >(
1146            &mut self,
1147            &FakeDeviceId: &FakeDeviceId,
1148            cb: F,
1149        ) -> O {
1150            let state = self.state.shared.borrow();
1151            cb(&state.groups, &state.gmp_state)
1152        }
1153    }
1154
1155    impl IgmpContext<FakeBindingsCtx> for FakeCoreCtx {
1156        type SendContext<'a> = &'a mut Self;
1157        fn with_igmp_state_mut<
1158            O,
1159            F: for<'a> FnOnce(
1160                Self::SendContext<'a>,
1161                GmpStateRef<'a, Ipv4, IgmpTypeLayout, FakeBindingsCtx>,
1162            ) -> O,
1163        >(
1164            &mut self,
1165            &FakeDeviceId: &FakeDeviceId,
1166            cb: F,
1167        ) -> O {
1168            let FakeIgmpCtx { igmp_enabled, shared, .. } = &mut self.state;
1169            let enabled = *igmp_enabled;
1170            let shared = Rc::clone(shared);
1171            let mut shared = shared.borrow_mut();
1172            let Shared { gmp_state, groups, config } = &mut *shared;
1173            cb(self, GmpStateRef { enabled, groups, gmp: gmp_state, config })
1174        }
1175    }
1176
1177    impl IgmpSendContext<FakeBindingsCtx> for &mut FakeCoreCtx {
1178        fn get_ip_addr_subnet(
1179            &mut self,
1180            _device: &FakeDeviceId,
1181        ) -> Option<AddrSubnet<Ipv4Addr, Ipv4DeviceAddr>> {
1182            self.state.addr_subnet
1183        }
1184    }
1185
1186    impl IpDeviceMtuContext<Ipv4> for &mut FakeCoreCtx {
1187        fn get_mtu(&mut self, _device: &FakeDeviceId) -> Mtu {
1188            Mtu::new(1500)
1189        }
1190    }
1191
1192    impl IpLayerHandler<Ipv4, FakeBindingsCtx> for &mut FakeCoreCtx {
1193        fn send_ip_packet_from_device<S>(
1194            &mut self,
1195            _bindings_ctx: &mut FakeBindingsCtx,
1196            _meta: SendIpPacketMeta<
1197                Ipv4,
1198                &Self::DeviceId,
1199                Option<SpecifiedAddr<<Ipv4 as Ip>::Addr>>,
1200            >,
1201            _body: S,
1202        ) -> Result<(), IpSendFrameError<S>>
1203        where
1204            S: Serializer,
1205            S::Buffer: BufferMut,
1206        {
1207            unimplemented!();
1208        }
1209
1210        fn send_ip_frame<S>(
1211            &mut self,
1212            bindings_ctx: &mut FakeBindingsCtx,
1213            device: &Self::DeviceId,
1214            destination: IpPacketDestination<Ipv4, &Self::DeviceId>,
1215            body: S,
1216        ) -> Result<(), IpSendFrameError<S>>
1217        where
1218            S: FragmentableIpSerializer<Ipv4, Buffer: BufferMut> + netstack3_filter::IpPacket<Ipv4>,
1219        {
1220            let addr = match destination {
1221                IpPacketDestination::Multicast(addr) => addr,
1222                _ => panic!("destination is not multicast: {:?}", destination),
1223            };
1224
1225            (*self)
1226                .send_frame(bindings_ctx, IgmpPacketMetadata::new(device.clone(), addr), body)
1227                .map_err(|err| err.err_into())
1228        }
1229    }
1230
1231    impl CounterContext<IgmpCounters> for &mut FakeCoreCtx {
1232        fn counters(&self) -> &IgmpCounters {
1233            <FakeCoreCtx as CounterContext<IgmpCounters>>::counters(self)
1234        }
1235    }
1236
1237    impl ResourceCounterContext<FakeDeviceId, IgmpCounters> for &mut FakeCoreCtx {
1238        fn per_resource_counters<'a>(&'a self, device_id: &'a FakeDeviceId) -> &'a IgmpCounters {
1239            <
1240                FakeCoreCtx as ResourceCounterContext<FakeDeviceId, IgmpCounters>
1241            >::per_resource_counters(self, device_id)
1242        }
1243    }
1244
1245    type CounterExpectations = IgmpCounters<u64>;
1246
1247    impl CounterExpectations {
1248        #[track_caller]
1249        fn assert_counters<CC: ResourceCounterContext<FakeDeviceId, IgmpCounters>>(
1250            &self,
1251            core_ctx: &mut CC,
1252        ) {
1253            assert_eq!(
1254                self,
1255                &CounterExpectations::from(core_ctx.counters()),
1256                "stack-wide counter mismatch"
1257            );
1258            assert_eq!(
1259                self,
1260                &CounterExpectations::from(core_ctx.per_resource_counters(&FakeDeviceId)),
1261                "device-specific counter mismatch"
1262            );
1263        }
1264    }
1265
1266    impl From<&IgmpCounters> for CounterExpectations {
1267        fn from(igmp_counters: &IgmpCounters) -> CounterExpectations {
1268            let IgmpCounters {
1269                rx_igmpv1_query,
1270                rx_igmpv2_query,
1271                rx_igmpv3_query,
1272                rx_igmpv1_report,
1273                rx_igmpv2_report,
1274                rx_igmpv3_report,
1275                rx_leave_group,
1276                rx_err_parse,
1277                rx_err_missing_router_alert_in_query,
1278                rx_err_rejected_general_query,
1279                rx_err_bad_ttl,
1280                tx_igmpv1_report,
1281                tx_igmpv2_report,
1282                tx_igmpv3_report,
1283                tx_leave_group,
1284                tx_err,
1285            } = igmp_counters;
1286            CounterExpectations {
1287                rx_igmpv1_query: rx_igmpv1_query.get(),
1288                rx_igmpv2_query: rx_igmpv2_query.get(),
1289                rx_igmpv3_query: rx_igmpv3_query.get(),
1290                rx_igmpv1_report: rx_igmpv1_report.get(),
1291                rx_igmpv2_report: rx_igmpv2_report.get(),
1292                rx_igmpv3_report: rx_igmpv3_report.get(),
1293                rx_leave_group: rx_leave_group.get(),
1294                rx_err_parse: rx_err_parse.get(),
1295                rx_err_missing_router_alert_in_query: rx_err_missing_router_alert_in_query.get(),
1296                rx_err_rejected_general_query: rx_err_rejected_general_query.get(),
1297                rx_err_bad_ttl: rx_err_bad_ttl.get(),
1298                tx_igmpv1_report: tx_igmpv1_report.get(),
1299                tx_igmpv2_report: tx_igmpv2_report.get(),
1300                tx_igmpv3_report: tx_igmpv3_report.get(),
1301                tx_leave_group: tx_leave_group.get(),
1302                tx_err: tx_err.get(),
1303            }
1304        }
1305    }
1306
1307    const MY_ADDR: SpecifiedAddr<Ipv4Addr> =
1308        unsafe { SpecifiedAddr::new_unchecked(Ipv4Addr::new([192, 168, 0, 2])) };
1309    const ROUTER_ADDR: Ipv4Addr = Ipv4Addr::new([192, 168, 0, 1]);
1310    const OTHER_HOST_ADDR: Ipv4Addr = Ipv4Addr::new([192, 168, 0, 3]);
1311    const GROUP_ADDR: MulticastAddr<Ipv4Addr> = <Ipv4 as gmp::testutil::TestIpExt>::GROUP_ADDR1;
1312    const GROUP_ADDR_2: MulticastAddr<Ipv4Addr> = <Ipv4 as gmp::testutil::TestIpExt>::GROUP_ADDR2;
1313    const GMP_TIMER_ID: IgmpTimerId<FakeWeakDeviceId<FakeDeviceId>> =
1314        IgmpTimerId::new(FakeWeakDeviceId(FakeDeviceId));
1315
1316    fn new_recv_pkt_info() -> LocalDeliveryPacketInfo<Ipv4, FakeIpHeaderInfo> {
1317        LocalDeliveryPacketInfo {
1318            header_info: FakeIpHeaderInfo {
1319                router_alert: true,
1320                hop_limit: 1,
1321                ..Default::default()
1322            },
1323            ..Default::default()
1324        }
1325    }
1326
1327    fn receive_igmp_v1_query(core_ctx: &mut FakeCoreCtx, bindings_ctx: &mut FakeBindingsCtx) {
1328        let ser = IgmpPacketBuilder::<Buf<Vec<u8>>, IgmpMembershipQueryV2>::new_with_resp_time(
1329            GROUP_ADDR.get(),
1330            Duration::ZERO.try_into().unwrap(),
1331        );
1332        let buff = ser.into_serializer().serialize_vec_outer().unwrap();
1333        core_ctx.receive_igmp_packet(
1334            bindings_ctx,
1335            &FakeDeviceId,
1336            ROUTER_ADDR,
1337            MY_ADDR,
1338            buff,
1339            &new_recv_pkt_info(),
1340        );
1341    }
1342
1343    fn receive_igmp_v2_query(
1344        core_ctx: &mut FakeCoreCtx,
1345        bindings_ctx: &mut FakeBindingsCtx,
1346        resp_time: NonZeroDuration,
1347    ) {
1348        let ser = IgmpPacketBuilder::<Buf<Vec<u8>>, IgmpMembershipQueryV2>::new_with_resp_time(
1349            GROUP_ADDR.get(),
1350            resp_time.get().try_into().unwrap(),
1351        );
1352        let buff = ser.into_serializer().serialize_vec_outer().unwrap();
1353        core_ctx.receive_igmp_packet(
1354            bindings_ctx,
1355            &FakeDeviceId,
1356            ROUTER_ADDR,
1357            MY_ADDR,
1358            buff,
1359            &new_recv_pkt_info(),
1360        );
1361    }
1362
1363    fn receive_igmp_v2_general_query(
1364        core_ctx: &mut FakeCoreCtx,
1365        bindings_ctx: &mut FakeBindingsCtx,
1366        resp_time: NonZeroDuration,
1367    ) {
1368        let ser = IgmpPacketBuilder::<Buf<Vec<u8>>, IgmpMembershipQueryV2>::new_with_resp_time(
1369            Ipv4Addr::new([0, 0, 0, 0]),
1370            resp_time.get().try_into().unwrap(),
1371        );
1372        let buff = ser.into_serializer().serialize_vec_outer().unwrap();
1373        core_ctx.receive_igmp_packet(
1374            bindings_ctx,
1375            &FakeDeviceId,
1376            ROUTER_ADDR,
1377            MY_ADDR,
1378            buff,
1379            &new_recv_pkt_info(),
1380        );
1381    }
1382
1383    fn receive_igmp_report(core_ctx: &mut FakeCoreCtx, bindings_ctx: &mut FakeBindingsCtx) {
1384        let ser = IgmpPacketBuilder::<Buf<Vec<u8>>, IgmpMembershipReportV2>::new(GROUP_ADDR.get());
1385        let buff = ser.into_serializer().serialize_vec_outer().unwrap();
1386        core_ctx.receive_igmp_packet(
1387            bindings_ctx,
1388            &FakeDeviceId,
1389            OTHER_HOST_ADDR,
1390            MY_ADDR,
1391            buff,
1392            &new_recv_pkt_info(),
1393        );
1394    }
1395
1396    fn setup_igmpv2_test_environment_with_addr_subnet(
1397        seed: u128,
1398        a: Option<AddrSubnet<Ipv4Addr, Ipv4DeviceAddr>>,
1399    ) -> FakeCtx {
1400        let mut ctx = FakeCtx::with_default_bindings_ctx(|bindings_ctx| {
1401            // We start with enabled true to make tests easier to write.
1402            let igmp_enabled = true;
1403            FakeCoreCtx::with_state(FakeIgmpCtx {
1404                shared: Rc::new(RefCell::new(Shared {
1405                    groups: MulticastGroupSet::default(),
1406                    gmp_state: GmpState::new_with_enabled_and_mode::<_, IntoCoreTimerCtx>(
1407                        bindings_ctx,
1408                        FakeWeakDeviceId(FakeDeviceId),
1409                        igmp_enabled,
1410                        IgmpMode::V2 { compat: false },
1411                    ),
1412                    config: Default::default(),
1413                })),
1414                igmp_enabled,
1415                addr_subnet: None,
1416                stack_wide_counters: Default::default(),
1417                device_specific_counters: Default::default(),
1418            })
1419        });
1420        ctx.bindings_ctx.seed_rng(seed);
1421        ctx.core_ctx.state.addr_subnet = a;
1422        ctx
1423    }
1424
1425    fn setup_igmpv2_test_environment(seed: u128) -> FakeCtx {
1426        setup_igmpv2_test_environment_with_addr_subnet(
1427            seed,
1428            Some(AddrSubnet::new(MY_ADDR.get(), 24).unwrap()),
1429        )
1430    }
1431
1432    fn ensure_ttl_ihl_rtr(core_ctx: &FakeCoreCtx) {
1433        for (_, frame) in core_ctx.frames() {
1434            assert_eq!(frame[8], IGMP_IP_TTL); // TTL,
1435            assert_eq!(&frame[20..24], &[148, 4, 0, 0]); // RTR
1436            assert_eq!(frame[0], 0x46); // IHL
1437        }
1438    }
1439
1440    #[test_case(Some(MY_ADDR); "specified_src")]
1441    #[test_case(None; "unspecified_src")]
1442    fn test_igmp_simple_integration(src_ip: Option<SpecifiedAddr<Ipv4Addr>>) {
1443        let check_report = |core_ctx: &mut FakeCoreCtx| {
1444            let expected_src_ip = src_ip.map_or(Ipv4::UNSPECIFIED_ADDRESS, |a| a.get());
1445
1446            let frames = core_ctx.take_frames();
1447            let (IgmpPacketMetadata { device: FakeDeviceId, dst_ip }, frame) = assert_matches!(
1448                &frames[..], [x] => x);
1449            assert_eq!(dst_ip, &GROUP_ADDR);
1450            let (body, src_ip, dst_ip, proto, ttl) = parse_ip_packet::<Ipv4>(frame).unwrap();
1451            assert_eq!(src_ip, expected_src_ip);
1452            assert_eq!(dst_ip, GROUP_ADDR.get());
1453            assert_eq!(proto, Ipv4Proto::Igmp);
1454            assert_eq!(ttl, IGMP_IP_TTL);
1455            let mut bv = &body[..];
1456            assert_matches!(
1457                IgmpPacket::parse(&mut bv, ()).unwrap(),
1458                IgmpPacket::MembershipReportV2(msg) => {
1459                    assert_eq!(msg.group_addr(), GROUP_ADDR.get());
1460                }
1461            );
1462        };
1463
1464        let addr_subnet = src_ip.map(|a| AddrSubnet::new(a.get(), 16).unwrap());
1465        run_with_many_seeds(|seed| {
1466            let FakeCtx { mut core_ctx, mut bindings_ctx } =
1467                setup_igmpv2_test_environment_with_addr_subnet(seed, addr_subnet);
1468
1469            // Joining a group should send a report.
1470            assert_eq!(
1471                core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
1472                GroupJoinResult::Joined(())
1473            );
1474            check_report(&mut core_ctx);
1475
1476            // Should send a report after a query.
1477            receive_igmp_v2_query(
1478                &mut core_ctx,
1479                &mut bindings_ctx,
1480                NonZeroDuration::from_secs(10).unwrap(),
1481            );
1482            core_ctx
1483                .state
1484                .gmp_state()
1485                .timers
1486                .assert_top(&gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(), &());
1487            assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(GMP_TIMER_ID));
1488            check_report(&mut core_ctx);
1489
1490            CounterExpectations { rx_igmpv2_query: 1, tx_igmpv2_report: 2, ..Default::default() }
1491                .assert_counters(&mut core_ctx);
1492        });
1493    }
1494
1495    #[test]
1496    fn test_igmp_integration_fallback_from_idle() {
1497        run_with_many_seeds(|seed| {
1498            let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_igmpv2_test_environment(seed);
1499            assert_eq!(
1500                core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
1501                GroupJoinResult::Joined(())
1502            );
1503            assert_eq!(core_ctx.frames().len(), 1);
1504
1505            core_ctx
1506                .state
1507                .gmp_state()
1508                .timers
1509                .assert_top(&gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(), &());
1510            assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(GMP_TIMER_ID));
1511            assert_eq!(core_ctx.frames().len(), 2);
1512
1513            receive_igmp_v2_query(
1514                &mut core_ctx,
1515                &mut bindings_ctx,
1516                NonZeroDuration::from_secs(10).unwrap(),
1517            );
1518
1519            // We have received a query, hence we are falling back to Delay
1520            // Member state.
1521            let group_state = core_ctx.state.groups().get(&GROUP_ADDR).unwrap().v1();
1522            match group_state.get_inner() {
1523                gmp::v1::MemberState::Delaying(_) => {}
1524                _ => panic!("Wrong State!"),
1525            }
1526
1527            core_ctx
1528                .state
1529                .gmp_state()
1530                .timers
1531                .assert_top(&gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(), &());
1532            assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(GMP_TIMER_ID));
1533            assert_eq!(core_ctx.frames().len(), 3);
1534            ensure_ttl_ihl_rtr(&core_ctx);
1535
1536            CounterExpectations { rx_igmpv2_query: 1, tx_igmpv2_report: 3, ..Default::default() }
1537                .assert_counters(&mut core_ctx);
1538        });
1539    }
1540
1541    #[test]
1542    fn test_igmpv2_integration_igmpv1_router_present() {
1543        run_with_many_seeds(|seed| {
1544            let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_igmpv2_test_environment(seed);
1545
1546            assert_eq!(core_ctx.state.gmp_state().mode, IgmpMode::V2 { compat: false });
1547            assert_eq!(
1548                core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
1549                GroupJoinResult::Joined(())
1550            );
1551            let now = bindings_ctx.now();
1552            core_ctx.state.gmp_state().timers.assert_range([(
1553                &gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(),
1554                now..=(now + IGMP_DEFAULT_UNSOLICITED_REPORT_INTERVAL),
1555            )]);
1556
1557            receive_igmp_v1_query(&mut core_ctx, &mut bindings_ctx);
1558            assert_eq!(core_ctx.frames().len(), 1);
1559
1560            // Since we have heard from the v1 router, we should have set our
1561            // flag.
1562            let now = bindings_ctx.now();
1563            let until = now.panicking_add(DEFAULT_V1_ROUTER_PRESENT_TIMEOUT);
1564            assert_eq!(
1565                core_ctx.state.gmp_state().mode,
1566                IgmpMode::V1(IgmpV1Mode::V2Compat { until })
1567            );
1568            assert_eq!(core_ctx.state.gmp_state().mode.should_send_v1(&mut bindings_ctx), true);
1569            assert_eq!(core_ctx.frames().len(), 1);
1570            core_ctx.state.gmp_state().timers.assert_range([(
1571                &gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(),
1572                now..=(now + IGMP_DEFAULT_UNSOLICITED_REPORT_INTERVAL),
1573            )]);
1574            bindings_ctx.timers.assert_timers_installed_range([(
1575                GMP_TIMER_ID,
1576                now..=(now + IGMP_DEFAULT_UNSOLICITED_REPORT_INTERVAL),
1577            )]);
1578
1579            core_ctx
1580                .state
1581                .gmp_state()
1582                .timers
1583                .assert_top(&gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(), &());
1584            assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(GMP_TIMER_ID));
1585            // After the first timer, we send out our V1 report.
1586            assert_eq!(core_ctx.frames().len(), 2);
1587            // The last frame being sent should be a V1 report.
1588            let (_, frame) = core_ctx.frames().last().unwrap();
1589            // 34 and 0x12 are hacky but they can quickly tell it is a V1
1590            // report.
1591            assert_eq!(frame[24], 0x12);
1592
1593            // Sleep until v1 router present is no more.
1594            bindings_ctx.timers.instant.time = until;
1595
1596            // After the elapsed time, we should reset our flag for v1 routers.
1597            assert_eq!(core_ctx.state.gmp_state().mode.should_send_v1(&mut bindings_ctx), false);
1598
1599            receive_igmp_v2_query(
1600                &mut core_ctx,
1601                &mut bindings_ctx,
1602                NonZeroDuration::from_secs(10).unwrap(),
1603            );
1604            core_ctx
1605                .state
1606                .gmp_state()
1607                .timers
1608                .assert_top(&gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(), &());
1609            assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(GMP_TIMER_ID));
1610            assert_eq!(core_ctx.frames().len(), 3);
1611            // Now we should get V2 report
1612            assert_eq!(core_ctx.frames().last().unwrap().1[24], 0x16);
1613            ensure_ttl_ihl_rtr(&core_ctx);
1614
1615            CounterExpectations {
1616                rx_igmpv1_query: 1,
1617                rx_igmpv2_query: 1,
1618                tx_igmpv2_report: 2,
1619                tx_igmpv1_report: 1,
1620                ..Default::default()
1621            }
1622            .assert_counters(&mut core_ctx);
1623        });
1624    }
1625
1626    #[test]
1627    fn test_igmp_integration_delay_reset_timer() {
1628        // This seed value was chosen to later produce a timer duration > 100ms.
1629        let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_igmpv2_test_environment(123456);
1630        assert_eq!(
1631            core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
1632            GroupJoinResult::Joined(())
1633        );
1634        let now = bindings_ctx.now();
1635        core_ctx.state.gmp_state().timers.assert_range([(
1636            &gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(),
1637            now..=(now + IGMP_DEFAULT_UNSOLICITED_REPORT_INTERVAL),
1638        )]);
1639        let instant1 = bindings_ctx.timers.timers()[0].0.clone();
1640        let start = bindings_ctx.now();
1641        let duration = Duration::from_micros(((instant1 - start).as_micros() / 2) as u64);
1642        assert!(duration.as_millis() > 100);
1643        receive_igmp_v2_query(
1644            &mut core_ctx,
1645            &mut bindings_ctx,
1646            NonZeroDuration::new(duration).unwrap(),
1647        );
1648        assert_eq!(core_ctx.frames().len(), 1);
1649        let now = bindings_ctx.now();
1650        core_ctx.state.gmp_state().timers.assert_range([(
1651            &gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(),
1652            now..=(now + duration),
1653        )]);
1654        let instant2 = bindings_ctx.timers.timers()[0].0.clone();
1655        // Because of the message, our timer should be reset to a nearer future.
1656        assert!(instant2 <= instant1);
1657        core_ctx
1658            .state
1659            .gmp_state()
1660            .timers
1661            .assert_top(&gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(), &());
1662        assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(GMP_TIMER_ID));
1663        assert!(bindings_ctx.now() - start <= duration);
1664        assert_eq!(core_ctx.frames().len(), 2);
1665        // Make sure it is a V2 report.
1666        assert_eq!(core_ctx.frames().last().unwrap().1[24], 0x16);
1667        ensure_ttl_ihl_rtr(&core_ctx);
1668
1669        CounterExpectations { rx_igmpv2_query: 1, tx_igmpv2_report: 2, ..Default::default() }
1670            .assert_counters(&mut core_ctx);
1671    }
1672
1673    #[test]
1674    fn test_igmp_integration_last_send_leave() {
1675        run_with_many_seeds(|seed| {
1676            let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_igmpv2_test_environment(seed);
1677            assert_eq!(
1678                core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
1679                GroupJoinResult::Joined(())
1680            );
1681            let now = bindings_ctx.now();
1682            core_ctx.state.gmp_state().timers.assert_range([(
1683                &gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(),
1684                now..=(now + IGMP_DEFAULT_UNSOLICITED_REPORT_INTERVAL),
1685            )]);
1686            // The initial unsolicited report.
1687            assert_eq!(core_ctx.frames().len(), 1);
1688            core_ctx
1689                .state
1690                .gmp_state()
1691                .timers
1692                .assert_top(&gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(), &());
1693            assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(GMP_TIMER_ID));
1694            // The report after the delay.
1695            assert_eq!(core_ctx.frames().len(), 2);
1696            assert_eq!(
1697                core_ctx.gmp_leave_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
1698                GroupLeaveResult::Left(())
1699            );
1700            // Our leave message.
1701            assert_eq!(core_ctx.frames().len(), 3);
1702
1703            let leave_frame = &core_ctx.frames().last().unwrap().1;
1704
1705            // Make sure it is a leave message.
1706            assert_eq!(leave_frame[24], 0x17);
1707            // Make sure the destination is ALL-ROUTERS (224.0.0.2).
1708            assert_eq!(leave_frame[16], 224);
1709            assert_eq!(leave_frame[17], 0);
1710            assert_eq!(leave_frame[18], 0);
1711            assert_eq!(leave_frame[19], 2);
1712            ensure_ttl_ihl_rtr(&core_ctx);
1713
1714            CounterExpectations { tx_igmpv2_report: 2, tx_leave_group: 1, ..Default::default() }
1715                .assert_counters(&mut core_ctx);
1716        });
1717    }
1718
1719    #[test]
1720    fn test_igmp_integration_always_idle_member() {
1721        run_with_many_seeds(|seed| {
1722            let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_igmpv2_test_environment(seed);
1723            assert_eq!(
1724                core_ctx.gmp_join_group(
1725                    &mut bindings_ctx,
1726                    &FakeDeviceId,
1727                    Ipv4::ALL_SYSTEMS_MULTICAST_ADDRESS
1728                ),
1729                GroupJoinResult::Joined(())
1730            );
1731            assert_eq!(core_ctx.frames().len(), 0);
1732            bindings_ctx.timers.assert_no_timers_installed();
1733
1734            CounterExpectations::default().assert_counters(&mut core_ctx);
1735        });
1736    }
1737
1738    #[test]
1739    fn test_igmp_integration_not_last_does_not_send_leave() {
1740        run_with_many_seeds(|seed| {
1741            let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_igmpv2_test_environment(seed);
1742            assert_eq!(
1743                core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
1744                GroupJoinResult::Joined(())
1745            );
1746            let now = bindings_ctx.now();
1747            core_ctx.state.gmp_state().timers.assert_range([(
1748                &gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(),
1749                now..=(now + IGMP_DEFAULT_UNSOLICITED_REPORT_INTERVAL),
1750            )]);
1751            assert_eq!(core_ctx.frames().len(), 1);
1752            receive_igmp_report(&mut core_ctx, &mut bindings_ctx);
1753            bindings_ctx.timers.assert_no_timers_installed();
1754            // The report should be discarded because we have received from
1755            // someone else.
1756            assert_eq!(core_ctx.frames().len(), 1);
1757            assert_eq!(
1758                core_ctx.gmp_leave_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
1759                GroupLeaveResult::Left(())
1760            );
1761            // A leave message is not sent.
1762            assert_eq!(core_ctx.frames().len(), 1);
1763            ensure_ttl_ihl_rtr(&core_ctx);
1764
1765            CounterExpectations { tx_igmpv2_report: 1, rx_igmpv2_report: 1, ..Default::default() }
1766                .assert_counters(&mut core_ctx);
1767        });
1768    }
1769
1770    #[test]
1771    fn test_receive_general_query() {
1772        run_with_many_seeds(|seed| {
1773            let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_igmpv2_test_environment(seed);
1774            assert_eq!(
1775                core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
1776                GroupJoinResult::Joined(())
1777            );
1778            assert_eq!(
1779                core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR_2),
1780                GroupJoinResult::Joined(())
1781            );
1782            let now = bindings_ctx.now();
1783            let range = now..=(now + IGMP_DEFAULT_UNSOLICITED_REPORT_INTERVAL);
1784            core_ctx.state.gmp_state().timers.assert_range([
1785                (&gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(), range.clone()),
1786                (&gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR_2).into(), range),
1787            ]);
1788            // The initial unsolicited report.
1789            assert_eq!(core_ctx.frames().len(), 2);
1790            assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(GMP_TIMER_ID));
1791            assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(GMP_TIMER_ID));
1792            assert_eq!(core_ctx.frames().len(), 4);
1793            const RESP_TIME: NonZeroDuration = NonZeroDuration::from_secs(10).unwrap();
1794            receive_igmp_v2_general_query(&mut core_ctx, &mut bindings_ctx, RESP_TIME);
1795            // Two new timers should be there.
1796            let now = bindings_ctx.now();
1797            let range = now..=(now + RESP_TIME.get());
1798            core_ctx.state.gmp_state().timers.assert_range([
1799                (&gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(), range.clone()),
1800                (&gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR_2).into(), range),
1801            ]);
1802            assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(GMP_TIMER_ID));
1803            assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(GMP_TIMER_ID));
1804            // Two new reports should be sent.
1805            assert_eq!(core_ctx.frames().len(), 6);
1806            ensure_ttl_ihl_rtr(&core_ctx);
1807
1808            CounterExpectations { rx_igmpv2_query: 1, tx_igmpv2_report: 6, ..Default::default() }
1809                .assert_counters(&mut core_ctx);
1810        });
1811    }
1812
1813    #[test]
1814    fn test_skip_igmp() {
1815        run_with_many_seeds(|seed| {
1816            // Test that we do not perform IGMP when IGMP is disabled.
1817
1818            let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_igmpv2_test_environment(seed);
1819            bindings_ctx.seed_rng(seed);
1820            // Test environment is created in enabled state.
1821            core_ctx.state.igmp_enabled = false;
1822            core_ctx.gmp_handle_disabled(&mut bindings_ctx, &FakeDeviceId);
1823
1824            // Assert that no observable effects have taken place.
1825            let assert_no_effect = |core_ctx: &FakeCoreCtx, bindings_ctx: &FakeBindingsCtx| {
1826                bindings_ctx.timers.assert_no_timers_installed();
1827                assert_empty(core_ctx.frames());
1828            };
1829
1830            assert_eq!(
1831                core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
1832                GroupJoinResult::Joined(())
1833            );
1834            // We should join the group but left in the GMP's non-member
1835            // state.
1836            assert_gmp_state!(core_ctx, &GROUP_ADDR, NonMember);
1837            assert_no_effect(&core_ctx, &bindings_ctx);
1838
1839            receive_igmp_report(&mut core_ctx, &mut bindings_ctx);
1840            // We should have done no state transitions/work.
1841            assert_gmp_state!(core_ctx, &GROUP_ADDR, NonMember);
1842            assert_no_effect(&core_ctx, &bindings_ctx);
1843
1844            receive_igmp_v2_query(
1845                &mut core_ctx,
1846                &mut bindings_ctx,
1847                NonZeroDuration::from_secs(10).unwrap(),
1848            );
1849            // We should have done no state transitions/work.
1850            assert_gmp_state!(core_ctx, &GROUP_ADDR, NonMember);
1851            assert_no_effect(&core_ctx, &bindings_ctx);
1852
1853            assert_eq!(
1854                core_ctx.gmp_leave_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
1855                GroupLeaveResult::Left(())
1856            );
1857            // We should have left the group but not executed any `Actions`.
1858            assert!(core_ctx.state.groups().get(&GROUP_ADDR).is_none());
1859            assert_no_effect(&core_ctx, &bindings_ctx);
1860
1861            CounterExpectations { rx_igmpv2_report: 1, rx_igmpv2_query: 1, ..Default::default() }
1862                .assert_counters(&mut core_ctx);
1863        });
1864    }
1865
1866    #[test]
1867    fn test_igmp_integration_with_local_join_leave() {
1868        run_with_many_seeds(|seed| {
1869            // Simple IGMP integration test to check that when we call top-level
1870            // multicast join and leave functions, IGMP is performed.
1871
1872            let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_igmpv2_test_environment(seed);
1873
1874            assert_eq!(
1875                core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
1876                GroupJoinResult::Joined(())
1877            );
1878            assert_gmp_state!(core_ctx, &GROUP_ADDR, Delaying);
1879            assert_eq!(core_ctx.frames().len(), 1);
1880            let now = bindings_ctx.now();
1881            let range = now..=(now + IGMP_DEFAULT_UNSOLICITED_REPORT_INTERVAL);
1882            core_ctx.state.gmp_state().timers.assert_range([(
1883                &gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(),
1884                range.clone(),
1885            )]);
1886            ensure_ttl_ihl_rtr(&core_ctx);
1887
1888            assert_eq!(
1889                core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
1890                GroupJoinResult::AlreadyMember
1891            );
1892            assert_gmp_state!(core_ctx, &GROUP_ADDR, Delaying);
1893            assert_eq!(core_ctx.frames().len(), 1);
1894            core_ctx.state.gmp_state().timers.assert_range([(
1895                &gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(),
1896                range.clone(),
1897            )]);
1898
1899            assert_eq!(
1900                core_ctx.gmp_leave_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
1901                GroupLeaveResult::StillMember
1902            );
1903            assert_gmp_state!(core_ctx, &GROUP_ADDR, Delaying);
1904            assert_eq!(core_ctx.frames().len(), 1);
1905            core_ctx.state.gmp_state().timers.assert_range([(
1906                &gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(),
1907                range,
1908            )]);
1909
1910            assert_eq!(
1911                core_ctx.gmp_leave_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
1912                GroupLeaveResult::Left(())
1913            );
1914            assert_eq!(core_ctx.frames().len(), 2);
1915            bindings_ctx.timers.assert_no_timers_installed();
1916            ensure_ttl_ihl_rtr(&core_ctx);
1917
1918            CounterExpectations { tx_igmpv2_report: 1, tx_leave_group: 1, ..Default::default() }
1919                .assert_counters(&mut core_ctx);
1920        });
1921    }
1922
1923    #[test]
1924    fn test_igmp_enable_disable() {
1925        run_with_many_seeds(|seed| {
1926            let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_igmpv2_test_environment(seed);
1927            assert_eq!(core_ctx.take_frames(), []);
1928
1929            assert_eq!(
1930                core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
1931                GroupJoinResult::Joined(())
1932            );
1933            assert_gmp_state!(core_ctx, &GROUP_ADDR, Delaying);
1934            {
1935                let frames = core_ctx.take_frames();
1936                let (IgmpPacketMetadata { device: FakeDeviceId, dst_ip }, frame) =
1937                    assert_matches!(&frames[..], [x] => x);
1938                assert_eq!(dst_ip, &GROUP_ADDR);
1939                let (body, src_ip, dst_ip, proto, ttl) = parse_ip_packet::<Ipv4>(frame).unwrap();
1940                assert_eq!(src_ip, MY_ADDR.get());
1941                assert_eq!(dst_ip, GROUP_ADDR.get());
1942                assert_eq!(proto, Ipv4Proto::Igmp);
1943                assert_eq!(ttl, IGMP_IP_TTL);
1944                let mut bv = &body[..];
1945                assert_matches!(
1946                    IgmpPacket::parse(&mut bv, ()).unwrap(),
1947                    IgmpPacket::MembershipReportV2(msg) => {
1948                        assert_eq!(msg.group_addr(), GROUP_ADDR.get());
1949                    }
1950                );
1951            }
1952
1953            // Should do nothing.
1954            core_ctx.gmp_handle_maybe_enabled(&mut bindings_ctx, &FakeDeviceId);
1955            assert_gmp_state!(core_ctx, &GROUP_ADDR, Delaying);
1956            assert_eq!(core_ctx.take_frames(), []);
1957
1958            // Should send done message.
1959            core_ctx.state.igmp_enabled = false;
1960            core_ctx.gmp_handle_disabled(&mut bindings_ctx, &FakeDeviceId);
1961            assert_gmp_state!(core_ctx, &GROUP_ADDR, NonMember);
1962            {
1963                let frames = core_ctx.take_frames();
1964                let (IgmpPacketMetadata { device: FakeDeviceId, dst_ip }, frame) =
1965                    assert_matches!(&frames[..], [x] => x);
1966                assert_eq!(dst_ip, &Ipv4::ALL_ROUTERS_MULTICAST_ADDRESS);
1967                let (body, src_ip, dst_ip, proto, ttl) = parse_ip_packet::<Ipv4>(frame).unwrap();
1968                assert_eq!(src_ip, MY_ADDR.get());
1969                assert_eq!(dst_ip, Ipv4::ALL_ROUTERS_MULTICAST_ADDRESS.get());
1970                assert_eq!(proto, Ipv4Proto::Igmp);
1971                assert_eq!(ttl, IGMP_IP_TTL);
1972                let mut bv = &body[..];
1973                assert_matches!(
1974                    IgmpPacket::parse(&mut bv, ()).unwrap(),
1975                    IgmpPacket::LeaveGroup(msg) => {
1976                        assert_eq!(msg.group_addr(), GROUP_ADDR.get());
1977                    }
1978                );
1979            }
1980
1981            // Should do nothing.
1982            core_ctx.gmp_handle_disabled(&mut bindings_ctx, &FakeDeviceId);
1983            assert_gmp_state!(core_ctx, &GROUP_ADDR, NonMember);
1984            assert_eq!(core_ctx.take_frames(), []);
1985
1986            // Should send report message.
1987            core_ctx.state.igmp_enabled = true;
1988            core_ctx.gmp_handle_maybe_enabled(&mut bindings_ctx, &FakeDeviceId);
1989            assert_gmp_state!(core_ctx, &GROUP_ADDR, Delaying);
1990            {
1991                let frames = core_ctx.take_frames();
1992                let (IgmpPacketMetadata { device: FakeDeviceId, dst_ip }, frame) =
1993                    assert_matches!(&frames[..], [x] => x);
1994                assert_eq!(dst_ip, &GROUP_ADDR);
1995                let (body, src_ip, dst_ip, proto, ttl) = parse_ip_packet::<Ipv4>(frame).unwrap();
1996                assert_eq!(src_ip, MY_ADDR.get());
1997                assert_eq!(dst_ip, GROUP_ADDR.get());
1998                assert_eq!(proto, Ipv4Proto::Igmp);
1999                assert_eq!(ttl, IGMP_IP_TTL);
2000                let mut bv = &body[..];
2001                assert_matches!(
2002                    IgmpPacket::parse(&mut bv, ()).unwrap(),
2003                    IgmpPacket::MembershipReportV2(msg) => {
2004                        assert_eq!(msg.group_addr(), GROUP_ADDR.get());
2005                    }
2006                );
2007            }
2008
2009            CounterExpectations { tx_igmpv2_report: 2, tx_leave_group: 1, ..Default::default() }
2010                .assert_counters(&mut core_ctx);
2011        });
2012    }
2013
2014    /// Test the basics of IGMPv3 report sending.
2015    #[test]
2016    fn send_igmpv3_report() {
2017        let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_igmpv2_test_environment(0);
2018        let sent_report_addr = Ipv4::get_multicast_addr(130);
2019        let sent_report_mode = GroupRecordType::ModeIsExclude;
2020        let sent_report_sources = Vec::<Ipv4Addr>::new();
2021        core_ctx.with_gmp_state_mut_and_ctx(&FakeDeviceId, |mut core_ctx, _| {
2022            core_ctx.send_report_v2(
2023                &mut bindings_ctx,
2024                &FakeDeviceId,
2025                [gmp::v2::GroupRecord::new_with_sources(
2026                    GmpEnabledGroup::new(sent_report_addr).unwrap(),
2027                    sent_report_mode,
2028                    sent_report_sources.iter(),
2029                )]
2030                .into_iter(),
2031            );
2032        });
2033
2034        let frames = core_ctx.take_frames();
2035        let (IgmpPacketMetadata { device: FakeDeviceId, dst_ip }, frame) =
2036            assert_matches!(&frames[..], [x] => x);
2037        assert_eq!(dst_ip, &ALL_IGMPV3_CAPABLE_ROUTERS);
2038        let mut buff = &frame[..];
2039        let ipv4 = buff.parse::<Ipv4Packet<_>>().expect("parse IPv4");
2040        assert_eq!(ipv4.ttl(), IGMP_IP_TTL);
2041        assert_eq!(ipv4.src_ip(), MY_ADDR.get());
2042        assert_eq!(ipv4.dst_ip(), ALL_IGMPV3_CAPABLE_ROUTERS.get());
2043        assert_eq!(ipv4.proto(), Ipv4Proto::Igmp);
2044        assert_eq!(ipv4.dscp_and_ecn(), IGMPV3_DSCP_AND_ECN);
2045        assert_eq!(
2046            ipv4.iter_options()
2047                .map(|o| {
2048                    assert_matches!(o, Ipv4Option::RouterAlert { data: 0 });
2049                })
2050                .count(),
2051            1
2052        );
2053        let igmp = buff.parse::<IgmpPacket<_>>().expect("parse IGMP");
2054        let report = assert_matches!(
2055            igmp,
2056            IgmpPacket::MembershipReportV3(report) => report
2057        );
2058        let report = report
2059            .body()
2060            .iter()
2061            .map(|r| {
2062                (
2063                    r.header().multicast_addr().clone(),
2064                    r.header().record_type().unwrap(),
2065                    r.sources().to_vec(),
2066                )
2067            })
2068            .collect::<Vec<_>>();
2069        assert_eq!(report, vec![(sent_report_addr.get(), sent_report_mode, sent_report_sources)]);
2070
2071        CounterExpectations { tx_igmpv3_report: 1, ..Default::default() }
2072            .assert_counters(&mut core_ctx);
2073    }
2074
2075    /// Tests IGMPv3 entering compatibility modes.
2076    #[test]
2077    fn igmpv3_version_compat() {
2078        let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_igmpv2_test_environment(0);
2079        core_ctx.with_gmp_state_mut(&FakeDeviceId, |state| {
2080            gmp::enter_mode(&mut bindings_ctx, state, IgmpMode::V3);
2081        });
2082
2083        for _ in 0..2 {
2084            assert_eq!(
2085                gmp::v1::handle_query_message(
2086                    &mut core_ctx,
2087                    &mut bindings_ctx,
2088                    &FakeDeviceId,
2089                    &gmp::testutil::FakeV1Query {
2090                        group_addr: Ipv4::UNSPECIFIED_ADDRESS,
2091                        max_response_time: Duration::ZERO
2092                    }
2093                ),
2094                Ok(())
2095            );
2096            // We should be in IGMPv1 compat mode and the compat instant is set
2097            // by the IGMPv3 RFC definition.
2098            let (gmp_state, config) = core_ctx.state.gmp_state_and_config();
2099            let until = bindings_ctx.now().panicking_add(
2100                gmp_state.v2_proto.older_version_querier_present_timeout(config).get(),
2101            );
2102            assert_eq!(gmp_state.mode, IgmpMode::V1(IgmpV1Mode::V3Compat { until }));
2103            assert_eq!(gmp_state.mode.should_send_v1(&mut bindings_ctx), true);
2104            bindings_ctx.timers.instant.sleep(Duration::from_secs(2));
2105        }
2106
2107        let (v2_deadline, ()) =
2108            core_ctx.state.gmp_state().timers.get(&gmp::TimerIdInner::V1Compat).unwrap();
2109        let prev_mode = core_ctx.state.gmp_state().mode;
2110        // Now receive an IGMPv2 query. The IGMPv1 compat deadline should not
2111        // move.
2112        assert_eq!(
2113            gmp::v1::handle_query_message(
2114                &mut core_ctx,
2115                &mut bindings_ctx,
2116                &FakeDeviceId,
2117                &gmp::testutil::FakeV1Query {
2118                    group_addr: Ipv4::UNSPECIFIED_ADDRESS,
2119                    max_response_time: Duration::from_secs(10)
2120                }
2121            ),
2122            Ok(())
2123        );
2124        assert_eq!(core_ctx.state.gmp_state().mode, prev_mode);
2125        assert_ne!(
2126            core_ctx.state.gmp_state().timers.get(&gmp::TimerIdInner::V1Compat).unwrap(),
2127            (v2_deadline, &())
2128        );
2129        // We haven't moved timers so we should still be sending IGMPv1 responses.
2130        assert_eq!(core_ctx.state.gmp_state().mode.should_send_v1(&mut bindings_ctx), true);
2131
2132        // Triggering the next timer should clear IGMPv2 compat back into IGMPv3
2133        // and we update the compat mode.
2134        assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(GMP_TIMER_ID));
2135        assert_eq!(core_ctx.state.gmp_state().mode, IgmpMode::V3);
2136        assert_eq!(core_ctx.state.gmp_state().mode.should_send_v1(&mut bindings_ctx), false);
2137    }
2138
2139    #[test]
2140    fn version_compat_clears_on_disable() {
2141        let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_igmpv2_test_environment(0);
2142        assert_eq!(core_ctx.state.gmp_state().mode, IgmpMode::V2 { compat: false });
2143        assert_eq!(
2144            gmp::v1::handle_query_message(
2145                &mut core_ctx,
2146                &mut bindings_ctx,
2147                &FakeDeviceId,
2148                &gmp::testutil::FakeV1Query {
2149                    group_addr: Ipv4::UNSPECIFIED_ADDRESS,
2150                    max_response_time: Duration::ZERO
2151                }
2152            ),
2153            Ok(())
2154        );
2155        assert_matches!(core_ctx.state.gmp_state().mode, IgmpMode::V1(IgmpV1Mode::V2Compat { .. }));
2156        core_ctx.state.igmp_enabled = false;
2157        core_ctx.gmp_handle_disabled(&mut bindings_ctx, &FakeDeviceId);
2158        assert_eq!(core_ctx.state.gmp_state().mode, IgmpMode::V2 { compat: false });
2159    }
2160
2161    #[test]
2162    fn user_mode_change() {
2163        let mut ctx = setup_igmpv2_test_environment(0);
2164        let FakeCtx { core_ctx, bindings_ctx } = &mut ctx;
2165        assert_eq!(core_ctx.gmp_get_mode(&FakeDeviceId), IgmpConfigMode::V2);
2166        assert_eq!(
2167            core_ctx.gmp_join_group(bindings_ctx, &FakeDeviceId, GROUP_ADDR),
2168            GroupJoinResult::Joined(())
2169        );
2170        // Ignore first reports.
2171        let _ = core_ctx.take_frames();
2172        assert_eq!(
2173            core_ctx.gmp_set_mode(bindings_ctx, &FakeDeviceId, IgmpConfigMode::V3),
2174            IgmpConfigMode::V2
2175        );
2176        assert_eq!(core_ctx.gmp_get_mode(&FakeDeviceId), IgmpConfigMode::V3);
2177        assert_eq!(core_ctx.state.gmp_state().mode, IgmpMode::V3);
2178        // No side-effects.
2179        assert_eq!(core_ctx.take_frames(), Vec::new());
2180
2181        // If we receive an IGMPv1 query, we'll go into compat mode but still
2182        // report IGMPv3 to the user.
2183        receive_igmp_v1_query(core_ctx, bindings_ctx);
2184        assert_matches!(core_ctx.state.gmp_state().mode, IgmpMode::V1(IgmpV1Mode::V3Compat { .. }));
2185        assert_eq!(core_ctx.gmp_get_mode(&FakeDeviceId), IgmpConfigMode::V3);
2186
2187        // Acknowledge query response.
2188        assert_eq!(bindings_ctx.trigger_next_timer(core_ctx), Some(GMP_TIMER_ID));
2189        assert_eq!(core_ctx.take_frames().len(), 1);
2190
2191        // Even if user attempts to set IGMPv3 again we'll keep it in compat.
2192        assert_eq!(
2193            core_ctx.gmp_set_mode(bindings_ctx, &FakeDeviceId, IgmpConfigMode::V3),
2194            IgmpConfigMode::V3
2195        );
2196        assert_eq!(core_ctx.take_frames(), Vec::new());
2197        let until = assert_matches!(
2198            core_ctx.state.gmp_state().mode,
2199            IgmpMode::V1(IgmpV1Mode::V3Compat { until }) => until
2200        );
2201        // If the user switches to IGMPv2, we keep the IGMPv1 compat information.
2202        assert_eq!(
2203            core_ctx.gmp_set_mode(bindings_ctx, &FakeDeviceId, IgmpConfigMode::V2),
2204            IgmpConfigMode::V3
2205        );
2206        assert_eq!(core_ctx.take_frames(), Vec::new());
2207        assert_eq!(core_ctx.state.gmp_state().mode, IgmpMode::V1(IgmpV1Mode::V2Compat { until }));
2208
2209        // Forcing IGMPv1 mode, however, exits compat.
2210        assert_eq!(
2211            core_ctx.gmp_set_mode(bindings_ctx, &FakeDeviceId, IgmpConfigMode::V1),
2212            IgmpConfigMode::V2
2213        );
2214        assert_eq!(core_ctx.take_frames(), Vec::new());
2215        assert_eq!(core_ctx.state.gmp_state().mode, IgmpMode::V1(IgmpV1Mode::Forced));
2216
2217        // Back to IGMPv2...
2218        assert_eq!(
2219            core_ctx.gmp_set_mode(bindings_ctx, &FakeDeviceId, IgmpConfigMode::V2),
2220            IgmpConfigMode::V1
2221        );
2222        assert_eq!(core_ctx.state.gmp_state().mode, IgmpMode::V2 { compat: false });
2223        // ...and then IGMPv3.
2224        assert_eq!(
2225            core_ctx.gmp_set_mode(bindings_ctx, &FakeDeviceId, IgmpConfigMode::V3),
2226            IgmpConfigMode::V2
2227        );
2228        assert_eq!(core_ctx.state.gmp_state().mode, IgmpMode::V3);
2229        assert_eq!(core_ctx.take_frames(), Vec::new());
2230
2231        // Receiving an IGMPv2 query while in IGMPv3 enters compat.
2232        receive_igmp_v2_query(core_ctx, bindings_ctx, NonZeroDuration::from_secs(1).unwrap());
2233        assert_eq!(core_ctx.state.gmp_state().mode, IgmpMode::V2 { compat: true });
2234        assert_eq!(core_ctx.gmp_get_mode(&FakeDeviceId), IgmpConfigMode::V3);
2235        // Acknowledge query response.
2236        assert_eq!(bindings_ctx.trigger_next_timer(core_ctx), Some(GMP_TIMER_ID));
2237        assert_eq!(core_ctx.take_frames().len(), 1);
2238
2239        // Even if user attempts to set IGMPv3 again we'll keep it in compat.
2240        assert_eq!(
2241            core_ctx.gmp_set_mode(bindings_ctx, &FakeDeviceId, IgmpConfigMode::V3),
2242            IgmpConfigMode::V3
2243        );
2244        assert_eq!(core_ctx.take_frames(), Vec::new());
2245        assert_eq!(core_ctx.state.gmp_state().mode, IgmpMode::V2 { compat: true });
2246
2247        // Force IGMPv2 exits IGMPv2 compat.
2248        assert_eq!(
2249            core_ctx.gmp_set_mode(bindings_ctx, &FakeDeviceId, IgmpConfigMode::V2),
2250            IgmpConfigMode::V3
2251        );
2252        assert_eq!(core_ctx.state.gmp_state().mode, IgmpMode::V2 { compat: false });
2253        assert_eq!(core_ctx.take_frames(), Vec::new());
2254        // No timers.
2255        core_ctx.state.gmp_state().timers.assert_timers([]);
2256    }
2257
2258    #[test]
2259    fn reject_bad_messages() {
2260        let mut ctx = setup_igmpv2_test_environment(0);
2261        let FakeCtx { core_ctx, bindings_ctx } = &mut ctx;
2262
2263        let v1_query = {
2264            let ser = IgmpPacketBuilder::<Buf<Vec<u8>>, IgmpMembershipQueryV2>::new_with_resp_time(
2265                Ipv4::UNSPECIFIED_ADDRESS,
2266                Duration::ZERO.try_into().unwrap(),
2267            );
2268            ser.into_serializer().serialize_vec_outer().unwrap()
2269        };
2270        let v2_query = {
2271            let ser = IgmpPacketBuilder::<Buf<Vec<u8>>, IgmpMembershipQueryV2>::new_with_resp_time(
2272                Ipv4::UNSPECIFIED_ADDRESS,
2273                Duration::from_secs(10).try_into().unwrap(),
2274            );
2275            ser.into_serializer().serialize_vec_outer().unwrap()
2276        };
2277        let v3_query = {
2278            let ser = IgmpMembershipQueryV3Builder::new(
2279                IgmpResponseTimeV3::new_exact(Duration::from_secs(1)).unwrap(),
2280                None,
2281                false,
2282                Igmpv3QRV::new(2),
2283                Igmpv3QQIC::new_exact(Duration::from_secs(125)).unwrap(),
2284                core::iter::empty(),
2285            );
2286            ser.into_serializer().serialize_vec_outer().unwrap()
2287        };
2288
2289        let base_header_info = new_recv_pkt_info().header_info;
2290
2291        // TTL must be 1.
2292        const BAD_TTL: u8 = 2;
2293        for q in [&v1_query, &v2_query, &v3_query] {
2294            assert_eq!(
2295                receive_igmp_packet(
2296                    core_ctx,
2297                    bindings_ctx,
2298                    &FakeDeviceId,
2299                    Ipv4::ALL_SYSTEMS_MULTICAST_ADDRESS.into(),
2300                    q.clone(),
2301                    &LocalDeliveryPacketInfo {
2302                        header_info: FakeIpHeaderInfo { hop_limit: BAD_TTL, ..base_header_info },
2303                        ..Default::default()
2304                    }
2305                ),
2306                Err(IgmpError::BadTtl(BAD_TTL))
2307            );
2308        }
2309
2310        // Multicast destination IPs must be all nodes.
2311        const BAD_DST_IP: MulticastAddr<Ipv4Addr> = GROUP_ADDR;
2312        for q in [&v1_query, &v2_query, &v3_query] {
2313            assert_eq!(
2314                receive_igmp_packet(
2315                    core_ctx,
2316                    bindings_ctx,
2317                    &FakeDeviceId,
2318                    BAD_DST_IP.into(),
2319                    q.clone(),
2320                    &new_recv_pkt_info(),
2321                ),
2322                Err(IgmpError::RejectedGeneralQuery { dst_ip: BAD_DST_IP.get() })
2323            );
2324        }
2325
2326        for q in [&v2_query, &v3_query] {
2327            assert_eq!(
2328                receive_igmp_packet(
2329                    core_ctx,
2330                    bindings_ctx,
2331                    &FakeDeviceId,
2332                    MY_ADDR,
2333                    q.clone(),
2334                    &LocalDeliveryPacketInfo {
2335                        header_info: FakeIpHeaderInfo {
2336                            // Router alert must be set.
2337                            router_alert: false,
2338                            ..base_header_info
2339                        },
2340                        ..Default::default()
2341                    },
2342                ),
2343                Err(IgmpError::MissingRouterAlertInQuery)
2344            );
2345        }
2346
2347        CounterExpectations {
2348            // Note: Queries with a Bad TTL aren't accounted for here.
2349            rx_igmpv1_query: 1,
2350            rx_igmpv2_query: 2,
2351            rx_igmpv3_query: 2,
2352            rx_err_rejected_general_query: 3,
2353            rx_err_missing_router_alert_in_query: 2,
2354            rx_err_bad_ttl: 3,
2355            ..Default::default()
2356        }
2357        .assert_counters(core_ctx);
2358    }
2359}