netstack3_ip/
gmp.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//! Group Management Protocols (GMPs).
6//!
7//! This module provides implementations of the Internet Group Management Protocol
8//! (IGMP) and the Multicast Listener Discovery (MLD) protocol. These allow
9//! hosts to join IPv4 and IPv6 multicast groups respectively.
10//!
11//! The term "Group Management Protocol" is defined in [RFC 4606]:
12//!
13//! > Due to the commonality of function, the term "Group Management Protocol",
14//! > or "GMP", will be used to refer to both IGMP and MLD.
15//!
16//! [RFC 4606]: https://tools.ietf.org/html/rfc4604
17
18// This macro is used by tests in both the `igmp` and `mld` modules.
19
20/// Assert that the GMP state machine for `$group` is in the given state.
21///
22/// `$ctx` is a `context::testutil::FakeCtx` whose state contains a `groups:
23/// MulticastGroupSet` field.
24#[cfg(test)]
25macro_rules! assert_gmp_state {
26    ($ctx:expr, $group:expr, NonMember) => {
27        assert_gmp_state!(@inner $ctx, $group, crate::internal::gmp::v1::MemberState::NonMember(_));
28    };
29    ($ctx:expr, $group:expr, Delaying) => {
30        assert_gmp_state!(@inner $ctx, $group, crate::internal::gmp::v1::MemberState::Delaying(_));
31    };
32    (@inner $ctx:expr, $group:expr, $pattern:pat) => {
33        assert!(matches!($ctx.state.groups().get($group).unwrap().v1().inner.as_ref().unwrap(), $pattern))
34    };
35}
36
37pub(crate) mod igmp;
38pub(crate) mod mld;
39#[cfg(test)]
40mod testutil;
41mod v1;
42mod v2;
43
44use core::fmt::Debug;
45use core::num::NonZeroU64;
46use core::time::Duration;
47
48use assert_matches::assert_matches;
49use log::info;
50use net_types::ip::{Ip, IpAddress, IpVersionMarker};
51use net_types::MulticastAddr;
52use netstack3_base::ref_counted_hash_map::{InsertResult, RefCountedHashMap, RemoveResult};
53use netstack3_base::{
54    AnyDevice, CoreTimerContext, DeviceIdContext, InspectableValue, Inspector,
55    InstantBindingsTypes, LocalTimerHeap, RngContext, TimerBindingsTypes, TimerContext,
56    WeakDeviceIdentifier,
57};
58use rand::Rng;
59
60/// The result of joining a multicast group.
61///
62/// `GroupJoinResult` is the result of joining a multicast group in a
63/// [`MulticastGroupSet`].
64#[cfg_attr(test, derive(Debug, Eq, PartialEq))]
65pub enum GroupJoinResult<O = ()> {
66    /// We were not previously a member of the group, so we joined the
67    /// group.
68    Joined(O),
69    /// We were already a member of the group, so we incremented the group's
70    /// reference count.
71    AlreadyMember,
72}
73
74impl<O> GroupJoinResult<O> {
75    /// Maps a [`GroupJoinResult::Joined`] variant to another type.
76    ///
77    /// If `self` is [`GroupJoinResult::AlreadyMember`], it is left as-is.
78    pub(crate) fn map<P, F: FnOnce(O) -> P>(self, f: F) -> GroupJoinResult<P> {
79        match self {
80            GroupJoinResult::Joined(output) => GroupJoinResult::Joined(f(output)),
81            GroupJoinResult::AlreadyMember => GroupJoinResult::AlreadyMember,
82        }
83    }
84}
85
86impl<O> From<InsertResult<O>> for GroupJoinResult<O> {
87    fn from(result: InsertResult<O>) -> Self {
88        match result {
89            InsertResult::Inserted(output) => GroupJoinResult::Joined(output),
90            InsertResult::AlreadyPresent => GroupJoinResult::AlreadyMember,
91        }
92    }
93}
94
95/// The result of leaving a multicast group.
96///
97/// `GroupLeaveResult` is the result of leaving a multicast group in
98/// [`MulticastGroupSet`].
99#[cfg_attr(test, derive(Debug, Eq, PartialEq))]
100pub enum GroupLeaveResult<T = ()> {
101    /// The reference count reached 0, so we left the group.
102    Left(T),
103    /// The reference count did not reach 0, so we are still a member of the
104    /// group.
105    StillMember,
106    /// We were not a member of the group.
107    NotMember,
108}
109
110impl<T> GroupLeaveResult<T> {
111    /// Maps a [`GroupLeaveResult::Left`] variant to another type.
112    ///
113    /// If `self` is [`GroupLeaveResult::StillMember`] or
114    /// [`GroupLeaveResult::NotMember`], it is left as-is.
115    pub(crate) fn map<U, F: FnOnce(T) -> U>(self, f: F) -> GroupLeaveResult<U> {
116        match self {
117            GroupLeaveResult::Left(value) => GroupLeaveResult::Left(f(value)),
118            GroupLeaveResult::StillMember => GroupLeaveResult::StillMember,
119            GroupLeaveResult::NotMember => GroupLeaveResult::NotMember,
120        }
121    }
122}
123
124impl<T> From<RemoveResult<T>> for GroupLeaveResult<T> {
125    fn from(result: RemoveResult<T>) -> Self {
126        match result {
127            RemoveResult::Removed(value) => GroupLeaveResult::Left(value),
128            RemoveResult::StillPresent => GroupLeaveResult::StillMember,
129            RemoveResult::NotPresent => GroupLeaveResult::NotMember,
130        }
131    }
132}
133
134/// A set of reference-counted multicast groups and associated data.
135///
136/// `MulticastGroupSet` is a set of multicast groups, each with associated data
137/// `T`. Each group is reference-counted, only being removed once its reference
138/// count reaches zero.
139#[cfg_attr(test, derive(Debug))]
140pub struct MulticastGroupSet<A: IpAddress, T> {
141    inner: RefCountedHashMap<MulticastAddr<A>, T>,
142}
143
144impl<A: IpAddress, T> Default for MulticastGroupSet<A, T> {
145    fn default() -> MulticastGroupSet<A, T> {
146        MulticastGroupSet { inner: RefCountedHashMap::default() }
147    }
148}
149
150impl<A: IpAddress, T> MulticastGroupSet<A, T> {
151    fn groups_mut(&mut self) -> impl Iterator<Item = (&MulticastAddr<A>, &mut T)> + '_ {
152        self.inner.iter_mut()
153    }
154
155    fn join_group_with<O, F: FnOnce() -> (T, O)>(
156        &mut self,
157        group: MulticastAddr<A>,
158        f: F,
159    ) -> GroupJoinResult<O> {
160        self.inner.insert_with(group, f).into()
161    }
162
163    fn leave_group(&mut self, group: MulticastAddr<A>) -> GroupLeaveResult<T> {
164        self.inner.remove(group).into()
165    }
166
167    /// Does the set contain the given group?
168    pub(crate) fn contains(&self, group: &MulticastAddr<A>) -> bool {
169        self.inner.contains_key(group)
170    }
171
172    #[cfg(test)]
173    fn get(&self, group: &MulticastAddr<A>) -> Option<&T> {
174        self.inner.get(group)
175    }
176
177    fn get_mut(&mut self, group: &MulticastAddr<A>) -> Option<&mut T> {
178        self.inner.get_mut(group)
179    }
180
181    fn iter_mut<'a>(&'a mut self) -> impl 'a + Iterator<Item = (&'a MulticastAddr<A>, &'a mut T)> {
182        self.inner.iter_mut()
183    }
184
185    fn iter<'a>(&'a self) -> impl 'a + Iterator<Item = (&'a MulticastAddr<A>, &'a T)> + Clone {
186        self.inner.iter()
187    }
188
189    fn is_empty(&self) -> bool {
190        self.inner.is_empty()
191    }
192}
193
194impl<A: IpAddress, T> InspectableValue for MulticastGroupSet<A, T> {
195    fn record<I: Inspector>(&self, name: &str, inspector: &mut I) {
196        inspector.record_child(name, |inspector| {
197            for (addr, ref_count) in self.inner.iter_ref_counts() {
198                inspector.record_display_child(addr, |inspector| {
199                    inspector.record_usize("Refs", ref_count.get())
200                });
201            }
202        });
203    }
204}
205
206/// An implementation of query operations on a Group Management Protocol (GMP).
207pub trait GmpQueryHandler<I: Ip, BC>: DeviceIdContext<AnyDevice> {
208    /// Returns true if the device is a member of the group.
209    fn gmp_is_in_group(
210        &mut self,
211        device: &Self::DeviceId,
212        group_addr: MulticastAddr<I::Addr>,
213    ) -> bool;
214}
215
216/// An implementation of a Group Management Protocol (GMP) such as the Internet
217/// Group Management Protocol, Version 2 (IGMPv2) for IPv4 or the Multicast
218/// Listener Discovery (MLD) protocol for IPv6.
219pub trait GmpHandler<I: IpExt, BC>: DeviceIdContext<AnyDevice> {
220    /// Handles GMP potentially being enabled.
221    ///
222    /// Attempts to transition memberships in the non-member state to a member
223    /// state. Should be called anytime a configuration change occurs which
224    /// results in GMP potentially being enabled. E.g. when IP or GMP
225    /// transitions to being enabled.
226    ///
227    /// This method is idempotent, once into the enabled state future calls are
228    /// no-ops.
229    fn gmp_handle_maybe_enabled(&mut self, bindings_ctx: &mut BC, device: &Self::DeviceId);
230
231    /// Handles GMP being disabled.
232    ///
233    /// All joined groups will transition to the non-member state but still
234    /// remain locally joined.
235    ///
236    /// This method is idempotent, once into the disabled state future calls are
237    /// no-ops.
238    fn gmp_handle_disabled(&mut self, bindings_ctx: &mut BC, device: &Self::DeviceId);
239
240    /// Joins the given multicast group.
241    fn gmp_join_group(
242        &mut self,
243        bindings_ctx: &mut BC,
244        device: &Self::DeviceId,
245        group_addr: MulticastAddr<I::Addr>,
246    ) -> GroupJoinResult;
247
248    /// Leaves the given multicast group.
249    fn gmp_leave_group(
250        &mut self,
251        bindings_ctx: &mut BC,
252        device: &Self::DeviceId,
253        group_addr: MulticastAddr<I::Addr>,
254    ) -> GroupLeaveResult;
255
256    /// Returns the current protocol mode.
257    fn gmp_get_mode(&mut self, device: &Self::DeviceId) -> I::GmpProtoConfigMode;
258
259    /// Sets the new user-configured protocol mode.
260    ///
261    /// Returns the previous mode. No packets are sent in response to switching
262    /// modes.
263    fn gmp_set_mode(
264        &mut self,
265        bindings_ctx: &mut BC,
266        device: &Self::DeviceId,
267        new_mode: I::GmpProtoConfigMode,
268    ) -> I::GmpProtoConfigMode;
269}
270
271impl<I: IpExt, BT: GmpBindingsTypes, CC: GmpStateContext<I, BT>> GmpQueryHandler<I, BT> for CC {
272    fn gmp_is_in_group(
273        &mut self,
274        device: &Self::DeviceId,
275        group_addr: MulticastAddr<I::Addr>,
276    ) -> bool {
277        self.with_multicast_groups(device, |groups| groups.contains(&group_addr))
278    }
279}
280
281impl<I: IpExt, BC: GmpBindingsContext, CC: GmpContext<I, BC>> GmpHandler<I, BC> for CC {
282    fn gmp_handle_maybe_enabled(&mut self, bindings_ctx: &mut BC, device: &Self::DeviceId) {
283        self.with_gmp_state_mut_and_ctx(device, |mut core_ctx, state| {
284            if !state.enabled {
285                return;
286            }
287            // Update enablement state tracking.
288            match core::mem::replace(
289                &mut state.gmp.enablement_idempotency_guard,
290                LastState::Enabled,
291            ) {
292                LastState::Disabled => {}
293                LastState::Enabled => {
294                    // Do nothing if we were already enabled.
295                    return;
296                }
297            }
298
299            match state.gmp.gmp_mode() {
300                GmpMode::V1 { compat: _ } => {
301                    v1::handle_enabled(&mut core_ctx, bindings_ctx, device, state);
302                }
303                GmpMode::V2 => {
304                    v2::handle_enabled(bindings_ctx, state);
305                }
306            }
307        })
308    }
309
310    fn gmp_handle_disabled(&mut self, bindings_ctx: &mut BC, device: &Self::DeviceId) {
311        self.with_gmp_state_mut_and_ctx(device, |mut core_ctx, mut state| {
312            assert!(!state.enabled, "handle_disabled called with enabled GMP state");
313            // Update enablement state tracking.
314            match core::mem::replace(
315                &mut state.gmp.enablement_idempotency_guard,
316                LastState::Disabled,
317            ) {
318                LastState::Enabled => {}
319                LastState::Disabled => {
320                    // Do nothing if we were already disabled.
321                    return;
322                }
323            }
324
325            match state.gmp.gmp_mode() {
326                GmpMode::V1 { .. } => {
327                    v1::handle_disabled(&mut core_ctx, bindings_ctx, device, state.as_mut());
328                }
329                GmpMode::V2 => {
330                    v2::handle_disabled(&mut core_ctx, bindings_ctx, device, state.as_mut());
331                }
332            }
333            // Ask protocol which mode to enter on disable.
334            let next_mode =
335                <CC::Inner<'_> as GmpContextInner<I, BC>>::mode_on_disable(&state.gmp.mode);
336            enter_mode(bindings_ctx, state.as_mut(), next_mode);
337            // Always reset v2 protocol state when disabled, regardless of which
338            // mode we're in.
339            state.gmp.v2_proto = Default::default();
340            // Always clear all timers on disable.
341            state.gmp.timers.clear(bindings_ctx);
342        })
343    }
344
345    fn gmp_join_group(
346        &mut self,
347        bindings_ctx: &mut BC,
348        device: &CC::DeviceId,
349        group_addr: MulticastAddr<I::Addr>,
350    ) -> GroupJoinResult {
351        self.with_gmp_state_mut_and_ctx(device, |mut core_ctx, state| match state.gmp.gmp_mode() {
352            GmpMode::V1 { compat: _ } => {
353                v1::join_group(&mut core_ctx, bindings_ctx, device, group_addr, state)
354            }
355            GmpMode::V2 => v2::join_group(bindings_ctx, group_addr, state),
356        })
357    }
358
359    fn gmp_leave_group(
360        &mut self,
361        bindings_ctx: &mut BC,
362        device: &CC::DeviceId,
363        group_addr: MulticastAddr<I::Addr>,
364    ) -> GroupLeaveResult {
365        self.with_gmp_state_mut_and_ctx(device, |mut core_ctx, state| match state.gmp.gmp_mode() {
366            GmpMode::V1 { compat: _ } => {
367                v1::leave_group(&mut core_ctx, bindings_ctx, device, group_addr, state)
368            }
369            GmpMode::V2 => v2::leave_group(bindings_ctx, group_addr, state),
370        })
371    }
372
373    fn gmp_get_mode(&mut self, device: &CC::DeviceId) -> I::GmpProtoConfigMode {
374        self.with_gmp_state_mut(device, |state| {
375            <CC::Inner<'_> as GmpContextInner<I, BC>>::mode_to_config(&state.gmp.mode)
376        })
377    }
378
379    fn gmp_set_mode(
380        &mut self,
381        bindings_ctx: &mut BC,
382        device: &CC::DeviceId,
383        new_mode: I::GmpProtoConfigMode,
384    ) -> I::GmpProtoConfigMode {
385        self.with_gmp_state_mut(device, |state| {
386            let old_mode =
387                <CC::Inner<'_> as GmpContextInner<I, BC>>::mode_to_config(&state.gmp.mode);
388            info!("GMP({}) mode change by user from {:?} to {:?}", I::NAME, old_mode, new_mode);
389            let new_mode = <CC::Inner<'_> as GmpContextInner<I, BC>>::config_to_mode(
390                &state.gmp.mode,
391                new_mode,
392            );
393            enter_mode(bindings_ctx, state, new_mode);
394            old_mode
395        })
396    }
397}
398
399/// Randomly generates a timeout in (0, period].
400fn random_report_timeout<R: Rng>(rng: &mut R, period: Duration) -> Duration {
401    let micros = if let Some(micros) =
402        NonZeroU64::new(u64::try_from(period.as_micros()).unwrap_or(u64::MAX))
403    {
404        // NB: gen_range panics if the range is empty, this must be inclusive
405        // end.
406        rng.gen_range(1..=micros.get())
407    } else {
408        1
409    };
410    // u64 will be enough here because the only input of the function is from
411    // the `MaxRespTime` field of the GMP query packets. The representable
412    // number of microseconds is bounded by 2^33.
413    Duration::from_micros(micros)
414}
415
416/// A timer ID for GMP to send a report.
417#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
418pub struct GmpTimerId<I: Ip, D: WeakDeviceIdentifier> {
419    pub(crate) device: D,
420    pub(crate) _marker: IpVersionMarker<I>,
421}
422
423impl<I: Ip, D: WeakDeviceIdentifier> GmpTimerId<I, D> {
424    fn device_id(&self) -> &D {
425        let Self { device, _marker: IpVersionMarker { .. } } = self;
426        device
427    }
428
429    const fn new(device: D) -> Self {
430        Self { device, _marker: IpVersionMarker::new() }
431    }
432}
433
434/// The bindings types for GMP.
435pub trait GmpBindingsTypes: InstantBindingsTypes + TimerBindingsTypes {}
436impl<BT> GmpBindingsTypes for BT where BT: InstantBindingsTypes + TimerBindingsTypes {}
437
438/// The bindings execution context for GMP.
439pub trait GmpBindingsContext: RngContext + TimerContext + GmpBindingsTypes {}
440impl<BC> GmpBindingsContext for BC where BC: RngContext + TimerContext + GmpBindingsTypes {}
441
442/// An extension trait to [`Ip`].
443pub trait IpExt: Ip {
444    /// The user-controllable mode configuration.
445    type GmpProtoConfigMode: Debug + Copy + Clone + Eq + PartialEq;
446
447    /// Returns true iff GMP should be performed for the multicast group.
448    fn should_perform_gmp(addr: MulticastAddr<Self::Addr>) -> bool;
449}
450
451/// The timer id kept in [`GmpState`]'s local timer heap.
452#[derive(Debug, Eq, PartialEq, Hash, Clone)]
453enum TimerIdInner<I: Ip> {
454    /// Timers scheduled by the v1 state machine.
455    V1(v1::DelayedReportTimerId<I>),
456    /// V1 compatibility mode exit timer.
457    V1Compat,
458    V2(v2::TimerId<I>),
459}
460
461impl<I: Ip> From<v1::DelayedReportTimerId<I>> for TimerIdInner<I> {
462    fn from(value: v1::DelayedReportTimerId<I>) -> Self {
463        Self::V1(value)
464    }
465}
466
467impl<I: Ip> From<v2::TimerId<I>> for TimerIdInner<I> {
468    fn from(value: v2::TimerId<I>) -> Self {
469        Self::V2(value)
470    }
471}
472
473/// Generic group management state.
474#[cfg_attr(test, derive(Debug))]
475pub struct GmpState<I: Ip, CC: GmpTypeLayout<I, BT>, BT: GmpBindingsTypes> {
476    timers: LocalTimerHeap<TimerIdInner<I>, (), BT>,
477    mode: CC::ProtoMode,
478    v2_proto: v2::ProtocolState<I>,
479    /// Keeps track of interface-wide enablement state.
480    ///
481    /// In [`v1`] each group keeps track of whether it's tracked by GMP or not,
482    /// but in [`v2`] we need to keep track of an interface-wide enabled state
483    /// to be able to handle `disable` only once. This is necessary because the
484    /// IP layer may call [`GmpHandler::gmp_handle_disabled`] multiple times
485    /// (since the interface-wide enabled state is an `and` of IP enabled and
486    /// GMP enabled) and we should avoid sending leave messages on
487    /// [`v2::handle_disabled`] multiple times.
488    ///
489    /// Note that no assumptions can be made between this flag and
490    /// [`GmpStateRef::enabled`], since part of that state is held in a
491    /// different lock. This exists _only_ to allow idempotency in
492    /// [`GmpHandler::gmp_handle_maybe_enabled`] and
493    /// [`GmpHandler::gmp_handle_disabled`].
494    enablement_idempotency_guard: LastState,
495}
496
497/// Supports [`GmpState::enablement_idempotency_guard`].
498#[cfg_attr(test, derive(Debug))]
499enum LastState {
500    Disabled,
501    Enabled,
502}
503
504impl LastState {
505    fn from_enabled(enabled: bool) -> Self {
506        if enabled {
507            Self::Enabled
508        } else {
509            Self::Disabled
510        }
511    }
512}
513
514// NB: This block is not bound on GmpBindingsContext because we don't need
515// RngContext to construct GmpState.
516impl<I: Ip, T: GmpTypeLayout<I, BC>, BC: GmpBindingsTypes + TimerContext> GmpState<I, T, BC> {
517    /// Constructs a new `GmpState` for `device`.
518    pub fn new<D: WeakDeviceIdentifier, CC: CoreTimerContext<GmpTimerId<I, D>, BC>>(
519        bindings_ctx: &mut BC,
520        device: D,
521    ) -> Self {
522        Self::new_with_enabled_and_mode::<D, CC>(bindings_ctx, device, false, Default::default())
523    }
524
525    /// Constructs a new `GmpState` for `device` assuming initial enabled state
526    /// `enabled` and `mode`.
527    ///
528    /// This is meant to be called directly only in test scenarios (besides
529    /// helping implement [`GmpState::new`] that is) where to decrease test
530    /// verbosity `GmpState` can be created in a state that assumes
531    /// [`GmpHandler::gmp_handle_maybe_enabled`] was called.
532    fn new_with_enabled_and_mode<
533        D: WeakDeviceIdentifier,
534        CC: CoreTimerContext<GmpTimerId<I, D>, BC>,
535    >(
536        bindings_ctx: &mut BC,
537        device: D,
538        enabled: bool,
539        mode: T::ProtoMode,
540    ) -> Self {
541        Self {
542            timers: LocalTimerHeap::new_with_context::<_, CC>(
543                bindings_ctx,
544                GmpTimerId::new(device),
545            ),
546            mode,
547            v2_proto: Default::default(),
548            enablement_idempotency_guard: LastState::from_enabled(enabled),
549        }
550    }
551}
552
553impl<I: IpExt, T: GmpTypeLayout<I, BT>, BT: GmpBindingsTypes> GmpState<I, T, BT> {
554    fn gmp_mode(&self) -> GmpMode {
555        self.mode.into()
556    }
557
558    pub(crate) fn mode(&self) -> &T::ProtoMode {
559        &self.mode
560    }
561}
562
563/// A reference to a device's GMP state.
564pub struct GmpStateRef<'a, I: IpExt, CC: GmpTypeLayout<I, BT>, BT: GmpBindingsTypes> {
565    /// True if GMP is enabled for the device.
566    pub enabled: bool,
567    /// Mutable reference to the multicast groups on a device.
568    pub groups: &'a mut MulticastGroupSet<I::Addr, GmpGroupState<I, BT>>,
569    /// Mutable reference to the device's GMP state.
570    pub gmp: &'a mut GmpState<I, CC, BT>,
571    /// Protocol specific configuration.
572    pub config: &'a CC::Config,
573}
574
575impl<'a, I: IpExt, CC: GmpTypeLayout<I, BT>, BT: GmpBindingsTypes> GmpStateRef<'a, I, CC, BT> {
576    fn as_mut(&mut self) -> GmpStateRef<'_, I, CC, BT> {
577        let Self { enabled, groups, gmp, config } = self;
578        GmpStateRef { enabled: *enabled, groups, gmp, config }
579    }
580}
581
582/// Provides IP-specific associated types for GMP.
583pub trait GmpTypeLayout<I: Ip, BT: GmpBindingsTypes>: Sized {
584    /// The type for protocol-specific configs.
585    type Config: Debug + v1::ProtocolConfig + v2::ProtocolConfig;
586    /// Protocol-specific mode.
587    type ProtoMode: Debug
588        + Copy
589        + Clone
590        + Eq
591        + PartialEq
592        + Into<GmpMode>
593        + Default
594        + InspectableValue;
595}
596
597/// The state kept by each muitlcast group the host is a member of.
598pub struct GmpGroupState<I: Ip, BT: GmpBindingsTypes> {
599    version_specific: GmpGroupStateByVersion<I, BT>,
600    // TODO(https://fxbug.dev/381241191): When we support SSM, each group should
601    // keep track of the source interest and filter modes.
602}
603
604impl<I: Ip, BT: GmpBindingsTypes> GmpGroupState<I, BT> {
605    /// Retrieves a mutable borrow to the v1 state machine value.
606    ///
607    /// # Panics
608    ///
609    /// Panics if the state machine is not in the v1 state. When switching
610    /// modes, GMP is responsible for updating all group states to the
611    /// appropriate version.
612    fn v1_mut(&mut self) -> &mut v1::GmpStateMachine<BT::Instant> {
613        match &mut self.version_specific {
614            GmpGroupStateByVersion::V1(v1) => return v1,
615            GmpGroupStateByVersion::V2(_) => {
616                panic!("expected GMP v1")
617            }
618        }
619    }
620
621    /// Retrieves a mutable borrow to the v2 state machine value.
622    ///
623    /// # Panics
624    ///
625    /// Panics if the state machine is not in the v2 state. When switching
626    /// modes, GMP is responsible for updating all group states to the
627    /// appropriate version.
628    fn v2_mut(&mut self) -> &mut v2::GroupState<I> {
629        match &mut self.version_specific {
630            GmpGroupStateByVersion::V2(v2) => return v2,
631            GmpGroupStateByVersion::V1(_) => {
632                panic!("expected GMP v2")
633            }
634        }
635    }
636
637    /// Like [`GmpGroupState::v1_mut`] but returns a non mutable borrow.
638    #[cfg(test)]
639    fn v1(&self) -> &v1::GmpStateMachine<BT::Instant> {
640        match &self.version_specific {
641            GmpGroupStateByVersion::V1(v1) => v1,
642            GmpGroupStateByVersion::V2(_) => panic!("group not in v1 mode"),
643        }
644    }
645
646    /// Like [`GmpGroupState::v2_mut`] but returns a non mutable borrow.
647    fn v2(&self) -> &v2::GroupState<I> {
648        match &self.version_specific {
649            GmpGroupStateByVersion::V2(v2) => v2,
650            GmpGroupStateByVersion::V1 { .. } => panic!("group not in v2 mode"),
651        }
652    }
653
654    /// Equivalent to [`GmpGroupState::v1_mut`] but drops all remaining state.
655    ///
656    /// # Panics
657    ///
658    /// See [`GmpGroupState::v1`].
659    fn into_v1(self) -> v1::GmpStateMachine<BT::Instant> {
660        let Self { version_specific } = self;
661        match version_specific {
662            GmpGroupStateByVersion::V1(v1) => v1,
663            GmpGroupStateByVersion::V2(_) => panic!("expected GMP v1"),
664        }
665    }
666
667    /// Equivalent to [`GmpGroupState::v2_mut`] but drops all remaining state.
668    ///
669    /// # Panics
670    ///
671    /// See [`GmpGroupState::v2`].
672    fn into_v2(self) -> v2::GroupState<I> {
673        let Self { version_specific } = self;
674        match version_specific {
675            GmpGroupStateByVersion::V2(v2) => v2,
676            GmpGroupStateByVersion::V1(_) => panic!("expected GMP v2"),
677        }
678    }
679
680    /// Creates a new `GmpGroupState` with associated v1 state machine.
681    fn new_v1(v1: v1::GmpStateMachine<BT::Instant>) -> Self {
682        Self { version_specific: GmpGroupStateByVersion::V1(v1) }
683    }
684
685    /// Creates a new `GmpGroupState` with associated v2 state.
686    fn new_v2(v2: v2::GroupState<I>) -> Self {
687        Self { version_specific: GmpGroupStateByVersion::V2(v2) }
688    }
689}
690
691/// GMP Compatibility mode.
692#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
693pub enum GmpMode {
694    /// GMP operating in v1 mode. This is the mode supporting MLDv1 and IGMPv2.
695    V1 {
696        /// Compat indicates v1 mode behavior.
697        ///
698        /// It is `true` if v1 is entered due to receiving a query from a v1
699        /// router. It is `false` if v1 is entered due to administrative action
700        /// (i.e. "forcing" v1 mode).
701        ///
702        /// It effectively controls whether we can revert back to v2 on
703        /// interface disable <-> re-enable.
704        compat: bool,
705    },
706    /// GMP operating in v2 mode. This is the mode supporting MLDv2 and IGMPv3.
707    #[default]
708    V2,
709}
710
711impl GmpMode {
712    fn is_v1(&self) -> bool {
713        match self {
714            Self::V1 { .. } => true,
715            Self::V2 => false,
716        }
717    }
718
719    fn is_v2(&self) -> bool {
720        match self {
721            Self::V2 => true,
722            Self::V1 { .. } => false,
723        }
724    }
725
726    fn maybe_enter_v1_compat(&self) -> Self {
727        match self {
728            // Enter v1 compat if in v2.
729            Self::V2 => Self::V1 { compat: true },
730            // Maintain compat value.
731            m @ Self::V1 { .. } => *m,
732        }
733    }
734
735    fn maybe_exit_v1_compat(&self) -> Self {
736        match self {
737            // Maintain mode if not in compat.
738            m @ Self::V2 | m @ Self::V1 { compat: false } => *m,
739            // Exit compat mode.
740            Self::V1 { compat: true } => Self::V2,
741        }
742    }
743}
744
745#[cfg_attr(test, derive(derivative::Derivative))]
746#[cfg_attr(test, derivative(Debug(bound = "")))]
747enum GmpGroupStateByVersion<I: Ip, BT: GmpBindingsTypes> {
748    V1(v1::GmpStateMachine<BT::Instant>),
749    V2(v2::GroupState<I>),
750}
751
752/// Provides immutable access to GMP state.
753pub trait GmpStateContext<I: IpExt, BT: GmpBindingsTypes>: DeviceIdContext<AnyDevice> {
754    /// The types used by this context.
755    type TypeLayout: GmpTypeLayout<I, BT>;
756
757    /// Calls the function with immutable access to the [`MulticastGroupSet`].
758    fn with_multicast_groups<
759        O,
760        F: FnOnce(&MulticastGroupSet<I::Addr, GmpGroupState<I, BT>>) -> O,
761    >(
762        &mut self,
763        device: &Self::DeviceId,
764        cb: F,
765    ) -> O {
766        self.with_gmp_state(device, |groups, _gmp_state| cb(groups))
767    }
768
769    /// Calls the function with immutable access to the [`MulticastGroupSet`]
770    /// and current GMP state.
771    fn with_gmp_state<
772        O,
773        F: FnOnce(
774            &MulticastGroupSet<I::Addr, GmpGroupState<I, BT>>,
775            &GmpState<I, Self::TypeLayout, BT>,
776        ) -> O,
777    >(
778        &mut self,
779        device: &Self::DeviceId,
780        cb: F,
781    ) -> O;
782}
783
784/// Provides common functionality for GMP context implementations.
785///
786/// This trait implements portions of a group management protocol.
787trait GmpContext<I: IpExt, BC: GmpBindingsContext>: DeviceIdContext<AnyDevice> {
788    /// The types used by this context.
789    type TypeLayout: GmpTypeLayout<I, BC>;
790
791    /// The inner context given to `with_gmp_state_mut_and_ctx`.
792    type Inner<'a>: GmpContextInner<I, BC, TypeLayout = Self::TypeLayout, DeviceId = Self::DeviceId>
793        + 'a;
794
795    /// Calls the function with mutable access to GMP state in [`GmpStateRef`]
796    /// and access to a [`GmpContextInner`] context.
797    fn with_gmp_state_mut_and_ctx<
798        O,
799        F: FnOnce(Self::Inner<'_>, GmpStateRef<'_, I, Self::TypeLayout, BC>) -> O,
800    >(
801        &mut self,
802        device: &Self::DeviceId,
803        cb: F,
804    ) -> O;
805
806    /// Calls the function with mutable access to GMP state in [`GmpStateRef`].
807    fn with_gmp_state_mut<O, F: FnOnce(GmpStateRef<'_, I, Self::TypeLayout, BC>) -> O>(
808        &mut self,
809        device: &Self::DeviceId,
810        cb: F,
811    ) -> O {
812        self.with_gmp_state_mut_and_ctx(device, |_core_ctx, state| cb(state))
813    }
814}
815
816/// The inner GMP context.
817///
818/// Provides access to external actions while holding the GMP state lock.
819trait GmpContextInner<I: IpExt, BC: GmpBindingsContext>: DeviceIdContext<AnyDevice> {
820    /// The types used by this context,
821    type TypeLayout: GmpTypeLayout<I, BC>;
822
823    /// Sends a GMPv1 message.
824    fn send_message_v1(
825        &mut self,
826        bindings_ctx: &mut BC,
827        device: &Self::DeviceId,
828        cur_mode: &<Self::TypeLayout as GmpTypeLayout<I, BC>>::ProtoMode,
829        group_addr: GmpEnabledGroup<I::Addr>,
830        msg_type: v1::GmpMessageType,
831    );
832
833    /// Sends a GMPv2 report message.
834    fn send_report_v2(
835        &mut self,
836        bindings_ctx: &mut BC,
837        device: &Self::DeviceId,
838        groups: impl Iterator<Item: v2::VerifiedReportGroupRecord<I::Addr> + Clone> + Clone,
839    );
840
841    /// Returns the compatibility mode to enter upon observing `query` with
842    /// `config`.
843    fn mode_update_from_v1_query<Q: v1::QueryMessage<I>>(
844        &mut self,
845        bindings_ctx: &mut BC,
846        query: &Q,
847        gmp_state: &GmpState<I, Self::TypeLayout, BC>,
848        config: &<Self::TypeLayout as GmpTypeLayout<I, BC>>::Config,
849    ) -> <Self::TypeLayout as GmpTypeLayout<I, BC>>::ProtoMode;
850
851    /// Returns the current operating mode as the user configuration value from
852    /// the current generic GMP mode + protocol state.
853    fn mode_to_config(
854        mode: &<Self::TypeLayout as GmpTypeLayout<I, BC>>::ProtoMode,
855    ) -> I::GmpProtoConfigMode;
856
857    /// Returns the new mode to enter based on the current mode `cur_mode` and
858    /// user-provided configuration `config`.
859    fn config_to_mode(
860        cur_mode: &<Self::TypeLayout as GmpTypeLayout<I, BC>>::ProtoMode,
861        config: I::GmpProtoConfigMode,
862    ) -> <Self::TypeLayout as GmpTypeLayout<I, BC>>::ProtoMode;
863
864    /// Returns the new mode to use as a result of disabling GMP on an
865    /// interface.
866    fn mode_on_disable(
867        cur_mode: &<Self::TypeLayout as GmpTypeLayout<I, BC>>::ProtoMode,
868    ) -> <Self::TypeLayout as GmpTypeLayout<I, BC>>::ProtoMode;
869
870    /// The mode to enter when GMP's compat timer fires.
871    ///
872    /// The returned mode *MUST* be [`GmpMode::V2`] when converted.
873    fn mode_on_exit_compat() -> <Self::TypeLayout as GmpTypeLayout<I, BC>>::ProtoMode;
874}
875
876fn handle_timer<I, BC, CC>(
877    core_ctx: &mut CC,
878    bindings_ctx: &mut BC,
879    timer: GmpTimerId<I, CC::WeakDeviceId>,
880) where
881    BC: GmpBindingsContext,
882    CC: GmpContext<I, BC>,
883    I: IpExt,
884{
885    let GmpTimerId { device, _marker: IpVersionMarker { .. } } = timer;
886    let Some(device) = device.upgrade() else {
887        return;
888    };
889    core_ctx.with_gmp_state_mut_and_ctx(&device, |mut core_ctx, state| {
890        let Some((timer_id, ())) = state.gmp.timers.pop(bindings_ctx) else {
891            return;
892        };
893        // No timers should be firing if the state is disabled.
894        assert!(state.enabled, "{timer_id:?} fired in GMP disabled state");
895
896        match (timer_id, state.gmp.gmp_mode()) {
897            (TimerIdInner::V1(v1), GmpMode::V1 { .. }) => {
898                v1::handle_timer(&mut core_ctx, bindings_ctx, &device, state, v1);
899            }
900            (TimerIdInner::V1Compat, GmpMode::V1 { compat: true }) => {
901                let mode = <CC::Inner<'_> as GmpContextInner<I, BC>>::mode_on_exit_compat();
902                debug_assert_eq!(mode.into(), GmpMode::V2);
903                enter_mode(bindings_ctx, state, mode);
904            }
905            (TimerIdInner::V2(timer), GmpMode::V2) => {
906                v2::handle_timer(&mut core_ctx, bindings_ctx, &device, timer, state);
907            }
908            (TimerIdInner::V1Compat, bad) => {
909                panic!("v1 compat timer fired in non v1 compat mode: {bad:?}")
910            }
911            bad @ (TimerIdInner::V1(_), GmpMode::V2)
912            | bad @ (TimerIdInner::V2(_), GmpMode::V1 { .. }) => {
913                panic!("incompatible timer fired {bad:?}")
914            }
915        }
916    });
917}
918
919/// Enters `mode` in the state referenced by `state`.
920///
921/// Mode changes cause all timers to be canceled, and the group state to be
922/// updated to the appropriate GMP version.
923///
924/// No-op if `new_mode` is current.
925fn enter_mode<I: IpExt, CC: GmpTypeLayout<I, BC>, BC: GmpBindingsContext>(
926    bindings_ctx: &mut BC,
927    state: GmpStateRef<'_, I, CC, BC>,
928    new_mode: CC::ProtoMode,
929) {
930    let GmpStateRef { enabled: _, gmp, groups, config: _ } = state;
931    let old_mode = core::mem::replace(&mut gmp.mode, new_mode);
932    match (old_mode.into(), gmp.gmp_mode()) {
933        (GmpMode::V1 { compat }, GmpMode::V1 { compat: new_compat }) => {
934            if new_compat != compat {
935                // While in v1 mode, we only allow exiting compat mode, not entering
936                // it again.
937                assert_eq!(new_compat, false, "attempted to enter compatibility mode from forced");
938                // Deschedule the compatibility mode exit timer.
939                assert_matches!(
940                    gmp.timers.cancel(bindings_ctx, &TimerIdInner::V1Compat),
941                    Some((_, ()))
942                );
943                info!("GMP({}) enter mode {:?}", I::NAME, &gmp.mode);
944            }
945            return;
946        }
947        (GmpMode::V2, GmpMode::V2) => {
948            // Same mode.
949            return;
950        }
951        (GmpMode::V1 { compat: _ }, GmpMode::V2) => {
952            // Transition to v2.
953            //
954            // Update the group state in each group to a default value which
955            // will not trigger any unsolicited reports and is ready to respond
956            // to incoming queries.
957            for (_, GmpGroupState { version_specific }) in groups.iter_mut() {
958                *version_specific =
959                    GmpGroupStateByVersion::V2(v2::GroupState::new_for_mode_transition())
960            }
961        }
962        (GmpMode::V2, GmpMode::V1 { compat: _ }) => {
963            // Transition to v1.
964            //
965            // Update the state machine in each group to the appropriate idle
966            // definition in GMPv1. This is a state with no timers that just
967            // waits to respond to queries.
968            for (_, GmpGroupState { version_specific }) in groups.iter_mut() {
969                *version_specific =
970                    GmpGroupStateByVersion::V1(v1::GmpStateMachine::new_for_mode_transition())
971            }
972            gmp.v2_proto.on_enter_v1();
973        }
974    };
975    info!("GMP({}) enter mode {:?}", I::NAME, new_mode);
976    gmp.timers.clear(bindings_ctx);
977    gmp.mode = new_mode;
978}
979
980fn schedule_v1_compat<I: IpExt, CC: GmpTypeLayout<I, BC>, BC: GmpBindingsContext>(
981    bindings_ctx: &mut BC,
982    state: GmpStateRef<'_, I, CC, BC>,
983) {
984    let GmpStateRef { gmp, config, .. } = state;
985    let timeout = gmp.v2_proto.older_version_querier_present_timeout(config);
986    let _: Option<_> =
987        gmp.timers.schedule_after(bindings_ctx, TimerIdInner::V1Compat, (), timeout.into());
988}
989
990/// Error returned when operating queries but the host is not a member.
991#[cfg_attr(test, derive(Debug, Eq, PartialEq))]
992struct NotAMemberErr<I: Ip>(I::Addr);
993
994/// The group targeted in a query message.
995enum QueryTarget<A> {
996    Unspecified,
997    Specified(MulticastAddr<A>),
998}
999
1000impl<A: IpAddress> QueryTarget<A> {
1001    fn new(addr: A) -> Option<Self> {
1002        if addr == <A::Version as Ip>::UNSPECIFIED_ADDRESS {
1003            Some(Self::Unspecified)
1004        } else {
1005            MulticastAddr::new(addr).map(Self::Specified)
1006        }
1007    }
1008}
1009
1010mod witness {
1011    use super::*;
1012
1013    /// A witness type for an IP multicast address that passes
1014    /// [`IpExt::should_perform_gmp`].
1015    #[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)]
1016    pub(super) struct GmpEnabledGroup<A>(MulticastAddr<A>);
1017
1018    impl<A: IpAddress<Version: IpExt>> GmpEnabledGroup<A> {
1019        /// Creates a new `GmpEnabledGroup` if `addr` should have GMP performed
1020        /// on it.
1021        pub fn new(addr: MulticastAddr<A>) -> Option<Self> {
1022            <A::Version as IpExt>::should_perform_gmp(addr).then_some(Self(addr))
1023        }
1024
1025        /// Like [`GmpEnabledGroup::new`] but returns a `Result` with `addr` on
1026        /// `Err`.
1027        pub fn try_new(addr: MulticastAddr<A>) -> Result<Self, MulticastAddr<A>> {
1028            Self::new(addr).ok_or(addr)
1029        }
1030
1031        /// Returns a copy of the multicast address witness.
1032        pub fn multicast_addr(&self) -> MulticastAddr<A> {
1033            let Self(addr) = self;
1034            *addr
1035        }
1036
1037        /// Consumes the witness returning a multicast address.
1038        pub fn into_multicast_addr(self) -> MulticastAddr<A> {
1039            let Self(addr) = self;
1040            addr
1041        }
1042    }
1043
1044    impl<A> AsRef<MulticastAddr<A>> for GmpEnabledGroup<A> {
1045        fn as_ref(&self) -> &MulticastAddr<A> {
1046            let Self(addr) = self;
1047            addr
1048        }
1049    }
1050}
1051use witness::GmpEnabledGroup;
1052
1053#[cfg(test)]
1054mod tests {
1055    use alloc::vec::Vec;
1056    use core::num::NonZeroU8;
1057
1058    use assert_matches::assert_matches;
1059    use ip_test_macro::ip_test;
1060    use net_types::Witness as _;
1061    use netstack3_base::testutil::{FakeDeviceId, FakeTimerCtxExt, FakeWeakDeviceId};
1062    use netstack3_base::InstantContext as _;
1063
1064    use testutil::{FakeCtx, FakeGmpContextInner, FakeV1Query, TestIpExt};
1065
1066    use super::*;
1067
1068    #[ip_test(I)]
1069    fn mode_change_state_clearing<I: TestIpExt>() {
1070        let FakeCtx { mut core_ctx, mut bindings_ctx } =
1071            testutil::new_context_with_mode::<I>(GmpMode::V1 { compat: false });
1072
1073        assert_eq!(
1074            core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1075            GroupJoinResult::Joined(())
1076        );
1077        // Drop the group join message so we can assert no more messages are
1078        // sent after this.
1079        core_ctx.inner.v1_messages.clear();
1080
1081        // We should now have timers installed and v1 state in groups.
1082        assert!(core_ctx.gmp.timers.iter().next().is_some());
1083        assert_matches!(
1084            core_ctx.groups.get(&I::GROUP_ADDR1).unwrap().version_specific,
1085            GmpGroupStateByVersion::V1(_)
1086        );
1087
1088        core_ctx.with_gmp_state_mut(&FakeDeviceId, |mut state| {
1089            enter_mode(&mut bindings_ctx, state.as_mut(), GmpMode::V2);
1090            assert_eq!(state.gmp.mode, GmpMode::V2);
1091        });
1092        // Timers were removed and state is now v2.
1093        core_ctx.gmp.timers.assert_timers([]);
1094        assert_matches!(
1095            core_ctx.groups.get(&I::GROUP_ADDR1).unwrap().version_specific,
1096            GmpGroupStateByVersion::V2(_)
1097        );
1098
1099        // Moving back moves the state back to v1.
1100        core_ctx.with_gmp_state_mut(&FakeDeviceId, |mut state| {
1101            enter_mode(&mut bindings_ctx, state.as_mut(), GmpMode::V1 { compat: false });
1102            assert_eq!(state.gmp.mode, GmpMode::V1 { compat: false });
1103        });
1104        assert_matches!(
1105            core_ctx.groups.get(&I::GROUP_ADDR1).unwrap().version_specific,
1106            GmpGroupStateByVersion::V1(_)
1107        );
1108
1109        // Throughout we should've generated no traffic.
1110        let FakeGmpContextInner { v1_messages, v2_messages } = &core_ctx.inner;
1111        assert_eq!(v1_messages, &Vec::new());
1112        assert_eq!(v2_messages, &Vec::<Vec<_>>::new());
1113    }
1114
1115    #[ip_test(I)]
1116    #[should_panic(expected = "attempted to enter compatibility mode from forced")]
1117    fn cant_enter_v1_compat<I: TestIpExt>() {
1118        let FakeCtx { mut core_ctx, mut bindings_ctx } =
1119            testutil::new_context_with_mode::<I>(GmpMode::V1 { compat: false });
1120        core_ctx.with_gmp_state_mut(&FakeDeviceId, |mut state| {
1121            enter_mode(&mut bindings_ctx, state.as_mut(), GmpMode::V1 { compat: true });
1122        });
1123    }
1124
1125    #[ip_test(I)]
1126    fn disable_exits_compat<I: TestIpExt>() {
1127        // Disabling in compat mode returns to v2.
1128        let FakeCtx { mut core_ctx, mut bindings_ctx } =
1129            testutil::new_context_with_mode::<I>(GmpMode::V1 { compat: true });
1130        core_ctx.enabled = false;
1131        core_ctx.gmp_handle_disabled(&mut bindings_ctx, &FakeDeviceId);
1132        assert_eq!(core_ctx.gmp.mode, GmpMode::V2);
1133
1134        // Same is not true for not compat v1.
1135        let FakeCtx { mut core_ctx, mut bindings_ctx } =
1136            testutil::new_context_with_mode::<I>(GmpMode::V1 { compat: false });
1137        core_ctx.enabled = false;
1138        core_ctx.gmp_handle_disabled(&mut bindings_ctx, &FakeDeviceId);
1139        assert_eq!(core_ctx.gmp.mode, GmpMode::V1 { compat: false });
1140    }
1141
1142    #[ip_test(I)]
1143    fn disable_clears_v2_state<I: TestIpExt>() {
1144        let FakeCtx { mut core_ctx, mut bindings_ctx } =
1145            testutil::new_context_with_mode::<I>(GmpMode::V1 { compat: false });
1146        let v2::ProtocolState { robustness_variable, query_interval, left_groups } =
1147            &mut core_ctx.gmp.v2_proto;
1148        *robustness_variable = robustness_variable.checked_add(1).unwrap();
1149        *query_interval = *query_interval + Duration::from_secs(20);
1150        *left_groups =
1151            [(GmpEnabledGroup::new(I::GROUP_ADDR1).unwrap(), NonZeroU8::new(1).unwrap())]
1152                .into_iter()
1153                .collect();
1154        core_ctx.enabled = false;
1155        core_ctx.gmp_handle_disabled(&mut bindings_ctx, &FakeDeviceId);
1156        assert_eq!(core_ctx.gmp.v2_proto, v2::ProtocolState::default());
1157    }
1158
1159    #[ip_test(I)]
1160    fn v1_compat_mode_on_timeout<I: TestIpExt>() {
1161        let FakeCtx { mut core_ctx, mut bindings_ctx } =
1162            testutil::new_context_with_mode::<I>(GmpMode::V2);
1163        assert_eq!(
1164            v1::handle_query_message(
1165                &mut core_ctx,
1166                &mut bindings_ctx,
1167                &FakeDeviceId,
1168                &FakeV1Query {
1169                    group_addr: I::GROUP_ADDR1.get(),
1170                    max_response_time: Duration::from_secs(1)
1171                }
1172            ),
1173            Err(NotAMemberErr(I::GROUP_ADDR1.get()))
1174        );
1175        // Now in v1 mode and a compat timer is scheduled.
1176        assert_eq!(core_ctx.gmp.mode, GmpMode::V1 { compat: true });
1177
1178        let timeout =
1179            core_ctx.gmp.v2_proto.older_version_querier_present_timeout(&core_ctx.config).into();
1180        core_ctx.gmp.timers.assert_timers([(
1181            TimerIdInner::V1Compat,
1182            (),
1183            bindings_ctx.now() + timeout,
1184        )]);
1185
1186        // Increment the time and see that the timer updates.
1187        bindings_ctx.timers.instant.sleep(timeout / 2);
1188        assert_eq!(
1189            v1::handle_query_message(
1190                &mut core_ctx,
1191                &mut bindings_ctx,
1192                &FakeDeviceId,
1193                &FakeV1Query {
1194                    group_addr: I::GROUP_ADDR1.get(),
1195                    max_response_time: Duration::from_secs(1)
1196                }
1197            ),
1198            Err(NotAMemberErr(I::GROUP_ADDR1.get()))
1199        );
1200        assert_eq!(core_ctx.gmp.mode, GmpMode::V1 { compat: true });
1201        core_ctx.gmp.timers.assert_timers([(
1202            TimerIdInner::V1Compat,
1203            (),
1204            bindings_ctx.now() + timeout,
1205        )]);
1206
1207        // Trigger the timer and observe a fallback to v2.
1208        let timer = bindings_ctx.trigger_next_timer(&mut core_ctx);
1209        assert_eq!(timer, Some(GmpTimerId::new(FakeWeakDeviceId(FakeDeviceId))));
1210        assert_eq!(core_ctx.gmp.mode, GmpMode::V2);
1211        // No more timers should exist, no frames are sent out.
1212        core_ctx.gmp.timers.assert_timers([]);
1213        let testutil::FakeGmpContextInner { v1_messages, v2_messages } = &core_ctx.inner;
1214        assert_eq!(v1_messages, &Vec::new());
1215        assert_eq!(v2_messages, &Vec::<Vec<_>>::new());
1216    }
1217}