Skip to main content

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, NestablePacketBuilder as _};
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        NetworkSerializationContext, NetworkSerializer, 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: NetworkSerializer,
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
1295            .into_serializer()
1296            .serialize_vec_outer(&mut NetworkSerializationContext::default())
1297            .unwrap();
1298        core_ctx.receive_igmp_packet(
1299            bindings_ctx,
1300            &FakeDeviceId,
1301            Ipv4SourceAddr::new(ROUTER_ADDR).unwrap(),
1302            MY_ADDR,
1303            buff,
1304            &new_recv_pkt_info(),
1305        );
1306    }
1307
1308    fn receive_igmp_v2_query(
1309        core_ctx: &mut FakeCoreCtx,
1310        bindings_ctx: &mut FakeBindingsCtx,
1311        resp_time: NonZeroDuration,
1312    ) {
1313        let ser = IgmpPacketBuilder::<Buf<Vec<u8>>, IgmpMembershipQueryV2>::new_with_resp_time(
1314            GROUP_ADDR.get(),
1315            resp_time.get().try_into().unwrap(),
1316        );
1317        let buff = ser
1318            .into_serializer()
1319            .serialize_vec_outer(&mut NetworkSerializationContext::default())
1320            .unwrap();
1321        core_ctx.receive_igmp_packet(
1322            bindings_ctx,
1323            &FakeDeviceId,
1324            Ipv4SourceAddr::new(ROUTER_ADDR).unwrap(),
1325            MY_ADDR,
1326            buff,
1327            &new_recv_pkt_info(),
1328        );
1329    }
1330
1331    fn receive_igmp_v2_general_query(
1332        core_ctx: &mut FakeCoreCtx,
1333        bindings_ctx: &mut FakeBindingsCtx,
1334        resp_time: NonZeroDuration,
1335    ) {
1336        let ser = IgmpPacketBuilder::<Buf<Vec<u8>>, IgmpMembershipQueryV2>::new_with_resp_time(
1337            Ipv4Addr::new([0, 0, 0, 0]),
1338            resp_time.get().try_into().unwrap(),
1339        );
1340        let buff = ser
1341            .into_serializer()
1342            .serialize_vec_outer(&mut NetworkSerializationContext::default())
1343            .unwrap();
1344        core_ctx.receive_igmp_packet(
1345            bindings_ctx,
1346            &FakeDeviceId,
1347            Ipv4SourceAddr::new(ROUTER_ADDR).unwrap(),
1348            MY_ADDR,
1349            buff,
1350            &new_recv_pkt_info(),
1351        );
1352    }
1353
1354    fn receive_igmp_report(core_ctx: &mut FakeCoreCtx, bindings_ctx: &mut FakeBindingsCtx) {
1355        let ser = IgmpPacketBuilder::<Buf<Vec<u8>>, IgmpMembershipReportV2>::new(GROUP_ADDR.get());
1356        let buff = ser
1357            .into_serializer()
1358            .serialize_vec_outer(&mut NetworkSerializationContext::default())
1359            .unwrap();
1360        core_ctx.receive_igmp_packet(
1361            bindings_ctx,
1362            &FakeDeviceId,
1363            Ipv4SourceAddr::new(OTHER_HOST_ADDR).unwrap(),
1364            MY_ADDR,
1365            buff,
1366            &new_recv_pkt_info(),
1367        );
1368    }
1369
1370    fn setup_igmpv2_test_environment_with_addr_subnet(
1371        seed: u128,
1372        a: Option<AddrSubnet<Ipv4Addr, Ipv4DeviceAddr>>,
1373    ) -> FakeCtx {
1374        let mut ctx = FakeCtx::with_default_bindings_ctx(|bindings_ctx| {
1375            // We start with enabled true to make tests easier to write.
1376            let igmp_enabled = true;
1377            FakeCoreCtx::with_state(FakeIgmpCtx {
1378                shared: Rc::new(RefCell::new(Shared {
1379                    groups: MulticastGroupSet::default(),
1380                    gmp_state: GmpState::new_with_enabled_and_mode::<_, IntoCoreTimerCtx>(
1381                        bindings_ctx,
1382                        FakeWeakDeviceId(FakeDeviceId),
1383                        igmp_enabled,
1384                        IgmpMode::V2 { compat: false },
1385                    ),
1386                    config: Default::default(),
1387                })),
1388                igmp_enabled,
1389                addr_subnet: None,
1390                stack_wide_counters: Default::default(),
1391                device_specific_counters: Default::default(),
1392            })
1393        });
1394        ctx.bindings_ctx.seed_rng(seed);
1395        ctx.core_ctx.state.addr_subnet = a;
1396        ctx
1397    }
1398
1399    fn setup_igmpv2_test_environment(seed: u128) -> FakeCtx {
1400        setup_igmpv2_test_environment_with_addr_subnet(
1401            seed,
1402            Some(AddrSubnet::new(MY_ADDR.get(), 24).unwrap()),
1403        )
1404    }
1405
1406    fn ensure_ttl_ihl_rtr(core_ctx: &FakeCoreCtx) {
1407        for (_, frame) in core_ctx.frames() {
1408            assert_eq!(frame[8], IGMP_IP_TTL); // TTL,
1409            assert_eq!(&frame[20..24], &[148, 4, 0, 0]); // RTR
1410            assert_eq!(frame[0], 0x46); // IHL
1411        }
1412    }
1413
1414    #[test_case(Some(MY_ADDR); "specified_src")]
1415    #[test_case(None; "unspecified_src")]
1416    fn test_igmp_simple_integration(src_ip: Option<SpecifiedAddr<Ipv4Addr>>) {
1417        let check_report = |core_ctx: &mut FakeCoreCtx| {
1418            let expected_src_ip = src_ip.map_or(Ipv4::UNSPECIFIED_ADDRESS, |a| a.get());
1419
1420            let frames = core_ctx.take_frames();
1421            let (IgmpPacketMetadata { device: FakeDeviceId, dst_ip }, frame) = assert_matches!(
1422                &frames[..], [x] => x);
1423            assert_eq!(dst_ip, &GROUP_ADDR);
1424            let (body, src_ip, dst_ip, proto, ttl) = parse_ip_packet::<Ipv4>(frame).unwrap();
1425            assert_eq!(src_ip, expected_src_ip);
1426            assert_eq!(dst_ip, GROUP_ADDR.get());
1427            assert_eq!(proto, Ipv4Proto::Igmp);
1428            assert_eq!(ttl, IGMP_IP_TTL);
1429            let mut bv = &body[..];
1430            assert_matches!(
1431                IgmpPacket::parse(&mut bv, ()).unwrap(),
1432                IgmpPacket::MembershipReportV2(msg) => {
1433                    assert_eq!(msg.group_addr(), GROUP_ADDR.get());
1434                }
1435            );
1436        };
1437
1438        let addr_subnet = src_ip.map(|a| AddrSubnet::new(a.get(), 16).unwrap());
1439        run_with_many_seeds(|seed| {
1440            let FakeCtx { mut core_ctx, mut bindings_ctx } =
1441                setup_igmpv2_test_environment_with_addr_subnet(seed, addr_subnet);
1442
1443            // Joining a group should send a report.
1444            assert_eq!(
1445                core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
1446                GroupJoinResult::Joined(())
1447            );
1448            check_report(&mut core_ctx);
1449
1450            // Should send a report after a query.
1451            receive_igmp_v2_query(
1452                &mut core_ctx,
1453                &mut bindings_ctx,
1454                NonZeroDuration::from_secs(10).unwrap(),
1455            );
1456            core_ctx
1457                .state
1458                .gmp_state()
1459                .timers
1460                .assert_top(&gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(), &());
1461            assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(GMP_TIMER_ID));
1462            check_report(&mut core_ctx);
1463
1464            CounterExpectations { rx_igmpv2_query: 1, tx_igmpv2_report: 2, ..Default::default() }
1465                .assert_counters(&mut core_ctx);
1466        });
1467    }
1468
1469    #[test]
1470    fn test_igmp_integration_fallback_from_idle() {
1471        run_with_many_seeds(|seed| {
1472            let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_igmpv2_test_environment(seed);
1473            assert_eq!(
1474                core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
1475                GroupJoinResult::Joined(())
1476            );
1477            assert_eq!(core_ctx.frames().len(), 1);
1478
1479            core_ctx
1480                .state
1481                .gmp_state()
1482                .timers
1483                .assert_top(&gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(), &());
1484            assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(GMP_TIMER_ID));
1485            assert_eq!(core_ctx.frames().len(), 2);
1486
1487            receive_igmp_v2_query(
1488                &mut core_ctx,
1489                &mut bindings_ctx,
1490                NonZeroDuration::from_secs(10).unwrap(),
1491            );
1492
1493            // We have received a query, hence we are falling back to Delay
1494            // Member state.
1495            let group_state = core_ctx.state.groups().get(&GROUP_ADDR).unwrap().v1();
1496            match group_state.get_inner() {
1497                gmp::v1::MemberState::Delaying(_) => {}
1498                _ => panic!("Wrong State!"),
1499            }
1500
1501            core_ctx
1502                .state
1503                .gmp_state()
1504                .timers
1505                .assert_top(&gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(), &());
1506            assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(GMP_TIMER_ID));
1507            assert_eq!(core_ctx.frames().len(), 3);
1508            ensure_ttl_ihl_rtr(&core_ctx);
1509
1510            CounterExpectations { rx_igmpv2_query: 1, tx_igmpv2_report: 3, ..Default::default() }
1511                .assert_counters(&mut core_ctx);
1512        });
1513    }
1514
1515    #[test]
1516    fn test_igmpv2_integration_igmpv1_router_present() {
1517        run_with_many_seeds(|seed| {
1518            let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_igmpv2_test_environment(seed);
1519
1520            assert_eq!(core_ctx.state.gmp_state().mode, IgmpMode::V2 { compat: false });
1521            assert_eq!(
1522                core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
1523                GroupJoinResult::Joined(())
1524            );
1525            let now = bindings_ctx.now();
1526            core_ctx.state.gmp_state().timers.assert_range([(
1527                &gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(),
1528                now..=(now + IGMP_DEFAULT_UNSOLICITED_REPORT_INTERVAL),
1529            )]);
1530
1531            receive_igmp_v1_query(&mut core_ctx, &mut bindings_ctx);
1532            assert_eq!(core_ctx.frames().len(), 1);
1533
1534            // Since we have heard from the v1 router, we should have set our
1535            // flag.
1536            let now = bindings_ctx.now();
1537            let until = now.panicking_add(DEFAULT_V1_ROUTER_PRESENT_TIMEOUT);
1538            assert_eq!(
1539                core_ctx.state.gmp_state().mode,
1540                IgmpMode::V1(IgmpV1Mode::V2Compat { until })
1541            );
1542            assert_eq!(core_ctx.state.gmp_state().mode.should_send_v1(&mut bindings_ctx), true);
1543            assert_eq!(core_ctx.frames().len(), 1);
1544            core_ctx.state.gmp_state().timers.assert_range([(
1545                &gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(),
1546                now..=(now + IGMP_DEFAULT_UNSOLICITED_REPORT_INTERVAL),
1547            )]);
1548            bindings_ctx.timers.assert_timers_installed_range([(
1549                GMP_TIMER_ID,
1550                now..=(now + IGMP_DEFAULT_UNSOLICITED_REPORT_INTERVAL),
1551            )]);
1552
1553            core_ctx
1554                .state
1555                .gmp_state()
1556                .timers
1557                .assert_top(&gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(), &());
1558            assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(GMP_TIMER_ID));
1559            // After the first timer, we send out our V1 report.
1560            assert_eq!(core_ctx.frames().len(), 2);
1561            // The last frame being sent should be a V1 report.
1562            let (_, frame) = core_ctx.frames().last().unwrap();
1563            // 34 and 0x12 are hacky but they can quickly tell it is a V1
1564            // report.
1565            assert_eq!(frame[24], 0x12);
1566
1567            // Sleep until v1 router present is no more.
1568            bindings_ctx.timers.instant.time = until;
1569
1570            // After the elapsed time, we should reset our flag for v1 routers.
1571            assert_eq!(core_ctx.state.gmp_state().mode.should_send_v1(&mut bindings_ctx), false);
1572
1573            receive_igmp_v2_query(
1574                &mut core_ctx,
1575                &mut bindings_ctx,
1576                NonZeroDuration::from_secs(10).unwrap(),
1577            );
1578            core_ctx
1579                .state
1580                .gmp_state()
1581                .timers
1582                .assert_top(&gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(), &());
1583            assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(GMP_TIMER_ID));
1584            assert_eq!(core_ctx.frames().len(), 3);
1585            // Now we should get V2 report
1586            assert_eq!(core_ctx.frames().last().unwrap().1[24], 0x16);
1587            ensure_ttl_ihl_rtr(&core_ctx);
1588
1589            CounterExpectations {
1590                rx_igmpv1_query: 1,
1591                rx_igmpv2_query: 1,
1592                tx_igmpv2_report: 2,
1593                tx_igmpv1_report: 1,
1594                ..Default::default()
1595            }
1596            .assert_counters(&mut core_ctx);
1597        });
1598    }
1599
1600    #[test]
1601    fn test_igmp_integration_delay_reset_timer() {
1602        // This seed value was chosen to later produce a timer duration > 100ms.
1603        let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_igmpv2_test_environment(123456);
1604        assert_eq!(
1605            core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
1606            GroupJoinResult::Joined(())
1607        );
1608        let now = bindings_ctx.now();
1609        core_ctx.state.gmp_state().timers.assert_range([(
1610            &gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(),
1611            now..=(now + IGMP_DEFAULT_UNSOLICITED_REPORT_INTERVAL),
1612        )]);
1613        let instant1 = bindings_ctx.timers.timers()[0].0.clone();
1614        let start = bindings_ctx.now();
1615        let duration = Duration::from_micros(((instant1 - start).as_micros() / 2) as u64);
1616        assert!(duration.as_millis() > 100);
1617        receive_igmp_v2_query(
1618            &mut core_ctx,
1619            &mut bindings_ctx,
1620            NonZeroDuration::new(duration).unwrap(),
1621        );
1622        assert_eq!(core_ctx.frames().len(), 1);
1623        let now = bindings_ctx.now();
1624        core_ctx.state.gmp_state().timers.assert_range([(
1625            &gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(),
1626            now..=(now + duration),
1627        )]);
1628        let instant2 = bindings_ctx.timers.timers()[0].0.clone();
1629        // Because of the message, our timer should be reset to a nearer future.
1630        assert!(instant2 <= instant1);
1631        core_ctx
1632            .state
1633            .gmp_state()
1634            .timers
1635            .assert_top(&gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(), &());
1636        assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(GMP_TIMER_ID));
1637        assert!(bindings_ctx.now() - start <= duration);
1638        assert_eq!(core_ctx.frames().len(), 2);
1639        // Make sure it is a V2 report.
1640        assert_eq!(core_ctx.frames().last().unwrap().1[24], 0x16);
1641        ensure_ttl_ihl_rtr(&core_ctx);
1642
1643        CounterExpectations { rx_igmpv2_query: 1, tx_igmpv2_report: 2, ..Default::default() }
1644            .assert_counters(&mut core_ctx);
1645    }
1646
1647    #[test]
1648    fn test_igmp_integration_last_send_leave() {
1649        run_with_many_seeds(|seed| {
1650            let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_igmpv2_test_environment(seed);
1651            assert_eq!(
1652                core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
1653                GroupJoinResult::Joined(())
1654            );
1655            let now = bindings_ctx.now();
1656            core_ctx.state.gmp_state().timers.assert_range([(
1657                &gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(),
1658                now..=(now + IGMP_DEFAULT_UNSOLICITED_REPORT_INTERVAL),
1659            )]);
1660            // The initial unsolicited report.
1661            assert_eq!(core_ctx.frames().len(), 1);
1662            core_ctx
1663                .state
1664                .gmp_state()
1665                .timers
1666                .assert_top(&gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(), &());
1667            assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(GMP_TIMER_ID));
1668            // The report after the delay.
1669            assert_eq!(core_ctx.frames().len(), 2);
1670            assert_eq!(
1671                core_ctx.gmp_leave_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
1672                GroupLeaveResult::Left(())
1673            );
1674            // Our leave message.
1675            assert_eq!(core_ctx.frames().len(), 3);
1676
1677            let leave_frame = &core_ctx.frames().last().unwrap().1;
1678
1679            // Make sure it is a leave message.
1680            assert_eq!(leave_frame[24], 0x17);
1681            // Make sure the destination is ALL-ROUTERS (224.0.0.2).
1682            assert_eq!(leave_frame[16], 224);
1683            assert_eq!(leave_frame[17], 0);
1684            assert_eq!(leave_frame[18], 0);
1685            assert_eq!(leave_frame[19], 2);
1686            ensure_ttl_ihl_rtr(&core_ctx);
1687
1688            CounterExpectations { tx_igmpv2_report: 2, tx_leave_group: 1, ..Default::default() }
1689                .assert_counters(&mut core_ctx);
1690        });
1691    }
1692
1693    #[test]
1694    fn test_igmp_integration_always_idle_member() {
1695        run_with_many_seeds(|seed| {
1696            let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_igmpv2_test_environment(seed);
1697            assert_eq!(
1698                core_ctx.gmp_join_group(
1699                    &mut bindings_ctx,
1700                    &FakeDeviceId,
1701                    Ipv4::ALL_SYSTEMS_MULTICAST_ADDRESS
1702                ),
1703                GroupJoinResult::Joined(())
1704            );
1705            assert_eq!(core_ctx.frames().len(), 0);
1706            bindings_ctx.timers.assert_no_timers_installed();
1707
1708            CounterExpectations::default().assert_counters(&mut core_ctx);
1709        });
1710    }
1711
1712    #[test]
1713    fn test_igmp_integration_not_last_does_not_send_leave() {
1714        run_with_many_seeds(|seed| {
1715            let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_igmpv2_test_environment(seed);
1716            assert_eq!(
1717                core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
1718                GroupJoinResult::Joined(())
1719            );
1720            let now = bindings_ctx.now();
1721            core_ctx.state.gmp_state().timers.assert_range([(
1722                &gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(),
1723                now..=(now + IGMP_DEFAULT_UNSOLICITED_REPORT_INTERVAL),
1724            )]);
1725            assert_eq!(core_ctx.frames().len(), 1);
1726            receive_igmp_report(&mut core_ctx, &mut bindings_ctx);
1727            bindings_ctx.timers.assert_no_timers_installed();
1728            // The report should be discarded because we have received from
1729            // someone else.
1730            assert_eq!(core_ctx.frames().len(), 1);
1731            assert_eq!(
1732                core_ctx.gmp_leave_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
1733                GroupLeaveResult::Left(())
1734            );
1735            // A leave message is not sent.
1736            assert_eq!(core_ctx.frames().len(), 1);
1737            ensure_ttl_ihl_rtr(&core_ctx);
1738
1739            CounterExpectations { tx_igmpv2_report: 1, rx_igmpv2_report: 1, ..Default::default() }
1740                .assert_counters(&mut core_ctx);
1741        });
1742    }
1743
1744    #[test]
1745    fn test_receive_general_query() {
1746        run_with_many_seeds(|seed| {
1747            let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_igmpv2_test_environment(seed);
1748            assert_eq!(
1749                core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
1750                GroupJoinResult::Joined(())
1751            );
1752            assert_eq!(
1753                core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR_2),
1754                GroupJoinResult::Joined(())
1755            );
1756            let now = bindings_ctx.now();
1757            let range = now..=(now + IGMP_DEFAULT_UNSOLICITED_REPORT_INTERVAL);
1758            core_ctx.state.gmp_state().timers.assert_range([
1759                (&gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(), range.clone()),
1760                (&gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR_2).into(), range),
1761            ]);
1762            // The initial unsolicited report.
1763            assert_eq!(core_ctx.frames().len(), 2);
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            assert_eq!(core_ctx.frames().len(), 4);
1767            const RESP_TIME: NonZeroDuration = NonZeroDuration::from_secs(10).unwrap();
1768            receive_igmp_v2_general_query(&mut core_ctx, &mut bindings_ctx, RESP_TIME);
1769            // Two new timers should be there.
1770            let now = bindings_ctx.now();
1771            let range = now..=(now + RESP_TIME.get());
1772            core_ctx.state.gmp_state().timers.assert_range([
1773                (&gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(), range.clone()),
1774                (&gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR_2).into(), range),
1775            ]);
1776            assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(GMP_TIMER_ID));
1777            assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(GMP_TIMER_ID));
1778            // Two new reports should be sent.
1779            assert_eq!(core_ctx.frames().len(), 6);
1780            ensure_ttl_ihl_rtr(&core_ctx);
1781
1782            CounterExpectations { rx_igmpv2_query: 1, tx_igmpv2_report: 6, ..Default::default() }
1783                .assert_counters(&mut core_ctx);
1784        });
1785    }
1786
1787    #[test]
1788    fn test_skip_igmp() {
1789        run_with_many_seeds(|seed| {
1790            // Test that we do not perform IGMP when IGMP is disabled.
1791
1792            let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_igmpv2_test_environment(seed);
1793            bindings_ctx.seed_rng(seed);
1794            // Test environment is created in enabled state.
1795            core_ctx.state.igmp_enabled = false;
1796            core_ctx.gmp_handle_disabled(&mut bindings_ctx, &FakeDeviceId);
1797
1798            // Assert that no observable effects have taken place.
1799            let assert_no_effect = |core_ctx: &FakeCoreCtx, bindings_ctx: &FakeBindingsCtx| {
1800                bindings_ctx.timers.assert_no_timers_installed();
1801                assert_empty(core_ctx.frames());
1802            };
1803
1804            assert_eq!(
1805                core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
1806                GroupJoinResult::Joined(())
1807            );
1808            // We should join the group but left in the GMP's non-member
1809            // state.
1810            assert_gmp_state!(core_ctx, &GROUP_ADDR, NonMember);
1811            assert_no_effect(&core_ctx, &bindings_ctx);
1812
1813            receive_igmp_report(&mut core_ctx, &mut bindings_ctx);
1814            // We should have done no state transitions/work.
1815            assert_gmp_state!(core_ctx, &GROUP_ADDR, NonMember);
1816            assert_no_effect(&core_ctx, &bindings_ctx);
1817
1818            receive_igmp_v2_query(
1819                &mut core_ctx,
1820                &mut bindings_ctx,
1821                NonZeroDuration::from_secs(10).unwrap(),
1822            );
1823            // We should have done no state transitions/work.
1824            assert_gmp_state!(core_ctx, &GROUP_ADDR, NonMember);
1825            assert_no_effect(&core_ctx, &bindings_ctx);
1826
1827            assert_eq!(
1828                core_ctx.gmp_leave_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
1829                GroupLeaveResult::Left(())
1830            );
1831            // We should have left the group but not executed any `Actions`.
1832            assert!(core_ctx.state.groups().get(&GROUP_ADDR).is_none());
1833            assert_no_effect(&core_ctx, &bindings_ctx);
1834
1835            CounterExpectations { rx_igmpv2_report: 1, rx_igmpv2_query: 1, ..Default::default() }
1836                .assert_counters(&mut core_ctx);
1837        });
1838    }
1839
1840    #[test]
1841    fn test_igmp_integration_with_local_join_leave() {
1842        run_with_many_seeds(|seed| {
1843            // Simple IGMP integration test to check that when we call top-level
1844            // multicast join and leave functions, IGMP is performed.
1845
1846            let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_igmpv2_test_environment(seed);
1847
1848            assert_eq!(
1849                core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
1850                GroupJoinResult::Joined(())
1851            );
1852            assert_gmp_state!(core_ctx, &GROUP_ADDR, Delaying);
1853            assert_eq!(core_ctx.frames().len(), 1);
1854            let now = bindings_ctx.now();
1855            let range = now..=(now + IGMP_DEFAULT_UNSOLICITED_REPORT_INTERVAL);
1856            core_ctx.state.gmp_state().timers.assert_range([(
1857                &gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(),
1858                range.clone(),
1859            )]);
1860            ensure_ttl_ihl_rtr(&core_ctx);
1861
1862            assert_eq!(
1863                core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
1864                GroupJoinResult::AlreadyMember
1865            );
1866            assert_gmp_state!(core_ctx, &GROUP_ADDR, Delaying);
1867            assert_eq!(core_ctx.frames().len(), 1);
1868            core_ctx.state.gmp_state().timers.assert_range([(
1869                &gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(),
1870                range.clone(),
1871            )]);
1872
1873            assert_eq!(
1874                core_ctx.gmp_leave_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
1875                GroupLeaveResult::StillMember
1876            );
1877            assert_gmp_state!(core_ctx, &GROUP_ADDR, Delaying);
1878            assert_eq!(core_ctx.frames().len(), 1);
1879            core_ctx.state.gmp_state().timers.assert_range([(
1880                &gmp::v1::DelayedReportTimerId::new_multicast(GROUP_ADDR).into(),
1881                range,
1882            )]);
1883
1884            assert_eq!(
1885                core_ctx.gmp_leave_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
1886                GroupLeaveResult::Left(())
1887            );
1888            assert_eq!(core_ctx.frames().len(), 2);
1889            bindings_ctx.timers.assert_no_timers_installed();
1890            ensure_ttl_ihl_rtr(&core_ctx);
1891
1892            CounterExpectations { tx_igmpv2_report: 1, tx_leave_group: 1, ..Default::default() }
1893                .assert_counters(&mut core_ctx);
1894        });
1895    }
1896
1897    #[test]
1898    fn test_igmp_enable_disable() {
1899        run_with_many_seeds(|seed| {
1900            let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_igmpv2_test_environment(seed);
1901            assert_eq!(core_ctx.take_frames(), []);
1902
1903            assert_eq!(
1904                core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, GROUP_ADDR),
1905                GroupJoinResult::Joined(())
1906            );
1907            assert_gmp_state!(core_ctx, &GROUP_ADDR, Delaying);
1908            {
1909                let frames = core_ctx.take_frames();
1910                let (IgmpPacketMetadata { device: FakeDeviceId, dst_ip }, frame) =
1911                    assert_matches!(&frames[..], [x] => x);
1912                assert_eq!(dst_ip, &GROUP_ADDR);
1913                let (body, src_ip, dst_ip, proto, ttl) = parse_ip_packet::<Ipv4>(frame).unwrap();
1914                assert_eq!(src_ip, MY_ADDR.get());
1915                assert_eq!(dst_ip, GROUP_ADDR.get());
1916                assert_eq!(proto, Ipv4Proto::Igmp);
1917                assert_eq!(ttl, IGMP_IP_TTL);
1918                let mut bv = &body[..];
1919                assert_matches!(
1920                    IgmpPacket::parse(&mut bv, ()).unwrap(),
1921                    IgmpPacket::MembershipReportV2(msg) => {
1922                        assert_eq!(msg.group_addr(), GROUP_ADDR.get());
1923                    }
1924                );
1925            }
1926
1927            // Should do nothing.
1928            core_ctx.gmp_handle_maybe_enabled(&mut bindings_ctx, &FakeDeviceId);
1929            assert_gmp_state!(core_ctx, &GROUP_ADDR, Delaying);
1930            assert_eq!(core_ctx.take_frames(), []);
1931
1932            // Should send done message.
1933            core_ctx.state.igmp_enabled = false;
1934            core_ctx.gmp_handle_disabled(&mut bindings_ctx, &FakeDeviceId);
1935            assert_gmp_state!(core_ctx, &GROUP_ADDR, NonMember);
1936            {
1937                let frames = core_ctx.take_frames();
1938                let (IgmpPacketMetadata { device: FakeDeviceId, dst_ip }, frame) =
1939                    assert_matches!(&frames[..], [x] => x);
1940                assert_eq!(dst_ip, &Ipv4::ALL_ROUTERS_MULTICAST_ADDRESS);
1941                let (body, src_ip, dst_ip, proto, ttl) = parse_ip_packet::<Ipv4>(frame).unwrap();
1942                assert_eq!(src_ip, MY_ADDR.get());
1943                assert_eq!(dst_ip, Ipv4::ALL_ROUTERS_MULTICAST_ADDRESS.get());
1944                assert_eq!(proto, Ipv4Proto::Igmp);
1945                assert_eq!(ttl, IGMP_IP_TTL);
1946                let mut bv = &body[..];
1947                assert_matches!(
1948                    IgmpPacket::parse(&mut bv, ()).unwrap(),
1949                    IgmpPacket::LeaveGroup(msg) => {
1950                        assert_eq!(msg.group_addr(), GROUP_ADDR.get());
1951                    }
1952                );
1953            }
1954
1955            // Should do nothing.
1956            core_ctx.gmp_handle_disabled(&mut bindings_ctx, &FakeDeviceId);
1957            assert_gmp_state!(core_ctx, &GROUP_ADDR, NonMember);
1958            assert_eq!(core_ctx.take_frames(), []);
1959
1960            // Should send report message.
1961            core_ctx.state.igmp_enabled = true;
1962            core_ctx.gmp_handle_maybe_enabled(&mut bindings_ctx, &FakeDeviceId);
1963            assert_gmp_state!(core_ctx, &GROUP_ADDR, Delaying);
1964            {
1965                let frames = core_ctx.take_frames();
1966                let (IgmpPacketMetadata { device: FakeDeviceId, dst_ip }, frame) =
1967                    assert_matches!(&frames[..], [x] => x);
1968                assert_eq!(dst_ip, &GROUP_ADDR);
1969                let (body, src_ip, dst_ip, proto, ttl) = parse_ip_packet::<Ipv4>(frame).unwrap();
1970                assert_eq!(src_ip, MY_ADDR.get());
1971                assert_eq!(dst_ip, GROUP_ADDR.get());
1972                assert_eq!(proto, Ipv4Proto::Igmp);
1973                assert_eq!(ttl, IGMP_IP_TTL);
1974                let mut bv = &body[..];
1975                assert_matches!(
1976                    IgmpPacket::parse(&mut bv, ()).unwrap(),
1977                    IgmpPacket::MembershipReportV2(msg) => {
1978                        assert_eq!(msg.group_addr(), GROUP_ADDR.get());
1979                    }
1980                );
1981            }
1982
1983            CounterExpectations { tx_igmpv2_report: 2, tx_leave_group: 1, ..Default::default() }
1984                .assert_counters(&mut core_ctx);
1985        });
1986    }
1987
1988    /// Test the basics of IGMPv3 report sending.
1989    #[test]
1990    fn send_igmpv3_report() {
1991        let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_igmpv2_test_environment(0);
1992        let sent_report_addr = Ipv4::get_multicast_addr(130);
1993        let sent_report_mode = GroupRecordType::ModeIsExclude;
1994        let sent_report_sources = Vec::<Ipv4Addr>::new();
1995        core_ctx.with_gmp_state_mut_and_ctx(&FakeDeviceId, |mut core_ctx, _| {
1996            core_ctx.send_report_v2(
1997                &mut bindings_ctx,
1998                &FakeDeviceId,
1999                [gmp::v2::GroupRecord::new_with_sources(
2000                    GmpEnabledGroup::new(sent_report_addr).unwrap(),
2001                    sent_report_mode,
2002                    sent_report_sources.iter(),
2003                )]
2004                .into_iter(),
2005            );
2006        });
2007
2008        let frames = core_ctx.take_frames();
2009        let (IgmpPacketMetadata { device: FakeDeviceId, dst_ip }, frame) =
2010            assert_matches!(&frames[..], [x] => x);
2011        assert_eq!(dst_ip, &ALL_IGMPV3_CAPABLE_ROUTERS);
2012        let mut buff = &frame[..];
2013        let ipv4 = buff.parse::<Ipv4Packet<_>>().expect("parse IPv4");
2014        assert_eq!(ipv4.ttl(), IGMP_IP_TTL);
2015        assert_eq!(ipv4.src_ip(), MY_ADDR.get());
2016        assert_eq!(ipv4.dst_ip(), ALL_IGMPV3_CAPABLE_ROUTERS.get());
2017        assert_eq!(ipv4.proto(), Ipv4Proto::Igmp);
2018        assert_eq!(ipv4.dscp_and_ecn(), IGMPV3_DSCP_AND_ECN);
2019        assert_eq!(
2020            ipv4.iter_options()
2021                .map(|o| {
2022                    assert_matches!(o, Ipv4Option::RouterAlert { data: 0 });
2023                })
2024                .count(),
2025            1
2026        );
2027        let igmp = buff.parse::<IgmpPacket<_>>().expect("parse IGMP");
2028        let report = assert_matches!(
2029            igmp,
2030            IgmpPacket::MembershipReportV3(report) => report
2031        );
2032        let report = report
2033            .body()
2034            .iter()
2035            .map(|r| {
2036                (
2037                    r.header().multicast_addr().clone(),
2038                    r.header().record_type().unwrap(),
2039                    r.sources().to_vec(),
2040                )
2041            })
2042            .collect::<Vec<_>>();
2043        assert_eq!(report, vec![(sent_report_addr.get(), sent_report_mode, sent_report_sources)]);
2044
2045        CounterExpectations { tx_igmpv3_report: 1, ..Default::default() }
2046            .assert_counters(&mut core_ctx);
2047    }
2048
2049    /// Tests IGMPv3 entering compatibility modes.
2050    #[test]
2051    fn igmpv3_version_compat() {
2052        let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_igmpv2_test_environment(0);
2053        core_ctx.with_gmp_state_mut(&FakeDeviceId, |state| {
2054            gmp::enter_mode(&mut bindings_ctx, state, IgmpMode::V3);
2055        });
2056
2057        for _ in 0..2 {
2058            assert_eq!(
2059                gmp::v1::handle_query_message(
2060                    &mut core_ctx,
2061                    &mut bindings_ctx,
2062                    &FakeDeviceId,
2063                    &gmp::testutil::FakeV1Query {
2064                        group_addr: Ipv4::UNSPECIFIED_ADDRESS,
2065                        max_response_time: Duration::ZERO
2066                    }
2067                ),
2068                Ok(())
2069            );
2070            // We should be in IGMPv1 compat mode and the compat instant is set
2071            // by the IGMPv3 RFC definition.
2072            let (gmp_state, config) = core_ctx.state.gmp_state_and_config();
2073            let until = bindings_ctx.now().panicking_add(
2074                gmp_state.v2_proto.older_version_querier_present_timeout(config).get(),
2075            );
2076            assert_eq!(gmp_state.mode, IgmpMode::V1(IgmpV1Mode::V3Compat { until }));
2077            assert_eq!(gmp_state.mode.should_send_v1(&mut bindings_ctx), true);
2078            bindings_ctx.timers.instant.sleep(Duration::from_secs(2));
2079        }
2080
2081        let (v2_deadline, ()) =
2082            core_ctx.state.gmp_state().timers.get(&gmp::TimerIdInner::V1Compat).unwrap();
2083        let prev_mode = core_ctx.state.gmp_state().mode;
2084        // Now receive an IGMPv2 query. The IGMPv1 compat deadline should not
2085        // move.
2086        assert_eq!(
2087            gmp::v1::handle_query_message(
2088                &mut core_ctx,
2089                &mut bindings_ctx,
2090                &FakeDeviceId,
2091                &gmp::testutil::FakeV1Query {
2092                    group_addr: Ipv4::UNSPECIFIED_ADDRESS,
2093                    max_response_time: Duration::from_secs(10)
2094                }
2095            ),
2096            Ok(())
2097        );
2098        assert_eq!(core_ctx.state.gmp_state().mode, prev_mode);
2099        assert_ne!(
2100            core_ctx.state.gmp_state().timers.get(&gmp::TimerIdInner::V1Compat).unwrap(),
2101            (v2_deadline, &())
2102        );
2103        // We haven't moved timers so we should still be sending IGMPv1 responses.
2104        assert_eq!(core_ctx.state.gmp_state().mode.should_send_v1(&mut bindings_ctx), true);
2105
2106        // Triggering the next timer should clear IGMPv2 compat back into IGMPv3
2107        // and we update the compat mode.
2108        assert_eq!(bindings_ctx.trigger_next_timer(&mut core_ctx), Some(GMP_TIMER_ID));
2109        assert_eq!(core_ctx.state.gmp_state().mode, IgmpMode::V3);
2110        assert_eq!(core_ctx.state.gmp_state().mode.should_send_v1(&mut bindings_ctx), false);
2111    }
2112
2113    #[test]
2114    fn version_compat_clears_on_disable() {
2115        let FakeCtx { mut core_ctx, mut bindings_ctx } = setup_igmpv2_test_environment(0);
2116        assert_eq!(core_ctx.state.gmp_state().mode, IgmpMode::V2 { compat: false });
2117        assert_eq!(
2118            gmp::v1::handle_query_message(
2119                &mut core_ctx,
2120                &mut bindings_ctx,
2121                &FakeDeviceId,
2122                &gmp::testutil::FakeV1Query {
2123                    group_addr: Ipv4::UNSPECIFIED_ADDRESS,
2124                    max_response_time: Duration::ZERO
2125                }
2126            ),
2127            Ok(())
2128        );
2129        assert_matches!(core_ctx.state.gmp_state().mode, IgmpMode::V1(IgmpV1Mode::V2Compat { .. }));
2130        core_ctx.state.igmp_enabled = false;
2131        core_ctx.gmp_handle_disabled(&mut bindings_ctx, &FakeDeviceId);
2132        assert_eq!(core_ctx.state.gmp_state().mode, IgmpMode::V2 { compat: false });
2133    }
2134
2135    #[test]
2136    fn user_mode_change() {
2137        let mut ctx = setup_igmpv2_test_environment(0);
2138        let FakeCtx { core_ctx, bindings_ctx } = &mut ctx;
2139        assert_eq!(core_ctx.gmp_get_mode(&FakeDeviceId), IgmpConfigMode::V2);
2140        assert_eq!(
2141            core_ctx.gmp_join_group(bindings_ctx, &FakeDeviceId, GROUP_ADDR),
2142            GroupJoinResult::Joined(())
2143        );
2144        // Ignore first reports.
2145        let _ = core_ctx.take_frames();
2146        assert_eq!(
2147            core_ctx.gmp_set_mode(bindings_ctx, &FakeDeviceId, IgmpConfigMode::V3),
2148            IgmpConfigMode::V2
2149        );
2150        assert_eq!(core_ctx.gmp_get_mode(&FakeDeviceId), IgmpConfigMode::V3);
2151        assert_eq!(core_ctx.state.gmp_state().mode, IgmpMode::V3);
2152        // No side-effects.
2153        assert_eq!(core_ctx.take_frames(), Vec::new());
2154
2155        // If we receive an IGMPv1 query, we'll go into compat mode but still
2156        // report IGMPv3 to the user.
2157        receive_igmp_v1_query(core_ctx, bindings_ctx);
2158        assert_matches!(core_ctx.state.gmp_state().mode, IgmpMode::V1(IgmpV1Mode::V3Compat { .. }));
2159        assert_eq!(core_ctx.gmp_get_mode(&FakeDeviceId), IgmpConfigMode::V3);
2160
2161        // Acknowledge query response.
2162        assert_eq!(bindings_ctx.trigger_next_timer(core_ctx), Some(GMP_TIMER_ID));
2163        assert_eq!(core_ctx.take_frames().len(), 1);
2164
2165        // Even if user attempts to set IGMPv3 again we'll keep it in compat.
2166        assert_eq!(
2167            core_ctx.gmp_set_mode(bindings_ctx, &FakeDeviceId, IgmpConfigMode::V3),
2168            IgmpConfigMode::V3
2169        );
2170        assert_eq!(core_ctx.take_frames(), Vec::new());
2171        let until = assert_matches!(
2172            core_ctx.state.gmp_state().mode,
2173            IgmpMode::V1(IgmpV1Mode::V3Compat { until }) => until
2174        );
2175        // If the user switches to IGMPv2, we keep the IGMPv1 compat information.
2176        assert_eq!(
2177            core_ctx.gmp_set_mode(bindings_ctx, &FakeDeviceId, IgmpConfigMode::V2),
2178            IgmpConfigMode::V3
2179        );
2180        assert_eq!(core_ctx.take_frames(), Vec::new());
2181        assert_eq!(core_ctx.state.gmp_state().mode, IgmpMode::V1(IgmpV1Mode::V2Compat { until }));
2182
2183        // Forcing IGMPv1 mode, however, exits compat.
2184        assert_eq!(
2185            core_ctx.gmp_set_mode(bindings_ctx, &FakeDeviceId, IgmpConfigMode::V1),
2186            IgmpConfigMode::V2
2187        );
2188        assert_eq!(core_ctx.take_frames(), Vec::new());
2189        assert_eq!(core_ctx.state.gmp_state().mode, IgmpMode::V1(IgmpV1Mode::Forced));
2190
2191        // Back to IGMPv2...
2192        assert_eq!(
2193            core_ctx.gmp_set_mode(bindings_ctx, &FakeDeviceId, IgmpConfigMode::V2),
2194            IgmpConfigMode::V1
2195        );
2196        assert_eq!(core_ctx.state.gmp_state().mode, IgmpMode::V2 { compat: false });
2197        // ...and then IGMPv3.
2198        assert_eq!(
2199            core_ctx.gmp_set_mode(bindings_ctx, &FakeDeviceId, IgmpConfigMode::V3),
2200            IgmpConfigMode::V2
2201        );
2202        assert_eq!(core_ctx.state.gmp_state().mode, IgmpMode::V3);
2203        assert_eq!(core_ctx.take_frames(), Vec::new());
2204
2205        // Receiving an IGMPv2 query while in IGMPv3 enters compat.
2206        receive_igmp_v2_query(core_ctx, bindings_ctx, NonZeroDuration::from_secs(1).unwrap());
2207        assert_eq!(core_ctx.state.gmp_state().mode, IgmpMode::V2 { compat: true });
2208        assert_eq!(core_ctx.gmp_get_mode(&FakeDeviceId), IgmpConfigMode::V3);
2209        // Acknowledge query response.
2210        assert_eq!(bindings_ctx.trigger_next_timer(core_ctx), Some(GMP_TIMER_ID));
2211        assert_eq!(core_ctx.take_frames().len(), 1);
2212
2213        // Even if user attempts to set IGMPv3 again we'll keep it in compat.
2214        assert_eq!(
2215            core_ctx.gmp_set_mode(bindings_ctx, &FakeDeviceId, IgmpConfigMode::V3),
2216            IgmpConfigMode::V3
2217        );
2218        assert_eq!(core_ctx.take_frames(), Vec::new());
2219        assert_eq!(core_ctx.state.gmp_state().mode, IgmpMode::V2 { compat: true });
2220
2221        // Force IGMPv2 exits IGMPv2 compat.
2222        assert_eq!(
2223            core_ctx.gmp_set_mode(bindings_ctx, &FakeDeviceId, IgmpConfigMode::V2),
2224            IgmpConfigMode::V3
2225        );
2226        assert_eq!(core_ctx.state.gmp_state().mode, IgmpMode::V2 { compat: false });
2227        assert_eq!(core_ctx.take_frames(), Vec::new());
2228        // No timers.
2229        core_ctx.state.gmp_state().timers.assert_timers([]);
2230    }
2231
2232    #[test]
2233    fn reject_bad_messages() {
2234        let mut ctx = setup_igmpv2_test_environment(0);
2235        let FakeCtx { core_ctx, bindings_ctx } = &mut ctx;
2236
2237        let v1_query = {
2238            let ser = IgmpPacketBuilder::<Buf<Vec<u8>>, IgmpMembershipQueryV2>::new_with_resp_time(
2239                Ipv4::UNSPECIFIED_ADDRESS,
2240                Duration::ZERO.try_into().unwrap(),
2241            );
2242            ser.into_serializer()
2243                .serialize_vec_outer(&mut NetworkSerializationContext::default())
2244                .unwrap()
2245        };
2246        let v2_query = {
2247            let ser = IgmpPacketBuilder::<Buf<Vec<u8>>, IgmpMembershipQueryV2>::new_with_resp_time(
2248                Ipv4::UNSPECIFIED_ADDRESS,
2249                Duration::from_secs(10).try_into().unwrap(),
2250            );
2251            ser.into_serializer()
2252                .serialize_vec_outer(&mut NetworkSerializationContext::default())
2253                .unwrap()
2254        };
2255        let v3_query = {
2256            let ser = IgmpMembershipQueryV3Builder::new(
2257                IgmpResponseTimeV3::new_exact(Duration::from_secs(1)).unwrap(),
2258                None,
2259                false,
2260                Igmpv3QRV::new(2),
2261                Igmpv3QQIC::new_exact(Duration::from_secs(125)).unwrap(),
2262                core::iter::empty(),
2263            );
2264            ser.into_serializer()
2265                .serialize_vec_outer(&mut NetworkSerializationContext::default())
2266                .unwrap()
2267        };
2268
2269        let base_header_info = new_recv_pkt_info().header_info;
2270
2271        // TTL must be 1.
2272        const BAD_TTL: u8 = 2;
2273        for q in [&v1_query, &v2_query, &v3_query] {
2274            assert_eq!(
2275                receive_igmp_packet(
2276                    core_ctx,
2277                    bindings_ctx,
2278                    &FakeDeviceId,
2279                    Ipv4::ALL_SYSTEMS_MULTICAST_ADDRESS.into(),
2280                    q.clone(),
2281                    &LocalDeliveryPacketInfo {
2282                        header_info: FakeIpHeaderInfo {
2283                            hop_limit: BAD_TTL,
2284                            ..base_header_info.clone()
2285                        },
2286                        ..Default::default()
2287                    }
2288                ),
2289                Err(IgmpError::BadTtl(BAD_TTL))
2290            );
2291        }
2292
2293        // Multicast destination IPs must be all nodes.
2294        const BAD_DST_IP: MulticastAddr<Ipv4Addr> = GROUP_ADDR;
2295        for q in [&v1_query, &v2_query, &v3_query] {
2296            assert_eq!(
2297                receive_igmp_packet(
2298                    core_ctx,
2299                    bindings_ctx,
2300                    &FakeDeviceId,
2301                    BAD_DST_IP.into(),
2302                    q.clone(),
2303                    &new_recv_pkt_info(),
2304                ),
2305                Err(IgmpError::RejectedGeneralQuery { dst_ip: BAD_DST_IP.get() })
2306            );
2307        }
2308
2309        for q in [&v2_query, &v3_query] {
2310            assert_eq!(
2311                receive_igmp_packet(
2312                    core_ctx,
2313                    bindings_ctx,
2314                    &FakeDeviceId,
2315                    MY_ADDR,
2316                    q.clone(),
2317                    &LocalDeliveryPacketInfo {
2318                        header_info: FakeIpHeaderInfo {
2319                            // Router alert must be set.
2320                            router_alert: false,
2321                            ..base_header_info.clone()
2322                        },
2323                        ..Default::default()
2324                    },
2325                ),
2326                Err(IgmpError::MissingRouterAlertInQuery)
2327            );
2328        }
2329
2330        CounterExpectations {
2331            // Note: Queries with a Bad TTL aren't accounted for here.
2332            rx_igmpv1_query: 1,
2333            rx_igmpv2_query: 2,
2334            rx_igmpv3_query: 2,
2335            rx_err_rejected_general_query: 3,
2336            rx_err_missing_router_alert_in_query: 2,
2337            rx_err_bad_ttl: 3,
2338            ..Default::default()
2339        }
2340        .assert_counters(core_ctx);
2341    }
2342}