Skip to main content

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