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