1use 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#[derive(Debug)]
26pub struct Ipv6RouteDiscoveryState<BT: Ipv6RouteDiscoveryBindingsTypes> {
27 routes: HashMap<Ipv6DiscoveredRoute, Ipv6DiscoveredRouteProperties>,
32 timers: LocalTimerHeap<Ipv6DiscoveredRoute, (), BT>,
33}
34
35impl<BT: Ipv6RouteDiscoveryBindingsTypes> Ipv6RouteDiscoveryState<BT> {
36 #[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 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#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
61pub struct Ipv6DiscoveredRoute {
62 pub subnet: Subnet<Ipv6Addr>,
64
65 pub gateway: Option<LinkLocalUnicastAddr<Ipv6Addr>>,
69}
70
71#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
73pub struct Ipv6DiscoveredRouteProperties {
74 pub route_preference: RoutePreference,
76}
77
78#[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#[derive(Copy, Clone, Debug, Eq, PartialEq, Derivative)]
92#[derivative(Default)]
93pub struct RouteDiscoveryConfiguration {
94 #[derivative(Default(value = "true"))]
96 pub allow_default_route: bool,
97}
98
99#[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 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
121pub trait Ipv6DiscoveredRoutesContext<BC>: DeviceIdContext<AnyDevice> {
126 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 fn del_discovered_ipv6_route(
138 &mut self,
139 bindings_ctx: &mut BC,
140 device_id: &Self::DeviceId,
141 route: Ipv6DiscoveredRoute,
142 );
143}
144
145pub trait Ipv6RouteDiscoveryContext<BT: Ipv6RouteDiscoveryBindingsTypes>:
147 DeviceIdContext<AnyDevice>
148{
149 type WithDiscoveredRoutesMutCtx<'a>: Ipv6DiscoveredRoutesContext<BT, DeviceId = Self::DeviceId>;
151
152 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
163pub trait Ipv6RouteDiscoveryBindingsTypes: TimerBindingsTypes + InstantBindingsTypes {}
165impl<BT> Ipv6RouteDiscoveryBindingsTypes for BT where BT: TimerBindingsTypes + InstantBindingsTypes {}
166
167pub trait Ipv6RouteDiscoveryBindingsContext:
169 Ipv6RouteDiscoveryBindingsTypes + TimerContext
170{
171}
172impl<BC> Ipv6RouteDiscoveryBindingsContext for BC where
173 BC: Ipv6RouteDiscoveryBindingsTypes + TimerContext
174{
175}
176
177pub trait RouteDiscoveryHandler<BC>: DeviceIdContext<AnyDevice> {
179 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 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 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 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 assert!(core_ctx.state.route_table.route_table.remove(&ROUTE1).is_some());
506 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}