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 log::debug;
13use net_types::LinkLocalUnicastAddr;
14use net_types::ip::{Ipv6Addr, Subnet};
15use netstack3_base::{
16    AnyDevice, CoreTimerContext, DeviceIdContext, HandleableTimer, InstantBindingsTypes,
17    LocalTimerHeap, TimerBindingsTypes, TimerContext, TokenBucket, WeakDeviceIdentifier,
18};
19use netstack3_hashmap::HashMap;
20use netstack3_hashmap::hash_map::Entry;
21use packet_formats::icmp::ndp::NonZeroNdpLifetime;
22
23use crate::internal::types::RoutePreference;
24
25/// The maximum number of discovered routes the stack will track.
26///
27/// If new routes are discovered that would exceed this limit, they are ignored.
28const MAX_DISCOVERED_ROUTES: usize = 4096;
29
30/// The maximum number of routes that can be discovered per second.
31///
32/// If new routes are discovered that would exceed this limit, they are ignored.
33const MAX_DISCOVERIES_PER_SECOND: u64 = 256;
34
35/// Route discovery state on a device.
36#[derive(Debug)]
37pub struct Ipv6RouteDiscoveryState<BT: Ipv6RouteDiscoveryBindingsTypes> {
38    // The valid (non-zero lifetime) discovered routes.
39    //
40    // Routes with a finite lifetime must have a timer set; routes with an
41    // infinite lifetime must not.
42    routes: HashMap<Ipv6DiscoveredRoute, Ipv6DiscoveredRouteProperties>,
43    timers: LocalTimerHeap<Ipv6DiscoveredRoute, (), BT>,
44    // Rate limit the discovery of routes. Add and update operations are rate
45    // limited whereas delete operations are not. Delete operations are self
46    // rate limiting, since they require the entry be present in the table.
47    rate_limit: TokenBucket<BT::Instant>,
48}
49
50impl<BT: Ipv6RouteDiscoveryBindingsTypes> Ipv6RouteDiscoveryState<BT> {
51    /// Gets the timer heap for route discovery.
52    #[cfg(any(test, feature = "testutils"))]
53    pub fn timers(&self) -> &LocalTimerHeap<Ipv6DiscoveredRoute, (), BT> {
54        &self.timers
55    }
56}
57
58impl<BC: Ipv6RouteDiscoveryBindingsContext> Ipv6RouteDiscoveryState<BC> {
59    /// Constructs the route discovery state for `device_id`.
60    pub fn new<D: WeakDeviceIdentifier, CC: CoreTimerContext<Ipv6DiscoveredRouteTimerId<D>, BC>>(
61        bindings_ctx: &mut BC,
62        device_id: D,
63    ) -> Self {
64        Self {
65            routes: Default::default(),
66            timers: LocalTimerHeap::new_with_context::<_, CC>(
67                bindings_ctx,
68                Ipv6DiscoveredRouteTimerId { device_id },
69            ),
70            rate_limit: TokenBucket::new(MAX_DISCOVERIES_PER_SECOND),
71        }
72    }
73}
74
75/// A discovered route.
76#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
77pub struct Ipv6DiscoveredRoute {
78    /// The destination subnet for the route.
79    pub subnet: Subnet<Ipv6Addr>,
80
81    /// The next-hop node for the route, if required.
82    ///
83    /// `None` indicates that the subnet is on-link/directly-connected.
84    pub gateway: Option<LinkLocalUnicastAddr<Ipv6Addr>>,
85}
86
87/// A discovered route's properties.
88#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
89pub struct Ipv6DiscoveredRouteProperties {
90    /// The preference of the route.
91    pub route_preference: RoutePreference,
92}
93
94/// A timer ID for IPv6 route discovery.
95#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
96pub struct Ipv6DiscoveredRouteTimerId<D: WeakDeviceIdentifier> {
97    device_id: D,
98}
99
100impl<D: WeakDeviceIdentifier> Ipv6DiscoveredRouteTimerId<D> {
101    pub(super) fn device_id(&self) -> &D {
102        &self.device_id
103    }
104}
105
106/// The configuration for route discovery.
107#[derive(Copy, Clone, Debug, Eq, PartialEq, Derivative)]
108#[derivative(Default)]
109pub struct RouteDiscoveryConfiguration {
110    /// Allow default route to be added for this device.
111    #[derivative(Default(value = "true"))]
112    pub allow_default_route: bool,
113}
114
115/// The configuration update for route discovery.
116#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)]
117#[allow(missing_docs)]
118pub struct RouteDiscoveryConfigurationUpdate {
119    pub allow_default_route: Option<bool>,
120}
121
122impl RouteDiscoveryConfiguration {
123    /// Updates the route discovery configuration.
124    ///
125    /// Returns the previous value of the updated fields.
126    pub fn update(
127        &mut self,
128        update: RouteDiscoveryConfigurationUpdate,
129    ) -> RouteDiscoveryConfigurationUpdate {
130        let RouteDiscoveryConfigurationUpdate { allow_default_route } = update;
131        let allow_default_route =
132            allow_default_route.map(|new| core::mem::replace(&mut self.allow_default_route, new));
133        RouteDiscoveryConfigurationUpdate { allow_default_route }
134    }
135}
136
137/// An implementation of the execution context available when accessing the IPv6
138/// route discovery state.
139///
140/// See [`Ipv6RouteDiscoveryContext::with_discovered_routes_mut`].
141pub trait Ipv6DiscoveredRoutesContext<BC>: DeviceIdContext<AnyDevice> {
142    /// Adds a newly discovered IPv6 route to the routing table.
143    fn add_discovered_ipv6_route(
144        &mut self,
145        bindings_ctx: &mut BC,
146        device_id: &Self::DeviceId,
147        route: Ipv6DiscoveredRoute,
148        properties: Ipv6DiscoveredRouteProperties,
149    );
150
151    /// Deletes a previously discovered (now invalidated) IPv6 route from the
152    /// routing table.
153    fn del_discovered_ipv6_route(
154        &mut self,
155        bindings_ctx: &mut BC,
156        device_id: &Self::DeviceId,
157        route: Ipv6DiscoveredRoute,
158    );
159}
160
161/// The execution context for IPv6 route discovery.
162pub trait Ipv6RouteDiscoveryContext<BT: Ipv6RouteDiscoveryBindingsTypes>:
163    DeviceIdContext<AnyDevice>
164{
165    /// The inner discovered routes context.
166    type WithDiscoveredRoutesMutCtx<'a>: Ipv6DiscoveredRoutesContext<BT, DeviceId = Self::DeviceId>;
167
168    /// Gets the route discovery state, mutably.
169    fn with_discovered_routes_mut<
170        O,
171        F: FnOnce(&mut Ipv6RouteDiscoveryState<BT>, &mut Self::WithDiscoveredRoutesMutCtx<'_>) -> O,
172    >(
173        &mut self,
174        device_id: &Self::DeviceId,
175        cb: F,
176    ) -> O;
177}
178
179/// The bindings types for IPv6 route discovery.
180pub trait Ipv6RouteDiscoveryBindingsTypes: TimerBindingsTypes + InstantBindingsTypes {}
181impl<BT> Ipv6RouteDiscoveryBindingsTypes for BT where BT: TimerBindingsTypes + InstantBindingsTypes {}
182
183/// The bindings execution context for IPv6 route discovery.
184pub trait Ipv6RouteDiscoveryBindingsContext:
185    Ipv6RouteDiscoveryBindingsTypes + TimerContext
186{
187}
188impl<BC> Ipv6RouteDiscoveryBindingsContext for BC where
189    BC: Ipv6RouteDiscoveryBindingsTypes + TimerContext
190{
191}
192
193/// An implementation of IPv6 route discovery.
194pub trait RouteDiscoveryHandler<BC>: DeviceIdContext<AnyDevice> {
195    /// Handles an update affecting discovered routes.
196    ///
197    /// A `None` value for `lifetime` indicates that the route is not valid and
198    /// must be invalidated if it has been discovered; a `Some(_)` value
199    /// indicates the new maximum lifetime that the route may be valid for
200    /// before being invalidated.
201    fn update_route(
202        &mut self,
203        bindings_ctx: &mut BC,
204        device_id: &Self::DeviceId,
205        route: Ipv6DiscoveredRoute,
206        properties: Ipv6DiscoveredRouteProperties,
207        lifetime: Option<NonZeroNdpLifetime>,
208        config: &RouteDiscoveryConfiguration,
209    );
210
211    /// Invalidates all discovered routes.
212    fn invalidate_routes(&mut self, bindings_ctx: &mut BC, device_id: &Self::DeviceId);
213}
214
215impl<BC: Ipv6RouteDiscoveryBindingsContext, CC: Ipv6RouteDiscoveryContext<BC>>
216    RouteDiscoveryHandler<BC> for CC
217{
218    fn update_route(
219        &mut self,
220        bindings_ctx: &mut BC,
221        device_id: &CC::DeviceId,
222        route: Ipv6DiscoveredRoute,
223        properties: Ipv6DiscoveredRouteProperties,
224        lifetime: Option<NonZeroNdpLifetime>,
225        config: &RouteDiscoveryConfiguration,
226    ) {
227        self.with_discovered_routes_mut(device_id, |state, core_ctx| {
228            let Ipv6RouteDiscoveryState { routes, timers, rate_limit } = state;
229            match lifetime {
230                Some(lifetime) => {
231                    if !config.allow_default_route && route.subnet.prefix() == 0 {
232                        return;
233                    }
234                    if !rate_limit.try_take(bindings_ctx) {
235                        debug!(
236                            "IPv6 Discovered routes rate limited. Ignoring update for {route:?}"
237                        );
238                        return;
239                    }
240
241                    let num_routes = routes.len();
242                    let newly_added = match routes.entry(route) {
243                        Entry::Occupied(mut entry) => {
244                            let old_properties = entry.get_mut();
245                            if old_properties.route_preference != properties.route_preference {
246                                core_ctx.del_discovered_ipv6_route(bindings_ctx, device_id, route);
247                                core_ctx.add_discovered_ipv6_route(
248                                    bindings_ctx,
249                                    device_id,
250                                    route,
251                                    properties,
252                                );
253                                *old_properties = properties;
254                            }
255                            false
256                        }
257                        Entry::Vacant(entry) => {
258                            if num_routes >= MAX_DISCOVERED_ROUTES {
259                                debug!(
260                                    "IPv6 Discovered Routes table is full. Not adding {route:?}"
261                                );
262                                return;
263                            }
264
265                            core_ctx.add_discovered_ipv6_route(
266                                bindings_ctx,
267                                device_id,
268                                route,
269                                properties,
270                            );
271                            let _: &mut _ = entry.insert(properties);
272                            true
273                        }
274                    };
275
276                    let prev_timer_fires_at = match lifetime {
277                        NonZeroNdpLifetime::Finite(lifetime) => {
278                            timers.schedule_after(bindings_ctx, route, (), lifetime.get())
279                        }
280                        // Routes with an infinite lifetime have no timers.
281                        NonZeroNdpLifetime::Infinite => timers.cancel(bindings_ctx, &route),
282                    };
283
284                    if newly_added {
285                        if let Some((prev_timer_fires_at, ())) = prev_timer_fires_at {
286                            panic!(
287                                "newly added route {:?} should not have already been \
288                                 scheduled to fire at {:?}",
289                                route, prev_timer_fires_at,
290                            )
291                        }
292                    }
293                }
294                None => {
295                    if routes.remove(&route).is_some() {
296                        invalidate_route(core_ctx, bindings_ctx, device_id, state, route);
297                    }
298                }
299            }
300        })
301    }
302
303    fn invalidate_routes(&mut self, bindings_ctx: &mut BC, device_id: &CC::DeviceId) {
304        self.with_discovered_routes_mut(device_id, |state, core_ctx| {
305            for (route, _properties) in core::mem::take(&mut state.routes).into_iter() {
306                invalidate_route(core_ctx, bindings_ctx, device_id, state, route);
307            }
308        })
309    }
310}
311
312impl<BC: Ipv6RouteDiscoveryBindingsContext, CC: Ipv6RouteDiscoveryContext<BC>>
313    HandleableTimer<CC, BC> for Ipv6DiscoveredRouteTimerId<CC::WeakDeviceId>
314{
315    fn handle(self, core_ctx: &mut CC, bindings_ctx: &mut BC, _: BC::UniqueTimerId) {
316        let Self { device_id } = self;
317        let Some(device_id) = device_id.upgrade() else {
318            return;
319        };
320        core_ctx.with_discovered_routes_mut(
321            &device_id,
322            |Ipv6RouteDiscoveryState { routes, timers, rate_limit: _ }, core_ctx| {
323                let Some((route, ())) = timers.pop(bindings_ctx) else {
324                    return;
325                };
326                let _properties =
327                    routes.remove(&route).expect("invalidated route should be discovered");
328                core_ctx.del_discovered_ipv6_route(bindings_ctx, &device_id, route);
329            },
330        )
331    }
332}
333
334fn invalidate_route<BC: Ipv6RouteDiscoveryBindingsContext, CC: Ipv6DiscoveredRoutesContext<BC>>(
335    core_ctx: &mut CC,
336    bindings_ctx: &mut BC,
337    device_id: &CC::DeviceId,
338    state: &mut Ipv6RouteDiscoveryState<BC>,
339    route: Ipv6DiscoveredRoute,
340) {
341    // Routes with an infinite lifetime have no timers.
342    let _: Option<(BC::Instant, ())> = state.timers.cancel(bindings_ctx, &route);
343    core_ctx.del_discovered_ipv6_route(bindings_ctx, device_id, route)
344}
345
346#[cfg(test)]
347mod tests {
348    use netstack3_base::testutil::{
349        FakeBindingsCtx, FakeCoreCtx, FakeDeviceId, FakeInstant, FakeTimerCtxExt as _,
350        FakeWeakDeviceId,
351    };
352    use netstack3_base::{CtxPair, IntoCoreTimerCtx};
353    use packet_formats::utils::NonZeroDuration;
354
355    use super::*;
356    use crate::internal::base::IPV6_DEFAULT_SUBNET;
357
358    #[derive(Default)]
359    struct FakeWithDiscoveredRoutesMutCtx {
360        route_table: HashMap<Ipv6DiscoveredRoute, Ipv6DiscoveredRouteProperties>,
361    }
362
363    impl DeviceIdContext<AnyDevice> for FakeWithDiscoveredRoutesMutCtx {
364        type DeviceId = FakeDeviceId;
365        type WeakDeviceId = FakeWeakDeviceId<FakeDeviceId>;
366    }
367
368    impl<C> Ipv6DiscoveredRoutesContext<C> for FakeWithDiscoveredRoutesMutCtx {
369        fn add_discovered_ipv6_route(
370            &mut self,
371            _bindings_ctx: &mut C,
372            FakeDeviceId: &Self::DeviceId,
373            route: Ipv6DiscoveredRoute,
374            properties: Ipv6DiscoveredRouteProperties,
375        ) {
376            let Self { route_table } = self;
377            let _: Option<Ipv6DiscoveredRouteProperties> = route_table.insert(route, properties);
378        }
379
380        fn del_discovered_ipv6_route(
381            &mut self,
382            _bindings_ctx: &mut C,
383            FakeDeviceId: &Self::DeviceId,
384            route: Ipv6DiscoveredRoute,
385        ) {
386            let Self { route_table } = self;
387            let _: Option<Ipv6DiscoveredRouteProperties> = route_table.remove(&route);
388        }
389    }
390
391    struct FakeIpv6RouteDiscoveryContext {
392        state: Ipv6RouteDiscoveryState<FakeBindingsCtxImpl>,
393        route_table: FakeWithDiscoveredRoutesMutCtx,
394    }
395
396    type FakeCoreCtxImpl = FakeCoreCtx<FakeIpv6RouteDiscoveryContext, (), FakeDeviceId>;
397
398    type FakeBindingsCtxImpl =
399        FakeBindingsCtx<Ipv6DiscoveredRouteTimerId<FakeWeakDeviceId<FakeDeviceId>>, (), (), ()>;
400
401    impl Ipv6RouteDiscoveryContext<FakeBindingsCtxImpl> for FakeCoreCtxImpl {
402        type WithDiscoveredRoutesMutCtx<'a> = FakeWithDiscoveredRoutesMutCtx;
403
404        fn with_discovered_routes_mut<
405            O,
406            F: FnOnce(
407                &mut Ipv6RouteDiscoveryState<FakeBindingsCtxImpl>,
408                &mut Self::WithDiscoveredRoutesMutCtx<'_>,
409            ) -> O,
410        >(
411            &mut self,
412            &FakeDeviceId: &Self::DeviceId,
413            cb: F,
414        ) -> O {
415            let FakeIpv6RouteDiscoveryContext { state, route_table, .. } = &mut self.state;
416            cb(state, route_table)
417        }
418    }
419
420    const ROUTE1: Ipv6DiscoveredRoute =
421        Ipv6DiscoveredRoute { subnet: IPV6_DEFAULT_SUBNET, gateway: None };
422    const PROP1: Ipv6DiscoveredRouteProperties =
423        Ipv6DiscoveredRouteProperties { route_preference: RoutePreference::Medium };
424    const ROUTE2: Ipv6DiscoveredRoute = Ipv6DiscoveredRoute {
425        subnet: unsafe {
426            Subnet::new_unchecked(Ipv6Addr::new([0x2620, 0x1012, 0x1000, 0x5000, 0, 0, 0, 0]), 64)
427        },
428        gateway: None,
429    };
430    const PROP2: Ipv6DiscoveredRouteProperties =
431        Ipv6DiscoveredRouteProperties { route_preference: RoutePreference::High };
432
433    const ONE_SECOND: NonZeroDuration = NonZeroDuration::from_secs(1).unwrap();
434    const TWO_SECONDS: NonZeroDuration = NonZeroDuration::from_secs(2).unwrap();
435
436    fn new_context() -> CtxPair<FakeCoreCtxImpl, FakeBindingsCtxImpl> {
437        CtxPair::with_default_bindings_ctx(|bindings_ctx| {
438            FakeCoreCtxImpl::with_state(FakeIpv6RouteDiscoveryContext {
439                state: Ipv6RouteDiscoveryState::new::<_, IntoCoreTimerCtx>(
440                    bindings_ctx,
441                    FakeWeakDeviceId(FakeDeviceId),
442                ),
443                route_table: Default::default(),
444            })
445        })
446    }
447
448    #[test]
449    fn new_route_no_lifetime() {
450        let CtxPair { mut core_ctx, mut bindings_ctx } = new_context();
451
452        RouteDiscoveryHandler::update_route(
453            &mut core_ctx,
454            &mut bindings_ctx,
455            &FakeDeviceId,
456            ROUTE1,
457            PROP1,
458            None,
459            &Default::default(),
460        );
461        bindings_ctx.timers.assert_no_timers_installed();
462    }
463
464    fn discover_new_route(
465        core_ctx: &mut FakeCoreCtxImpl,
466        bindings_ctx: &mut FakeBindingsCtxImpl,
467        route: Ipv6DiscoveredRoute,
468        properties: Ipv6DiscoveredRouteProperties,
469        duration: NonZeroNdpLifetime,
470    ) {
471        RouteDiscoveryHandler::update_route(
472            core_ctx,
473            bindings_ctx,
474            &FakeDeviceId,
475            route,
476            properties,
477            Some(duration),
478            &Default::default(),
479        );
480
481        let route_table = &core_ctx.state.route_table.route_table;
482        assert_eq!(route_table.get(&route), Some(&properties), "route_table={route_table:?}");
483
484        let expect = match duration {
485            NonZeroNdpLifetime::Finite(duration) => Some((FakeInstant::from(duration.get()), &())),
486            NonZeroNdpLifetime::Infinite => None,
487        };
488        assert_eq!(core_ctx.state.state.timers.get(&route), expect);
489    }
490
491    fn trigger_next_timer(
492        core_ctx: &mut FakeCoreCtxImpl,
493        bindings_ctx: &mut FakeBindingsCtxImpl,
494        route: Ipv6DiscoveredRoute,
495    ) {
496        core_ctx.state.state.timers.assert_top(&route, &());
497        assert_eq!(
498            bindings_ctx.trigger_next_timer(core_ctx),
499            Some(Ipv6DiscoveredRouteTimerId { device_id: FakeWeakDeviceId(FakeDeviceId) })
500        );
501    }
502
503    fn assert_route_invalidated(
504        core_ctx: &mut FakeCoreCtxImpl,
505        bindings_ctx: &mut FakeBindingsCtxImpl,
506        route: Ipv6DiscoveredRoute,
507    ) {
508        let route_table = &core_ctx.state.route_table.route_table;
509        assert!(!route_table.contains_key(&route), "route_table={route_table:?}");
510        bindings_ctx.timers.assert_no_timers_installed();
511    }
512
513    fn assert_single_invalidation_timer(
514        core_ctx: &mut FakeCoreCtxImpl,
515        bindings_ctx: &mut FakeBindingsCtxImpl,
516        route: Ipv6DiscoveredRoute,
517    ) {
518        trigger_next_timer(core_ctx, bindings_ctx, route);
519        assert_route_invalidated(core_ctx, bindings_ctx, route);
520    }
521
522    #[test]
523    fn invalidated_route_not_found() {
524        let CtxPair { mut core_ctx, mut bindings_ctx } = new_context();
525
526        discover_new_route(
527            &mut core_ctx,
528            &mut bindings_ctx,
529            ROUTE1,
530            PROP1,
531            NonZeroNdpLifetime::Infinite,
532        );
533
534        // Fake the route already being removed from underneath the route
535        // discovery table.
536        assert!(core_ctx.state.route_table.route_table.remove(&ROUTE1).is_some());
537        // Invalidating the route should ignore the fact that the route is not
538        // in the route table.
539        update_to_invalidate_check_invalidation(&mut core_ctx, &mut bindings_ctx, ROUTE1, PROP1);
540    }
541
542    #[test]
543    fn new_route_with_infinite_lifetime() {
544        let CtxPair { mut core_ctx, mut bindings_ctx } = new_context();
545
546        discover_new_route(
547            &mut core_ctx,
548            &mut bindings_ctx,
549            ROUTE1,
550            PROP1,
551            NonZeroNdpLifetime::Infinite,
552        );
553        bindings_ctx.timers.assert_no_timers_installed();
554    }
555
556    #[test]
557    fn update_route_from_infinite_to_finite_lifetime() {
558        let CtxPair { mut core_ctx, mut bindings_ctx } = new_context();
559
560        discover_new_route(
561            &mut core_ctx,
562            &mut bindings_ctx,
563            ROUTE1,
564            PROP1,
565            NonZeroNdpLifetime::Infinite,
566        );
567        bindings_ctx.timers.assert_no_timers_installed();
568
569        RouteDiscoveryHandler::update_route(
570            &mut core_ctx,
571            &mut bindings_ctx,
572            &FakeDeviceId,
573            ROUTE1,
574            PROP1,
575            Some(NonZeroNdpLifetime::Finite(ONE_SECOND)),
576            &Default::default(),
577        );
578        assert_eq!(
579            core_ctx.state.state.timers.get(&ROUTE1),
580            Some((FakeInstant::from(ONE_SECOND.get()), &()))
581        );
582        assert_single_invalidation_timer(&mut core_ctx, &mut bindings_ctx, ROUTE1);
583    }
584
585    fn update_to_invalidate_check_invalidation(
586        core_ctx: &mut FakeCoreCtxImpl,
587        bindings_ctx: &mut FakeBindingsCtxImpl,
588        route: Ipv6DiscoveredRoute,
589        properties: Ipv6DiscoveredRouteProperties,
590    ) {
591        RouteDiscoveryHandler::update_route(
592            core_ctx,
593            bindings_ctx,
594            &FakeDeviceId,
595            route,
596            properties,
597            None,
598            &Default::default(),
599        );
600        assert_route_invalidated(core_ctx, bindings_ctx, route);
601    }
602
603    #[test]
604    fn invalidate_route_with_infinite_lifetime() {
605        let CtxPair { mut core_ctx, mut bindings_ctx } = new_context();
606
607        discover_new_route(
608            &mut core_ctx,
609            &mut bindings_ctx,
610            ROUTE1,
611            PROP1,
612            NonZeroNdpLifetime::Infinite,
613        );
614        bindings_ctx.timers.assert_no_timers_installed();
615
616        update_to_invalidate_check_invalidation(&mut core_ctx, &mut bindings_ctx, ROUTE1, PROP1);
617    }
618    #[test]
619    fn new_route_with_finite_lifetime() {
620        let CtxPair { mut core_ctx, mut bindings_ctx } = new_context();
621
622        discover_new_route(
623            &mut core_ctx,
624            &mut bindings_ctx,
625            ROUTE1,
626            PROP1,
627            NonZeroNdpLifetime::Finite(ONE_SECOND),
628        );
629        assert_single_invalidation_timer(&mut core_ctx, &mut bindings_ctx, ROUTE1);
630    }
631
632    #[test]
633    fn update_route_from_finite_to_infinite_lifetime() {
634        let CtxPair { mut core_ctx, mut bindings_ctx } = new_context();
635
636        discover_new_route(
637            &mut core_ctx,
638            &mut bindings_ctx,
639            ROUTE1,
640            PROP1,
641            NonZeroNdpLifetime::Finite(ONE_SECOND),
642        );
643
644        RouteDiscoveryHandler::update_route(
645            &mut core_ctx,
646            &mut bindings_ctx,
647            &FakeDeviceId,
648            ROUTE1,
649            PROP1,
650            Some(NonZeroNdpLifetime::Infinite),
651            &Default::default(),
652        );
653        bindings_ctx.timers.assert_no_timers_installed();
654    }
655
656    #[test]
657    fn update_route_from_finite_to_finite_lifetime() {
658        let CtxPair { mut core_ctx, mut bindings_ctx } = new_context();
659
660        discover_new_route(
661            &mut core_ctx,
662            &mut bindings_ctx,
663            ROUTE1,
664            PROP1,
665            NonZeroNdpLifetime::Finite(ONE_SECOND),
666        );
667
668        RouteDiscoveryHandler::update_route(
669            &mut core_ctx,
670            &mut bindings_ctx,
671            &FakeDeviceId,
672            ROUTE1,
673            PROP1,
674            Some(NonZeroNdpLifetime::Finite(TWO_SECONDS)),
675            &Default::default(),
676        );
677        assert_eq!(
678            core_ctx.state.state.timers.get(&ROUTE1),
679            Some((FakeInstant::from(TWO_SECONDS.get()), &()))
680        );
681        assert_single_invalidation_timer(&mut core_ctx, &mut bindings_ctx, ROUTE1);
682    }
683
684    #[test]
685    fn invalidate_route_with_finite_lifetime() {
686        let CtxPair { mut core_ctx, mut bindings_ctx } = new_context();
687
688        discover_new_route(
689            &mut core_ctx,
690            &mut bindings_ctx,
691            ROUTE1,
692            PROP1,
693            NonZeroNdpLifetime::Finite(ONE_SECOND),
694        );
695
696        update_to_invalidate_check_invalidation(&mut core_ctx, &mut bindings_ctx, ROUTE1, PROP1);
697    }
698
699    #[test]
700    fn invalidate_all_routes() {
701        let CtxPair { mut core_ctx, mut bindings_ctx } = new_context();
702        discover_new_route(
703            &mut core_ctx,
704            &mut bindings_ctx,
705            ROUTE1,
706            PROP1,
707            NonZeroNdpLifetime::Finite(ONE_SECOND),
708        );
709        discover_new_route(
710            &mut core_ctx,
711            &mut bindings_ctx,
712            ROUTE2,
713            PROP2,
714            NonZeroNdpLifetime::Finite(TWO_SECONDS),
715        );
716
717        RouteDiscoveryHandler::invalidate_routes(&mut core_ctx, &mut bindings_ctx, &FakeDeviceId);
718        bindings_ctx.timers.assert_no_timers_installed();
719        let route_table = &core_ctx.state.route_table.route_table;
720        assert!(route_table.is_empty(), "route_table={route_table:?}");
721    }
722
723    fn make_route(i: u16) -> Ipv6DiscoveredRoute {
724        Ipv6DiscoveredRoute {
725            subnet: Subnet::new(Ipv6Addr::new([0x2001, 0xdb8, 0, 0, 0, 0, 0, i]), 128)
726                .expect("should be a valid IPv6 Subnet"),
727            gateway: None,
728        }
729    }
730
731    #[test]
732    fn max_discovered_routes() {
733        let CtxPair { mut core_ctx, mut bindings_ctx } = new_context();
734
735        // Fill the routing table to the limit.
736        for i in 0..MAX_DISCOVERED_ROUTES {
737            let route = make_route(i as u16);
738            discover_new_route(
739                &mut core_ctx,
740                &mut bindings_ctx,
741                route,
742                PROP1,
743                NonZeroNdpLifetime::Infinite,
744            );
745            // NB: Advance the clock to avoid rate limiting in this test.
746            bindings_ctx.timers.instant.sleep(core::time::Duration::from_secs(1));
747        }
748        assert_eq!(core_ctx.state.route_table.route_table.len(), MAX_DISCOVERED_ROUTES);
749
750        // Try to add one more route, it should be ignored.
751        let extra_route = make_route(MAX_DISCOVERED_ROUTES as u16);
752        RouteDiscoveryHandler::update_route(
753            &mut core_ctx,
754            &mut bindings_ctx,
755            &FakeDeviceId,
756            extra_route,
757            PROP1,
758            Some(NonZeroNdpLifetime::Infinite),
759            &Default::default(),
760        );
761        assert_eq!(core_ctx.state.route_table.route_table.len(), MAX_DISCOVERED_ROUTES);
762        assert!(!core_ctx.state.route_table.route_table.contains_key(&extra_route));
763
764        // Update an existing route, it should be allowed.
765        let route_to_update = make_route(0);
766        RouteDiscoveryHandler::update_route(
767            &mut core_ctx,
768            &mut bindings_ctx,
769            &FakeDeviceId,
770            route_to_update,
771            PROP2,
772            Some(NonZeroNdpLifetime::Infinite),
773            &Default::default(),
774        );
775        assert_eq!(core_ctx.state.route_table.route_table.get(&route_to_update), Some(&PROP2));
776
777        // Delete an existing route, it should be allowed.
778        let route_to_delete = make_route(0);
779        RouteDiscoveryHandler::update_route(
780            &mut core_ctx,
781            &mut bindings_ctx,
782            &FakeDeviceId,
783            route_to_delete,
784            PROP2,
785            None,
786            &Default::default(),
787        );
788        assert_eq!(core_ctx.state.route_table.route_table.len(), MAX_DISCOVERED_ROUTES - 1);
789        assert!(!core_ctx.state.route_table.route_table.contains_key(&route_to_delete));
790
791        // Now we should be able to add the extra route.
792        discover_new_route(
793            &mut core_ctx,
794            &mut bindings_ctx,
795            extra_route,
796            PROP1,
797            NonZeroNdpLifetime::Infinite,
798        );
799        assert_eq!(core_ctx.state.route_table.route_table.len(), MAX_DISCOVERED_ROUTES);
800        assert!(core_ctx.state.route_table.route_table.contains_key(&extra_route));
801    }
802
803    #[test]
804    fn rate_limiting() {
805        let CtxPair { mut core_ctx, mut bindings_ctx } = new_context();
806
807        // We can add up to the limit routes immediately (initial burst).
808        for i in 0..MAX_DISCOVERIES_PER_SECOND {
809            let route = make_route(i as u16);
810            discover_new_route(
811                &mut core_ctx,
812                &mut bindings_ctx,
813                route,
814                PROP1,
815                NonZeroNdpLifetime::Infinite,
816            );
817        }
818        assert_eq!(
819            core_ctx.state.route_table.route_table.len(),
820            MAX_DISCOVERIES_PER_SECOND as usize
821        );
822
823        // Rate limiting should prevent an additional route from being added.
824        let extra_route = make_route(MAX_DISCOVERIES_PER_SECOND as u16);
825        RouteDiscoveryHandler::update_route(
826            &mut core_ctx,
827            &mut bindings_ctx,
828            &FakeDeviceId,
829            extra_route,
830            PROP1,
831            Some(NonZeroNdpLifetime::Infinite),
832            &Default::default(),
833        );
834        assert_eq!(
835            core_ctx.state.route_table.route_table.len(),
836            MAX_DISCOVERIES_PER_SECOND as usize
837        );
838        assert!(!core_ctx.state.route_table.route_table.contains_key(&extra_route));
839
840        // Advance time by 1 second. The rate limit should be reset and
841        // the add should succeed.
842        bindings_ctx.timers.instant.sleep(core::time::Duration::from_secs(1));
843        discover_new_route(
844            &mut core_ctx,
845            &mut bindings_ctx,
846            extra_route,
847            PROP1,
848            NonZeroNdpLifetime::Infinite,
849        );
850        assert_eq!(
851            core_ctx.state.route_table.route_table.len(),
852            (MAX_DISCOVERIES_PER_SECOND + 1) as usize
853        );
854        assert!(core_ctx.state.route_table.route_table.contains_key(&extra_route));
855    }
856}