Skip to main content

netstack3_ip/multicast_forwarding/
packet_queue.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//! Declares types and functionality related to queued multicast packets.
6
7use alloc::vec::Vec;
8use arrayvec::ArrayVec;
9use core::time::Duration;
10use derivative::Derivative;
11use lru_cache::LruCache;
12use net_types::ip::{Ip, IpVersionMarker};
13use netstack3_base::{
14    CoreTimerContext, Inspectable, Inspector, Instant as _, LocalFrameDestination,
15    StrongDeviceIdentifier as _, WeakDeviceIdentifier,
16};
17use packet::{Buf, ParseBufferMut};
18use packet_formats::ip::IpPacket;
19use zerocopy::SplitByteSlice;
20
21use crate::IpLayerIpExt;
22use crate::internal::multicast_forwarding::{
23    MulticastForwardingBindingsContext, MulticastForwardingBindingsTypes,
24    MulticastForwardingTimerId,
25};
26use crate::multicast_forwarding::MulticastRouteKey;
27
28/// The number of packets that the stack is willing to queue for a given
29/// [`MulticastRouteKey`] while waiting for an applicable route to be installed.
30///
31/// This value is consistent with the defaults on both Netstack2 and Linux.
32pub(crate) const PACKET_QUEUE_LEN: usize = 3;
33
34/// The maximum number of pending multicast routes that can be queued.
35///
36/// The size of each entry is dominated by the packet queue (up to
37/// PACKET_QUEUE_LEN). With 1000 entries, each with 3 standard MTU packets, this
38/// limit is approximately 4.5MB.
39const MAX_PENDING_ROUTES: usize = 1000;
40
41/// The amount of time the stack is willing to queue a packet while waiting
42/// for an applicable route to be installed.
43///
44/// This value is consistent with the defaults on both Netstack2 and Linux.
45const PENDING_ROUTE_EXPIRATION: Duration = Duration::from_secs(10);
46
47/// The minimum amount of time after a garbage-collection run across the
48/// [`MulticastForwardingPendingPackets`] table that the stack will wait before
49/// performing another garbage-collection.
50///
51/// This value is consistent with the defaults on both Netstack2 and Linux.
52const PENDING_ROUTE_GC_PERIOD: Duration = Duration::from_secs(10);
53
54/// A table of pending multicast packets that have not yet been forwarded.
55///
56/// Packets are placed in this table when, during forwarding, there is no route
57/// in the [`MulticastRouteTable`] via which to forward them. If/when such a
58/// route is installed, the packets stored here can be forwarded accordingly.
59#[derive(Derivative)]
60#[derivative(Debug(bound = ""))]
61pub struct MulticastForwardingPendingPackets<
62    I: IpLayerIpExt,
63    D: WeakDeviceIdentifier,
64    BT: MulticastForwardingBindingsTypes,
65> {
66    table: LruCache<MulticastRouteKey<I>, PacketQueue<I, D, BT>>,
67    /// Periodically triggers invocations of [`Self::run_garbage_collection`].
68    ///
69    /// All interactions with the `gc_timer` must uphold the invariant that the
70    /// timer is not scheduled if [`Self::table`] is empty.
71    ///
72    /// Note: When [`Self`] is held by [`MulticastForwardingEnabledState`], it
73    /// is lock protected, which prevents method calls on it from racing. E.g.
74    /// no overlapping calls to [`Self::try_queue_packet`], [`Self::remove`],
75    /// or [`Self::run_garbage_collection`].
76    gc_timer: BT::Timer,
77}
78
79impl<I: IpLayerIpExt, D: WeakDeviceIdentifier, BC: MulticastForwardingBindingsContext<I, D::Strong>>
80    MulticastForwardingPendingPackets<I, D, BC>
81{
82    pub(crate) fn new<CC>(bindings_ctx: &mut BC) -> Self
83    where
84        CC: CoreTimerContext<MulticastForwardingTimerId<I>, BC>,
85    {
86        Self {
87            table: LruCache::new(MAX_PENDING_ROUTES),
88            gc_timer: CC::new_timer(
89                bindings_ctx,
90                MulticastForwardingTimerId::PendingPacketsGc(IpVersionMarker::<I>::new()),
91            ),
92        }
93    }
94
95    /// Attempt to queue the packet in the pending_table.
96    ///
97    /// If the table becomes newly occupied, the GC timer is scheduled.
98    pub(crate) fn try_queue_packet<B>(
99        &mut self,
100        bindings_ctx: &mut BC,
101        key: MulticastRouteKey<I>,
102        packet: &I::Packet<B>,
103        dev: &D::Strong,
104        frame_dst: Option<LocalFrameDestination>,
105    ) -> QueuePacketOutcome
106    where
107        B: SplitByteSlice,
108    {
109        let was_empty = self.table.is_empty();
110        let outcome = if let Some(queue) = self.table.get_mut(&key) {
111            match queue.try_push(|| QueuedPacket::new(dev, packet, frame_dst)) {
112                Ok(()) => QueuePacketOutcome::QueuedInExistingQueue,
113                Err(PacketQueueFullError) => QueuePacketOutcome::ExistingQueueFull,
114            }
115        } else {
116            let mut queue = PacketQueue::new(bindings_ctx);
117            queue
118                .try_push(|| QueuedPacket::new(dev, packet, frame_dst))
119                .expect("newly instantiated queue must have capacity");
120
121            let prev = self.table.insert(key, queue);
122            debug_assert!(prev.is_none());
123            QueuePacketOutcome::QueuedInNewQueue
124        };
125
126        // If the table is newly non-empty, schedule the GC. The timer must not
127        // already be scheduled (given the invariants on `gc_timer`).
128        if was_empty && !self.table.is_empty() {
129            let prev = bindings_ctx.schedule_timer(PENDING_ROUTE_GC_PERIOD, &mut self.gc_timer);
130            debug_assert!(prev.is_none());
131        }
132
133        outcome
134    }
135
136    #[cfg(any(debug_assertions, test))]
137    pub(crate) fn contains(&self, key: &MulticastRouteKey<I>) -> bool {
138        self.table.iter().any(|(k, _)| k == key)
139    }
140
141    /// Remove the key from the pending table, returning its queue of packets.
142    ///
143    /// If the table becomes newly empty, the GC timer is canceled.
144    pub(crate) fn remove(
145        &mut self,
146        key: &MulticastRouteKey<I>,
147        bindings_ctx: &mut BC,
148    ) -> Option<PacketQueue<I, D, BC>> {
149        let was_empty = self.table.is_empty();
150        let queue = self.table.remove(key);
151
152        // If the table is newly empty, cancel the GC. Note, we don't assert on
153        // the previous state of the timer, because it's possible cancelation
154        // will race with the timer firing.
155        if !was_empty && self.table.is_empty() {
156            let _: Option<BC::Instant> = bindings_ctx.cancel_timer(&mut self.gc_timer);
157        }
158
159        queue
160    }
161
162    /// Removes expired [`PacketQueue`] entries from [`Self`].
163    ///
164    /// Returns the number of packets removed as a result.
165    pub(crate) fn run_garbage_collection(&mut self, bindings_ctx: &mut BC) -> u64 {
166        let now = bindings_ctx.now();
167        let mut removed_count = 0u64;
168        let expired_keys: Vec<_> = self
169            .table
170            .iter()
171            .filter_map(
172                |(key, queue)| if queue.expires_at <= now { Some(key.clone()) } else { None },
173            )
174            .collect();
175
176        for key in expired_keys {
177            let queue = self.table.remove(&key).expect("expired key must be present");
178            // NB: "as" conversion is safe because queue_len has a maximum
179            // value of `PACKET_QUEUE_LEN`, which fits in a u64.
180            removed_count += queue.queue.len() as u64;
181        }
182
183        // If the table is still not empty, reschedule the GC. Note that we
184        // don't assert on the previous state of the timer, because it's
185        // possible that starting GC raced with a new timer being scheduled.
186        if !self.table.is_empty() {
187            let _: Option<BC::Instant> =
188                bindings_ctx.schedule_timer(PENDING_ROUTE_GC_PERIOD, &mut self.gc_timer);
189        }
190
191        removed_count
192    }
193}
194
195impl<I: IpLayerIpExt, D: WeakDeviceIdentifier, BT: MulticastForwardingBindingsTypes> Inspectable
196    for MulticastForwardingPendingPackets<I, D, BT>
197{
198    fn record<II: Inspector>(&self, inspector: &mut II) {
199        let MulticastForwardingPendingPackets { table, gc_timer: _ } = self;
200        // NB: Don't record all routes, as the size of the table may be quite
201        // large, and its contents are dictated by network traffic.
202        inspector.record_usize("NumRoutes", table.len())
203    }
204}
205
206/// Possible outcomes from calling [`MulticastForwardingPendingPackets::try_queue_packet`].
207#[derive(Debug, PartialEq)]
208pub(crate) enum QueuePacketOutcome {
209    /// The packet was successfully queued. There was no existing
210    /// [`PacketQueue`] for the given route key, so a new one was instantiated.
211    QueuedInNewQueue,
212    /// The packet was successfully queued. It was added onto an existing
213    /// [`PacketQueue`] for the given route key.
214    QueuedInExistingQueue,
215    /// The packet was not queued. There was an existing [`PacketQueue`] for the
216    /// given route key, but that queue was full.
217    ExistingQueueFull,
218}
219
220/// A queue of multicast packets that are pending the installation of a route.
221#[derive(Derivative)]
222#[derivative(Debug(bound = ""))]
223pub struct PacketQueue<I: Ip, D: WeakDeviceIdentifier, BT: MulticastForwardingBindingsTypes> {
224    queue: ArrayVec<QueuedPacket<I, D>, PACKET_QUEUE_LEN>,
225    /// The time after which the PacketQueue is allowed to be garbage collected.
226    expires_at: BT::Instant,
227}
228
229impl<I: IpLayerIpExt, D: WeakDeviceIdentifier, BC: MulticastForwardingBindingsContext<I, D::Strong>>
230    PacketQueue<I, D, BC>
231{
232    fn new(bindings_ctx: &mut BC) -> Self {
233        Self {
234            queue: Default::default(),
235            expires_at: bindings_ctx.now().panicking_add(PENDING_ROUTE_EXPIRATION),
236        }
237    }
238
239    /// Try to push a packet into the queue, returning an error when full.
240    ///
241    /// Note: the packet is taken as a builder closure, because constructing the
242    /// packet is an expensive operation (requiring a `Vec` allocation). By
243    /// taking a closure we can defer construction until we're certain the queue
244    /// has the free space to hold it.
245    fn try_push(
246        &mut self,
247        packet_builder: impl FnOnce() -> QueuedPacket<I, D>,
248    ) -> Result<(), PacketQueueFullError> {
249        if self.queue.is_full() {
250            return Err(PacketQueueFullError);
251        }
252        self.queue.push(packet_builder());
253        Ok(())
254    }
255}
256
257#[derive(Debug)]
258struct PacketQueueFullError;
259
260impl<I: Ip, D: WeakDeviceIdentifier, BT: MulticastForwardingBindingsTypes> IntoIterator
261    for PacketQueue<I, D, BT>
262{
263    type Item = QueuedPacket<I, D>;
264    type IntoIter = <ArrayVec<QueuedPacket<I, D>, PACKET_QUEUE_LEN> as IntoIterator>::IntoIter;
265    fn into_iter(self) -> Self::IntoIter {
266        let Self { queue, expires_at: _ } = self;
267        queue.into_iter()
268    }
269}
270
271/// An individual multicast packet that's queued.
272#[derive(Debug, PartialEq)]
273pub struct QueuedPacket<I: Ip, D: WeakDeviceIdentifier> {
274    /// The device on which the packet arrived.
275    pub(crate) device: D,
276    /// The packet.
277    pub(crate) packet: ValidIpPacketBuf<I>,
278    /// The link layer (L2) destination that the packet was sent to, or `None`
279    /// if the packet arrived above the link layer (e.g. a Pure IP device).
280    pub(crate) frame_dst: Option<LocalFrameDestination>,
281}
282
283impl<I: IpLayerIpExt, D: WeakDeviceIdentifier> QueuedPacket<I, D> {
284    fn new<B: SplitByteSlice>(
285        device: &D::Strong,
286        packet: &I::Packet<B>,
287        frame_dst: Option<LocalFrameDestination>,
288    ) -> Self {
289        QueuedPacket {
290            device: device.downgrade(),
291            packet: ValidIpPacketBuf::new(packet),
292            frame_dst,
293        }
294    }
295}
296
297/// A buffer containing a known-to-be valid IP packet.
298///
299/// The only constructor of this type takes an `I::Packet`, which is already
300/// parsed & validated.
301#[derive(Clone, Debug, PartialEq)]
302pub(crate) struct ValidIpPacketBuf<I: Ip> {
303    buffer: Buf<Vec<u8>>,
304    _version_marker: IpVersionMarker<I>,
305}
306
307impl<I: IpLayerIpExt> ValidIpPacketBuf<I> {
308    fn new<B: SplitByteSlice>(packet: &I::Packet<B>) -> Self {
309        Self { buffer: Buf::new(packet.to_vec(), ..), _version_marker: Default::default() }
310    }
311
312    /// Parses the internal buffer into a mutable IP Packet.
313    ///
314    /// # Panics
315    ///
316    /// This function panics if called multiple times. Parsing moves the cursor
317    /// in the underlying buffer from the start of the IP header to the start
318    /// of the IP body.
319    pub(crate) fn parse_ip_packet_mut(&mut self) -> I::Packet<&mut [u8]> {
320        // NB: Safe to unwrap here because the buffer is known to be valid.
321        self.buffer.parse_mut().unwrap()
322    }
323
324    pub(crate) fn into_inner(self) -> Buf<Vec<u8>> {
325        let Self { buffer, _version_marker } = self;
326        buffer
327    }
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333
334    use assert_matches::assert_matches;
335    use ip_test_macro::ip_test;
336    use netstack3_base::testutil::{
337        FakeInstant, FakeTimerCtxExt, FakeWeakDeviceId, MultipleDevicesId,
338    };
339    use netstack3_base::{CounterContext, InstantContext, StrongDeviceIdentifier, TimerContext};
340    use packet::ParseBuffer;
341    use static_assertions::const_assert;
342    use test_case::test_case;
343
344    use crate::internal::multicast_forwarding;
345    use crate::internal::multicast_forwarding::counters::MulticastForwardingCounters;
346    use crate::internal::multicast_forwarding::testutil::{
347        FakeBindingsCtx, FakeCoreCtx, TestIpExt,
348    };
349
350    #[ip_test(I)]
351    #[test_case(None; "no_frame_dst")]
352    #[test_case(Some(LocalFrameDestination::Multicast); "some_frame_dst")]
353    fn queue_packet<I: TestIpExt>(frame_dst: Option<LocalFrameDestination>) {
354        const DEV: MultipleDevicesId = MultipleDevicesId::A;
355        let key1 = MulticastRouteKey::new(I::SRC1, I::DST1).unwrap();
356        let key2 = MulticastRouteKey::new(I::SRC2, I::DST2).unwrap();
357        let key3 = MulticastRouteKey::new(I::SRC1, I::DST2).unwrap();
358
359        // NB: technically the packet's addresses only match `key1`, but for the
360        // sake of this test that doesn't cause problems.
361        let buf = multicast_forwarding::testutil::new_ip_packet_buf::<I>(I::SRC1, I::DST1);
362        let mut buf_ref = buf.as_ref();
363        let packet = buf_ref.parse::<I::Packet<_>>().expect("parse should succeed");
364
365        let mut bindings_ctx = FakeBindingsCtx::<I, MultipleDevicesId>::default();
366
367        let mut pending_table =
368            MulticastForwardingPendingPackets::<
369                I,
370                <MultipleDevicesId as StrongDeviceIdentifier>::Weak,
371                _,
372            >::new::<FakeCoreCtx<I, MultipleDevicesId>>(&mut bindings_ctx);
373
374        // The first packet gets a new queue.
375        assert_eq!(
376            pending_table.try_queue_packet(
377                &mut bindings_ctx,
378                key1.clone(),
379                &packet,
380                &DEV,
381                frame_dst
382            ),
383            QueuePacketOutcome::QueuedInNewQueue
384        );
385        // The second - Nth packets uses the existing queue.
386        for _ in 1..PACKET_QUEUE_LEN {
387            assert_eq!(
388                pending_table.try_queue_packet(
389                    &mut bindings_ctx,
390                    key1.clone(),
391                    &packet,
392                    &DEV,
393                    frame_dst
394                ),
395                QueuePacketOutcome::QueuedInExistingQueue
396            );
397        }
398        // The Nth +1 packet is rejected.
399        assert_eq!(
400            pending_table.try_queue_packet(
401                &mut bindings_ctx,
402                key1.clone(),
403                &packet,
404                &DEV,
405                frame_dst
406            ),
407            QueuePacketOutcome::ExistingQueueFull
408        );
409
410        // A packet with a different key gets a new queue.
411        assert_eq!(
412            pending_table.try_queue_packet(
413                &mut bindings_ctx,
414                key2.clone(),
415                &packet,
416                &DEV,
417                frame_dst
418            ),
419            QueuePacketOutcome::QueuedInNewQueue
420        );
421
422        // Based on the calls above, `key1` should have a full queue, `key2`
423        // should have a queue with only 1 packet, and `key3` shouldn't have
424        // a queue.
425        let expected_packet = QueuedPacket::new(&DEV, &packet, frame_dst);
426        let queue =
427            pending_table.remove(&key1, &mut bindings_ctx).expect("key1 should have a queue");
428        assert_eq!(queue.queue.len(), PACKET_QUEUE_LEN);
429        for packet in queue.queue.as_slice() {
430            assert_eq!(packet, &expected_packet);
431        }
432
433        let queue =
434            pending_table.remove(&key2, &mut bindings_ctx).expect("key2 should have a queue");
435        let packet = assert_matches!(&queue.queue[..], [p] => p);
436        assert_eq!(packet, &expected_packet);
437
438        assert_matches!(pending_table.remove(&key3, &mut bindings_ctx), None);
439    }
440
441    /// Helper to observe the next scheduled GC for the core_ctx pending table.
442    fn next_gc_time<I: TestIpExt>(
443        core_ctx: &mut FakeCoreCtx<I, MultipleDevicesId>,
444        bindings_ctx: &mut FakeBindingsCtx<I, MultipleDevicesId>,
445    ) -> Option<FakeInstant> {
446        multicast_forwarding::testutil::with_pending_table(core_ctx, |pending_table| {
447            bindings_ctx.scheduled_instant(&mut pending_table.gc_timer)
448        })
449    }
450
451    /// Helper to queue packet in the core_ctx pending table.
452    fn try_queue_packet<I: TestIpExt>(
453        core_ctx: &mut FakeCoreCtx<I, MultipleDevicesId>,
454        bindings_ctx: &mut FakeBindingsCtx<I, MultipleDevicesId>,
455        key: MulticastRouteKey<I>,
456        dev: &MultipleDevicesId,
457        frame_dst: Option<LocalFrameDestination>,
458    ) -> QueuePacketOutcome {
459        let buf =
460            multicast_forwarding::testutil::new_ip_packet_buf::<I>(key.src_addr(), key.dst_addr());
461        let mut buf_ref = buf.as_ref();
462        let packet = buf_ref.parse::<I::Packet<_>>().expect("parse should succeed");
463        multicast_forwarding::testutil::with_pending_table(core_ctx, |pending_table| {
464            pending_table.try_queue_packet(bindings_ctx, key, &packet, dev, frame_dst)
465        })
466    }
467
468    /// Helper to remove a packet queue in the core_ctx pending table.
469    fn remove_packet_queue<I: TestIpExt>(
470        core_ctx: &mut FakeCoreCtx<I, MultipleDevicesId>,
471        bindings_ctx: &mut FakeBindingsCtx<I, MultipleDevicesId>,
472        key: &MulticastRouteKey<I>,
473    ) -> Option<
474        PacketQueue<I, FakeWeakDeviceId<MultipleDevicesId>, FakeBindingsCtx<I, MultipleDevicesId>>,
475    > {
476        multicast_forwarding::testutil::with_pending_table(core_ctx, |pending_table| {
477            pending_table.remove(key, bindings_ctx)
478        })
479    }
480
481    /// Helper to trigger the GC.
482    fn run_gc<I: TestIpExt>(
483        core_ctx: &mut FakeCoreCtx<I, MultipleDevicesId>,
484        bindings_ctx: &mut FakeBindingsCtx<I, MultipleDevicesId>,
485    ) {
486        assert_matches!(
487            &bindings_ctx.trigger_timers_until_instant(bindings_ctx.now(), core_ctx)[..],
488            [MulticastForwardingTimerId::PendingPacketsGc(_)]
489        );
490    }
491
492    #[ip_test(I)]
493    fn garbage_collection<I: TestIpExt>() {
494        const DEV: MultipleDevicesId = MultipleDevicesId::A;
495        const FRAME_DST: Option<LocalFrameDestination> = None;
496        let key1 = MulticastRouteKey::<I>::new(I::SRC1, I::DST1).unwrap();
497        let key2 = MulticastRouteKey::<I>::new(I::SRC2, I::DST2).unwrap();
498
499        let mut api = multicast_forwarding::testutil::new_api();
500        assert!(api.enable());
501        let (core_ctx, bindings_ctx) = api.contexts();
502
503        // NB: As written, the test requires that
504        //  1. `PENDING_ROUTE_GC_PERIOD` >= `PENDING_ROUTE_EXPIRATION`, and
505        //  2. `PENDING_ROUTE_EXPIRATION > 0`.
506        // If the values are ever changed such that that is not true, the test
507        // will need to be re-written.
508        const_assert!(PENDING_ROUTE_GC_PERIOD.checked_sub(PENDING_ROUTE_EXPIRATION).is_some());
509        const_assert!(!PENDING_ROUTE_EXPIRATION.is_zero());
510
511        // The GC shouldn't be scheduled with an empty table.
512        assert!(next_gc_time(core_ctx, bindings_ctx).is_none());
513        let counters: &MulticastForwardingCounters<I> = core_ctx.counters();
514        assert_eq!(counters.pending_table_gc.get(), 0);
515        assert_eq!(counters.pending_packet_drops_gc.get(), 0);
516
517        // Queue a packet, and expect the GC to be scheduled.
518        let expected_first_gc = bindings_ctx.now() + PENDING_ROUTE_GC_PERIOD;
519        assert_eq!(
520            try_queue_packet(core_ctx, bindings_ctx, key1.clone(), &DEV, FRAME_DST),
521            QueuePacketOutcome::QueuedInNewQueue
522        );
523        assert_eq!(next_gc_time(core_ctx, bindings_ctx), Some(expected_first_gc));
524
525        // Sleep until we're ready to GC, and then queue a second packet under a
526        // new key. Expect that the GC timer is still scheduled for the original
527        // instant.
528        bindings_ctx.timers.instant.sleep(PENDING_ROUTE_GC_PERIOD);
529        assert_eq!(
530            try_queue_packet(core_ctx, bindings_ctx, key2.clone(), &DEV, FRAME_DST),
531            QueuePacketOutcome::QueuedInNewQueue
532        );
533        assert_eq!(next_gc_time(core_ctx, bindings_ctx), Some(expected_first_gc));
534
535        // Run the GC, and verify that it was rescheduled after the fact
536        // (because `key2` still exists in the table).
537        run_gc(core_ctx, bindings_ctx);
538        let expected_second_gc = bindings_ctx.timers.instant.now() + PENDING_ROUTE_GC_PERIOD;
539        assert_eq!(next_gc_time(core_ctx, bindings_ctx), Some(expected_second_gc));
540
541        // Verify that `key1` was removed, but `key2` remains.
542        let counters: &MulticastForwardingCounters<I> = core_ctx.counters();
543        assert_eq!(counters.pending_table_gc.get(), 1);
544        assert_eq!(counters.pending_packet_drops_gc.get(), 1);
545        assert_matches!(remove_packet_queue(core_ctx, bindings_ctx, &key1), None);
546        assert_matches!(remove_packet_queue(core_ctx, bindings_ctx, &key2), Some(_));
547
548        // Now that we've explicitly removed `key2`, the table is empty and the
549        // GC should have been canceled.
550        assert!(next_gc_time(core_ctx, bindings_ctx).is_none());
551
552        // Finally, verify that if the GC clears the table, it doesn't
553        // reschedule itself.
554        assert_eq!(
555            try_queue_packet(core_ctx, bindings_ctx, key1.clone(), &DEV, FRAME_DST),
556            QueuePacketOutcome::QueuedInNewQueue
557        );
558        assert_eq!(next_gc_time(core_ctx, bindings_ctx), Some(expected_second_gc));
559        bindings_ctx.timers.instant.sleep(PENDING_ROUTE_GC_PERIOD);
560        run_gc(core_ctx, bindings_ctx);
561        let counters: &MulticastForwardingCounters<I> = core_ctx.counters();
562        assert_eq!(counters.pending_table_gc.get(), 2);
563        assert_eq!(counters.pending_packet_drops_gc.get(), 2);
564        assert_matches!(remove_packet_queue(core_ctx, bindings_ctx, &key1), None);
565        assert!(next_gc_time(core_ctx, bindings_ctx).is_none());
566    }
567}