netstack3_ip/gmp/
v2.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 v2 common implementation.
6//!
7//! GMPv2 is the common implementation of a fictitious GMP protocol that covers
8//! the common parts of MLDv2 ([RFC 3810]) and IGMPv3 ([RFC 3376]).
9//!
10//! [RFC 3810]: https://datatracker.ietf.org/doc/html/rfc3810
11//! [RFC 3376]: https://datatracker.ietf.org/doc/html/rfc3376
12
13use core::num::NonZeroU8;
14use core::time::Duration;
15
16use net_types::ip::{Ip, IpAddress};
17use net_types::{MulticastAddr, Witness as _};
18use netstack3_base::{Instant as _, LocalTimerHeap};
19use netstack3_hashmap::{HashMap, HashSet};
20use packet_formats::gmp::{GmpReportGroupRecord, GroupRecordType};
21use packet_formats::utils::NonZeroDuration;
22
23use crate::internal::gmp::{
24    self, GmpBindingsContext, GmpContext, GmpContextInner, GmpEnabledGroup, GmpGroupState, GmpMode,
25    GmpStateRef, GmpTypeLayout, GroupJoinResult, GroupLeaveResult, IpExt, NotAMemberErr,
26    QueryTarget,
27};
28
29/// The default value for Query Response Interval defined in [RFC 3810
30/// section 9.3] and [RFC 3376 section 8.3].
31///
32/// [RFC 3810 section 9.3]:
33///     https://datatracker.ietf.org/doc/html/rfc3810#section-9.3
34/// [RFC 3376 section 8.3]:
35///     https://datatracker.ietf.org/doc/html/rfc3376#section-8.3
36pub(super) const DEFAULT_QUERY_RESPONSE_INTERVAL: NonZeroDuration =
37    NonZeroDuration::from_secs(10).unwrap();
38
39/// The default value for Unsolicited Report Interval defined in [RFC 3810
40/// section 9.11] and [RFC 3376 section 8.11].
41///
42/// [RFC 3810 section 9.11]:
43///     https://datatracker.ietf.org/doc/html/rfc3810#section-9.3
44/// [RFC 3376 section 8.11]:
45///     https://datatracker.ietf.org/doc/html/rfc3376#section-8.3
46pub(super) const DEFAULT_UNSOLICITED_REPORT_INTERVAL: NonZeroDuration =
47    NonZeroDuration::from_secs(1).unwrap();
48
49/// The default value for the Robustness Variable defined in [RFC 3810
50/// section 9.1] and [RFC 3376 section 8.1].
51///
52/// [RFC 3810 section 9.1]:
53///     https://datatracker.ietf.org/doc/html/rfc3810#section-9.1
54/// [RFC 3376 section 8.1]:
55///     https://datatracker.ietf.org/doc/html/rfc3376#section-8.1
56pub(super) const DEFAULT_ROBUSTNESS_VARIABLE: NonZeroU8 = NonZeroU8::new(2).unwrap();
57
58/// The default value for the Query Interval defined in [RFC 3810
59/// section 9.2] and [RFC 3376 section 8.2].
60///
61/// [RFC 3810 section 9.2]:
62///     https://datatracker.ietf.org/doc/html/rfc3810#section-9.2
63/// [RFC 3376 section 8.2]:
64///     https://datatracker.ietf.org/doc/html/rfc3376#section-8.2
65pub(super) const DEFAULT_QUERY_INTERVAL: NonZeroDuration = NonZeroDuration::from_secs(125).unwrap();
66
67/// A delay to use before issuing state change reports in response to interface
68/// state changes (e.g leaving/joining groups).
69///
70/// Note that this delay does not exist on any of the related RFCs. The RFCs
71/// state that state change reports should be sent immediately when the state
72/// change occurs, the delay here is chosen to be small enough that it can be
73/// seen as immediate when looking at the network.
74///
75/// This delay introduces some advantages compared to a to-the-letter RFC
76/// implementation:
77///
78/// - It gives the system some time to consolidate State Change Reports into one
79///   in the case of quick successive changes.
80/// - Quick successive changes on different multicast groups do not quickly
81///   consume the retransmission counters of still pending changes to different
82///   groups.
83/// - State Change Reports are always sent from the same place in the code: when
84///   [`TimerId::StateChange`] timers fire.
85///
86/// [An equivalent delay is in use on linux][linux-mld].
87///
88/// [linux-mld]: https://github.com/torvalds/linux/blob/62b5a46999c74497fe10eabd7d19701c505b23e3/net/ipv6/mcast.c#L2670
89const STATE_CHANGE_REPORT_DELAY: Duration = Duration::from_millis(5);
90
91#[cfg_attr(test, derive(Debug))]
92pub(super) struct GroupState<I: Ip> {
93    filter_mode_retransmission_counter: u8,
94    recorded_sources: HashSet<I::Addr>,
95    // TODO(https://fxbug.dev/381241191): Include per-source retransmission
96    // counter when SSM is supported.
97}
98
99impl<I: Ip> GroupState<I> {
100    pub(super) fn new_for_mode_transition() -> Self {
101        Self { recorded_sources: Default::default(), filter_mode_retransmission_counter: 0 }
102    }
103}
104
105#[derive(Debug, Eq, PartialEq, Hash, Clone)]
106pub(super) enum TimerId<I: Ip> {
107    GeneralQuery,
108    MulticastAddress(GmpEnabledGroup<I::Addr>),
109    StateChange,
110}
111
112/// Global protocol state required for v2 support.
113///
114/// This is kept always available in protocol-global state since we need to
115/// store some possibly network-learned values when entering v1 compat mode (for
116/// timers).
117#[derive(Debug)]
118#[cfg_attr(test, derive(Eq, PartialEq))]
119pub(super) struct ProtocolState<I: Ip> {
120    /// The robustness variable on the link.
121    ///
122    /// Defined in [RFC 3810 section 9.1] and [RFC 3376 section 8.1].
123    ///
124    /// It starts with a default value and may be learned from queriers in the
125    /// network.
126    ///
127    /// [RFC 3810 section 9.1]: https://datatracker.ietf.org/doc/html/rfc3810#section-9.1
128    /// [RFC 3376 section 8.1]: https://datatracker.ietf.org/doc/html/rfc3376#section-8.1
129    pub robustness_variable: NonZeroU8,
130    /// The query interval on the link.
131    ///
132    /// Defined in [RFC 3810 section 9.2] and [RFC 3376 section 8.2].
133    ///
134    /// It starts with a default value and may be learned from queriers in the
135    /// network.
136    ///
137    /// [RFC 3810 section 9.2]: https://datatracker.ietf.org/doc/html/rfc3810#section-9.2
138    /// [RFC 3376 section 8.2]: https://datatracker.ietf.org/doc/html/rfc3376#section-8.2
139    pub query_interval: NonZeroDuration,
140
141    /// GMPv2-only state tracking pending group exit retransmissions.
142    ///
143    /// This is kept apart from the per-interface multicast group state so we
144    /// can keep minimal state on left groups and have an easier statement of
145    /// what groups we're part of.
146    // TODO(https://fxbug.dev/381241191): Reconsider this field when we
147    // introduce SSM. The group membership state-tracking is expected to change
148    // and it might become easier to keep left groups alongside still-member
149    // groups.
150    pub left_groups: HashMap<GmpEnabledGroup<I::Addr>, NonZeroU8>,
151}
152
153impl<I: Ip> Default for ProtocolState<I> {
154    fn default() -> Self {
155        Self {
156            robustness_variable: DEFAULT_ROBUSTNESS_VARIABLE,
157            query_interval: DEFAULT_QUERY_INTERVAL,
158            left_groups: Default::default(),
159        }
160    }
161}
162
163impl<I: Ip> ProtocolState<I> {
164    /// Calculates the Older Version Querier Present Timeout.
165    ///
166    /// From [RFC 3810 section 9.12] and [RFC 3376 section 8.12]:
167    ///
168    /// > This value MUST be ([Robustness Variable] times (the [Query Interval]
169    /// > in the last Query received)) plus ([Query Response Interval]).
170    ///
171    /// [RFC 3810 section 9.12]: https://datatracker.ietf.org/doc/html/rfc3810#section-9.12
172    /// [RFC 3376 section 8.12]: https://datatracker.ietf.org/doc/html/rfc3376#section-8.12
173    pub(super) fn older_version_querier_present_timeout<C: ProtocolConfig>(
174        &self,
175        config: &C,
176    ) -> NonZeroDuration {
177        self.query_interval
178            .saturating_mul(self.robustness_variable.into())
179            .saturating_add(config.query_response_interval().into())
180    }
181
182    /// Updates [`ProtocolState`] due to a GMP mode change out of v2 mode.
183    ///
184    /// `ProtocolState` discards any protocol-specific state but *maintains*
185    /// network-learned parameters on mode changes.
186    pub(super) fn on_enter_v1(&mut self) {
187        let Self { robustness_variable: _, query_interval: _, left_groups } = self;
188        // left_groups are effectively pending responses and, from RFC 3810
189        // section 8.2.1:
190        //
191        // Whenever a host changes its compatibility mode, it cancels all its
192        // pending responses and retransmission timers.
193        *left_groups = HashMap::new();
194    }
195}
196
197/// V2 protocol-specific configuration.
198///
199/// This trait abstracts over the storage of configurations specified in [RFC
200/// 3810] and [RFC 3376] that can be administratively changed.
201///
202/// [RFC 3810]: https://datatracker.ietf.org/doc/html/rfc3810
203/// [RFC 3376]: https://datatracker.ietf.org/doc/html/rfc3376
204pub trait ProtocolConfig {
205    /// The Query Response Interval defined in [RFC 3810 section 9.3] and [RFC
206    /// 3376 section 8.3].
207    ///
208    /// Note that the RFCs mostly define this value in terms of the maximum
209    /// response code sent by queriers (routers), but later text references this
210    /// configuration to calculate timeouts.
211    ///
212    /// [RFC 3810 section 9.3]:
213    ///     https://datatracker.ietf.org/doc/html/rfc3810#section-9.3
214    /// [RFC 3376 section 8.3]:
215    ///     https://datatracker.ietf.org/doc/html/rfc3376#section-8.3
216    fn query_response_interval(&self) -> NonZeroDuration;
217
218    /// The Unsolicited Report Interval defined in [RFC 3810 section 9.11] and
219    /// [RFC 3376 section 8.11].
220    ///
221    /// [RFC 3810 section 9.11]:
222    ///     https://datatracker.ietf.org/doc/html/rfc3810#section-9.11
223    /// [RFC 3376 section 8.11]:
224    ///     https://datatracker.ietf.org/doc/html/rfc3376#section-8.11
225    fn unsolicited_report_interval(&self) -> NonZeroDuration;
226}
227
228/// Trait abstracting a GMPv2 query.
229///
230/// The getters in this trait represent fields in the membership query messages
231/// defined in [RFC 3376 section 4.1] and [RFC 3810 section 5.1].
232///
233/// [RFC 3376 section 4.1]:
234///     https://datatracker.ietf.org/doc/html/rfc3376#section-4.1
235/// [RFC 3810 section 5.1]:
236///     https://datatracker.ietf.org/doc/html/rfc3810#section-5.1
237pub(super) trait QueryMessage<I: Ip> {
238    /// Reinterprets this as a v1 query message.
239    fn as_v1(&self) -> impl gmp::v1::QueryMessage<I> + '_;
240
241    /// Gets the Querier's Robustness Variable (QRV).
242    fn robustness_variable(&self) -> u8;
243
244    /// Gets the Querier's Query Interval Code (QQIC) interpreted as a duration.
245    fn query_interval(&self) -> Duration;
246
247    /// Gets the group address.
248    fn group_address(&self) -> I::Addr;
249
250    /// Gets the maximum response time.
251    fn max_response_time(&self) -> Duration;
252
253    /// Gets an iterator to the source addresses being queried.
254    fn sources(&self) -> impl Iterator<Item = I::Addr> + '_;
255}
256
257#[derive(Eq, PartialEq, Debug)]
258pub(super) enum QueryError<I: Ip> {
259    NotAMember(I::Addr),
260    Disabled,
261}
262
263impl<I: Ip> From<NotAMemberErr<I>> for QueryError<I> {
264    fn from(NotAMemberErr(addr): NotAMemberErr<I>) -> Self {
265        Self::NotAMember(addr)
266    }
267}
268
269/// An enhancement to [`GmpReportGroupRecord`] that guarantees the yielded group
270/// address is [`GmpEnabledGroup`].
271pub(super) trait VerifiedReportGroupRecord<A: IpAddress>: GmpReportGroupRecord<A> {
272    // NB: We don't have any use for this method. It exists as a statement that
273    // the type implementing it holds a reference to GmpEnabledGroup.
274    #[allow(unused)]
275    fn gmp_enabled_group_addr(&self) -> &GmpEnabledGroup<A>;
276}
277
278#[derive(Clone)]
279pub(super) struct GroupRecord<A, Iter> {
280    group: GmpEnabledGroup<A>,
281    record_type: GroupRecordType,
282    iter: Iter,
283}
284
285impl<A> GroupRecord<A, core::iter::Empty<A>> {
286    pub(super) fn new(group: GmpEnabledGroup<A>, record_type: GroupRecordType) -> Self {
287        Self { group, record_type, iter: core::iter::empty() }
288    }
289}
290
291impl<A, Iter> GroupRecord<A, Iter> {
292    pub(super) fn new_with_sources(
293        group: GmpEnabledGroup<A>,
294        record_type: GroupRecordType,
295        iter: Iter,
296    ) -> Self {
297        Self { group, record_type, iter }
298    }
299}
300
301impl<A: IpAddress<Version: IpExt>, Iter: Iterator<Item: core::borrow::Borrow<A>> + Clone>
302    GmpReportGroupRecord<A> for GroupRecord<A, Iter>
303{
304    fn group(&self) -> MulticastAddr<A> {
305        self.group.multicast_addr()
306    }
307
308    fn record_type(&self) -> GroupRecordType {
309        self.record_type
310    }
311
312    fn sources(&self) -> impl Iterator<Item: core::borrow::Borrow<A>> + '_ {
313        self.iter.clone()
314    }
315}
316
317impl<A: IpAddress<Version: IpExt>, Iter: Iterator<Item: core::borrow::Borrow<A>> + Clone>
318    VerifiedReportGroupRecord<A> for GroupRecord<A, Iter>
319{
320    fn gmp_enabled_group_addr(&self) -> &GmpEnabledGroup<A> {
321        &self.group
322    }
323}
324
325/// Handles a query message from the network.
326///
327/// The RFC algorithm is specified on [RFC 3376 section 5.2] and [RFC 3810
328/// section 6.2].
329///
330/// [RFC 3376 section 5.2]:
331///     https://datatracker.ietf.org/doc/html/rfc3376#section-5.2
332/// [RFC 3810 section 6.2]:
333///     https://datatracker.ietf.org/doc/html/rfc3810#section-6.2
334pub(super) fn handle_query_message<
335    I: IpExt,
336    CC: GmpContext<I, BC>,
337    BC: GmpBindingsContext,
338    Q: QueryMessage<I>,
339>(
340    core_ctx: &mut CC,
341    bindings_ctx: &mut BC,
342    device: &CC::DeviceId,
343    query: &Q,
344) -> Result<(), QueryError<I>> {
345    core_ctx.with_gmp_state_mut_and_ctx(device, |mut core_ctx, state| {
346        // Ignore queries if we're not in enabled state.
347        if !state.enabled {
348            return Err(QueryError::Disabled);
349        }
350        match state.gmp.gmp_mode() {
351            GmpMode::V1 { .. } => {
352                return gmp::v1::handle_query_message_inner(
353                    &mut core_ctx,
354                    bindings_ctx,
355                    device,
356                    state,
357                    &query.as_v1(),
358                )
359                .map_err(Into::into);
360            }
361            GmpMode::V2 => {}
362        }
363        let GmpStateRef { enabled: _, groups, gmp, config: _ } = state;
364        // Update parameters if non zero given in query.
365        if let Some(qrv) = NonZeroU8::new(query.robustness_variable()) {
366            gmp.v2_proto.robustness_variable = qrv;
367        }
368        if let Some(qqic) = NonZeroDuration::new(query.query_interval()) {
369            gmp.v2_proto.query_interval = qqic;
370        }
371
372        let target = query.group_address();
373        let target = QueryTarget::new(target).ok_or(QueryError::NotAMember(target))?;
374
375        // Common early bailout.
376        let target = match target {
377            // General query.
378            QueryTarget::Unspecified => {
379                // RFC: When a new valid General Query arrives on an interface,
380                // the node checks whether it has any per-interface listening
381                // state record to report on, or not.
382                if groups.is_empty() {
383                    return Ok(());
384                }
385
386                // None target from now on marks a general query.
387                None
388            }
389            // Group-Specific or Group-And-Source-Specific query.
390            QueryTarget::Specified(multicast_addr) => {
391                // RFC: Similarly, when a new valid Multicast Address (and
392                // Source) Specific Query arrives on an interface, the node
393                // checks whether it has a per-interface listening state record
394                // that corresponds to the queried multicast address (and
395                // source), or not.
396
397                // TODO(https://fxbug.dev/381241191): We should also consider
398                // source lists here when we support SSM.
399
400                let group = groups
401                    .get_mut(&multicast_addr)
402                    .ok_or_else(|| QueryError::NotAMember(multicast_addr.get()))?;
403
404                // `Some` target marks a specific query.
405                Some((group.v2_mut(), multicast_addr))
406            }
407        };
408
409        // RFC: If it does, a delay for a response is randomly selected
410        // in the range (0, [Maximum Response Delay]).
411        let now = bindings_ctx.now();
412        let delay = now.saturating_add(gmp::random_report_timeout(
413            &mut bindings_ctx.rng(),
414            query.max_response_time(),
415        ));
416
417        // RFC: If there is a pending response to a previous General Query
418        // scheduled sooner than the selected delay, no additional response
419        // needs to be scheduled.
420        match gmp.timers.get(&TimerId::GeneralQuery.into()) {
421            Some((instant, ())) => {
422                if instant <= delay {
423                    return Ok(());
424                }
425            }
426            None => {}
427        }
428
429        let (group, addr) = match target {
430            // RFC: If the received Query is a General Query, the Interface
431            // Timer is used to schedule a response to the General Query after
432            // the selected delay.  Any previously pending response to a General
433            // Query is canceled.
434            None => {
435                let _: Option<_> = gmp.timers.schedule_instant(
436                    bindings_ctx,
437                    TimerId::GeneralQuery.into(),
438                    (),
439                    delay,
440                );
441                return Ok(());
442            }
443            Some(specific) => specific,
444        };
445
446        // The RFC quote for the next part is a bit long-winded but the
447        // algorithm is simple. Full quote:
448        //
449        //  If the received Query is a Multicast Address Specific Query or a
450        //  Multicast Address and Source Specific Query and there is no pending
451        //  response to a previous Query for this multicast address, then the
452        //  Multicast Address Timer is used to schedule a report.  If the
453        //  received Query is a Multicast Address and Source Specific Query, the
454        //  list of queried sources is recorded to be used when generating a
455        //  response.
456        //
457        //  If there is already a pending response to a previous Query scheduled
458        //  for this multicast address, and either the new Query is a Multicast
459        //  Address Specific Query or the recorded source list associated with
460        //  the multicast address is empty, then the multicast address source
461        //  list is cleared and a single response is scheduled, using the
462        //  Multicast Address Timer.  The new response is scheduled to be sent
463        //  at the earliest of the remaining time for the pending report and the
464        //  selected delay.
465        //
466        //  If the received Query is a Multicast Address and Source Specific
467        //  Query and there is a pending response for this multicast address
468        //  with a non-empty source list, then the multicast address source list
469        //  is augmented to contain the list of sources in the new Query, and a
470        //  single response is scheduled using the Multicast Address Timer.  The
471        //  new response is scheduled to be sent at the earliest of the
472        //  remaining time for the pending report and the selected delay.
473
474        // Ignore any queries to non GMP-enabled groups.
475        let addr = GmpEnabledGroup::try_new(addr)
476            .map_err(|addr| QueryError::NotAMember(addr.into_addr()))?;
477
478        let timer_id = TimerId::MulticastAddress(addr).into();
479        let scheduled = gmp.timers.get(&timer_id);
480        let mut sources = query.sources().peekable();
481
482        let (delay, clear_sources) = match scheduled {
483            // There is a scheduled report.
484            Some((t, ())) => {
485                // Only reschedule the timer if scheduling for earlier.
486                let delay = (delay < t).then_some(delay);
487                // Per the second paragraph above, clear sources if address
488                // query or if the pending report is already for an empty source
489                // list (meaning we don't restrict the old report to the new
490                // sources).
491                let is_address_query = sources.peek().is_none();
492                let clear_sources = group.recorded_sources.is_empty() || is_address_query;
493                (delay, clear_sources)
494            }
495            // No scheduled report, use new delay and record sources.
496            None => (Some(delay), false),
497        };
498
499        if clear_sources {
500            group.recorded_sources = Default::default();
501        } else {
502            group.recorded_sources.extend(sources);
503        }
504
505        if let Some(delay) = delay {
506            let _: Option<_> = gmp.timers.schedule_instant(bindings_ctx, timer_id, (), delay);
507        }
508
509        Ok(())
510    })
511}
512
513/// Joins `group_addr`.
514///
515/// This is called whenever a socket joins a group, network actions are only
516/// taken when the action actually results in a newly joined group, otherwise
517/// the group's reference counter is simply updated.
518///
519/// The reference for changing interface state is in [RFC 3376 section 5.1] and
520/// [RFC 3810 section 6.1].
521///
522/// [RFC 3376 section 5.1]:
523///     https://datatracker.ietf.org/doc/html/rfc3376#section-5.1
524/// [RFC 3810 section 6.1]:
525///     https://datatracker.ietf.org/doc/html/rfc3810#section-6.1
526pub(super) fn join_group<I: IpExt, CC: GmpTypeLayout<I, BC>, BC: GmpBindingsContext>(
527    bindings_ctx: &mut BC,
528    group_addr: MulticastAddr<I::Addr>,
529    state: GmpStateRef<'_, I, CC, BC>,
530) -> GroupJoinResult {
531    let GmpStateRef { enabled, groups, gmp, config: _ } = state;
532    debug_assert!(gmp.gmp_mode().is_v2());
533    groups.join_group_with(group_addr, || {
534        let filter_mode_retransmission_counter = match GmpEnabledGroup::new(group_addr) {
535            Some(group_addr) => {
536                // We've just joined a group, remove anything any pending state from the
537                // left groups.
538                let _: Option<_> = gmp.v2_proto.left_groups.remove(&group_addr);
539
540                if enabled {
541                    trigger_state_change_report(bindings_ctx, &mut gmp.timers);
542                    gmp.v2_proto.robustness_variable.get()
543                } else {
544                    0
545                }
546            }
547            None => 0,
548        };
549
550        let state =
551            GroupState { recorded_sources: Default::default(), filter_mode_retransmission_counter };
552
553        (GmpGroupState::new_v2(state), ())
554    })
555}
556
557/// Leaves `group_addr`.
558///
559/// This is called whenever a socket leaves a group, network actions are only
560/// taken when the action actually results in a newly left group, otherwise the
561/// group's reference counter is simply updated.
562///
563/// The reference for changing interface state is in [RFC 3376 section 5.1] and
564/// [RFC 3810 section 6.1].
565///
566/// [RFC 3376 section 5.1]:
567///     https://datatracker.ietf.org/doc/html/rfc3376#section-5.1
568/// [RFC 3810 section
569///     6.1]:https://datatracker.ietf.org/doc/html/rfc3810#section-6.1
570pub(super) fn leave_group<I: IpExt, CC: GmpTypeLayout<I, BC>, BC: GmpBindingsContext>(
571    bindings_ctx: &mut BC,
572    group_addr: MulticastAddr<I::Addr>,
573    state: GmpStateRef<'_, I, CC, BC>,
574) -> GroupLeaveResult {
575    let GmpStateRef { enabled, groups, gmp, config: _ } = state;
576    debug_assert!(gmp.gmp_mode().is_v2());
577    groups.leave_group(group_addr).map(|state| {
578        let group_addr = if let Some(a) = GmpEnabledGroup::new(group_addr) { a } else { return };
579
580        // Cancel existing query timers since we've left the group.
581        let _: Option<_> =
582            gmp.timers.cancel(bindings_ctx, &TimerId::MulticastAddress(group_addr).into());
583
584        // Nothing to do with old state since we're resetting the retransmission
585        // counter.
586        let GroupState { filter_mode_retransmission_counter: _, recorded_sources: _ } =
587            state.into_v2();
588
589        if !enabled {
590            return;
591        }
592        assert_eq!(
593            gmp.v2_proto.left_groups.insert(group_addr, gmp.v2_proto.robustness_variable),
594            None
595        );
596        trigger_state_change_report(bindings_ctx, &mut gmp.timers);
597    })
598}
599
600/// Schedules a state change report to be sent in response to an interface state
601/// change.
602///
603/// Schedule the State Change timer if it's not scheduled already or if it's
604/// scheduled to fire later than the [`STATE_CHANGE_REPORT_DELAY`] in the
605/// future. This guarantees that the report will go out at most
606/// [`STATE_CHANGE_REPORT_DELAY`] in the future, which should be seen as
607/// "immediate". See documentation on [`STATE_CHANGE_REPORT_DELAY`] for details.
608fn trigger_state_change_report<I: IpExt, BC: GmpBindingsContext>(
609    bindings_ctx: &mut BC,
610    timers: &mut LocalTimerHeap<gmp::TimerIdInner<I>, (), BC>,
611) {
612    let now = bindings_ctx.now();
613    let timer_id = TimerId::StateChange.into();
614    let schedule_timer = timers.get(&timer_id).is_none_or(|(scheduled, ())| {
615        scheduled.saturating_duration_since(now) > STATE_CHANGE_REPORT_DELAY
616    });
617    if schedule_timer {
618        let _: Option<_> =
619            timers.schedule_after(bindings_ctx, timer_id, (), STATE_CHANGE_REPORT_DELAY);
620    }
621}
622
623/// Handles an expire timer.
624///
625/// The timer expiration algorithm is described in [RFC 3376 section 5.1] and
626/// [RFC 3376 section 5.2] for IGMP and [RFC 3810 section 6.3] for MLD.
627///
628/// [RFC 3376 section 5.1]:
629///     https://datatracker.ietf.org/doc/html/rfc3376#section-5.1
630/// [RFC 3376 section 5.2]:
631///     https://datatracker.ietf.org/doc/html/rfc3376#section-5.2
632/// [RFC 3810 section 6.3]:
633///     https://datatracker.ietf.org/doc/html/rfc3810#section-6.3
634pub(super) fn handle_timer<I: IpExt, CC: GmpContextInner<I, BC>, BC: GmpBindingsContext>(
635    core_ctx: &mut CC,
636    bindings_ctx: &mut BC,
637    device: &CC::DeviceId,
638    timer: TimerId<I>,
639    state: GmpStateRef<'_, I, CC::TypeLayout, BC>,
640) {
641    match timer {
642        TimerId::GeneralQuery => handle_general_query_timer(core_ctx, bindings_ctx, device, state),
643        TimerId::MulticastAddress(multicast_addr) => {
644            handle_multicast_address_timer(core_ctx, bindings_ctx, device, multicast_addr, state)
645        }
646        TimerId::StateChange => handle_state_change_timer(core_ctx, bindings_ctx, device, state),
647    }
648}
649
650/// Handles general query timers.
651///
652/// Quote from RFC 3810:
653///
654/// > If the expired timer is the Interface Timer (i.e., there is a pending
655/// > response to a General Query), then one Current State Record is sent for
656/// > each multicast address for which the specified interface has listening
657/// > state [...]. The Current State Record carries the multicast address and
658/// > its associated filter mode (MODE_IS_INCLUDE or MODE_IS_EXCLUDE) and Source
659/// > list. Multiple Current State Records are packed into individual Report
660/// > messages, to the extent possible.
661fn handle_general_query_timer<I: IpExt, CC: GmpContextInner<I, BC>, BC: GmpBindingsContext>(
662    core_ctx: &mut CC,
663    bindings_ctx: &mut BC,
664    device: &CC::DeviceId,
665    state: GmpStateRef<'_, I, CC::TypeLayout, BC>,
666) {
667    let GmpStateRef { enabled: _, groups, gmp: _, config: _ } = state;
668    let report = groups.iter().filter_map(|(addr, state)| {
669        // TODO(https://fxbug.dev/381241191): Update to include SSM in group
670        // records.
671        let _ = state;
672
673        // Ignore any groups that are not enabled for GMP.
674        let group = GmpEnabledGroup::new(*addr)?;
675
676        // Given we don't support SSM, all the groups we're currently joined
677        // should be reported in exclude mode with an empty source list.
678        //
679        // See https://datatracker.ietf.org/doc/html/rfc3810#section-5.2.12 and
680        // https://datatracker.ietf.org/doc/html/rfc3376#section-4.2.12 for
681        // group record type descriptions.
682
683        Some(GroupRecord::new(group, GroupRecordType::ModeIsExclude))
684    });
685    core_ctx.send_report_v2(bindings_ctx, device, report)
686}
687
688/// Handles a multicast address timer for `multicast_addr`.
689///
690/// RFC 3810 quote:
691///
692/// > If the expired timer is a Multicast Address Timer and the list of recorded
693/// > sources for that multicast address is empty (i.e., there is a pending
694/// > response to a Multicast Address Specific Query), then if, and only if, the
695/// > interface has listening state for that multicast address, a single Current
696/// > State Record is sent for that address. The Current State Record carries
697/// > the multicast address and its associated filter mode (MODE_IS_INCLUDE or
698/// > MODE_IS_EXCLUDE) and source list, if any.
699/// >
700/// > If the expired timer is a Multicast Address Timer and the list of recorded
701/// > sources for that multicast address is non-empty (i.e., there is a pending
702/// > response to a Multicast Address and Source Specific Query), then if, and
703/// > only if, the interface has listening state for that multicast address, the
704/// > contents of the corresponding Current State Record are determined from the
705/// > per- interface state and the pending response record, as specified in the
706/// > following table:
707/// >
708/// >                        set of sources in the
709/// > per-interface state  pending response record  Current State Record
710/// > -------------------  -----------------------  --------------------
711/// >  INCLUDE (A)                   B                IS_IN (A*B)
712/// >
713/// >  EXCLUDE (A)                   B                IS_IN (B-A)
714fn handle_multicast_address_timer<I: IpExt, CC: GmpContextInner<I, BC>, BC: GmpBindingsContext>(
715    core_ctx: &mut CC,
716    bindings_ctx: &mut BC,
717    device: &CC::DeviceId,
718    multicast_addr: GmpEnabledGroup<I::Addr>,
719    state: GmpStateRef<'_, I, CC::TypeLayout, BC>,
720) {
721    let GmpStateRef { enabled: _, groups, gmp: _, config: _ } = state;
722    // Invariant: multicast address timers are removed when we remove interest
723    // from the group.
724    let state = groups
725        .get_mut(multicast_addr.as_ref())
726        .expect("multicast timer fired for removed address")
727        .v2_mut();
728    let recorded_sources = core::mem::take(&mut state.recorded_sources);
729
730    let (mode, sources) = if recorded_sources.is_empty() {
731        // Multicast Address Specific Query.
732
733        // TODO(https://fxbug.dev/381241191): Update to include SSM-enabled
734        // filter mode. For now, ModeIsExclude is all that needs to be reported
735        // for any group we're a member of.
736
737        (GroupRecordType::ModeIsExclude, either::Either::Left(core::iter::empty::<&I::Addr>()))
738    } else {
739        // Multicast Address And Source Specific Query. The mode is always
740        // include.
741
742        // TODO(https://fxbug.dev/381241191): Actually calculate set
743        // intersection or union when SSM is available.
744
745        (GroupRecordType::ModeIsInclude, either::Either::Right(recorded_sources.iter()))
746    };
747    core_ctx.send_report_v2(
748        bindings_ctx,
749        device,
750        core::iter::once(GroupRecord::new_with_sources(multicast_addr, mode, sources)),
751    );
752}
753
754/// Handles the interface state change timer.
755///
756/// Note: sometimes referred to in the RFCs as `Retransmission Timer for a
757/// multicast address`. This is actually an interface-wide timer that
758/// "synchronizes" the retransmission instant for all the multicast addresses
759/// with pending reports.
760///
761/// RFC quote:
762///
763/// > If the expired timer is a Retransmission Timer for a multicast address
764/// > (i.e., there is a pending State Change Report for that multicast address),
765/// > the contents of the report are determined as follows. If the report should
766/// > contain a Filter Mode Change Record, i.e., the Filter Mode Retransmission
767/// > Counter for that multicast address has a value higher than zero, then, if
768/// > the current filter mode of the interface is INCLUDE, a TO_IN record is
769/// > included in the report; otherwise a TO_EX record is included.  In both
770/// > cases, the Filter Mode Retransmission Counter for that multicast address
771/// > is decremented by one unit after the transmission of the report.
772/// >
773/// > If instead the report should contain Source List Change Records, i.e., the
774/// > Filter Mode Retransmission Counter for that multicast address is zero, an
775/// > ALLOW and a BLOCK record is included.
776fn handle_state_change_timer<I: IpExt, CC: GmpContextInner<I, BC>, BC: GmpBindingsContext>(
777    core_ctx: &mut CC,
778    bindings_ctx: &mut BC,
779    device: &CC::DeviceId,
780    state: GmpStateRef<'_, I, CC::TypeLayout, BC>,
781) {
782    let GmpStateRef { enabled: _, groups, gmp, config } = state;
783
784    let joined_groups = groups.iter().filter_map(|(multicast_addr, state)| {
785        let GroupState { filter_mode_retransmission_counter, recorded_sources: _ } = state.v2();
786        if *filter_mode_retransmission_counter == 0 {
787            return None;
788        }
789        let multicast_addr = GmpEnabledGroup::new(*multicast_addr)?;
790        Some(GroupRecord::new(
791            multicast_addr,
792            // TODO(https://fxbug.dev/381241191): Take the filter mode from
793            // group state. Joined groups for now are always exclude mode.
794            GroupRecordType::ChangeToExcludeMode,
795        ))
796    });
797    let left_groups = gmp.v2_proto.left_groups.keys().map(|multicast_addr| {
798        GroupRecord::new(
799            *multicast_addr,
800            // TODO(https://fxbug.dev/381241191): Take the filter mode from
801            // group state. Left groups for now are always include mode.
802            GroupRecordType::ChangeToIncludeMode,
803        )
804    });
805    let state_change_report = joined_groups.chain(left_groups);
806    core_ctx.send_report_v2(bindings_ctx, device, state_change_report);
807
808    // Subtract the retransmission counters across the board.
809    let has_more = groups.iter_mut().fold(false, |has_more, (_, g)| {
810        let v2 = g.v2_mut();
811        v2.filter_mode_retransmission_counter =
812            v2.filter_mode_retransmission_counter.saturating_sub(1);
813        has_more || v2.filter_mode_retransmission_counter != 0
814    });
815    gmp.v2_proto.left_groups.retain(|_, counter| match NonZeroU8::new(counter.get() - 1) {
816        None => false,
817        Some(new_value) => {
818            *counter = new_value;
819            true
820        }
821    });
822    let has_more = has_more || !gmp.v2_proto.left_groups.is_empty();
823    if has_more {
824        let delay = gmp::random_report_timeout(
825            &mut bindings_ctx.rng(),
826            config.unsolicited_report_interval().get(),
827        );
828        assert_eq!(
829            gmp.timers.schedule_after(bindings_ctx, TimerId::StateChange.into(), (), delay),
830            None
831        );
832    }
833}
834
835/// Takes GMP actions when GMP becomes enabled.
836///
837/// This happens whenever the GMP switches to on or IP is enabled on an
838/// interface (i.e. interface up). The side-effects here are not _quite_ covered
839/// by the RFC, but the interpretation is that enablement is equivalent to all
840/// the tracked groups becoming newly joined and we want to inform routers on
841/// the network about it.
842pub(super) fn handle_enabled<I: IpExt, CC: GmpTypeLayout<I, BC>, BC: GmpBindingsContext>(
843    bindings_ctx: &mut BC,
844    state: GmpStateRef<'_, I, CC, BC>,
845) {
846    let GmpStateRef { enabled: _, groups, gmp, config: _ } = state;
847
848    let needs_report = groups.iter_mut().fold(false, |needs_report, (multicast_addr, state)| {
849        if !I::should_perform_gmp(*multicast_addr) {
850            return needs_report;
851        }
852        let GroupState { filter_mode_retransmission_counter, recorded_sources: _ } = state.v2_mut();
853        *filter_mode_retransmission_counter = gmp.v2_proto.robustness_variable.get();
854        true
855    });
856    if needs_report {
857        trigger_state_change_report(bindings_ctx, &mut gmp.timers);
858    }
859}
860
861/// Takes GMP actions when GMP becomes disabled.
862///
863/// This happens whenever the GMP switches to off or IP is disabled on an
864/// interface (i.e. interface down). The side-effects here are not _quite_
865/// covered by the RFC, but the interpretation is that disablement is equivalent
866/// to all the tracked groups being left and we want to inform routers on the
867/// network about it.
868///
869/// Unlike [`handle_enabled`], however, given this may be a last-ditch effort to
870/// notify a router that an admin is turning off an interface, we immediately
871/// send a _single_ report saying we've left all our groups. Given the interface
872/// is possibly about to go off, we can't schedule any timers.
873pub(super) fn handle_disabled<I: IpExt, CC: GmpContextInner<I, BC>, BC: GmpBindingsContext>(
874    core_ctx: &mut CC,
875    bindings_ctx: &mut BC,
876    device: &CC::DeviceId,
877    state: GmpStateRef<'_, I, CC::TypeLayout, BC>,
878) {
879    let GmpStateRef { enabled: _, groups, gmp, config: _ } = state;
880    // Clear all group retransmission state and cancel all timers.
881    for (_, state) in groups.iter_mut() {
882        *state.v2_mut() = GroupState {
883            filter_mode_retransmission_counter: 0,
884            recorded_sources: Default::default(),
885        };
886    }
887
888    let member_groups =
889        groups.iter().filter_map(|(multicast_addr, _)| GmpEnabledGroup::new(*multicast_addr));
890    // Also include any non-member groups that might've been waiting
891    // retransmissions.
892    let non_member_groups = gmp.v2_proto.left_groups.keys().copied();
893
894    let mut report = member_groups
895        .chain(non_member_groups)
896        .map(|addr| GroupRecord::new(addr, GroupRecordType::ChangeToIncludeMode))
897        .peekable();
898    if report.peek().is_none() {
899        // Nothing to report.
900        return;
901    }
902    core_ctx.send_report_v2(bindings_ctx, device, report);
903}
904
905#[cfg(test)]
906mod tests {
907    use alloc::vec;
908    use alloc::vec::Vec;
909
910    use assert_matches::assert_matches;
911    use ip_test_macro::ip_test;
912    use net_types::Witness as _;
913    use netstack3_base::InstantContext as _;
914    use netstack3_base::testutil::{FakeDeviceId, FakeTimerCtxExt, FakeWeakDeviceId};
915    use test_case::{test_case, test_matrix};
916
917    use super::*;
918    use crate::gmp::GmpTimerId;
919    use crate::internal::gmp::testutil::{
920        self, FakeCtx, FakeGmpBindingsContext, FakeGmpContext, FakeV2Query, TestIpExt,
921    };
922    use crate::internal::gmp::{GmpHandler as _, GroupJoinResult};
923
924    #[derive(Debug, Eq, PartialEq)]
925    enum SpecificQuery {
926        Multicast,
927        MulticastAndSource,
928    }
929
930    fn join_and_ignore_unsolicited<I: IpExt>(
931        ctx: &mut FakeCtx<I>,
932        groups: impl IntoIterator<Item = MulticastAddr<I::Addr>>,
933    ) {
934        let FakeCtx { core_ctx, bindings_ctx } = ctx;
935        for group in groups {
936            assert_eq!(
937                core_ctx.gmp_join_group(bindings_ctx, &FakeDeviceId, group),
938                GroupJoinResult::Joined(())
939            );
940        }
941        while !core_ctx.gmp.timers.is_empty() {
942            assert_eq!(
943                bindings_ctx.trigger_next_timer(core_ctx),
944                Some(GmpTimerId::new(FakeWeakDeviceId(FakeDeviceId)))
945            );
946        }
947        core_ctx.inner.v2_messages.clear();
948    }
949
950    impl<I: IpExt> TimerId<I> {
951        fn multicast(addr: MulticastAddr<I::Addr>) -> Self {
952            Self::MulticastAddress(GmpEnabledGroup::new(addr).unwrap())
953        }
954    }
955
956    #[ip_test(I)]
957    fn v2_query_handoff_in_v1_mode<I: TestIpExt>() {
958        let FakeCtx { mut core_ctx, mut bindings_ctx } =
959            testutil::new_context_with_mode::<I>(GmpMode::V1 { compat: true });
960        assert_eq!(
961            core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
962            GroupJoinResult::Joined(())
963        );
964        assert_eq!(
965            bindings_ctx.trigger_next_timer(&mut core_ctx),
966            Some(GmpTimerId::new(FakeWeakDeviceId(FakeDeviceId)))
967        );
968        // v1 group should be idle now.
969        assert_matches!(
970            core_ctx.groups.get(&I::GROUP_ADDR1).unwrap().v1().get_inner(),
971            gmp::v1::MemberState::Idle(_)
972        );
973        handle_query_message(
974            &mut core_ctx,
975            &mut bindings_ctx,
976            &FakeDeviceId,
977            &FakeV2Query { group_addr: I::GROUP_ADDR1.get(), ..Default::default() },
978        )
979        .expect("handle query");
980        // v1 group reacts to the query.
981        assert_matches!(
982            core_ctx.groups.get(&I::GROUP_ADDR1).unwrap().v1().get_inner(),
983            gmp::v1::MemberState::Delaying(_)
984        );
985    }
986
987    #[ip_test(I)]
988    fn general_query_ignored_if_no_groups<I: TestIpExt>() {
989        let FakeCtx { mut core_ctx, mut bindings_ctx } =
990            testutil::new_context_with_mode::<I>(GmpMode::V2);
991        handle_query_message(
992            &mut core_ctx,
993            &mut bindings_ctx,
994            &FakeDeviceId,
995            &FakeV2Query { group_addr: I::UNSPECIFIED_ADDRESS, ..Default::default() },
996        )
997        .expect("handle query");
998        assert_eq!(core_ctx.gmp.timers.get(&TimerId::GeneralQuery.into()), None);
999    }
1000
1001    #[ip_test(I)]
1002    fn query_errors_if_not_multicast<I: TestIpExt>() {
1003        let FakeCtx { mut core_ctx, mut bindings_ctx } =
1004            testutil::new_context_with_mode::<I>(GmpMode::V2);
1005        let query = FakeV2Query { group_addr: I::LOOPBACK_ADDRESS.get(), ..Default::default() };
1006        assert_eq!(
1007            handle_query_message(&mut core_ctx, &mut bindings_ctx, &FakeDeviceId, &query,),
1008            Err(QueryError::NotAMember(query.group_addr))
1009        );
1010    }
1011
1012    #[ip_test(I)]
1013    fn general_query_scheduled<I: TestIpExt>() {
1014        let FakeCtx { mut core_ctx, mut bindings_ctx } =
1015            testutil::new_context_with_mode::<I>(GmpMode::V2);
1016        assert_eq!(
1017            core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1018            GroupJoinResult::Joined(())
1019        );
1020        let query = FakeV2Query { group_addr: I::UNSPECIFIED_ADDRESS, ..Default::default() };
1021
1022        let general_query_timer = TimerId::GeneralQuery.into();
1023
1024        handle_query_message(&mut core_ctx, &mut bindings_ctx, &FakeDeviceId, &query)
1025            .expect("handle query");
1026        let now = bindings_ctx.now();
1027        let (scheduled, ()) = core_ctx.gmp.timers.assert_range_single(
1028            &general_query_timer,
1029            now..=now.panicking_add(query.max_response_time),
1030        );
1031
1032        // Any further queries are ignored  if we have a pending general query
1033        // in the past.
1034
1035        // Advance time enough to guarantee we can't pick an earlier time.
1036        bindings_ctx.timers.instant.sleep(query.max_response_time);
1037
1038        let query = FakeV2Query { group_addr: I::UNSPECIFIED_ADDRESS, ..Default::default() };
1039        handle_query_message(&mut core_ctx, &mut bindings_ctx, &FakeDeviceId, &query)
1040            .expect("handle query");
1041        assert_eq!(core_ctx.gmp.timers.get(&general_query_timer), Some((scheduled, &())));
1042
1043        let query = FakeV2Query { group_addr: I::GROUP_ADDR1.get(), ..Default::default() };
1044        handle_query_message(&mut core_ctx, &mut bindings_ctx, &FakeDeviceId, &query)
1045            .expect("handle query");
1046        assert_eq!(core_ctx.gmp.timers.get(&TimerId::multicast(I::GROUP_ADDR1).into()), None);
1047    }
1048
1049    #[ip_test(I)]
1050    fn specific_query_ignored_if_not_member<I: TestIpExt>() {
1051        let FakeCtx { mut core_ctx, mut bindings_ctx } =
1052            testutil::new_context_with_mode::<I>(GmpMode::V2);
1053        assert_eq!(
1054            core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, I::GROUP_ADDR2),
1055            GroupJoinResult::Joined(())
1056        );
1057        let query = FakeV2Query { group_addr: I::GROUP_ADDR1.get(), ..Default::default() };
1058        assert_eq!(
1059            handle_query_message(&mut core_ctx, &mut bindings_ctx, &FakeDeviceId, &query),
1060            Err(QueryError::NotAMember(query.group_addr))
1061        );
1062    }
1063
1064    #[ip_test(I)]
1065    fn leave_group_cancels_multicast_address_timer<I: TestIpExt>() {
1066        let FakeCtx { mut core_ctx, mut bindings_ctx } =
1067            testutil::new_context_with_mode::<I>(GmpMode::V2);
1068        assert_eq!(
1069            core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1070            GroupJoinResult::Joined(())
1071        );
1072        let query = FakeV2Query { group_addr: I::GROUP_ADDR1.get(), ..Default::default() };
1073        handle_query_message(&mut core_ctx, &mut bindings_ctx, &FakeDeviceId, &query)
1074            .expect("handle query");
1075        assert_matches!(
1076            core_ctx.gmp.timers.get(&TimerId::multicast(I::GROUP_ADDR1).into()),
1077            Some(_)
1078        );
1079        assert_eq!(
1080            core_ctx.gmp_leave_group(&mut bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1081            GroupLeaveResult::Left(())
1082        );
1083        assert_matches!(core_ctx.gmp.timers.get(&TimerId::multicast(I::GROUP_ADDR1).into()), None);
1084    }
1085
1086    #[ip_test(I)]
1087    #[test_matrix(
1088        [SpecificQuery::Multicast, SpecificQuery::MulticastAndSource],
1089        [SpecificQuery::Multicast, SpecificQuery::MulticastAndSource]
1090    )]
1091    fn schedule_specific_query<I: TestIpExt>(first: SpecificQuery, second: SpecificQuery) {
1092        let FakeCtx { mut core_ctx, mut bindings_ctx } =
1093            testutil::new_context_with_mode::<I>(GmpMode::V2);
1094        assert_eq!(
1095            core_ctx.gmp_join_group(&mut bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1096            GroupJoinResult::Joined(())
1097        );
1098
1099        let sources = match first {
1100            SpecificQuery::Multicast => Default::default(),
1101            SpecificQuery::MulticastAndSource => {
1102                (1..3).map(|i| I::get_other_ip_address(i).get()).collect()
1103            }
1104        };
1105
1106        let query1 =
1107            FakeV2Query { group_addr: I::GROUP_ADDR1.get(), sources, ..Default::default() };
1108        handle_query_message(&mut core_ctx, &mut bindings_ctx, &FakeDeviceId, &query1)
1109            .expect("handle query");
1110        // Sources are recorded.
1111        assert_eq!(
1112            core_ctx.groups.get(&I::GROUP_ADDR1).unwrap().v2().recorded_sources,
1113            query1.sources.iter().copied().collect()
1114        );
1115        // Timer is scheduled.
1116        let now = bindings_ctx.now();
1117        let (scheduled, ()) = core_ctx.gmp.timers.assert_range_single(
1118            &TimerId::multicast(I::GROUP_ADDR1).into(),
1119            now..=now.panicking_add(query1.max_response_time),
1120        );
1121
1122        let sources = match second {
1123            SpecificQuery::Multicast => Default::default(),
1124            SpecificQuery::MulticastAndSource => {
1125                (3..5).map(|i| I::get_other_ip_address(i).get()).collect()
1126            }
1127        };
1128        let query2 = FakeV2Query {
1129            group_addr: I::GROUP_ADDR1.get(),
1130            // Send a follow up query on a shorter timeline.
1131            max_response_time: DEFAULT_QUERY_RESPONSE_INTERVAL.get() / 2,
1132            sources,
1133            ..Default::default()
1134        };
1135        handle_query_message(&mut core_ctx, &mut bindings_ctx, &FakeDeviceId, &query2)
1136            .expect("handle query");
1137
1138        let (new_scheduled, ()) = core_ctx.gmp.timers.assert_range_single(
1139            &TimerId::multicast(I::GROUP_ADDR1).into(),
1140            now..=now.panicking_add(query2.max_response_time),
1141        );
1142        // Scheduled time is allowed to change, but always to an earlier time.
1143        assert!(new_scheduled <= scheduled, "{new_scheduled:?} <= {scheduled:?}");
1144        // Now check the group state.
1145        let recorded_sources = &core_ctx.groups.get(&I::GROUP_ADDR1).unwrap().v2().recorded_sources;
1146        match (first, second) {
1147            (SpecificQuery::Multicast, _) | (_, SpecificQuery::Multicast) => {
1148                // If any of the queries is multicast-specific then:
1149                // - Never added any sources.
1150                // - Newer sources must not override previous
1151                //   multicast-specific.
1152                // - New multicast-specific overrides previous sources.
1153                assert_eq!(recorded_sources, &HashSet::new());
1154            }
1155            (SpecificQuery::MulticastAndSource, SpecificQuery::MulticastAndSource) => {
1156                // List is augmented with the union.
1157                assert_eq!(
1158                    recorded_sources,
1159                    &query1.sources.iter().chain(query2.sources.iter()).copied().collect()
1160                );
1161            }
1162        }
1163    }
1164
1165    #[ip_test(I)]
1166    fn send_general_query_response<I: TestIpExt>() {
1167        let mut ctx = testutil::new_context_with_mode::<I>(GmpMode::V2);
1168        join_and_ignore_unsolicited(&mut ctx, [I::GROUP_ADDR1, I::GROUP_ADDR2]);
1169        let FakeCtx { core_ctx, bindings_ctx } = &mut ctx;
1170        handle_query_message(core_ctx, bindings_ctx, &FakeDeviceId, &FakeV2Query::default())
1171            .expect("handle query");
1172        assert_eq!(
1173            bindings_ctx.trigger_next_timer(core_ctx),
1174            Some(GmpTimerId::new(FakeWeakDeviceId(FakeDeviceId)))
1175        );
1176        assert_eq!(
1177            core_ctx.inner.v2_messages,
1178            vec![vec![
1179                (I::GROUP_ADDR1, GroupRecordType::ModeIsExclude, vec![]),
1180                (I::GROUP_ADDR2, GroupRecordType::ModeIsExclude, vec![]),
1181            ]]
1182        );
1183    }
1184
1185    #[ip_test(I)]
1186    fn send_multicast_address_specific_query_response<I: TestIpExt>() {
1187        let mut ctx = testutil::new_context_with_mode::<I>(GmpMode::V2);
1188        join_and_ignore_unsolicited(&mut ctx, [I::GROUP_ADDR1]);
1189        let FakeCtx { core_ctx, bindings_ctx } = &mut ctx;
1190        handle_query_message(
1191            core_ctx,
1192            bindings_ctx,
1193            &FakeDeviceId,
1194            &FakeV2Query { group_addr: I::GROUP_ADDR1.get(), ..Default::default() },
1195        )
1196        .expect("handle query");
1197        assert_eq!(
1198            bindings_ctx.trigger_next_timer(core_ctx),
1199            Some(GmpTimerId::new(FakeWeakDeviceId(FakeDeviceId)))
1200        );
1201        assert_eq!(
1202            core_ctx.inner.v2_messages,
1203            vec![vec![(I::GROUP_ADDR1, GroupRecordType::ModeIsExclude, vec![])]]
1204        );
1205    }
1206
1207    #[ip_test(I)]
1208    fn send_multicast_address_and_source_specific_query_response<I: TestIpExt>() {
1209        let mut ctx = testutil::new_context_with_mode::<I>(GmpMode::V2);
1210        join_and_ignore_unsolicited(&mut ctx, [I::GROUP_ADDR1]);
1211        let FakeCtx { core_ctx, bindings_ctx } = &mut ctx;
1212        let query = FakeV2Query {
1213            group_addr: I::GROUP_ADDR1.get(),
1214            sources: vec![I::get_other_ip_address(1).get(), I::get_other_ip_address(2).get()],
1215            ..Default::default()
1216        };
1217        handle_query_message(core_ctx, bindings_ctx, &FakeDeviceId, &query).expect("handle query");
1218        assert_eq!(
1219            bindings_ctx.trigger_next_timer(core_ctx),
1220            Some(GmpTimerId::new(FakeWeakDeviceId(FakeDeviceId)))
1221        );
1222        assert_eq!(
1223            core_ctx.inner.v2_messages,
1224            vec![vec![(I::GROUP_ADDR1, GroupRecordType::ModeIsInclude, query.sources)]]
1225        );
1226    }
1227
1228    #[ip_test(I)]
1229    #[test_case(2)]
1230    #[test_case(4)]
1231    fn join_group_unsolicited_reports<I: TestIpExt>(robustness_variable: u8) {
1232        let mut ctx = testutil::new_context_with_mode::<I>(GmpMode::V2);
1233        let FakeCtx { core_ctx, bindings_ctx } = &mut ctx;
1234        core_ctx.gmp.v2_proto.robustness_variable = NonZeroU8::new(robustness_variable).unwrap();
1235        assert_eq!(
1236            core_ctx.gmp_join_group(bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1237            GroupJoinResult::Joined(())
1238        );
1239        // Nothing is sent immediately.
1240        assert_eq!(core_ctx.inner.v2_messages, Vec::<Vec<_>>::new());
1241        let now = bindings_ctx.now();
1242        assert_eq!(
1243            core_ctx.gmp.timers.get(&TimerId::StateChange.into()),
1244            Some((now.panicking_add(STATE_CHANGE_REPORT_DELAY), &()))
1245        );
1246        let mut count = 0;
1247        while let Some(timer) = bindings_ctx.trigger_next_timer(core_ctx) {
1248            count += 1;
1249            assert_eq!(timer, GmpTimerId::new(FakeWeakDeviceId(FakeDeviceId)));
1250            let messages = core::mem::take(&mut core_ctx.inner.v2_messages);
1251            assert_eq!(
1252                messages,
1253                vec![vec![(I::GROUP_ADDR1, GroupRecordType::ChangeToExcludeMode, vec![])]]
1254            );
1255
1256            if count != robustness_variable {
1257                let now = bindings_ctx.now();
1258                core_ctx.gmp.timers.assert_range([(
1259                    &TimerId::StateChange.into(),
1260                    now..=now.panicking_add(core_ctx.config.unsolicited_report_interval().get()),
1261                )]);
1262            }
1263        }
1264        assert_eq!(count, robustness_variable);
1265        core_ctx.gmp.timers.assert_timers([]);
1266
1267        // Joining again has no side-effects.
1268        assert_eq!(
1269            core_ctx.gmp_join_group(bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1270            GroupJoinResult::AlreadyMember
1271        );
1272        // No timers, no messages.
1273        core_ctx.gmp.timers.assert_timers([]);
1274        assert_eq!(core_ctx.inner.v2_messages, Vec::<Vec<_>>::new());
1275    }
1276
1277    #[ip_test(I)]
1278    #[test_case(2)]
1279    #[test_case(4)]
1280    fn leave_group_unsolicited_reports<I: TestIpExt>(robustness_variable: u8) {
1281        let mut ctx = testutil::new_context_with_mode::<I>(GmpMode::V2);
1282        join_and_ignore_unsolicited(&mut ctx, [I::GROUP_ADDR1]);
1283        let FakeCtx { core_ctx, bindings_ctx } = &mut ctx;
1284        core_ctx.gmp.v2_proto.robustness_variable = NonZeroU8::new(robustness_variable).unwrap();
1285
1286        // Join the same group again. Like two sockets are interested in this
1287        // group.
1288        assert_eq!(
1289            core_ctx.gmp_join_group(bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1290            GroupJoinResult::AlreadyMember
1291        );
1292
1293        // Leaving non member has no side-effects.
1294        assert_eq!(
1295            core_ctx.gmp_leave_group(bindings_ctx, &FakeDeviceId, I::GROUP_ADDR2),
1296            GroupLeaveResult::NotMember
1297        );
1298        core_ctx.gmp.timers.assert_timers([]);
1299        assert_eq!(core_ctx.inner.v2_messages, Vec::<Vec<_>>::new());
1300
1301        // First leave we're still member and no side-effects.
1302        assert_eq!(
1303            core_ctx.gmp_leave_group(bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1304            GroupLeaveResult::StillMember
1305        );
1306        core_ctx.gmp.timers.assert_timers([]);
1307        assert_eq!(core_ctx.inner.v2_messages, Vec::<Vec<_>>::new());
1308
1309        assert_eq!(
1310            core_ctx.gmp_leave_group(bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1311            GroupLeaveResult::Left(())
1312        );
1313        let mut count = 0;
1314        while let Some(timer) = bindings_ctx.trigger_next_timer(core_ctx) {
1315            count += 1;
1316            assert_eq!(timer, GmpTimerId::new(FakeWeakDeviceId(FakeDeviceId)));
1317
1318            let messages = core::mem::take(&mut core_ctx.inner.v2_messages);
1319            assert_eq!(
1320                messages,
1321                vec![vec![(I::GROUP_ADDR1, GroupRecordType::ChangeToIncludeMode, vec![])]]
1322            );
1323
1324            if count != robustness_variable {
1325                let now = bindings_ctx.now();
1326                core_ctx.gmp.timers.assert_range([(
1327                    &TimerId::StateChange.into(),
1328                    now..=now.panicking_add(core_ctx.config.unsolicited_report_interval().get()),
1329                )]);
1330            }
1331        }
1332        assert_eq!(count, robustness_variable);
1333        core_ctx.gmp.timers.assert_timers([]);
1334        assert_eq!(core_ctx.gmp.v2_proto.left_groups, HashMap::new());
1335
1336        // Leave same group again, no side-effects.
1337        assert_eq!(
1338            core_ctx.gmp_leave_group(bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1339            GroupLeaveResult::NotMember
1340        );
1341        core_ctx.gmp.timers.assert_timers([]);
1342        assert_eq!(core_ctx.inner.v2_messages, Vec::<Vec<_>>::new());
1343    }
1344
1345    #[ip_test(I)]
1346    #[test_matrix(
1347        0..=3,
1348        0..=3
1349    )]
1350    fn join_and_leave<I: TestIpExt>(wait_join: u8, wait_leave: u8) {
1351        let mut ctx = testutil::new_context_with_mode::<I>(GmpMode::V2);
1352        let FakeCtx { core_ctx, bindings_ctx } = &mut ctx;
1353        // NB: This matches the maximum value given to test inputs, but the
1354        // test_matrix macro only accepts literals.
1355        core_ctx.gmp.v2_proto.robustness_variable = NonZeroU8::new(3).unwrap();
1356
1357        let wait_reports = |core_ctx: &mut FakeGmpContext<I>,
1358                            bindings_ctx: &mut FakeGmpBindingsContext<I>,
1359                            mode,
1360                            count: u8| {
1361            for _ in 0..count {
1362                assert_eq!(
1363                    bindings_ctx.trigger_next_timer(core_ctx),
1364                    Some(GmpTimerId::new(FakeWeakDeviceId(FakeDeviceId)))
1365                );
1366            }
1367            let messages = core::mem::take(&mut core_ctx.inner.v2_messages);
1368            assert_eq!(messages.len(), usize::from(count));
1369            for m in messages {
1370                assert_eq!(m, vec![(I::GROUP_ADDR1, mode, vec![])]);
1371            }
1372        };
1373
1374        for _ in 0..3 {
1375            assert_eq!(
1376                core_ctx.gmp_join_group(bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1377                GroupJoinResult::Joined(())
1378            );
1379            assert_eq!(core_ctx.inner.v2_messages, Vec::<Vec<_>>::new());
1380            let now = bindings_ctx.now();
1381            core_ctx.gmp.timers.assert_range([(
1382                &TimerId::StateChange.into(),
1383                now..=now.panicking_add(STATE_CHANGE_REPORT_DELAY),
1384            )]);
1385            wait_reports(core_ctx, bindings_ctx, GroupRecordType::ChangeToExcludeMode, wait_join);
1386
1387            assert_eq!(
1388                core_ctx.gmp_leave_group(bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1389                GroupLeaveResult::Left(())
1390            );
1391            assert_eq!(core_ctx.inner.v2_messages, Vec::<Vec<_>>::new());
1392            let now = bindings_ctx.now();
1393            core_ctx.gmp.timers.assert_range([(
1394                &TimerId::StateChange.into(),
1395                now..=now.panicking_add(STATE_CHANGE_REPORT_DELAY),
1396            )]);
1397            wait_reports(core_ctx, bindings_ctx, GroupRecordType::ChangeToIncludeMode, wait_leave);
1398        }
1399    }
1400
1401    #[derive(Debug)]
1402    enum GroupOp {
1403        Join,
1404        Leave,
1405    }
1406    #[ip_test(I)]
1407    #[test_matrix(
1408        0..=3,
1409        [GroupOp::Join, GroupOp::Leave]
1410    )]
1411    fn merge_reports<I: TestIpExt>(wait_reports: u8, which_op: GroupOp) {
1412        let mut ctx = testutil::new_context_with_mode::<I>(GmpMode::V2);
1413        match which_op {
1414            GroupOp::Join => {}
1415            GroupOp::Leave => {
1416                // If we're testing leave, join the group first.
1417                join_and_ignore_unsolicited(&mut ctx, [I::GROUP_ADDR1]);
1418            }
1419        }
1420
1421        let FakeCtx { core_ctx, bindings_ctx } = &mut ctx;
1422        // NB: This matches the maximum value given to test inputs, but the
1423        // test_matrix macro only accepts literals.
1424        core_ctx.gmp.v2_proto.robustness_variable = NonZeroU8::new(3).unwrap();
1425
1426        // Join another group that we'll have our report merged with.
1427        assert_eq!(
1428            core_ctx.gmp_join_group(bindings_ctx, &FakeDeviceId, I::GROUP_ADDR2),
1429            GroupJoinResult::Joined(())
1430        );
1431        for _ in 0..wait_reports {
1432            assert_eq!(
1433                bindings_ctx.trigger_next_timer(core_ctx),
1434                Some(GmpTimerId::new(FakeWeakDeviceId(FakeDeviceId)))
1435            );
1436        }
1437        // Drop all messages this is tested elsewhere, just ensure the number of
1438        // reports sent out so far is what we expect.
1439        assert_eq!(
1440            core::mem::take(&mut core_ctx.inner.v2_messages).len(),
1441            usize::from(wait_reports)
1442        );
1443        let expect_record_type = match which_op {
1444            GroupOp::Join => {
1445                assert_eq!(
1446                    core_ctx.gmp_join_group(bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1447                    GroupJoinResult::Joined(())
1448                );
1449                GroupRecordType::ChangeToExcludeMode
1450            }
1451            GroupOp::Leave => {
1452                assert_eq!(
1453                    core_ctx.gmp_leave_group(bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1454                    GroupLeaveResult::Left(())
1455                );
1456                GroupRecordType::ChangeToIncludeMode
1457            }
1458        };
1459        // No messages are generated immediately:
1460        assert_eq!(core_ctx.inner.v2_messages, Vec::<Vec<_>>::new());
1461        // The next report is at _most_ the delay away.
1462        let now = bindings_ctx.now();
1463        core_ctx.gmp.timers.assert_range([(
1464            &TimerId::StateChange.into(),
1465            now..=now.panicking_add(STATE_CHANGE_REPORT_DELAY),
1466        )]);
1467        // We should see robustness_variable reports, the first (reports -
1468        // wait_reports) should contain the join group retransmission still.
1469        let reports = core_ctx.gmp.v2_proto.robustness_variable.get();
1470
1471        // Collect all the messages we expect to see as we drive the timer.
1472        let expected_messages = (0..reports)
1473            .map(|count| {
1474                assert_eq!(
1475                    bindings_ctx.trigger_next_timer(core_ctx),
1476                    Some(GmpTimerId::new(FakeWeakDeviceId(FakeDeviceId)))
1477                );
1478                let mut expect = vec![(I::GROUP_ADDR1, expect_record_type, vec![])];
1479                if count < reports - wait_reports {
1480                    expect.push((I::GROUP_ADDR2, GroupRecordType::ChangeToExcludeMode, vec![]));
1481                }
1482                expect
1483            })
1484            .collect::<Vec<_>>();
1485        assert_eq!(core_ctx.inner.v2_messages, expected_messages);
1486        core_ctx.gmp.timers.assert_timers([]);
1487        assert_eq!(core_ctx.gmp.v2_proto.left_groups, HashMap::new());
1488    }
1489
1490    #[ip_test(I)]
1491    fn enable_disable<I: TestIpExt>() {
1492        let mut ctx = testutil::new_context_with_mode::<I>(GmpMode::V2);
1493        join_and_ignore_unsolicited(&mut ctx, [I::GROUP_ADDR1, I::GROUP_ADDR2]);
1494        let FakeCtx { core_ctx, bindings_ctx } = &mut ctx;
1495
1496        // We call maybe enable again, but if we're already enabled there
1497        // are no side-effects.
1498        core_ctx.gmp_handle_maybe_enabled(bindings_ctx, &FakeDeviceId);
1499        core_ctx.gmp.timers.assert_timers([]);
1500        assert_eq!(core_ctx.inner.v2_messages, Vec::<Vec<_>>::new());
1501
1502        // Disable and observe a single leave report and no timers.
1503        core_ctx.enabled = false;
1504        core_ctx.gmp_handle_disabled(bindings_ctx, &FakeDeviceId);
1505        core_ctx.gmp.timers.assert_timers([]);
1506        assert_eq!(
1507            core::mem::take(&mut core_ctx.inner.v2_messages),
1508            vec![vec![
1509                (I::GROUP_ADDR1, GroupRecordType::ChangeToIncludeMode, vec![],),
1510                (I::GROUP_ADDR2, GroupRecordType::ChangeToIncludeMode, vec![],),
1511            ]]
1512        );
1513
1514        // Disable again no side-effects.
1515        core_ctx.gmp_handle_disabled(bindings_ctx, &FakeDeviceId);
1516        core_ctx.gmp.timers.assert_timers([]);
1517        assert_eq!(core_ctx.inner.v2_messages, Vec::<Vec<_>>::new());
1518
1519        // Re-enable and observe robustness_variable state changes.
1520        core_ctx.enabled = true;
1521        core_ctx.gmp_handle_maybe_enabled(bindings_ctx, &FakeDeviceId);
1522        let now = bindings_ctx.now();
1523        core_ctx.gmp.timers.assert_range([(
1524            &TimerId::StateChange.into(),
1525            now..=now.panicking_add(STATE_CHANGE_REPORT_DELAY),
1526        )]);
1527        // No messages yet, this behaves exactly like joining many groups all
1528        // at once.
1529        assert_eq!(core_ctx.inner.v2_messages, Vec::<Vec<_>>::new());
1530
1531        while let Some(timer) = bindings_ctx.trigger_next_timer(core_ctx) {
1532            assert_eq!(timer, GmpTimerId::new(FakeWeakDeviceId(FakeDeviceId)));
1533        }
1534        let expect_messages = core::iter::repeat_with(|| {
1535            vec![
1536                (I::GROUP_ADDR1, GroupRecordType::ChangeToExcludeMode, vec![]),
1537                (I::GROUP_ADDR2, GroupRecordType::ChangeToExcludeMode, vec![]),
1538            ]
1539        })
1540        .take(core_ctx.gmp.v2_proto.robustness_variable.get().into())
1541        .collect::<Vec<_>>();
1542        assert_eq!(core::mem::take(&mut core_ctx.inner.v2_messages), expect_messages);
1543
1544        // Disable one more time while we're in the process of leaving one of
1545        // the groups to show that we allow it to piggyback on the last report
1546        // once.
1547        assert_eq!(
1548            core_ctx.gmp_leave_group(bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1549            GroupLeaveResult::Left(())
1550        );
1551        assert_eq!(
1552            core_ctx.gmp.v2_proto.left_groups.get(&GmpEnabledGroup::new(I::GROUP_ADDR1).unwrap()),
1553            Some(&core_ctx.gmp.v2_proto.robustness_variable)
1554        );
1555        // Disable and observe a single leave report INCLUDING the already left
1556        // group and no timers.
1557        core_ctx.enabled = false;
1558        core_ctx.gmp_handle_disabled(bindings_ctx, &FakeDeviceId);
1559        core_ctx.gmp.timers.assert_timers([]);
1560        assert_eq!(
1561            core::mem::take(&mut core_ctx.inner.v2_messages),
1562            vec![vec![
1563                (I::GROUP_ADDR1, GroupRecordType::ChangeToIncludeMode, vec![],),
1564                (I::GROUP_ADDR2, GroupRecordType::ChangeToIncludeMode, vec![],),
1565            ]]
1566        );
1567    }
1568
1569    #[ip_test(I)]
1570    fn ignore_query_if_disabled<I: TestIpExt>() {
1571        let mut ctx = testutil::new_context_with_mode::<I>(GmpMode::V2);
1572        let FakeCtx { core_ctx, bindings_ctx } = &mut ctx;
1573        core_ctx.enabled = false;
1574        core_ctx.gmp_handle_disabled(bindings_ctx, &FakeDeviceId);
1575
1576        assert_eq!(
1577            core_ctx.gmp_join_group(bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1578            GroupJoinResult::Joined(())
1579        );
1580
1581        // Receive a general query.
1582        assert_eq!(
1583            handle_query_message(core_ctx, bindings_ctx, &FakeDeviceId, &FakeV2Query::default()),
1584            Err(QueryError::Disabled)
1585        );
1586        // No side-effects.
1587        core_ctx.gmp.timers.assert_timers([]);
1588        assert_eq!(core_ctx.inner.v2_messages, Vec::<Vec<_>>::new());
1589    }
1590
1591    #[ip_test(I)]
1592    fn clears_v2_proto_state_on_mode_change<I: TestIpExt>() {
1593        let mut ctx = testutil::new_context_with_mode::<I>(GmpMode::V2);
1594        join_and_ignore_unsolicited(&mut ctx, [I::GROUP_ADDR1]);
1595
1596        let FakeCtx { core_ctx, bindings_ctx } = &mut ctx;
1597        let query = FakeV2Query {
1598            robustness_variable: DEFAULT_ROBUSTNESS_VARIABLE.get() + 1,
1599            query_interval: DEFAULT_QUERY_INTERVAL.get() + Duration::from_secs(1),
1600            ..Default::default()
1601        };
1602        handle_query_message(core_ctx, bindings_ctx, &FakeDeviceId, &query).expect("handle query");
1603        assert_eq!(
1604            core_ctx.gmp_leave_group(bindings_ctx, &FakeDeviceId, I::GROUP_ADDR1),
1605            GroupLeaveResult::Left(())
1606        );
1607        let robustness_variable = NonZeroU8::new(query.robustness_variable).unwrap();
1608        let query_interval = NonZeroDuration::new(query.query_interval).unwrap();
1609        assert_eq!(
1610            core_ctx.gmp.v2_proto,
1611            ProtocolState {
1612                robustness_variable,
1613                query_interval,
1614                left_groups: [(GmpEnabledGroup::new(I::GROUP_ADDR1).unwrap(), robustness_variable)]
1615                    .into_iter()
1616                    .collect()
1617            }
1618        );
1619
1620        core_ctx.with_gmp_state_mut(&FakeDeviceId, |state| {
1621            gmp::enter_mode(bindings_ctx, state, GmpMode::V1 { compat: false });
1622        });
1623
1624        assert_eq!(
1625            core_ctx.gmp.v2_proto,
1626            ProtocolState { robustness_variable, query_interval, left_groups: HashMap::new() }
1627        );
1628    }
1629}