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::MulticastAddr;
51use net_types::ip::{Ip, IpAddress, IpVersionMarker};
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.random_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 { Self::Enabled } else { Self::Disabled }
507    }
508}
509
510// NB: This block is not bound on GmpBindingsContext because we don't need
511// RngContext to construct GmpState.
512impl<I: Ip, T: GmpTypeLayout<I, BC>, BC: GmpBindingsTypes + TimerContext> GmpState<I, T, BC> {
513    /// Constructs a new `GmpState` for `device`.
514    pub fn new<D: WeakDeviceIdentifier, CC: CoreTimerContext<GmpTimerId<I, D>, BC>>(
515        bindings_ctx: &mut BC,
516        device: D,
517    ) -> Self {
518        Self::new_with_enabled_and_mode::<D, CC>(bindings_ctx, device, false, Default::default())
519    }
520
521    /// Constructs a new `GmpState` for `device` assuming initial enabled state
522    /// `enabled` and `mode`.
523    ///
524    /// This is meant to be called directly only in test scenarios (besides
525    /// helping implement [`GmpState::new`] that is) where to decrease test
526    /// verbosity `GmpState` can be created in a state that assumes
527    /// [`GmpHandler::gmp_handle_maybe_enabled`] was called.
528    fn new_with_enabled_and_mode<
529        D: WeakDeviceIdentifier,
530        CC: CoreTimerContext<GmpTimerId<I, D>, BC>,
531    >(
532        bindings_ctx: &mut BC,
533        device: D,
534        enabled: bool,
535        mode: T::ProtoMode,
536    ) -> Self {
537        Self {
538            timers: LocalTimerHeap::new_with_context::<_, CC>(
539                bindings_ctx,
540                GmpTimerId::new(device),
541            ),
542            mode,
543            v2_proto: Default::default(),
544            enablement_idempotency_guard: LastState::from_enabled(enabled),
545        }
546    }
547}
548
549impl<I: IpExt, T: GmpTypeLayout<I, BT>, BT: GmpBindingsTypes> GmpState<I, T, BT> {
550    fn gmp_mode(&self) -> GmpMode {
551        self.mode.into()
552    }
553
554    pub(crate) fn mode(&self) -> &T::ProtoMode {
555        &self.mode
556    }
557}
558
559/// A reference to a device's GMP state.
560pub struct GmpStateRef<'a, I: IpExt, CC: GmpTypeLayout<I, BT>, BT: GmpBindingsTypes> {
561    /// True if GMP is enabled for the device.
562    pub enabled: bool,
563    /// Mutable reference to the multicast groups on a device.
564    pub groups: &'a mut MulticastGroupSet<I::Addr, GmpGroupState<I, BT>>,
565    /// Mutable reference to the device's GMP state.
566    pub gmp: &'a mut GmpState<I, CC, BT>,
567    /// Protocol specific configuration.
568    pub config: &'a CC::Config,
569}
570
571impl<'a, I: IpExt, CC: GmpTypeLayout<I, BT>, BT: GmpBindingsTypes> GmpStateRef<'a, I, CC, BT> {
572    fn as_mut(&mut self) -> GmpStateRef<'_, I, CC, BT> {
573        let Self { enabled, groups, gmp, config } = self;
574        GmpStateRef { enabled: *enabled, groups, gmp, config }
575    }
576}
577
578/// Provides IP-specific associated types for GMP.
579pub trait GmpTypeLayout<I: Ip, BT: GmpBindingsTypes>: Sized {
580    /// The type for protocol-specific configs.
581    type Config: Debug + v1::ProtocolConfig + v2::ProtocolConfig;
582    /// Protocol-specific mode.
583    type ProtoMode: Debug
584        + Copy
585        + Clone
586        + Eq
587        + PartialEq
588        + Into<GmpMode>
589        + Default
590        + InspectableValue;
591}
592
593/// The state kept by each muitlcast group the host is a member of.
594pub struct GmpGroupState<I: Ip, BT: GmpBindingsTypes> {
595    version_specific: GmpGroupStateByVersion<I, BT>,
596    // TODO(https://fxbug.dev/381241191): When we support SSM, each group should
597    // keep track of the source interest and filter modes.
598}
599
600impl<I: Ip, BT: GmpBindingsTypes> GmpGroupState<I, BT> {
601    /// Retrieves a mutable borrow to the v1 state machine value.
602    ///
603    /// # Panics
604    ///
605    /// Panics if the state machine is not in the v1 state. When switching
606    /// modes, GMP is responsible for updating all group states to the
607    /// appropriate version.
608    fn v1_mut(&mut self) -> &mut v1::GmpStateMachine<BT::Instant> {
609        match &mut self.version_specific {
610            GmpGroupStateByVersion::V1(v1) => return v1,
611            GmpGroupStateByVersion::V2(_) => {
612                panic!("expected GMP v1")
613            }
614        }
615    }
616
617    /// Retrieves a mutable borrow to the v2 state machine value.
618    ///
619    /// # Panics
620    ///
621    /// Panics if the state machine is not in the v2 state. When switching
622    /// modes, GMP is responsible for updating all group states to the
623    /// appropriate version.
624    fn v2_mut(&mut self) -> &mut v2::GroupState<I> {
625        match &mut self.version_specific {
626            GmpGroupStateByVersion::V2(v2) => return v2,
627            GmpGroupStateByVersion::V1(_) => {
628                panic!("expected GMP v2")
629            }
630        }
631    }
632
633    /// Like [`GmpGroupState::v1_mut`] but returns a non mutable borrow.
634    #[cfg(test)]
635    fn v1(&self) -> &v1::GmpStateMachine<BT::Instant> {
636        match &self.version_specific {
637            GmpGroupStateByVersion::V1(v1) => v1,
638            GmpGroupStateByVersion::V2(_) => panic!("group not in v1 mode"),
639        }
640    }
641
642    /// Like [`GmpGroupState::v2_mut`] but returns a non mutable borrow.
643    fn v2(&self) -> &v2::GroupState<I> {
644        match &self.version_specific {
645            GmpGroupStateByVersion::V2(v2) => v2,
646            GmpGroupStateByVersion::V1 { .. } => panic!("group not in v2 mode"),
647        }
648    }
649
650    /// Equivalent to [`GmpGroupState::v1_mut`] but drops all remaining state.
651    ///
652    /// # Panics
653    ///
654    /// See [`GmpGroupState::v1`].
655    fn into_v1(self) -> v1::GmpStateMachine<BT::Instant> {
656        let Self { version_specific } = self;
657        match version_specific {
658            GmpGroupStateByVersion::V1(v1) => v1,
659            GmpGroupStateByVersion::V2(_) => panic!("expected GMP v1"),
660        }
661    }
662
663    /// Equivalent to [`GmpGroupState::v2_mut`] but drops all remaining state.
664    ///
665    /// # Panics
666    ///
667    /// See [`GmpGroupState::v2`].
668    fn into_v2(self) -> v2::GroupState<I> {
669        let Self { version_specific } = self;
670        match version_specific {
671            GmpGroupStateByVersion::V2(v2) => v2,
672            GmpGroupStateByVersion::V1(_) => panic!("expected GMP v2"),
673        }
674    }
675
676    /// Creates a new `GmpGroupState` with associated v1 state machine.
677    fn new_v1(v1: v1::GmpStateMachine<BT::Instant>) -> Self {
678        Self { version_specific: GmpGroupStateByVersion::V1(v1) }
679    }
680
681    /// Creates a new `GmpGroupState` with associated v2 state.
682    fn new_v2(v2: v2::GroupState<I>) -> Self {
683        Self { version_specific: GmpGroupStateByVersion::V2(v2) }
684    }
685}
686
687/// GMP Compatibility mode.
688#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
689pub enum GmpMode {
690    /// GMP operating in v1 mode. This is the mode supporting MLDv1 and IGMPv2.
691    V1 {
692        /// Compat indicates v1 mode behavior.
693        ///
694        /// It is `true` if v1 is entered due to receiving a query from a v1
695        /// router. It is `false` if v1 is entered due to administrative action
696        /// (i.e. "forcing" v1 mode).
697        ///
698        /// It effectively controls whether we can revert back to v2 on
699        /// interface disable <-> re-enable.
700        compat: bool,
701    },
702    /// GMP operating in v2 mode. This is the mode supporting MLDv2 and IGMPv3.
703    #[default]
704    V2,
705}
706
707impl GmpMode {
708    fn is_v1(&self) -> bool {
709        match self {
710            Self::V1 { .. } => true,
711            Self::V2 => false,
712        }
713    }
714
715    fn is_v2(&self) -> bool {
716        match self {
717            Self::V2 => true,
718            Self::V1 { .. } => false,
719        }
720    }
721
722    fn maybe_enter_v1_compat(&self) -> Self {
723        match self {
724            // Enter v1 compat if in v2.
725            Self::V2 => Self::V1 { compat: true },
726            // Maintain compat value.
727            m @ Self::V1 { .. } => *m,
728        }
729    }
730
731    fn maybe_exit_v1_compat(&self) -> Self {
732        match self {
733            // Maintain mode if not in compat.
734            m @ Self::V2 | m @ Self::V1 { compat: false } => *m,
735            // Exit compat mode.
736            Self::V1 { compat: true } => Self::V2,
737        }
738    }
739}
740
741#[cfg_attr(test, derive(derivative::Derivative))]
742#[cfg_attr(test, derivative(Debug(bound = "")))]
743enum GmpGroupStateByVersion<I: Ip, BT: GmpBindingsTypes> {
744    V1(v1::GmpStateMachine<BT::Instant>),
745    V2(v2::GroupState<I>),
746}
747
748/// Provides immutable access to GMP state.
749pub trait GmpStateContext<I: IpExt, BT: GmpBindingsTypes>: DeviceIdContext<AnyDevice> {
750    /// The types used by this context.
751    type TypeLayout: GmpTypeLayout<I, BT>;
752
753    /// Calls the function with immutable access to the [`MulticastGroupSet`].
754    fn with_multicast_groups<
755        O,
756        F: FnOnce(&MulticastGroupSet<I::Addr, GmpGroupState<I, BT>>) -> O,
757    >(
758        &mut self,
759        device: &Self::DeviceId,
760        cb: F,
761    ) -> O {
762        self.with_gmp_state(device, |groups, _gmp_state| cb(groups))
763    }
764
765    /// Calls the function with immutable access to the [`MulticastGroupSet`]
766    /// and current GMP state.
767    fn with_gmp_state<
768        O,
769        F: FnOnce(
770            &MulticastGroupSet<I::Addr, GmpGroupState<I, BT>>,
771            &GmpState<I, Self::TypeLayout, BT>,
772        ) -> O,
773    >(
774        &mut self,
775        device: &Self::DeviceId,
776        cb: F,
777    ) -> O;
778}
779
780/// Provides common functionality for GMP context implementations.
781///
782/// This trait implements portions of a group management protocol.
783trait GmpContext<I: IpExt, BC: GmpBindingsContext>: DeviceIdContext<AnyDevice> {
784    /// The types used by this context.
785    type TypeLayout: GmpTypeLayout<I, BC>;
786
787    /// The inner context given to `with_gmp_state_mut_and_ctx`.
788    type Inner<'a>: GmpContextInner<I, BC, TypeLayout = Self::TypeLayout, DeviceId = Self::DeviceId>
789        + 'a;
790
791    /// Calls the function with mutable access to GMP state in [`GmpStateRef`]
792    /// and access to a [`GmpContextInner`] context.
793    fn with_gmp_state_mut_and_ctx<
794        O,
795        F: FnOnce(Self::Inner<'_>, GmpStateRef<'_, I, Self::TypeLayout, BC>) -> O,
796    >(
797        &mut self,
798        device: &Self::DeviceId,
799        cb: F,
800    ) -> O;
801
802    /// Calls the function with mutable access to GMP state in [`GmpStateRef`].
803    fn with_gmp_state_mut<O, F: FnOnce(GmpStateRef<'_, I, Self::TypeLayout, BC>) -> O>(
804        &mut self,
805        device: &Self::DeviceId,
806        cb: F,
807    ) -> O {
808        self.with_gmp_state_mut_and_ctx(device, |_core_ctx, state| cb(state))
809    }
810}
811
812/// The inner GMP context.
813///
814/// Provides access to external actions while holding the GMP state lock.
815trait GmpContextInner<I: IpExt, BC: GmpBindingsContext>: DeviceIdContext<AnyDevice> {
816    /// The types used by this context,
817    type TypeLayout: GmpTypeLayout<I, BC>;
818
819    /// Sends a GMPv1 message.
820    fn send_message_v1(
821        &mut self,
822        bindings_ctx: &mut BC,
823        device: &Self::DeviceId,
824        cur_mode: &<Self::TypeLayout as GmpTypeLayout<I, BC>>::ProtoMode,
825        group_addr: GmpEnabledGroup<I::Addr>,
826        msg_type: v1::GmpMessageType,
827    );
828
829    /// Sends a GMPv2 report message.
830    fn send_report_v2(
831        &mut self,
832        bindings_ctx: &mut BC,
833        device: &Self::DeviceId,
834        groups: impl Iterator<Item: v2::VerifiedReportGroupRecord<I::Addr> + Clone> + Clone,
835    );
836
837    /// Returns the compatibility mode to enter upon observing `query` with
838    /// `config`.
839    fn mode_update_from_v1_query<Q: v1::QueryMessage<I>>(
840        &mut self,
841        bindings_ctx: &mut BC,
842        query: &Q,
843        gmp_state: &GmpState<I, Self::TypeLayout, BC>,
844        config: &<Self::TypeLayout as GmpTypeLayout<I, BC>>::Config,
845    ) -> <Self::TypeLayout as GmpTypeLayout<I, BC>>::ProtoMode;
846
847    /// Returns the current operating mode as the user configuration value from
848    /// the current generic GMP mode + protocol state.
849    fn mode_to_config(
850        mode: &<Self::TypeLayout as GmpTypeLayout<I, BC>>::ProtoMode,
851    ) -> I::GmpProtoConfigMode;
852
853    /// Returns the new mode to enter based on the current mode `cur_mode` and
854    /// user-provided configuration `config`.
855    fn config_to_mode(
856        cur_mode: &<Self::TypeLayout as GmpTypeLayout<I, BC>>::ProtoMode,
857        config: I::GmpProtoConfigMode,
858    ) -> <Self::TypeLayout as GmpTypeLayout<I, BC>>::ProtoMode;
859
860    /// Returns the new mode to use as a result of disabling GMP on an
861    /// interface.
862    fn mode_on_disable(
863        cur_mode: &<Self::TypeLayout as GmpTypeLayout<I, BC>>::ProtoMode,
864    ) -> <Self::TypeLayout as GmpTypeLayout<I, BC>>::ProtoMode;
865
866    /// The mode to enter when GMP's compat timer fires.
867    ///
868    /// The returned mode *MUST* be [`GmpMode::V2`] when converted.
869    fn mode_on_exit_compat() -> <Self::TypeLayout as GmpTypeLayout<I, BC>>::ProtoMode;
870}
871
872fn handle_timer<I, BC, CC>(
873    core_ctx: &mut CC,
874    bindings_ctx: &mut BC,
875    timer: GmpTimerId<I, CC::WeakDeviceId>,
876) where
877    BC: GmpBindingsContext,
878    CC: GmpContext<I, BC>,
879    I: IpExt,
880{
881    let GmpTimerId { device, _marker: IpVersionMarker { .. } } = timer;
882    let Some(device) = device.upgrade() else {
883        return;
884    };
885    core_ctx.with_gmp_state_mut_and_ctx(&device, |mut core_ctx, state| {
886        let Some((timer_id, ())) = state.gmp.timers.pop(bindings_ctx) else {
887            return;
888        };
889        // No timers should be firing if the state is disabled.
890        assert!(state.enabled, "{timer_id:?} fired in GMP disabled state");
891
892        match (timer_id, state.gmp.gmp_mode()) {
893            (TimerIdInner::V1(v1), GmpMode::V1 { .. }) => {
894                v1::handle_timer(&mut core_ctx, bindings_ctx, &device, state, v1);
895            }
896            (TimerIdInner::V1Compat, GmpMode::V1 { compat: true }) => {
897                let mode = <CC::Inner<'_> as GmpContextInner<I, BC>>::mode_on_exit_compat();
898                debug_assert_eq!(mode.into(), GmpMode::V2);
899                enter_mode(bindings_ctx, state, mode);
900            }
901            (TimerIdInner::V2(timer), GmpMode::V2) => {
902                v2::handle_timer(&mut core_ctx, bindings_ctx, &device, timer, state);
903            }
904            (TimerIdInner::V1Compat, bad) => {
905                panic!("v1 compat timer fired in non v1 compat mode: {bad:?}")
906            }
907            bad @ (TimerIdInner::V1(_), GmpMode::V2)
908            | bad @ (TimerIdInner::V2(_), GmpMode::V1 { .. }) => {
909                panic!("incompatible timer fired {bad:?}")
910            }
911        }
912    });
913}
914
915/// Enters `mode` in the state referenced by `state`.
916///
917/// Mode changes cause all timers to be canceled, and the group state to be
918/// updated to the appropriate GMP version.
919///
920/// No-op if `new_mode` is current.
921fn enter_mode<I: IpExt, CC: GmpTypeLayout<I, BC>, BC: GmpBindingsContext>(
922    bindings_ctx: &mut BC,
923    state: GmpStateRef<'_, I, CC, BC>,
924    new_mode: CC::ProtoMode,
925) {
926    let GmpStateRef { enabled: _, gmp, groups, config: _ } = state;
927    let old_mode = core::mem::replace(&mut gmp.mode, new_mode);
928    match (old_mode.into(), gmp.gmp_mode()) {
929        (GmpMode::V1 { compat }, GmpMode::V1 { compat: new_compat }) => {
930            if new_compat != compat {
931                // While in v1 mode, we only allow exiting compat mode, not entering
932                // it again.
933                assert_eq!(new_compat, false, "attempted to enter compatibility mode from forced");
934                // Deschedule the compatibility mode exit timer.
935                assert_matches!(
936                    gmp.timers.cancel(bindings_ctx, &TimerIdInner::V1Compat),
937                    Some((_, ()))
938                );
939                info!("GMP({}) enter mode {:?}", I::NAME, &gmp.mode);
940            }
941            return;
942        }
943        (GmpMode::V2, GmpMode::V2) => {
944            // Same mode.
945            return;
946        }
947        (GmpMode::V1 { compat: _ }, GmpMode::V2) => {
948            // Transition to v2.
949            //
950            // Update the group state in each group to a default value which
951            // will not trigger any unsolicited reports and is ready to respond
952            // to incoming queries.
953            for (_, GmpGroupState { version_specific }) in groups.iter_mut() {
954                *version_specific =
955                    GmpGroupStateByVersion::V2(v2::GroupState::new_for_mode_transition())
956            }
957        }
958        (GmpMode::V2, GmpMode::V1 { compat: _ }) => {
959            // Transition to v1.
960            //
961            // Update the state machine in each group to the appropriate idle
962            // definition in GMPv1. This is a state with no timers that just
963            // waits to respond to queries.
964            for (_, GmpGroupState { version_specific }) in groups.iter_mut() {
965                *version_specific =
966                    GmpGroupStateByVersion::V1(v1::GmpStateMachine::new_for_mode_transition())
967            }
968            gmp.v2_proto.on_enter_v1();
969        }
970    };
971    info!("GMP({}) enter mode {:?}", I::NAME, new_mode);
972    gmp.timers.clear(bindings_ctx);
973    gmp.mode = new_mode;
974}
975
976fn schedule_v1_compat<I: IpExt, CC: GmpTypeLayout<I, BC>, BC: GmpBindingsContext>(
977    bindings_ctx: &mut BC,
978    state: GmpStateRef<'_, I, CC, BC>,
979) {
980    let GmpStateRef { gmp, config, .. } = state;
981    let timeout = gmp.v2_proto.older_version_querier_present_timeout(config);
982    let _: Option<_> =
983        gmp.timers.schedule_after(bindings_ctx, TimerIdInner::V1Compat, (), timeout.into());
984}
985
986/// Error returned when operating queries but the host is not a member.
987#[cfg_attr(test, derive(Debug, Eq, PartialEq))]
988struct NotAMemberErr<I: Ip>(I::Addr);
989
990/// The group targeted in a query message.
991enum QueryTarget<A> {
992    Unspecified,
993    Specified(MulticastAddr<A>),
994}
995
996impl<A: IpAddress> QueryTarget<A> {
997    fn new(addr: A) -> Option<Self> {
998        if addr == <A::Version as Ip>::UNSPECIFIED_ADDRESS {
999            Some(Self::Unspecified)
1000        } else {
1001            MulticastAddr::new(addr).map(Self::Specified)
1002        }
1003    }
1004}
1005
1006mod witness {
1007    use super::*;
1008
1009    /// A witness type for an IP multicast address that passes
1010    /// [`IpExt::should_perform_gmp`].
1011    #[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)]
1012    pub(super) struct GmpEnabledGroup<A>(MulticastAddr<A>);
1013
1014    impl<A: IpAddress<Version: IpExt>> GmpEnabledGroup<A> {
1015        /// Creates a new `GmpEnabledGroup` if `addr` should have GMP performed
1016        /// on it.
1017        pub fn new(addr: MulticastAddr<A>) -> Option<Self> {
1018            <A::Version as IpExt>::should_perform_gmp(addr).then_some(Self(addr))
1019        }
1020
1021        /// Like [`GmpEnabledGroup::new`] but returns a `Result` with `addr` on
1022        /// `Err`.
1023        pub fn try_new(addr: MulticastAddr<A>) -> Result<Self, MulticastAddr<A>> {
1024            Self::new(addr).ok_or(addr)
1025        }
1026
1027        /// Returns a copy of the multicast address witness.
1028        pub fn multicast_addr(&self) -> MulticastAddr<A> {
1029            let Self(addr) = self;
1030            *addr
1031        }
1032
1033        /// Consumes the witness returning a multicast address.
1034        pub fn into_multicast_addr(self) -> MulticastAddr<A> {
1035            let Self(addr) = self;
1036            addr
1037        }
1038    }
1039
1040    impl<A> AsRef<MulticastAddr<A>> for GmpEnabledGroup<A> {
1041        fn as_ref(&self) -> &MulticastAddr<A> {
1042            let Self(addr) = self;
1043            addr
1044        }
1045    }
1046}
1047use witness::GmpEnabledGroup;
1048
1049#[cfg(test)]
1050mod tests {
1051    use alloc::vec::Vec;
1052    use core::num::NonZeroU8;
1053
1054    use assert_matches::assert_matches;
1055    use ip_test_macro::ip_test;
1056    use net_types::Witness as _;
1057    use netstack3_base::InstantContext as _;
1058    use netstack3_base::testutil::{FakeDeviceId, FakeTimerCtxExt, FakeWeakDeviceId};
1059
1060    use testutil::{FakeCtx, FakeGmpContextInner, FakeV1Query, TestIpExt};
1061
1062    use super::*;
1063
1064    #[ip_test(I)]
1065    fn mode_change_state_clearing<I: TestIpExt>() {
1066        let FakeCtx { mut core_ctx, mut bindings_ctx } =
1067            testutil::new_context_with_mode::<I>(GmpMode::V1 { compat: false });
1068
1069        assert_eq!(
1070            core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1071            GroupJoinResult::Joined(())
1072        );
1073        // Drop the group join message so we can assert no more messages are
1074        // sent after this.
1075        core_ctx.inner.v1_messages.clear();
1076
1077        // We should now have timers installed and v1 state in groups.
1078        assert!(core_ctx.gmp.timers.iter().next().is_some());
1079        assert_matches!(
1080            core_ctx.groups.get(&I::GROUP_ADDR1).unwrap().version_specific,
1081            GmpGroupStateByVersion::V1(_)
1082        );
1083
1084        core_ctx.with_gmp_state_mut(&FakeDeviceId, |mut state| {
1085            enter_mode(&mut bindings_ctx, state.as_mut(), GmpMode::V2);
1086            assert_eq!(state.gmp.mode, GmpMode::V2);
1087        });
1088        // Timers were removed and state is now v2.
1089        core_ctx.gmp.timers.assert_timers([]);
1090        assert_matches!(
1091            core_ctx.groups.get(&I::GROUP_ADDR1).unwrap().version_specific,
1092            GmpGroupStateByVersion::V2(_)
1093        );
1094
1095        // Moving back moves the state back to v1.
1096        core_ctx.with_gmp_state_mut(&FakeDeviceId, |mut state| {
1097            enter_mode(&mut bindings_ctx, state.as_mut(), GmpMode::V1 { compat: false });
1098            assert_eq!(state.gmp.mode, GmpMode::V1 { compat: false });
1099        });
1100        assert_matches!(
1101            core_ctx.groups.get(&I::GROUP_ADDR1).unwrap().version_specific,
1102            GmpGroupStateByVersion::V1(_)
1103        );
1104
1105        // Throughout we should've generated no traffic.
1106        let FakeGmpContextInner { v1_messages, v2_messages } = &core_ctx.inner;
1107        assert_eq!(v1_messages, &Vec::new());
1108        assert_eq!(v2_messages, &Vec::<Vec<_>>::new());
1109    }
1110
1111    #[ip_test(I)]
1112    #[should_panic(expected = "attempted to enter compatibility mode from forced")]
1113    fn cant_enter_v1_compat<I: TestIpExt>() {
1114        let FakeCtx { mut core_ctx, mut bindings_ctx } =
1115            testutil::new_context_with_mode::<I>(GmpMode::V1 { compat: false });
1116        core_ctx.with_gmp_state_mut(&FakeDeviceId, |mut state| {
1117            enter_mode(&mut bindings_ctx, state.as_mut(), GmpMode::V1 { compat: true });
1118        });
1119    }
1120
1121    #[ip_test(I)]
1122    fn disable_exits_compat<I: TestIpExt>() {
1123        // Disabling in compat mode returns to v2.
1124        let FakeCtx { mut core_ctx, mut bindings_ctx } =
1125            testutil::new_context_with_mode::<I>(GmpMode::V1 { compat: true });
1126        core_ctx.enabled = false;
1127        core_ctx.gmp_handle_disabled(&mut bindings_ctx, &FakeDeviceId);
1128        assert_eq!(core_ctx.gmp.mode, GmpMode::V2);
1129
1130        // Same is not true for not compat v1.
1131        let FakeCtx { mut core_ctx, mut bindings_ctx } =
1132            testutil::new_context_with_mode::<I>(GmpMode::V1 { compat: false });
1133        core_ctx.enabled = false;
1134        core_ctx.gmp_handle_disabled(&mut bindings_ctx, &FakeDeviceId);
1135        assert_eq!(core_ctx.gmp.mode, GmpMode::V1 { compat: false });
1136    }
1137
1138    #[ip_test(I)]
1139    fn disable_clears_v2_state<I: TestIpExt>() {
1140        let FakeCtx { mut core_ctx, mut bindings_ctx } =
1141            testutil::new_context_with_mode::<I>(GmpMode::V1 { compat: false });
1142        let v2::ProtocolState { robustness_variable, query_interval, left_groups } =
1143            &mut core_ctx.gmp.v2_proto;
1144        *robustness_variable = robustness_variable.checked_add(1).unwrap();
1145        *query_interval = *query_interval + Duration::from_secs(20);
1146        *left_groups =
1147            [(GmpEnabledGroup::new(I::GROUP_ADDR1).unwrap(), NonZeroU8::new(1).unwrap())]
1148                .into_iter()
1149                .collect();
1150        core_ctx.enabled = false;
1151        core_ctx.gmp_handle_disabled(&mut bindings_ctx, &FakeDeviceId);
1152        assert_eq!(core_ctx.gmp.v2_proto, v2::ProtocolState::default());
1153    }
1154
1155    #[ip_test(I)]
1156    fn v1_compat_mode_on_timeout<I: TestIpExt>() {
1157        let FakeCtx { mut core_ctx, mut bindings_ctx } =
1158            testutil::new_context_with_mode::<I>(GmpMode::V2);
1159        assert_eq!(
1160            v1::handle_query_message(
1161                &mut core_ctx,
1162                &mut bindings_ctx,
1163                &FakeDeviceId,
1164                &FakeV1Query {
1165                    group_addr: I::GROUP_ADDR1.get(),
1166                    max_response_time: Duration::from_secs(1)
1167                }
1168            ),
1169            Err(NotAMemberErr(I::GROUP_ADDR1.get()))
1170        );
1171        // Now in v1 mode and a compat timer is scheduled.
1172        assert_eq!(core_ctx.gmp.mode, GmpMode::V1 { compat: true });
1173
1174        let timeout =
1175            core_ctx.gmp.v2_proto.older_version_querier_present_timeout(&core_ctx.config).into();
1176        core_ctx.gmp.timers.assert_timers([(
1177            TimerIdInner::V1Compat,
1178            (),
1179            bindings_ctx.now() + timeout,
1180        )]);
1181
1182        // Increment the time and see that the timer updates.
1183        bindings_ctx.timers.instant.sleep(timeout / 2);
1184        assert_eq!(
1185            v1::handle_query_message(
1186                &mut core_ctx,
1187                &mut bindings_ctx,
1188                &FakeDeviceId,
1189                &FakeV1Query {
1190                    group_addr: I::GROUP_ADDR1.get(),
1191                    max_response_time: Duration::from_secs(1)
1192                }
1193            ),
1194            Err(NotAMemberErr(I::GROUP_ADDR1.get()))
1195        );
1196        assert_eq!(core_ctx.gmp.mode, GmpMode::V1 { compat: true });
1197        core_ctx.gmp.timers.assert_timers([(
1198            TimerIdInner::V1Compat,
1199            (),
1200            bindings_ctx.now() + timeout,
1201        )]);
1202
1203        // Trigger the timer and observe a fallback to v2.
1204        let timer = bindings_ctx.trigger_next_timer(&mut core_ctx);
1205        assert_eq!(timer, Some(GmpTimerId::new(FakeWeakDeviceId(FakeDeviceId))));
1206        assert_eq!(core_ctx.gmp.mode, GmpMode::V2);
1207        // No more timers should exist, no frames are sent out.
1208        core_ctx.gmp.timers.assert_timers([]);
1209        let testutil::FakeGmpContextInner { v1_messages, v2_messages } = &core_ctx.inner;
1210        assert_eq!(v1_messages, &Vec::new());
1211        assert_eq!(v2_messages, &Vec::<Vec<_>>::new());
1212    }
1213}