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