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