netstack3_ip/device/
route_discovery.rs

1// Copyright 2022 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//! IPv6 Route Discovery as defined by [RFC 4861 section 6.3.4].
6//!
7//! [RFC 4861 section 6.3.4]: https://datatracker.ietf.org/doc/html/rfc4861#section-6.3.4
8
9use core::hash::Hash;
10
11use derivative::Derivative;
12use net_types::LinkLocalUnicastAddr;
13use net_types::ip::{Ipv6Addr, Subnet};
14use netstack3_base::{
15    AnyDevice, CoreTimerContext, DeviceIdContext, HandleableTimer, InstantBindingsTypes,
16    LocalTimerHeap, TimerBindingsTypes, TimerContext, WeakDeviceIdentifier,
17};
18use netstack3_hashmap::HashSet;
19use packet_formats::icmp::ndp::NonZeroNdpLifetime;
20
21/// Route discovery state on a device.
22#[derive(Debug)]
23pub struct Ipv6RouteDiscoveryState<BT: Ipv6RouteDiscoveryBindingsTypes> {
24    // The valid (non-zero lifetime) discovered routes.
25    //
26    // Routes with a finite lifetime must have a timer set; routes with an
27    // infinite lifetime must not.
28    routes: HashSet<Ipv6DiscoveredRoute>,
29    timers: LocalTimerHeap<Ipv6DiscoveredRoute, (), BT>,
30}
31
32impl<BT: Ipv6RouteDiscoveryBindingsTypes> Ipv6RouteDiscoveryState<BT> {
33    /// Gets the timer heap for route discovery.
34    #[cfg(any(test, feature = "testutils"))]
35    pub fn timers(&self) -> &LocalTimerHeap<Ipv6DiscoveredRoute, (), BT> {
36        &self.timers
37    }
38}
39
40impl<BC: Ipv6RouteDiscoveryBindingsContext> Ipv6RouteDiscoveryState<BC> {
41    /// Constructs the route discovery state for `device_id`.
42    pub fn new<D: WeakDeviceIdentifier, CC: CoreTimerContext<Ipv6DiscoveredRouteTimerId<D>, BC>>(
43        bindings_ctx: &mut BC,
44        device_id: D,
45    ) -> Self {
46        Self {
47            routes: Default::default(),
48            timers: LocalTimerHeap::new_with_context::<_, CC>(
49                bindings_ctx,
50                Ipv6DiscoveredRouteTimerId { device_id },
51            ),
52        }
53    }
54}
55
56/// A discovered route.
57#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
58pub struct Ipv6DiscoveredRoute {
59    /// The destination subnet for the route.
60    pub subnet: Subnet<Ipv6Addr>,
61
62    /// The next-hop node for the route, if required.
63    ///
64    /// `None` indicates that the subnet is on-link/directly-connected.
65    pub gateway: Option<LinkLocalUnicastAddr<Ipv6Addr>>,
66}
67
68/// A timer ID for IPv6 route discovery.
69#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
70pub struct Ipv6DiscoveredRouteTimerId<D: WeakDeviceIdentifier> {
71    device_id: D,
72}
73
74impl<D: WeakDeviceIdentifier> Ipv6DiscoveredRouteTimerId<D> {
75    pub(super) fn device_id(&self) -> &D {
76        &self.device_id
77    }
78}
79
80/// The configuration for route discovery.
81#[derive(Copy, Clone, Debug, Eq, PartialEq, Derivative)]
82#[derivative(Default)]
83pub struct RouteDiscoveryConfiguration {
84    /// Allow default route to be added for this device.
85    #[derivative(Default(value = "true"))]
86    pub allow_default_route: bool,
87}
88
89/// The configuration update for route discovery.
90#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)]
91#[allow(missing_docs)]
92pub struct RouteDiscoveryConfigurationUpdate {
93    pub allow_default_route: Option<bool>,
94}
95
96impl RouteDiscoveryConfiguration {
97    /// Updates the route discovery configuration.
98    ///
99    /// Returns the previous value of the updated fields.
100    pub fn update(
101        &mut self,
102        update: RouteDiscoveryConfigurationUpdate,
103    ) -> RouteDiscoveryConfigurationUpdate {
104        let RouteDiscoveryConfigurationUpdate { allow_default_route } = update;
105        let allow_default_route =
106            allow_default_route.map(|new| core::mem::replace(&mut self.allow_default_route, new));
107        RouteDiscoveryConfigurationUpdate { allow_default_route }
108    }
109}
110
111/// An implementation of the execution context available when accessing the IPv6
112/// route discovery state.
113///
114/// See [`Ipv6RouteDiscoveryContext::with_discovered_routes_mut`].
115pub trait Ipv6DiscoveredRoutesContext<BC>: DeviceIdContext<AnyDevice> {
116    /// Adds a newly discovered IPv6 route to the routing table.
117    fn add_discovered_ipv6_route(
118        &mut self,
119        bindings_ctx: &mut BC,
120        device_id: &Self::DeviceId,
121        route: Ipv6DiscoveredRoute,
122    );
123
124    /// Deletes a previously discovered (now invalidated) IPv6 route from the
125    /// routing table.
126    fn del_discovered_ipv6_route(
127        &mut self,
128        bindings_ctx: &mut BC,
129        device_id: &Self::DeviceId,
130        route: Ipv6DiscoveredRoute,
131    );
132}
133
134/// The execution context for IPv6 route discovery.
135pub trait Ipv6RouteDiscoveryContext<BT: Ipv6RouteDiscoveryBindingsTypes>:
136    DeviceIdContext<AnyDevice>
137{
138    /// The inner discovered routes context.
139    type WithDiscoveredRoutesMutCtx<'a>: Ipv6DiscoveredRoutesContext<BT, DeviceId = Self::DeviceId>;
140
141    /// Gets the route discovery state, mutably.
142    fn with_discovered_routes_mut<
143        O,
144        F: FnOnce(&mut Ipv6RouteDiscoveryState<BT>, &mut Self::WithDiscoveredRoutesMutCtx<'_>) -> O,
145    >(
146        &mut self,
147        device_id: &Self::DeviceId,
148        cb: F,
149    ) -> O;
150}
151
152/// The bindings types for IPv6 route discovery.
153pub trait Ipv6RouteDiscoveryBindingsTypes: TimerBindingsTypes + InstantBindingsTypes {}
154impl<BT> Ipv6RouteDiscoveryBindingsTypes for BT where BT: TimerBindingsTypes + InstantBindingsTypes {}
155
156/// The bindings execution context for IPv6 route discovery.
157pub trait Ipv6RouteDiscoveryBindingsContext:
158    Ipv6RouteDiscoveryBindingsTypes + TimerContext
159{
160}
161impl<BC> Ipv6RouteDiscoveryBindingsContext for BC where
162    BC: Ipv6RouteDiscoveryBindingsTypes + TimerContext
163{
164}
165
166/// An implementation of IPv6 route discovery.
167pub trait RouteDiscoveryHandler<BC>: DeviceIdContext<AnyDevice> {
168    /// Handles an update affecting discovered routes.
169    ///
170    /// A `None` value for `lifetime` indicates that the route is not valid and
171    /// must be invalidated if it has been discovered; a `Some(_)` value
172    /// indicates the new maximum lifetime that the route may be valid for
173    /// before being invalidated.
174    fn update_route(
175        &mut self,
176        bindings_ctx: &mut BC,
177        device_id: &Self::DeviceId,
178        route: Ipv6DiscoveredRoute,
179        lifetime: Option<NonZeroNdpLifetime>,
180        config: &RouteDiscoveryConfiguration,
181    );
182
183    /// Invalidates all discovered routes.
184    fn invalidate_routes(&mut self, bindings_ctx: &mut BC, device_id: &Self::DeviceId);
185}
186
187impl<BC: Ipv6RouteDiscoveryBindingsContext, CC: Ipv6RouteDiscoveryContext<BC>>
188    RouteDiscoveryHandler<BC> for CC
189{
190    fn update_route(
191        &mut self,
192        bindings_ctx: &mut BC,
193        device_id: &CC::DeviceId,
194        route: Ipv6DiscoveredRoute,
195        lifetime: Option<NonZeroNdpLifetime>,
196        config: &RouteDiscoveryConfiguration,
197    ) {
198        self.with_discovered_routes_mut(device_id, |state, core_ctx| {
199            let Ipv6RouteDiscoveryState { routes, timers } = state;
200            match lifetime {
201                Some(lifetime) => {
202                    if !config.allow_default_route && route.subnet.prefix() == 0 {
203                        return;
204                    }
205                    let newly_added = routes.insert(route.clone());
206                    if newly_added {
207                        core_ctx.add_discovered_ipv6_route(bindings_ctx, device_id, route);
208                    }
209
210                    let prev_timer_fires_at = match lifetime {
211                        NonZeroNdpLifetime::Finite(lifetime) => {
212                            timers.schedule_after(bindings_ctx, route, (), lifetime.get())
213                        }
214                        // Routes with an infinite lifetime have no timers.
215                        NonZeroNdpLifetime::Infinite => timers.cancel(bindings_ctx, &route),
216                    };
217
218                    if newly_added {
219                        if let Some((prev_timer_fires_at, ())) = prev_timer_fires_at {
220                            panic!(
221                                "newly added route {:?} should not have already been \
222                                 scheduled to fire at {:?}",
223                                route, prev_timer_fires_at,
224                            )
225                        }
226                    }
227                }
228                None => {
229                    if routes.remove(&route) {
230                        invalidate_route(core_ctx, bindings_ctx, device_id, state, route);
231                    }
232                }
233            }
234        })
235    }
236
237    fn invalidate_routes(&mut self, bindings_ctx: &mut BC, device_id: &CC::DeviceId) {
238        self.with_discovered_routes_mut(device_id, |state, core_ctx| {
239            for route in core::mem::take(&mut state.routes).into_iter() {
240                invalidate_route(core_ctx, bindings_ctx, device_id, state, route);
241            }
242        })
243    }
244}
245
246impl<BC: Ipv6RouteDiscoveryBindingsContext, CC: Ipv6RouteDiscoveryContext<BC>>
247    HandleableTimer<CC, BC> for Ipv6DiscoveredRouteTimerId<CC::WeakDeviceId>
248{
249    fn handle(self, core_ctx: &mut CC, bindings_ctx: &mut BC, _: BC::UniqueTimerId) {
250        let Self { device_id } = self;
251        let Some(device_id) = device_id.upgrade() else {
252            return;
253        };
254        core_ctx.with_discovered_routes_mut(
255            &device_id,
256            |Ipv6RouteDiscoveryState { routes, timers }, core_ctx| {
257                let Some((route, ())) = timers.pop(bindings_ctx) else {
258                    return;
259                };
260                assert!(routes.remove(&route), "invalidated route should be discovered");
261                core_ctx.del_discovered_ipv6_route(bindings_ctx, &device_id, route);
262            },
263        )
264    }
265}
266
267fn invalidate_route<BC: Ipv6RouteDiscoveryBindingsContext, CC: Ipv6DiscoveredRoutesContext<BC>>(
268    core_ctx: &mut CC,
269    bindings_ctx: &mut BC,
270    device_id: &CC::DeviceId,
271    state: &mut Ipv6RouteDiscoveryState<BC>,
272    route: Ipv6DiscoveredRoute,
273) {
274    // Routes with an infinite lifetime have no timers.
275    let _: Option<(BC::Instant, ())> = state.timers.cancel(bindings_ctx, &route);
276    core_ctx.del_discovered_ipv6_route(bindings_ctx, device_id, route)
277}
278
279#[cfg(test)]
280mod tests {
281    use netstack3_base::testutil::{
282        FakeBindingsCtx, FakeCoreCtx, FakeDeviceId, FakeInstant, FakeTimerCtxExt as _,
283        FakeWeakDeviceId,
284    };
285    use netstack3_base::{CtxPair, IntoCoreTimerCtx};
286    use packet_formats::utils::NonZeroDuration;
287
288    use super::*;
289    use crate::internal::base::IPV6_DEFAULT_SUBNET;
290
291    #[derive(Default)]
292    struct FakeWithDiscoveredRoutesMutCtx {
293        route_table: HashSet<Ipv6DiscoveredRoute>,
294    }
295
296    impl DeviceIdContext<AnyDevice> for FakeWithDiscoveredRoutesMutCtx {
297        type DeviceId = FakeDeviceId;
298        type WeakDeviceId = FakeWeakDeviceId<FakeDeviceId>;
299    }
300
301    impl<C> Ipv6DiscoveredRoutesContext<C> for FakeWithDiscoveredRoutesMutCtx {
302        fn add_discovered_ipv6_route(
303            &mut self,
304            _bindings_ctx: &mut C,
305            FakeDeviceId: &Self::DeviceId,
306            route: Ipv6DiscoveredRoute,
307        ) {
308            let Self { route_table } = self;
309            let _newly_inserted = route_table.insert(route);
310        }
311
312        fn del_discovered_ipv6_route(
313            &mut self,
314            _bindings_ctx: &mut C,
315            FakeDeviceId: &Self::DeviceId,
316            route: Ipv6DiscoveredRoute,
317        ) {
318            let Self { route_table } = self;
319            let _: bool = route_table.remove(&route);
320        }
321    }
322
323    struct FakeIpv6RouteDiscoveryContext {
324        state: Ipv6RouteDiscoveryState<FakeBindingsCtxImpl>,
325        route_table: FakeWithDiscoveredRoutesMutCtx,
326    }
327
328    type FakeCoreCtxImpl = FakeCoreCtx<FakeIpv6RouteDiscoveryContext, (), FakeDeviceId>;
329
330    type FakeBindingsCtxImpl =
331        FakeBindingsCtx<Ipv6DiscoveredRouteTimerId<FakeWeakDeviceId<FakeDeviceId>>, (), (), ()>;
332
333    impl Ipv6RouteDiscoveryContext<FakeBindingsCtxImpl> for FakeCoreCtxImpl {
334        type WithDiscoveredRoutesMutCtx<'a> = FakeWithDiscoveredRoutesMutCtx;
335
336        fn with_discovered_routes_mut<
337            O,
338            F: FnOnce(
339                &mut Ipv6RouteDiscoveryState<FakeBindingsCtxImpl>,
340                &mut Self::WithDiscoveredRoutesMutCtx<'_>,
341            ) -> O,
342        >(
343            &mut self,
344            &FakeDeviceId: &Self::DeviceId,
345            cb: F,
346        ) -> O {
347            let FakeIpv6RouteDiscoveryContext { state, route_table, .. } = &mut self.state;
348            cb(state, route_table)
349        }
350    }
351
352    const ROUTE1: Ipv6DiscoveredRoute =
353        Ipv6DiscoveredRoute { subnet: IPV6_DEFAULT_SUBNET, gateway: None };
354    const ROUTE2: Ipv6DiscoveredRoute = Ipv6DiscoveredRoute {
355        subnet: unsafe {
356            Subnet::new_unchecked(Ipv6Addr::new([0x2620, 0x1012, 0x1000, 0x5000, 0, 0, 0, 0]), 64)
357        },
358        gateway: None,
359    };
360
361    const ONE_SECOND: NonZeroDuration = NonZeroDuration::from_secs(1).unwrap();
362    const TWO_SECONDS: NonZeroDuration = NonZeroDuration::from_secs(2).unwrap();
363
364    fn new_context() -> CtxPair<FakeCoreCtxImpl, FakeBindingsCtxImpl> {
365        CtxPair::with_default_bindings_ctx(|bindings_ctx| {
366            FakeCoreCtxImpl::with_state(FakeIpv6RouteDiscoveryContext {
367                state: Ipv6RouteDiscoveryState::new::<_, IntoCoreTimerCtx>(
368                    bindings_ctx,
369                    FakeWeakDeviceId(FakeDeviceId),
370                ),
371                route_table: Default::default(),
372            })
373        })
374    }
375
376    #[test]
377    fn new_route_no_lifetime() {
378        let CtxPair { mut core_ctx, mut bindings_ctx } = new_context();
379
380        RouteDiscoveryHandler::update_route(
381            &mut core_ctx,
382            &mut bindings_ctx,
383            &FakeDeviceId,
384            ROUTE1,
385            None,
386            &Default::default(),
387        );
388        bindings_ctx.timers.assert_no_timers_installed();
389    }
390
391    fn discover_new_route(
392        core_ctx: &mut FakeCoreCtxImpl,
393        bindings_ctx: &mut FakeBindingsCtxImpl,
394        route: Ipv6DiscoveredRoute,
395        duration: NonZeroNdpLifetime,
396    ) {
397        RouteDiscoveryHandler::update_route(
398            core_ctx,
399            bindings_ctx,
400            &FakeDeviceId,
401            route,
402            Some(duration),
403            &Default::default(),
404        );
405
406        let route_table = &core_ctx.state.route_table.route_table;
407        assert!(route_table.contains(&route), "route_table={route_table:?}");
408
409        let expect = match duration {
410            NonZeroNdpLifetime::Finite(duration) => Some((FakeInstant::from(duration.get()), &())),
411            NonZeroNdpLifetime::Infinite => None,
412        };
413        assert_eq!(core_ctx.state.state.timers.get(&route), expect);
414    }
415
416    fn trigger_next_timer(
417        core_ctx: &mut FakeCoreCtxImpl,
418        bindings_ctx: &mut FakeBindingsCtxImpl,
419        route: Ipv6DiscoveredRoute,
420    ) {
421        core_ctx.state.state.timers.assert_top(&route, &());
422        assert_eq!(
423            bindings_ctx.trigger_next_timer(core_ctx),
424            Some(Ipv6DiscoveredRouteTimerId { device_id: FakeWeakDeviceId(FakeDeviceId) })
425        );
426    }
427
428    fn assert_route_invalidated(
429        core_ctx: &mut FakeCoreCtxImpl,
430        bindings_ctx: &mut FakeBindingsCtxImpl,
431        route: Ipv6DiscoveredRoute,
432    ) {
433        let route_table = &core_ctx.state.route_table.route_table;
434        assert!(!route_table.contains(&route), "route_table={route_table:?}");
435        bindings_ctx.timers.assert_no_timers_installed();
436    }
437
438    fn assert_single_invalidation_timer(
439        core_ctx: &mut FakeCoreCtxImpl,
440        bindings_ctx: &mut FakeBindingsCtxImpl,
441        route: Ipv6DiscoveredRoute,
442    ) {
443        trigger_next_timer(core_ctx, bindings_ctx, route);
444        assert_route_invalidated(core_ctx, bindings_ctx, route);
445    }
446
447    #[test]
448    fn invalidated_route_not_found() {
449        let CtxPair { mut core_ctx, mut bindings_ctx } = new_context();
450
451        discover_new_route(&mut core_ctx, &mut bindings_ctx, ROUTE1, NonZeroNdpLifetime::Infinite);
452
453        // Fake the route already being removed from underneath the route
454        // discovery table.
455        assert!(core_ctx.state.route_table.route_table.remove(&ROUTE1));
456        // Invalidating the route should ignore the fact that the route is not
457        // in the route table.
458        update_to_invalidate_check_invalidation(&mut core_ctx, &mut bindings_ctx, ROUTE1);
459    }
460
461    #[test]
462    fn new_route_with_infinite_lifetime() {
463        let CtxPair { mut core_ctx, mut bindings_ctx } = new_context();
464
465        discover_new_route(&mut core_ctx, &mut bindings_ctx, ROUTE1, NonZeroNdpLifetime::Infinite);
466        bindings_ctx.timers.assert_no_timers_installed();
467    }
468
469    #[test]
470    fn update_route_from_infinite_to_finite_lifetime() {
471        let CtxPair { mut core_ctx, mut bindings_ctx } = new_context();
472
473        discover_new_route(&mut core_ctx, &mut bindings_ctx, ROUTE1, NonZeroNdpLifetime::Infinite);
474        bindings_ctx.timers.assert_no_timers_installed();
475
476        RouteDiscoveryHandler::update_route(
477            &mut core_ctx,
478            &mut bindings_ctx,
479            &FakeDeviceId,
480            ROUTE1,
481            Some(NonZeroNdpLifetime::Finite(ONE_SECOND)),
482            &Default::default(),
483        );
484        assert_eq!(
485            core_ctx.state.state.timers.get(&ROUTE1),
486            Some((FakeInstant::from(ONE_SECOND.get()), &()))
487        );
488        assert_single_invalidation_timer(&mut core_ctx, &mut bindings_ctx, ROUTE1);
489    }
490
491    fn update_to_invalidate_check_invalidation(
492        core_ctx: &mut FakeCoreCtxImpl,
493        bindings_ctx: &mut FakeBindingsCtxImpl,
494        route: Ipv6DiscoveredRoute,
495    ) {
496        RouteDiscoveryHandler::update_route(
497            core_ctx,
498            bindings_ctx,
499            &FakeDeviceId,
500            ROUTE1,
501            None,
502            &Default::default(),
503        );
504        assert_route_invalidated(core_ctx, bindings_ctx, route);
505    }
506
507    #[test]
508    fn invalidate_route_with_infinite_lifetime() {
509        let CtxPair { mut core_ctx, mut bindings_ctx } = new_context();
510
511        discover_new_route(&mut core_ctx, &mut bindings_ctx, ROUTE1, NonZeroNdpLifetime::Infinite);
512        bindings_ctx.timers.assert_no_timers_installed();
513
514        update_to_invalidate_check_invalidation(&mut core_ctx, &mut bindings_ctx, ROUTE1);
515    }
516    #[test]
517    fn new_route_with_finite_lifetime() {
518        let CtxPair { mut core_ctx, mut bindings_ctx } = new_context();
519
520        discover_new_route(
521            &mut core_ctx,
522            &mut bindings_ctx,
523            ROUTE1,
524            NonZeroNdpLifetime::Finite(ONE_SECOND),
525        );
526        assert_single_invalidation_timer(&mut core_ctx, &mut bindings_ctx, ROUTE1);
527    }
528
529    #[test]
530    fn update_route_from_finite_to_infinite_lifetime() {
531        let CtxPair { mut core_ctx, mut bindings_ctx } = new_context();
532
533        discover_new_route(
534            &mut core_ctx,
535            &mut bindings_ctx,
536            ROUTE1,
537            NonZeroNdpLifetime::Finite(ONE_SECOND),
538        );
539
540        RouteDiscoveryHandler::update_route(
541            &mut core_ctx,
542            &mut bindings_ctx,
543            &FakeDeviceId,
544            ROUTE1,
545            Some(NonZeroNdpLifetime::Infinite),
546            &Default::default(),
547        );
548        bindings_ctx.timers.assert_no_timers_installed();
549    }
550
551    #[test]
552    fn update_route_from_finite_to_finite_lifetime() {
553        let CtxPair { mut core_ctx, mut bindings_ctx } = new_context();
554
555        discover_new_route(
556            &mut core_ctx,
557            &mut bindings_ctx,
558            ROUTE1,
559            NonZeroNdpLifetime::Finite(ONE_SECOND),
560        );
561
562        RouteDiscoveryHandler::update_route(
563            &mut core_ctx,
564            &mut bindings_ctx,
565            &FakeDeviceId,
566            ROUTE1,
567            Some(NonZeroNdpLifetime::Finite(TWO_SECONDS)),
568            &Default::default(),
569        );
570        assert_eq!(
571            core_ctx.state.state.timers.get(&ROUTE1),
572            Some((FakeInstant::from(TWO_SECONDS.get()), &()))
573        );
574        assert_single_invalidation_timer(&mut core_ctx, &mut bindings_ctx, ROUTE1);
575    }
576
577    #[test]
578    fn invalidate_route_with_finite_lifetime() {
579        let CtxPair { mut core_ctx, mut bindings_ctx } = new_context();
580
581        discover_new_route(
582            &mut core_ctx,
583            &mut bindings_ctx,
584            ROUTE1,
585            NonZeroNdpLifetime::Finite(ONE_SECOND),
586        );
587
588        update_to_invalidate_check_invalidation(&mut core_ctx, &mut bindings_ctx, ROUTE1);
589    }
590
591    #[test]
592    fn invalidate_all_routes() {
593        let CtxPair { mut core_ctx, mut bindings_ctx } = new_context();
594        discover_new_route(
595            &mut core_ctx,
596            &mut bindings_ctx,
597            ROUTE1,
598            NonZeroNdpLifetime::Finite(ONE_SECOND),
599        );
600        discover_new_route(
601            &mut core_ctx,
602            &mut bindings_ctx,
603            ROUTE2,
604            NonZeroNdpLifetime::Finite(TWO_SECONDS),
605        );
606
607        RouteDiscoveryHandler::invalidate_routes(&mut core_ctx, &mut bindings_ctx, &FakeDeviceId);
608        bindings_ctx.timers.assert_no_timers_installed();
609        let route_table = &core_ctx.state.route_table.route_table;
610        assert!(route_table.is_empty(), "route_table={route_table:?}");
611    }
612}