netstack3_ip/gmp/
v1.rs

1// Copyright 2024 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//! GMP V1 common implementation.
6//!
7//! GMPv1 is the common implementation of a fictitious GMP protocol that covers
8//! the common parts of MLDv1 and IGMPv1, IGMPv2.
9
10use core::time::Duration;
11
12use assert_matches::assert_matches;
13use net_types::ip::Ip;
14use net_types::MulticastAddr;
15use netstack3_base::Instant;
16use packet_formats::utils::NonZeroDuration;
17use rand::Rng;
18
19use crate::internal::gmp::{
20    self, GmpBindingsContext, GmpContext, GmpContextInner, GmpEnabledGroup, GmpGroupState, GmpMode,
21    GmpStateRef, GroupJoinResult, GroupLeaveResult, IpExt, NotAMemberErr, QueryTarget,
22};
23
24/// Timers installed by GMP v1.
25///
26/// The timer always refers to a delayed report.
27#[derive(Debug, Eq, PartialEq, Hash, Clone)]
28pub(super) struct DelayedReportTimerId<I: Ip>(pub(super) GmpEnabledGroup<I::Addr>);
29
30#[cfg(test)]
31impl<I: IpExt> DelayedReportTimerId<I> {
32    pub(super) fn new_multicast(addr: MulticastAddr<I::Addr>) -> Self {
33        Self(GmpEnabledGroup::try_new(addr).expect("not GMP enabled"))
34    }
35}
36
37/// A type of GMP v1 message.
38#[derive(Debug, Eq, PartialEq)]
39pub(super) enum GmpMessageType {
40    Report,
41    Leave,
42}
43
44/// Actions to take as a consequence of joining a group.
45#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
46pub(super) struct JoinGroupActions {
47    send_report_and_schedule_timer: Option<Duration>,
48}
49
50impl JoinGroupActions {
51    const NOOP: Self = Self { send_report_and_schedule_timer: None };
52}
53
54/// Actions to take as a consequence of leaving a group.
55#[derive(Debug, PartialEq, Eq)]
56pub(super) struct LeaveGroupActions {
57    send_leave: bool,
58    stop_timer: bool,
59}
60
61impl LeaveGroupActions {
62    const NOOP: Self = Self { send_leave: false, stop_timer: false };
63}
64
65/// Actions to take as a consequence of handling a received report message.
66#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
67pub(super) struct ReportReceivedActions {
68    pub(super) stop_timer: bool,
69}
70
71impl ReportReceivedActions {
72    const NOOP: Self = Self { stop_timer: false };
73}
74
75/// Actions to take as a consequence of receiving a query message.
76#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
77pub(super) enum QueryReceivedActions {
78    ScheduleTimer(Duration),
79    StopTimerAndSendReport,
80    None,
81}
82
83/// Actions to take as a consequence of a report timer expiring.
84#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
85pub(super) struct ReportTimerExpiredActions;
86
87/// This trait is used to model configuration values used by GMP and abstracting
88/// the different parts of MLDv1 and IGMPv1/v2.
89pub trait ProtocolConfig {
90    /// The maximum delay to wait to send an unsolicited report.
91    fn unsolicited_report_interval(&self) -> Duration;
92
93    /// Whether the host should send a leave message even if it is not the last
94    /// host in the group.
95    fn send_leave_anyway(&self) -> bool;
96
97    /// Get the _real_ `MAX_RESP_TIME`
98    ///
99    /// `None` indicates that the maximum response time is zero and thus a
100    /// response should be sent immediately.
101    fn get_max_resp_time(&self, resp_time: Duration) -> Option<NonZeroDuration>;
102}
103
104/// The transition between one state and the next.
105///
106/// A `Transition` includes the next state to enter and any actions to take
107/// while executing the transition.
108struct Transition<S, Actions>(S, Actions);
109
110/// Represents Non Member-specific state variables.
111///
112/// Memberships may be a non-member when joined locally but are not performing
113/// GMP.
114///
115/// Note that the special all-nodes addresses 224.0.0.1 and ff02::1 are modelled
116/// as permanently in `NonMember` state instead of `Idle` state in NS3.
117#[cfg_attr(test, derive(Debug))]
118pub(super) struct NonMember;
119
120/// Represents Delaying Member-specific state variables.
121#[cfg_attr(test, derive(Debug))]
122pub(super) struct DelayingMember<I: Instant> {
123    /// The expiration time for the current timer. Useful to check if the timer
124    /// needs to be reset when a query arrives.
125    timer_expiration: I,
126
127    /// Used to indicate whether we need to send out a Leave message when we are
128    /// leaving the group. This flag will become false once we heard about
129    /// another reporter.
130    last_reporter: bool,
131}
132
133/// Represents Idle Member-specific state variables.
134#[cfg_attr(test, derive(Debug))]
135pub(super) struct IdleMember {
136    /// Used to indicate whether we need to send out a Leave message when we are
137    /// leaving the group.
138    last_reporter: bool,
139}
140
141/// The state for a multicast group membership.
142///
143/// The terms used here are biased towards [IGMPv2]. In [MLD], their names are
144/// {Non, Delaying, Idle}-Listener instead.
145///
146/// [IGMPv2]: https://tools.ietf.org/html/rfc2236
147/// [MLD]: https://tools.ietf.org/html/rfc2710
148#[cfg_attr(test, derive(Debug))]
149pub(super) enum MemberState<I: Instant> {
150    NonMember(NonMember),
151    Delaying(DelayingMember<I>),
152    Idle(IdleMember),
153}
154
155impl<I: Instant> From<NonMember> for MemberState<I> {
156    fn from(s: NonMember) -> Self {
157        MemberState::NonMember(s)
158    }
159}
160
161impl<I: Instant> From<DelayingMember<I>> for MemberState<I> {
162    fn from(s: DelayingMember<I>) -> Self {
163        MemberState::Delaying(s)
164    }
165}
166
167impl<I: Instant> From<IdleMember> for MemberState<I> {
168    fn from(s: IdleMember) -> Self {
169        MemberState::Idle(s)
170    }
171}
172
173impl<S, A> Transition<S, A> {
174    fn into_state_actions<I: Instant>(self) -> (MemberState<I>, A)
175    where
176        MemberState<I>: From<S>,
177    {
178        (self.0.into(), self.1)
179    }
180}
181
182/// Compute the next state and actions to take for a member state (Delaying or
183/// Idle member) that has received a query message.
184///
185/// # Arguments
186/// * `last_reporter` indicates if the last report was sent by this node.
187/// * `timer_expiration` is `None` if there are currently no timers, otherwise
188///   `Some(t)` where `t` is the old instant when the currently installed timer
189///   should fire. That is, `None` if an Idle member and `Some` if a Delaying
190///   member.
191/// * `max_resp_time` is the maximum response time required by Query message.
192fn member_query_received<R: Rng, I: Instant, C: ProtocolConfig>(
193    rng: &mut R,
194    last_reporter: bool,
195    timer_expiration: Option<I>,
196    max_resp_time: Duration,
197    now: I,
198    cfg: &C,
199) -> (MemberState<I>, QueryReceivedActions) {
200    let (transition, actions) = match cfg.get_max_resp_time(max_resp_time) {
201        None => (IdleMember { last_reporter }.into(), QueryReceivedActions::StopTimerAndSendReport),
202        Some(max_resp_time) => {
203            let max_resp_time = max_resp_time.get();
204            let new_deadline = now.saturating_add(max_resp_time);
205
206            let (timer_expiration, action) = match timer_expiration {
207                Some(old) if new_deadline >= old => (old, QueryReceivedActions::None),
208                None | Some(_) => {
209                    let delay = gmp::random_report_timeout(rng, max_resp_time);
210                    (now.saturating_add(delay), QueryReceivedActions::ScheduleTimer(delay))
211                }
212            };
213
214            (DelayingMember { last_reporter, timer_expiration }.into(), action)
215        }
216    };
217
218    (transition, actions)
219}
220
221impl NonMember {
222    fn join_group<I: Instant, R: Rng, C: ProtocolConfig>(
223        self,
224        rng: &mut R,
225        now: I,
226        cfg: &C,
227    ) -> Transition<DelayingMember<I>, JoinGroupActions> {
228        let duration = cfg.unsolicited_report_interval();
229        let delay = gmp::random_report_timeout(rng, duration);
230        let actions = JoinGroupActions { send_report_and_schedule_timer: Some(delay) };
231        Transition(
232            DelayingMember {
233                last_reporter: true,
234                timer_expiration: now.checked_add(delay).expect("timer expiration overflowed"),
235            },
236            actions,
237        )
238    }
239
240    fn leave_group(self) -> Transition<NonMember, LeaveGroupActions> {
241        Transition(NonMember, LeaveGroupActions::NOOP)
242    }
243}
244
245impl<I: Instant> DelayingMember<I> {
246    fn query_received<R: Rng, C: ProtocolConfig>(
247        self,
248        rng: &mut R,
249        max_resp_time: Duration,
250        now: I,
251        cfg: &C,
252    ) -> (MemberState<I>, QueryReceivedActions) {
253        let DelayingMember { last_reporter, timer_expiration } = self;
254        member_query_received(rng, last_reporter, Some(timer_expiration), max_resp_time, now, cfg)
255    }
256
257    fn leave_group<C: ProtocolConfig>(self, cfg: &C) -> Transition<NonMember, LeaveGroupActions> {
258        let actions = LeaveGroupActions {
259            send_leave: self.last_reporter || cfg.send_leave_anyway(),
260            stop_timer: true,
261        };
262        Transition(NonMember, actions)
263    }
264
265    fn report_received(self) -> Transition<IdleMember, ReportReceivedActions> {
266        Transition(IdleMember { last_reporter: false }, ReportReceivedActions { stop_timer: true })
267    }
268
269    fn report_timer_expired(self) -> Transition<IdleMember, ReportTimerExpiredActions> {
270        Transition(IdleMember { last_reporter: true }, ReportTimerExpiredActions)
271    }
272}
273
274impl IdleMember {
275    fn query_received<I: Instant, R: Rng, C: ProtocolConfig>(
276        self,
277        rng: &mut R,
278        max_resp_time: Duration,
279        now: I,
280        cfg: &C,
281    ) -> (MemberState<I>, QueryReceivedActions) {
282        let IdleMember { last_reporter } = self;
283        member_query_received(rng, last_reporter, None, max_resp_time, now, cfg)
284    }
285
286    fn leave_group<C: ProtocolConfig>(self, cfg: &C) -> Transition<NonMember, LeaveGroupActions> {
287        let actions = LeaveGroupActions {
288            send_leave: self.last_reporter || cfg.send_leave_anyway(),
289            stop_timer: false,
290        };
291        Transition(NonMember, actions)
292    }
293}
294
295impl<I: Instant> MemberState<I> {
296    /// Performs the "join group" transition, producing a new `MemberState` and
297    /// set of actions to execute.
298    fn join_group<R: Rng, C: ProtocolConfig>(
299        cfg: &C,
300        rng: &mut R,
301        now: I,
302        gmp_disabled: bool,
303    ) -> (MemberState<I>, JoinGroupActions) {
304        let non_member = NonMember;
305        if gmp_disabled {
306            (non_member.into(), JoinGroupActions::NOOP)
307        } else {
308            non_member.join_group(rng, now, cfg).into_state_actions()
309        }
310    }
311
312    /// Performs the "leave group" transition, consuming the state by value, and
313    /// returning the next state and a set of actions to execute.
314    fn leave_group<C: ProtocolConfig>(self, cfg: &C) -> (MemberState<I>, LeaveGroupActions) {
315        // Rust can infer these types, but since we're just discarding `_state`,
316        // we explicitly make sure it's the state we expect in case we introduce
317        // a bug.
318        match self {
319            MemberState::NonMember(state) => state.leave_group(),
320            MemberState::Delaying(state) => state.leave_group(cfg),
321            MemberState::Idle(state) => state.leave_group(cfg),
322        }
323        .into_state_actions()
324    }
325
326    fn query_received<R: Rng, C: ProtocolConfig>(
327        self,
328        rng: &mut R,
329        max_resp_time: Duration,
330        now: I,
331        cfg: &C,
332    ) -> (MemberState<I>, QueryReceivedActions) {
333        match self {
334            state @ MemberState::NonMember(_) => (state, QueryReceivedActions::None),
335            MemberState::Delaying(state) => state.query_received(rng, max_resp_time, now, cfg),
336            MemberState::Idle(state) => state.query_received(rng, max_resp_time, now, cfg),
337        }
338    }
339
340    fn report_received(self) -> (MemberState<I>, ReportReceivedActions) {
341        match self {
342            state @ MemberState::Idle(_) | state @ MemberState::NonMember(_) => {
343                (state, ReportReceivedActions::NOOP)
344            }
345            MemberState::Delaying(state) => state.report_received().into_state_actions(),
346        }
347    }
348
349    fn report_timer_expired(self) -> (MemberState<I>, ReportTimerExpiredActions) {
350        match self {
351            MemberState::Idle(_) | MemberState::NonMember(_) => {
352                unreachable!("got report timer in non-delaying state")
353            }
354            MemberState::Delaying(state) => state.report_timer_expired().into_state_actions(),
355        }
356    }
357}
358
359#[cfg_attr(test, derive(Debug))]
360pub struct GmpStateMachine<I: Instant> {
361    // Invariant: `inner` is always `Some`. It is stored as an `Option` so that
362    // methods can `.take()` the `MemberState` in order to perform transitions
363    // that consume `MemberState` by value. However, a new `MemberState` is
364    // always put back in its place so that `inner` is `Some` by the time the
365    // methods return.
366    pub(super) inner: Option<MemberState<I>>,
367}
368
369impl<I: Instant> GmpStateMachine<I> {
370    /// When a "join group" command is received.
371    ///
372    /// `join_group` initializes a new state machine in the Non-Member state and
373    /// then immediately executes the "join group" transition. The new state
374    /// machine is returned along with any actions to take.
375    pub(super) fn join_group<R: Rng, C: ProtocolConfig>(
376        rng: &mut R,
377        now: I,
378        gmp_disabled: bool,
379        cfg: &C,
380    ) -> (GmpStateMachine<I>, JoinGroupActions) {
381        let (state, actions) = MemberState::join_group(cfg, rng, now, gmp_disabled);
382        (GmpStateMachine { inner: Some(state) }, actions)
383    }
384
385    /// Attempts to join the group if the group is currently in the non-member
386    /// state.
387    ///
388    /// If the group is in a member state (delaying/idle), this method does
389    /// nothing.
390    fn join_if_non_member<R: Rng, C: ProtocolConfig>(
391        &mut self,
392        rng: &mut R,
393        now: I,
394        cfg: &C,
395    ) -> JoinGroupActions {
396        self.update(|s| match s {
397            MemberState::NonMember(s) => s.join_group(rng, now, cfg).into_state_actions(),
398            state @ MemberState::Delaying(_) | state @ MemberState::Idle(_) => {
399                (state, JoinGroupActions::NOOP)
400            }
401        })
402    }
403
404    /// Leaves the group if the group is in a member state.
405    ///
406    /// Does nothing if the group is in a non-member state.
407    fn leave_if_member<C: ProtocolConfig>(&mut self, cfg: &C) -> LeaveGroupActions {
408        self.update(|s| s.leave_group(cfg))
409    }
410
411    /// When a "leave group" command is received.
412    ///
413    /// `leave_group` consumes the state machine by value since we don't allow
414    /// storing a state machine in the Non-Member state.
415    pub(super) fn leave_group<C: ProtocolConfig>(self, cfg: &C) -> LeaveGroupActions {
416        // This `unwrap` is safe because we maintain the invariant that `inner`
417        // is always `Some`.
418        let (_state, actions) = self.inner.unwrap().leave_group(cfg);
419        actions
420    }
421
422    /// When a query is received, and we have to respond within max_resp_time.
423    pub(super) fn query_received<R: Rng, C: ProtocolConfig>(
424        &mut self,
425        rng: &mut R,
426        max_resp_time: Duration,
427        now: I,
428        cfg: &C,
429    ) -> QueryReceivedActions {
430        self.update(|s| s.query_received(rng, max_resp_time, now, cfg))
431    }
432
433    /// We have received a report from another host on our local network.
434    pub(super) fn report_received(&mut self) -> ReportReceivedActions {
435        self.update(MemberState::report_received)
436    }
437
438    /// The timer installed has expired.
439    pub(super) fn report_timer_expired(&mut self) -> ReportTimerExpiredActions {
440        self.update(MemberState::report_timer_expired)
441    }
442
443    /// Update the state with no argument.
444    fn update<A, F: FnOnce(MemberState<I>) -> (MemberState<I>, A)>(&mut self, f: F) -> A {
445        let (s, a) = f(self.inner.take().unwrap());
446        self.inner = Some(s);
447        a
448    }
449
450    /// Returns a new state machine to use for a group after a transition from a
451    /// different GMP mode.
452    ///
453    /// Neither the IGMPv3 or MLDv2 RFCs explicitly state what mode a member
454    /// should be in as part of a state transition. Our best attempt here is to
455    /// create members in Idle state, which will respond to queries but do not
456    /// generate any unsolicited reports as the outcome of the transition. That
457    /// roughly matches the expectation around the RFC text:
458    ///
459    /// > Whenever a host changes its compatibility mode, it cancels all its
460    /// > pending responses and retransmission timers.
461    pub(super) fn new_for_mode_transition() -> Self {
462        Self { inner: Some(MemberState::Idle(IdleMember { last_reporter: false })) }
463    }
464
465    #[cfg(test)]
466    pub(super) fn get_inner(&self) -> &MemberState<I> {
467        self.inner.as_ref().unwrap()
468    }
469}
470
471pub(super) fn handle_timer<I, CC, BC>(
472    core_ctx: &mut CC,
473    bindings_ctx: &mut BC,
474    device: &CC::DeviceId,
475    state: GmpStateRef<'_, I, CC::TypeLayout, BC>,
476    timer: DelayedReportTimerId<I>,
477) where
478    BC: GmpBindingsContext,
479    CC: GmpContextInner<I, BC>,
480    I: IpExt,
481{
482    let GmpStateRef { enabled: _, groups, gmp, config: _ } = state;
483    debug_assert!(gmp.gmp_mode().is_v1());
484    let DelayedReportTimerId(group_addr) = timer;
485    let ReportTimerExpiredActions {} = groups
486        .get_mut(group_addr.as_ref())
487        .expect("get state for group with expired report timer")
488        .v1_mut()
489        .report_timer_expired();
490
491    core_ctx.send_message_v1(bindings_ctx, &device, &gmp.mode, group_addr, GmpMessageType::Report);
492}
493
494pub(super) fn handle_report_message<I, BC, CC>(
495    core_ctx: &mut CC,
496    bindings_ctx: &mut BC,
497    device: &CC::DeviceId,
498    group_addr: MulticastAddr<I::Addr>,
499) -> Result<(), NotAMemberErr<I>>
500where
501    BC: GmpBindingsContext,
502    CC: GmpContext<I, BC>,
503    I: IpExt,
504{
505    core_ctx.with_gmp_state_mut(device, |state| {
506        let GmpStateRef { enabled: _, groups, gmp, config: _ } = state;
507        // Ignore reports if we're not in v1 mode. We're acting as an
508        // IGMPv3/MLDv2 host only. From RFCs:
509        //
510        //   RFC 3810 8.2.2: In the Presence of MLDv1 Multicast Address
511        //   Listeners.
512        //
513        //     An MLDv2 host may be placed on a link where there are
514        //     MLDv1 hosts. A host MAY allow its MLDv2 Multicast Listener Report
515        //     to be suppressed by a Version 1 Multicast Listener Report.
516        //
517        //   RFC 3376 7.2.2: In the Presence of Older Version Group Members.
518        //
519        //     An IGMPv3 host may be placed on a network where there are hosts
520        //     that have not yet been upgraded to IGMPv3.  A host MAY allow its
521        //     IGMPv3 Membership Record to be suppressed by either a Version 1
522        //     Membership Report, or a Version 2 Membership Report.
523        if !gmp.gmp_mode().is_v1() {
524            return Ok(());
525        }
526        let group_addr =
527            GmpEnabledGroup::try_new(group_addr).map_err(|addr| NotAMemberErr(*addr))?;
528        let ReportReceivedActions { stop_timer } = groups
529            .get_mut(group_addr.as_ref())
530            .ok_or_else(|| NotAMemberErr(*group_addr.multicast_addr()))
531            .map(|a| a.v1_mut().report_received())?;
532        if stop_timer {
533            assert_matches!(
534                gmp.timers.cancel(bindings_ctx, &DelayedReportTimerId(group_addr).into()),
535                Some(_)
536            );
537        }
538        Ok(())
539    })
540}
541
542/// A trait abstracting GMP v1 query messages.
543pub(super) trait QueryMessage<I: Ip> {
544    /// Returns the group address in the query.
545    fn group_addr(&self) -> I::Addr;
546
547    /// Returns tha maximum response time for the query.
548    fn max_response_time(&self) -> Duration;
549}
550
551pub(super) fn handle_query_message<I, CC, BC, Q>(
552    core_ctx: &mut CC,
553    bindings_ctx: &mut BC,
554    device: &CC::DeviceId,
555    query: &Q,
556) -> Result<(), NotAMemberErr<I>>
557where
558    BC: GmpBindingsContext,
559    CC: GmpContext<I, BC>,
560    I: IpExt,
561    Q: QueryMessage<I>,
562{
563    core_ctx.with_gmp_state_mut_and_ctx(device, |mut core_ctx, mut state| {
564        // Allow protocol to decide entering v1 compatibility mode if we see a
565        // v1 message and we're not in forced v1.
566        //
567        //   RFC 3810 8.2.1: The Host Compatibility Mode of an interface is set
568        //   to MLDv1 whenever an MLDv1 Multicast Address Listener Query is
569        //   received on that interface.
570        //
571        //   RFC 3376 7.2.1: In order to switch gracefully between versions of
572        //   IGMP, hosts keep both an IGMPv1 Querier Present timer and an IGMPv2
573        //   Querier Present timer per interface. IGMPv1 Querier Present is set
574        //   to Older Version Querier Present Timeout seconds whenever an IGMPv1
575        //   Membership Query is received.  IGMPv2 Querier Present is set to
576        //   Older Version Querier Present Timeout seconds whenever an IGMPv2
577        //   General Query is received.
578        let new_mode =
579            core_ctx.mode_update_from_v1_query(bindings_ctx, query, &state.gmp, &state.config);
580        gmp::enter_mode(bindings_ctx, state.as_mut(), new_mode);
581        let compat = match state.gmp.gmp_mode() {
582            GmpMode::V1 { compat } => compat,
583            GmpMode::V2 => {
584                // NB: We don't currently support a configuration that refuses
585                // entering compatibility mode, but that's allowed in RFC 3810
586                // for MLDv2, we just don't have a use for it. If need be, this
587                // panic can be replaced with a return to ignore the v1 query
588                // instead.
589                panic!("protocol refused to switch to v1");
590            }
591        };
592        if compat {
593            gmp::schedule_v1_compat(bindings_ctx, state.as_mut())
594        }
595
596        handle_query_message_inner(&mut core_ctx, bindings_ctx, device, state, query)
597    })
598}
599
600pub(super) fn handle_query_message_inner<I, CC, BC, Q>(
601    core_ctx: &mut CC,
602    bindings_ctx: &mut BC,
603    device: &CC::DeviceId,
604    state: GmpStateRef<'_, I, CC::TypeLayout, BC>,
605    query: &Q,
606) -> Result<(), NotAMemberErr<I>>
607where
608    BC: GmpBindingsContext,
609    CC: GmpContextInner<I, BC>,
610    I: IpExt,
611    Q: QueryMessage<I>,
612{
613    let GmpStateRef { enabled: _, groups, gmp, config } = state;
614
615    let now = bindings_ctx.now();
616
617    let target = query.group_addr();
618    let target = QueryTarget::new(target).ok_or(NotAMemberErr(target))?;
619    let iter = match target {
620        QueryTarget::Unspecified => either::Either::Left(
621            groups
622                .iter_mut()
623                .filter_map(|(addr, state)| GmpEnabledGroup::new(*addr).map(|addr| (addr, state))),
624        ),
625        QueryTarget::Specified(group_addr) => {
626            let group_addr =
627                GmpEnabledGroup::try_new(group_addr).map_err(|addr| NotAMemberErr(*addr))?;
628            let state = groups
629                .get_mut(group_addr.as_ref())
630                .ok_or_else(|| NotAMemberErr(*group_addr.into_multicast_addr()))?;
631            either::Either::Right(core::iter::once((group_addr, state)))
632        }
633    };
634
635    for (group_addr, state) in iter {
636        let actions = state.v1_mut().query_received(
637            &mut bindings_ctx.rng(),
638            query.max_response_time(),
639            now,
640            config,
641        );
642        let send_msg = match actions {
643            QueryReceivedActions::None => None,
644            QueryReceivedActions::ScheduleTimer(delay) => {
645                let _: Option<(BC::Instant, ())> = gmp.timers.schedule_after(
646                    bindings_ctx,
647                    DelayedReportTimerId(group_addr).into(),
648                    (),
649                    delay,
650                );
651                None
652            }
653            QueryReceivedActions::StopTimerAndSendReport => {
654                let _: Option<(BC::Instant, ())> =
655                    gmp.timers.cancel(bindings_ctx, &DelayedReportTimerId(group_addr).into());
656                Some(GmpMessageType::Report)
657            }
658        };
659
660        if let Some(msg) = send_msg {
661            core_ctx.send_message_v1(bindings_ctx, device, &gmp.mode, group_addr, msg);
662        }
663    }
664
665    Ok(())
666}
667
668pub(super) fn handle_enabled<I, CC, BC>(
669    core_ctx: &mut CC,
670    bindings_ctx: &mut BC,
671    device: &CC::DeviceId,
672    state: GmpStateRef<'_, I, CC::TypeLayout, BC>,
673) where
674    BC: GmpBindingsContext,
675    CC: GmpContextInner<I, BC>,
676    I: IpExt,
677{
678    let GmpStateRef { enabled: _, groups, gmp, config } = state;
679    debug_assert!(gmp.gmp_mode().is_v1());
680
681    let now = bindings_ctx.now();
682
683    for (group_addr, state) in groups.iter_mut() {
684        let group_addr = match GmpEnabledGroup::new(*group_addr) {
685            Some(a) => a,
686            None => continue,
687        };
688
689        let JoinGroupActions { send_report_and_schedule_timer } =
690            state.v1_mut().join_if_non_member(&mut bindings_ctx.rng(), now, config);
691        let Some(delay) = send_report_and_schedule_timer else {
692            continue;
693        };
694        assert_matches!(
695            gmp.timers.schedule_after(
696                bindings_ctx,
697                DelayedReportTimerId(group_addr).into(),
698                (),
699                delay
700            ),
701            None
702        );
703        core_ctx.send_message_v1(
704            bindings_ctx,
705            device,
706            &gmp.mode,
707            group_addr,
708            GmpMessageType::Report,
709        );
710    }
711}
712
713pub(super) fn handle_disabled<I, CC, BC>(
714    core_ctx: &mut CC,
715    bindings_ctx: &mut BC,
716    device: &CC::DeviceId,
717    state: GmpStateRef<'_, I, CC::TypeLayout, BC>,
718) where
719    BC: GmpBindingsContext,
720    CC: GmpContextInner<I, BC>,
721    I: IpExt,
722{
723    let GmpStateRef { enabled: _, groups, gmp, config } = state;
724    debug_assert!(gmp.gmp_mode().is_v1());
725
726    for (group_addr, state) in groups.groups_mut() {
727        let group_addr = match GmpEnabledGroup::new(*group_addr) {
728            Some(a) => a,
729            None => continue,
730        };
731        let LeaveGroupActions { send_leave, stop_timer } = state.v1_mut().leave_if_member(config);
732        if stop_timer {
733            assert_matches!(
734                gmp.timers.cancel(bindings_ctx, &DelayedReportTimerId(group_addr).into()),
735                Some(_)
736            );
737        }
738        if send_leave {
739            core_ctx.send_message_v1(
740                bindings_ctx,
741                device,
742                &gmp.mode,
743                group_addr,
744                GmpMessageType::Leave,
745            );
746        }
747    }
748}
749
750pub(super) fn join_group<I, CC, BC>(
751    core_ctx: &mut CC,
752    bindings_ctx: &mut BC,
753    device: &CC::DeviceId,
754    group_addr: MulticastAddr<I::Addr>,
755    state: GmpStateRef<'_, I, CC::TypeLayout, BC>,
756) -> GroupJoinResult
757where
758    BC: GmpBindingsContext,
759    CC: GmpContextInner<I, BC>,
760    I: IpExt,
761{
762    let GmpStateRef { enabled, groups, gmp, config } = state;
763    debug_assert!(gmp.gmp_mode().is_v1());
764    let now = bindings_ctx.now();
765
766    let group_addr_witness = GmpEnabledGroup::try_new(group_addr);
767    let gmp_disabled = !enabled || group_addr_witness.is_err();
768    let result = groups.join_group_with(group_addr, || {
769        let (state, actions) =
770            GmpStateMachine::join_group(&mut bindings_ctx.rng(), now, gmp_disabled, config);
771        (GmpGroupState::new_v1(state), actions)
772    });
773    result.map(|JoinGroupActions { send_report_and_schedule_timer }| {
774        if let Some(delay) = send_report_and_schedule_timer {
775            // Invariant: if gmp_disabled then a non-member group must not
776            // generate any actions.
777            let group_addr = group_addr_witness.expect("generated report for GMP-disabled group");
778            assert_matches!(
779                gmp.timers.schedule_after(
780                    bindings_ctx,
781                    DelayedReportTimerId(group_addr).into(),
782                    (),
783                    delay
784                ),
785                None
786            );
787
788            core_ctx.send_message_v1(
789                bindings_ctx,
790                device,
791                &gmp.mode,
792                group_addr,
793                GmpMessageType::Report,
794            );
795        }
796    })
797}
798
799pub(super) fn leave_group<I, CC, BC>(
800    core_ctx: &mut CC,
801    bindings_ctx: &mut BC,
802    device: &CC::DeviceId,
803    group_addr: MulticastAddr<I::Addr>,
804    state: GmpStateRef<'_, I, CC::TypeLayout, BC>,
805) -> GroupLeaveResult
806where
807    BC: GmpBindingsContext,
808    CC: GmpContextInner<I, BC>,
809    I: IpExt,
810{
811    let GmpStateRef { enabled: _, groups, gmp, config } = state;
812    debug_assert!(gmp.gmp_mode().is_v1());
813
814    groups.leave_group(group_addr).map(|state| {
815        let actions = state.into_v1().leave_group(config);
816        let group_addr = match GmpEnabledGroup::new(group_addr) {
817            Some(addr) => addr,
818            None => {
819                // Invariant: No actions must be generated for non member
820                // groups.
821                assert_eq!(actions, LeaveGroupActions::NOOP);
822                return;
823            }
824        };
825        let LeaveGroupActions { send_leave, stop_timer } = actions;
826        if stop_timer {
827            assert_matches!(
828                gmp.timers.cancel(bindings_ctx, &DelayedReportTimerId(group_addr).into()),
829                Some(_)
830            );
831        }
832        if send_leave {
833            core_ctx.send_message_v1(
834                bindings_ctx,
835                device,
836                &gmp.mode,
837                group_addr,
838                GmpMessageType::Leave,
839            );
840        }
841    })
842}
843
844#[cfg(test)]
845mod test {
846    use assert_matches::assert_matches;
847    use ip_test_macro::ip_test;
848    use netstack3_base::testutil::{new_rng, FakeDeviceId, FakeInstant};
849    use test_util::assert_lt;
850
851    use super::*;
852
853    const DEFAULT_UNSOLICITED_REPORT_INTERVAL: Duration = Duration::from_secs(10);
854
855    /// Whether to send leave group message if our flag is not set.
856    #[derive(Debug, Default)]
857    struct FakeConfig(bool);
858
859    impl ProtocolConfig for FakeConfig {
860        fn unsolicited_report_interval(&self) -> Duration {
861            DEFAULT_UNSOLICITED_REPORT_INTERVAL
862        }
863
864        fn send_leave_anyway(&self) -> bool {
865            let Self(cfg) = self;
866            *cfg
867        }
868
869        fn get_max_resp_time(&self, resp_time: Duration) -> Option<NonZeroDuration> {
870            NonZeroDuration::new(resp_time)
871        }
872    }
873
874    type FakeGmpStateMachine = GmpStateMachine<FakeInstant>;
875
876    #[test]
877    fn test_gmp_state_non_member_to_delay_should_set_flag() {
878        let (s, _actions) = FakeGmpStateMachine::join_group(
879            &mut new_rng(0),
880            FakeInstant::default(),
881            false,
882            &FakeConfig::default(),
883        );
884        match s.get_inner() {
885            MemberState::Delaying(s) => assert!(s.last_reporter),
886            _ => panic!("Wrong State!"),
887        }
888    }
889
890    #[test]
891    fn test_gmp_state_non_member_to_delay_actions() {
892        let (_state, actions) = FakeGmpStateMachine::join_group(
893            &mut new_rng(0),
894            FakeInstant::default(),
895            false,
896            &FakeConfig::default(),
897        );
898        assert_matches!(
899            actions,
900            JoinGroupActions { send_report_and_schedule_timer: Some(d) } if d <= DEFAULT_UNSOLICITED_REPORT_INTERVAL
901        );
902    }
903
904    #[test]
905    fn test_gmp_state_delay_no_reset_timer() {
906        let mut rng = new_rng(0);
907        let cfg = FakeConfig::default();
908        let (mut s, _actions) =
909            FakeGmpStateMachine::join_group(&mut rng, FakeInstant::default(), false, &cfg);
910        assert_eq!(
911            s.query_received(
912                &mut rng,
913                DEFAULT_UNSOLICITED_REPORT_INTERVAL + Duration::from_secs(1),
914                FakeInstant::default(),
915                &cfg
916            ),
917            QueryReceivedActions::None,
918        );
919    }
920
921    #[test]
922    fn test_gmp_state_delay_reset_timer() {
923        let mut rng = new_rng(10);
924        let cfg = FakeConfig::default();
925        let (mut s, JoinGroupActions { send_report_and_schedule_timer }) =
926            FakeGmpStateMachine::join_group(&mut rng, FakeInstant::default(), false, &cfg);
927        let first_duration = send_report_and_schedule_timer.expect("starts delaying member");
928        let actions = s.query_received(
929            &mut rng,
930            first_duration.checked_sub(Duration::from_micros(1)).unwrap(),
931            FakeInstant::default(),
932            &cfg,
933        );
934        let new_duration = assert_matches!(actions,
935            QueryReceivedActions::ScheduleTimer(d) => d
936        );
937        assert_lt!(new_duration, first_duration);
938    }
939
940    #[test]
941    fn test_gmp_state_delay_to_idle_with_report_no_flag() {
942        let (mut s, _actions) = FakeGmpStateMachine::join_group(
943            &mut new_rng(0),
944            FakeInstant::default(),
945            false,
946            &FakeConfig::default(),
947        );
948        assert_eq!(s.report_received(), ReportReceivedActions { stop_timer: true });
949        match s.get_inner() {
950            MemberState::Idle(s) => {
951                assert!(!s.last_reporter);
952            }
953            _ => panic!("Wrong State!"),
954        }
955    }
956
957    #[test]
958    fn test_gmp_state_delay_to_idle_without_report_set_flag() {
959        let (mut s, _actions) = FakeGmpStateMachine::join_group(
960            &mut new_rng(0),
961            FakeInstant::default(),
962            false,
963            &FakeConfig::default(),
964        );
965        assert_eq!(s.report_timer_expired(), ReportTimerExpiredActions,);
966        match s.get_inner() {
967            MemberState::Idle(s) => {
968                assert!(s.last_reporter);
969            }
970            _ => panic!("Wrong State!"),
971        }
972    }
973
974    #[test]
975    fn test_gmp_state_leave_should_send_leave() {
976        let mut rng = new_rng(0);
977        let cfg = FakeConfig::default();
978        let (s, _actions) =
979            FakeGmpStateMachine::join_group(&mut rng, FakeInstant::default(), false, &cfg);
980        assert_eq!(s.leave_group(&cfg), LeaveGroupActions { send_leave: true, stop_timer: true });
981        let (mut s, _actions) =
982            FakeGmpStateMachine::join_group(&mut rng, FakeInstant::default(), false, &cfg);
983        assert_eq!(s.report_timer_expired(), ReportTimerExpiredActions,);
984        assert_eq!(s.leave_group(&cfg), LeaveGroupActions { send_leave: true, stop_timer: false });
985    }
986
987    #[test]
988    fn test_gmp_state_delay_to_other_states_should_stop_timer() {
989        let mut rng = new_rng(0);
990        let cfg = FakeConfig::default();
991        let (s, _actions) =
992            FakeGmpStateMachine::join_group(&mut rng, FakeInstant::default(), false, &cfg);
993        assert_eq!(s.leave_group(&cfg), LeaveGroupActions { send_leave: true, stop_timer: true },);
994        let (mut s, _actions) =
995            FakeGmpStateMachine::join_group(&mut rng, FakeInstant::default(), false, &cfg);
996        assert_eq!(s.report_received(), ReportReceivedActions { stop_timer: true });
997    }
998
999    #[test]
1000    fn test_gmp_state_other_states_to_delay_should_schedule_timer() {
1001        let mut rng = new_rng(0);
1002        let cfg = FakeConfig::default();
1003        let (mut s, actions) =
1004            FakeGmpStateMachine::join_group(&mut rng, FakeInstant::default(), false, &cfg);
1005        assert_matches!(
1006            actions,
1007            JoinGroupActions { send_report_and_schedule_timer: Some(d) } if d <= DEFAULT_UNSOLICITED_REPORT_INTERVAL
1008        );
1009        assert_eq!(s.report_received(), ReportReceivedActions { stop_timer: true });
1010        assert_eq!(
1011            s.query_received(&mut rng, Duration::from_secs(1), FakeInstant::default(), &cfg),
1012            QueryReceivedActions::ScheduleTimer(Duration::from_micros(1))
1013        );
1014    }
1015
1016    #[test]
1017    fn test_gmp_state_leave_send_anyway_do_send() {
1018        let mut cfg = FakeConfig::default();
1019        let (mut s, _actions) =
1020            FakeGmpStateMachine::join_group(&mut new_rng(0), FakeInstant::default(), false, &cfg);
1021        cfg = FakeConfig(true);
1022        assert_eq!(s.report_received(), ReportReceivedActions { stop_timer: true });
1023        match s.get_inner() {
1024            MemberState::Idle(s) => assert!(!s.last_reporter),
1025            _ => panic!("Wrong State!"),
1026        }
1027        assert_eq!(s.leave_group(&cfg), LeaveGroupActions { send_leave: true, stop_timer: false });
1028    }
1029
1030    #[test]
1031    fn test_gmp_state_leave_not_the_last_do_nothing() {
1032        let cfg = FakeConfig::default();
1033        let (mut s, _actions) =
1034            FakeGmpStateMachine::join_group(&mut new_rng(0), FakeInstant::default(), false, &cfg);
1035        assert_eq!(s.report_received(), ReportReceivedActions { stop_timer: true });
1036        assert_eq!(s.leave_group(&cfg), LeaveGroupActions { send_leave: false, stop_timer: false })
1037    }
1038
1039    #[ip_test(I)]
1040    fn ignores_reports_if_v2<I: gmp::testutil::TestIpExt>() {
1041        let gmp::testutil::FakeCtx { mut core_ctx, mut bindings_ctx } =
1042            gmp::testutil::new_context_with_mode::<I>(GmpMode::V2);
1043        assert_eq!(
1044            handle_report_message(&mut core_ctx, &mut bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1045            // Report is ignored, we don't even check that we've joined that
1046            // group.
1047            Ok(())
1048        );
1049        // No timers are installed.
1050        core_ctx.gmp.timers.assert_timers([]);
1051        // Mode doesn't change.
1052        assert_eq!(core_ctx.gmp.mode, GmpMode::V2);
1053    }
1054}