blob: 147314e7e0f95d3a04a37bcc105ab50b0ebb9d9b [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! IPv6 Route Discovery as defined by [RFC 4861 section 6.3.4].
//!
//! [RFC 4861 section 6.3.4]: https://datatracker.ietf.org/doc/html/rfc4861#section-6.3.4
use core::hash::Hash;
use fakealloc::collections::HashSet;
use net_types::{
ip::{Ipv6, Ipv6Addr, Subnet},
LinkLocalUnicastAddr,
};
use packet_formats::icmp::ndp::NonZeroNdpLifetime;
use crate::{
context::{EventContext, TimerContext, TimerHandler},
ip::IpDeviceIdContext,
};
#[derive(Default)]
#[cfg_attr(test, derive(Debug, Eq, PartialEq))]
pub(super) struct Ipv6RouteDiscoveryState {
// The valid (non-zero lifetime) discovered routes.
//
// Routes with a finite lifetime must have a timer set; routes with an
// infinite lifetime must not.
routes: HashSet<Ipv6DiscoveredRoute>,
}
/// A discovered route.
#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
pub struct Ipv6DiscoveredRoute {
/// The destination subnet for the route.
pub subnet: Subnet<Ipv6Addr>,
/// The next-hop node for the route, if required.
///
/// `None` indicates that the subnet is on-link/directly-connected.
pub gateway: Option<LinkLocalUnicastAddr<Ipv6Addr>>,
}
/// The action taken on a route.
#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
pub enum Ipv6RouteDiscoverAction {
/// Indicates that a route was newly discovered.
Discovered,
/// Indicates that a previously discovered route was invalidated.
Invalidated,
}
/// An IPv6 route discovery event.
#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
pub struct Ipv6RouteDiscoveryEvent<DeviceId> {
/// The device ID for the event.
pub device_id: DeviceId,
/// The route triggering the event.
pub route: Ipv6DiscoveredRoute,
/// The change on the route.
pub action: Ipv6RouteDiscoverAction,
}
/// A timer ID for IPv6 route discovery.
#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
pub(crate) struct Ipv6DiscoveredRouteTimerId<DeviceId> {
device_id: DeviceId,
route: Ipv6DiscoveredRoute,
}
/// The state context provided to IPv6 route discovery.
pub(super) trait Ipv6RouteDiscoveryStateContext<C>: IpDeviceIdContext<Ipv6> {
/// Gets the route discovery state, mutably.
fn get_discovered_routes_mut(
&mut self,
device_id: Self::DeviceId,
) -> &mut Ipv6RouteDiscoveryState;
}
/// The non-synchronized execution context for IPv6 route discovery.
trait Ipv6RouteDiscoveryNonSyncContext<DeviceId>:
TimerContext<Ipv6DiscoveredRouteTimerId<DeviceId>> + EventContext<Ipv6RouteDiscoveryEvent<DeviceId>>
{
}
impl<
DeviceId,
C: TimerContext<Ipv6DiscoveredRouteTimerId<DeviceId>>
+ EventContext<Ipv6RouteDiscoveryEvent<DeviceId>>,
> Ipv6RouteDiscoveryNonSyncContext<DeviceId> for C
{
}
/// The execution context for IPv6 route discovery.
trait Ipv6RouteDiscoveryContext<C: Ipv6RouteDiscoveryNonSyncContext<Self::DeviceId>>:
Ipv6RouteDiscoveryStateContext<C>
{
}
impl<C: Ipv6RouteDiscoveryNonSyncContext<SC::DeviceId>, SC: Ipv6RouteDiscoveryStateContext<C>>
Ipv6RouteDiscoveryContext<C> for SC
{
}
/// An implementation of IPv6 route discovery.
pub(crate) trait RouteDiscoveryHandler<C>: IpDeviceIdContext<Ipv6> {
/// Handles an update affecting discovered routes.
///
/// A `None` value for `lifetime` indicates that the route is not valid and
/// must be invalidated if it has been discovered; a `Some(_)` value
/// indicates the new maximum lifetime that the route may be valid for
/// before being invalidated.
fn update_route(
&mut self,
ctx: &mut C,
device_id: Self::DeviceId,
route: Ipv6DiscoveredRoute,
lifetime: Option<NonZeroNdpLifetime>,
);
/// Invalidates all discovered routes.
fn invalidate_routes(&mut self, ctx: &mut C, device_id: Self::DeviceId);
}
impl<C: Ipv6RouteDiscoveryNonSyncContext<SC::DeviceId>, SC: Ipv6RouteDiscoveryContext<C>>
RouteDiscoveryHandler<C> for SC
{
fn update_route(
&mut self,
ctx: &mut C,
device_id: SC::DeviceId,
route: Ipv6DiscoveredRoute,
lifetime: Option<NonZeroNdpLifetime>,
) {
let Ipv6RouteDiscoveryState { routes } = self.get_discovered_routes_mut(device_id);
match lifetime {
Some(lifetime) => {
let newly_added = routes.insert(route.clone());
let timer_id = Ipv6DiscoveredRouteTimerId { device_id, route };
let prev_timer_fires_at: Option<C::Instant> = match lifetime {
NonZeroNdpLifetime::Finite(lifetime) => {
ctx.schedule_timer(lifetime.get(), timer_id)
}
// Routes with an infinite lifetime have no timers
//
// TODO(https://fxbug.dev/97751): Hold timers scheduled to
// fire at infinity.
NonZeroNdpLifetime::Infinite => ctx.cancel_timer(timer_id),
};
if newly_added {
if let Some(prev_timer_fires_at) = prev_timer_fires_at {
panic!("newly added timer ID {:?} should not have already been scheduled to fire at {:?}", timer_id, prev_timer_fires_at);
}
send_event(ctx, device_id, route, Ipv6RouteDiscoverAction::Discovered);
}
}
None => {
if routes.remove(&route) {
invalidate_route(ctx, device_id, route);
}
}
}
}
fn invalidate_routes(&mut self, ctx: &mut C, device_id: SC::DeviceId) {
let Ipv6RouteDiscoveryState { routes } = self.get_discovered_routes_mut(device_id);
for route in core::mem::take(routes).into_iter() {
invalidate_route(ctx, device_id, route);
}
}
}
impl<C: Ipv6RouteDiscoveryNonSyncContext<SC::DeviceId>, SC: Ipv6RouteDiscoveryContext<C>>
TimerHandler<C, Ipv6DiscoveredRouteTimerId<SC::DeviceId>> for SC
{
fn handle_timer(
&mut self,
ctx: &mut C,
Ipv6DiscoveredRouteTimerId { device_id, route }: Ipv6DiscoveredRouteTimerId<SC::DeviceId>,
) {
let Ipv6RouteDiscoveryState { routes } = self.get_discovered_routes_mut(device_id);
assert!(routes.remove(&route), "invalidated route should be discovered");
send_event(ctx, device_id, route, Ipv6RouteDiscoverAction::Invalidated);
}
}
fn invalidate_route<DeviceId: Copy, C: Ipv6RouteDiscoveryNonSyncContext<DeviceId>>(
ctx: &mut C,
device_id: DeviceId,
route: Ipv6DiscoveredRoute,
) {
// Routes with an infinite lifetime have no timers.
//
// TODO(https://fxbug.dev/97751): Hold timers scheduled to fire at infinity.
let _: Option<C::Instant> = ctx.cancel_timer(Ipv6DiscoveredRouteTimerId { device_id, route });
send_event(ctx, device_id, route, Ipv6RouteDiscoverAction::Invalidated);
}
fn send_event<DeviceId, C: Ipv6RouteDiscoveryNonSyncContext<DeviceId>>(
ctx: &mut C,
device_id: DeviceId,
route: Ipv6DiscoveredRoute,
action: Ipv6RouteDiscoverAction,
) {
ctx.on_event(Ipv6RouteDiscoveryEvent { device_id, route, action })
}
#[cfg(test)]
mod tests {
use core::{
convert::{AsMut, TryInto as _},
num::NonZeroU64,
time::Duration,
};
use net_types::{ip::Ip as _, Witness as _};
use packet::{BufferMut, InnerPacketBuilder as _, Serializer as _};
use packet_formats::{
icmp::{
ndp::{
options::{NdpOptionBuilder, PrefixInformation},
OptionSequenceBuilder, RouterAdvertisement,
},
IcmpPacketBuilder, IcmpUnusedCode,
},
ip::Ipv6Proto,
ipv6::Ipv6PacketBuilder,
utils::NonZeroDuration,
};
use super::*;
use crate::{
context::testutil::{
DummyCtx, DummyEventCtx, DummyInstant, DummyNonSyncCtx, DummySyncCtx,
DummyTimerCtxExt as _,
},
device::FrameDestination,
ip::{device::Ipv6DeviceTimerId, receive_ipv6_packet, DummyDeviceId, IPV6_DEFAULT_SUBNET},
testutil::{DummyEventDispatcherConfig, TestIpExt as _},
Ctx, DeviceId, TimerId, TimerIdInner,
};
#[derive(Default)]
struct MockIpv6RouteDiscoveryContext {
state: Ipv6RouteDiscoveryState,
}
type MockCtx = DummySyncCtx<MockIpv6RouteDiscoveryContext, (), DummyDeviceId>;
type MockNonSyncCtx = DummyNonSyncCtx<
Ipv6DiscoveredRouteTimerId<DummyDeviceId>,
Ipv6RouteDiscoveryEvent<DummyDeviceId>,
(),
>;
impl Ipv6RouteDiscoveryStateContext<MockNonSyncCtx> for MockCtx {
fn get_discovered_routes_mut(
&mut self,
DummyDeviceId: Self::DeviceId,
) -> &mut Ipv6RouteDiscoveryState {
let MockIpv6RouteDiscoveryContext { state } = self.get_mut();
state
}
}
const ROUTE1: Ipv6DiscoveredRoute =
Ipv6DiscoveredRoute { subnet: IPV6_DEFAULT_SUBNET, gateway: None };
const ROUTE2: Ipv6DiscoveredRoute = Ipv6DiscoveredRoute {
subnet: unsafe {
Subnet::new_unchecked(Ipv6Addr::new([0x2620, 0x1012, 0x1000, 0x5000, 0, 0, 0, 0]), 64)
},
gateway: None,
};
const ONE_SECOND: NonZeroDuration =
NonZeroDuration::from_nonzero_secs(const_unwrap::const_unwrap_option(NonZeroU64::new(1)));
const TWO_SECONDS: NonZeroDuration =
NonZeroDuration::from_nonzero_secs(const_unwrap::const_unwrap_option(NonZeroU64::new(2)));
#[test]
fn new_route_no_lifetime() {
let DummyCtx { mut sync_ctx, mut non_sync_ctx } =
DummyCtx::with_sync_ctx(MockCtx::default());
RouteDiscoveryHandler::update_route(
&mut sync_ctx,
&mut non_sync_ctx,
DummyDeviceId,
ROUTE1,
None,
);
assert_eq!(non_sync_ctx.take_events(), []);
non_sync_ctx.timer_ctx().assert_no_timers_installed();
}
fn discover_new_route(
sync_ctx: &mut MockCtx,
non_sync_ctx: &mut MockNonSyncCtx,
route: Ipv6DiscoveredRoute,
duration: NonZeroNdpLifetime,
) {
RouteDiscoveryHandler::update_route(
sync_ctx,
non_sync_ctx,
DummyDeviceId,
route,
Some(duration),
);
assert_eq!(
non_sync_ctx.take_events(),
[Ipv6RouteDiscoveryEvent {
device_id: DummyDeviceId,
route,
action: Ipv6RouteDiscoverAction::Discovered
}]
);
non_sync_ctx.timer_ctx().assert_some_timers_installed(
match duration {
NonZeroNdpLifetime::Finite(duration) => Some((
Ipv6DiscoveredRouteTimerId { device_id: DummyDeviceId, route },
DummyInstant::from(duration.get()),
)),
NonZeroNdpLifetime::Infinite => None,
}
.into_iter(),
)
}
fn assert_single_invalidation_timer(
sync_ctx: &mut MockCtx,
non_sync_ctx: &mut MockNonSyncCtx,
route: Ipv6DiscoveredRoute,
) {
assert_eq!(
non_sync_ctx.trigger_next_timer(sync_ctx, TimerHandler::handle_timer),
Some(Ipv6DiscoveredRouteTimerId { device_id: DummyDeviceId, route })
);
assert_eq!(
non_sync_ctx.take_events(),
[Ipv6RouteDiscoveryEvent {
device_id: DummyDeviceId,
route,
action: Ipv6RouteDiscoverAction::Invalidated
}]
);
non_sync_ctx.timer_ctx().assert_no_timers_installed();
}
#[test]
fn new_route_with_infinite_lifetime() {
let DummyCtx { mut sync_ctx, mut non_sync_ctx } =
DummyCtx::with_sync_ctx(MockCtx::default());
discover_new_route(&mut sync_ctx, &mut non_sync_ctx, ROUTE1, NonZeroNdpLifetime::Infinite);
non_sync_ctx.timer_ctx().assert_no_timers_installed();
}
#[test]
fn update_route_from_infinite_to_finite_lifetime() {
let DummyCtx { mut sync_ctx, mut non_sync_ctx } =
DummyCtx::with_sync_ctx(MockCtx::default());
discover_new_route(&mut sync_ctx, &mut non_sync_ctx, ROUTE1, NonZeroNdpLifetime::Infinite);
non_sync_ctx.timer_ctx().assert_no_timers_installed();
RouteDiscoveryHandler::update_route(
&mut sync_ctx,
&mut non_sync_ctx,
DummyDeviceId,
ROUTE1,
Some(NonZeroNdpLifetime::Finite(ONE_SECOND)),
);
assert_eq!(non_sync_ctx.take_events(), []);
non_sync_ctx.timer_ctx().assert_some_timers_installed([(
Ipv6DiscoveredRouteTimerId { device_id: DummyDeviceId, route: ROUTE1 },
DummyInstant::from(ONE_SECOND.get()),
)]);
assert_single_invalidation_timer(&mut sync_ctx, &mut non_sync_ctx, ROUTE1);
}
fn update_to_invalidate_check_invalidation(
sync_ctx: &mut MockCtx,
non_sync_ctx: &mut MockNonSyncCtx,
route: Ipv6DiscoveredRoute,
) {
RouteDiscoveryHandler::update_route(sync_ctx, non_sync_ctx, DummyDeviceId, ROUTE1, None);
assert_eq!(
non_sync_ctx.take_events(),
[Ipv6RouteDiscoveryEvent {
device_id: DummyDeviceId,
route,
action: Ipv6RouteDiscoverAction::Invalidated
}]
);
non_sync_ctx.timer_ctx().assert_no_timers_installed();
}
#[test]
fn invalidate_route_with_infinite_lifetime() {
let DummyCtx { mut sync_ctx, mut non_sync_ctx } =
DummyCtx::with_sync_ctx(MockCtx::default());
discover_new_route(&mut sync_ctx, &mut non_sync_ctx, ROUTE1, NonZeroNdpLifetime::Infinite);
non_sync_ctx.timer_ctx().assert_no_timers_installed();
update_to_invalidate_check_invalidation(&mut sync_ctx, &mut non_sync_ctx, ROUTE1);
}
#[test]
fn new_route_with_finite_lifetime() {
let DummyCtx { mut sync_ctx, mut non_sync_ctx } =
DummyCtx::with_sync_ctx(MockCtx::default());
discover_new_route(
&mut sync_ctx,
&mut non_sync_ctx,
ROUTE1,
NonZeroNdpLifetime::Finite(ONE_SECOND),
);
assert_single_invalidation_timer(&mut sync_ctx, &mut non_sync_ctx, ROUTE1);
}
#[test]
fn update_route_from_finite_to_infinite_lifetime() {
let DummyCtx { mut sync_ctx, mut non_sync_ctx } =
DummyCtx::with_sync_ctx(MockCtx::default());
discover_new_route(
&mut sync_ctx,
&mut non_sync_ctx,
ROUTE1,
NonZeroNdpLifetime::Finite(ONE_SECOND),
);
RouteDiscoveryHandler::update_route(
&mut sync_ctx,
&mut non_sync_ctx,
DummyDeviceId,
ROUTE1,
Some(NonZeroNdpLifetime::Infinite),
);
assert_eq!(non_sync_ctx.take_events(), []);
non_sync_ctx.timer_ctx().assert_no_timers_installed();
}
#[test]
fn update_route_from_finite_to_finite_lifetime() {
let DummyCtx { mut sync_ctx, mut non_sync_ctx } =
DummyCtx::with_sync_ctx(MockCtx::default());
discover_new_route(
&mut sync_ctx,
&mut non_sync_ctx,
ROUTE1,
NonZeroNdpLifetime::Finite(ONE_SECOND),
);
RouteDiscoveryHandler::update_route(
&mut sync_ctx,
&mut non_sync_ctx,
DummyDeviceId,
ROUTE1,
Some(NonZeroNdpLifetime::Finite(TWO_SECONDS)),
);
assert_eq!(non_sync_ctx.take_events(), []);
non_sync_ctx.timer_ctx().assert_timers_installed([(
Ipv6DiscoveredRouteTimerId { device_id: DummyDeviceId, route: ROUTE1 },
DummyInstant::from(TWO_SECONDS.get()),
)]);
assert_single_invalidation_timer(&mut sync_ctx, &mut non_sync_ctx, ROUTE1);
}
#[test]
fn invalidate_route_with_finite_lifetime() {
let DummyCtx { mut sync_ctx, mut non_sync_ctx } =
DummyCtx::with_sync_ctx(MockCtx::default());
discover_new_route(
&mut sync_ctx,
&mut non_sync_ctx,
ROUTE1,
NonZeroNdpLifetime::Finite(ONE_SECOND),
);
update_to_invalidate_check_invalidation(&mut sync_ctx, &mut non_sync_ctx, ROUTE1);
}
#[test]
fn invalidate_all_routes() {
let DummyCtx { mut sync_ctx, mut non_sync_ctx } =
DummyCtx::with_sync_ctx(MockCtx::default());
discover_new_route(
&mut sync_ctx,
&mut non_sync_ctx,
ROUTE1,
NonZeroNdpLifetime::Finite(ONE_SECOND),
);
discover_new_route(
&mut sync_ctx,
&mut non_sync_ctx,
ROUTE2,
NonZeroNdpLifetime::Finite(TWO_SECONDS),
);
RouteDiscoveryHandler::invalidate_routes(&mut sync_ctx, &mut non_sync_ctx, DummyDeviceId);
assert_eq!(
non_sync_ctx.take_events().into_iter().collect::<HashSet<_>>(),
HashSet::from([
Ipv6RouteDiscoveryEvent {
device_id: DummyDeviceId,
route: ROUTE1,
action: Ipv6RouteDiscoverAction::Invalidated
},
Ipv6RouteDiscoveryEvent {
device_id: DummyDeviceId,
route: ROUTE2,
action: Ipv6RouteDiscoverAction::Invalidated
},
])
);
non_sync_ctx.timer_ctx().assert_no_timers_installed();
}
fn router_advertisement_buf(
src_ip: LinkLocalUnicastAddr<Ipv6Addr>,
router_lifetime_secs: u16,
on_link_prefix: Subnet<Ipv6Addr>,
on_link_prefix_flag: bool,
on_link_prefix_valid_lifetime_secs: u32,
) -> impl BufferMut {
let src_ip: Ipv6Addr = src_ip.get();
let dst_ip = Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS.get();
let p = PrefixInformation::new(
on_link_prefix.prefix(),
on_link_prefix_flag,
false, /* autonomous_address_configuration_flag */
on_link_prefix_valid_lifetime_secs,
0, /* preferred_lifetime */
on_link_prefix.network(),
);
let options = &[NdpOptionBuilder::PrefixInformation(p)];
OptionSequenceBuilder::new(options.iter())
.into_serializer()
.encapsulate(IcmpPacketBuilder::<Ipv6, &[u8], _>::new(
src_ip,
dst_ip,
IcmpUnusedCode,
RouterAdvertisement::new(
0, /* hop_limit */
false, /* managed_flag */
false, /* other_config_flag */
router_lifetime_secs,
0, /* reachable_time */
0, /* retransmit_timer */
),
))
.encapsulate(Ipv6PacketBuilder::new(
src_ip,
dst_ip,
crate::ip::device::integration::REQUIRED_NDP_IP_PACKET_HOP_LIMIT,
Ipv6Proto::Icmpv6,
))
.serialize_vec_outer()
.unwrap()
.unwrap_b()
}
fn setup() -> (crate::testutil::DummyCtx, DeviceId, DummyEventDispatcherConfig<Ipv6Addr>) {
let DummyEventDispatcherConfig {
local_mac,
remote_mac: _,
local_ip: _,
remote_ip: _,
subnet: _,
} = Ipv6::DUMMY_CONFIG;
let mut ctx = crate::testutil::DummyCtx::default();
let Ctx { sync_ctx, non_sync_ctx } = &mut ctx;
let device_id =
sync_ctx.state.device.add_ethernet_device(local_mac, Ipv6::MINIMUM_LINK_MTU.into());
crate::ip::device::update_ipv6_configuration(sync_ctx, non_sync_ctx, device_id, |config| {
config.ip_config.ip_enabled = true;
});
non_sync_ctx.timer_ctx().assert_no_timers_installed();
(ctx, device_id, Ipv6::DUMMY_CONFIG)
}
fn as_secs(d: NonZeroDuration) -> u16 {
d.get().as_secs().try_into().unwrap()
}
fn timer_id(route: Ipv6DiscoveredRoute, device_id: DeviceId) -> TimerId {
TimerId(TimerIdInner::Ipv6Device(Ipv6DeviceTimerId::RouteDiscovery(
Ipv6DiscoveredRouteTimerId { device_id, route },
)))
}
#[test]
fn discovery_integration() {
let (
mut ctx,
device_id,
DummyEventDispatcherConfig {
local_mac: _,
remote_mac,
local_ip: _,
remote_ip: _,
subnet,
},
) = setup();
let Ctx { sync_ctx, non_sync_ctx } = &mut ctx;
let src_ip = remote_mac.to_ipv6_link_local().addr();
let buf = |router_lifetime_secs, on_link_prefix_flag, prefix_valid_lifetime_secs| {
router_advertisement_buf(
src_ip,
router_lifetime_secs,
subnet,
on_link_prefix_flag,
prefix_valid_lifetime_secs,
)
};
let timer_id = |route| timer_id(route, device_id);
let check_event = |non_sync_ctx: &mut crate::testutil::DummyNonSyncCtx,
event: Option<Ipv6RouteDiscoveryEvent<_>>| {
assert_eq!(
AsMut::<DummyEventCtx<_>>::as_mut(non_sync_ctx)
.take()
.into_iter()
.collect::<HashSet<_>>(),
event.map_or_else(HashSet::default, |e| HashSet::from([e.into()])),
);
};
// Do nothing as router with no valid lifetime has not been discovered
// yet and prefix does not make on-link determination.
receive_ipv6_packet(
sync_ctx,
non_sync_ctx,
device_id,
FrameDestination::Unicast,
buf(0, false, as_secs(ONE_SECOND).into()),
);
check_event(non_sync_ctx, None);
non_sync_ctx.timer_ctx().assert_no_timers_installed();
// Discover a default router only as on-link prefix has no valid
// lifetime.
receive_ipv6_packet(
sync_ctx,
non_sync_ctx,
device_id,
FrameDestination::Unicast,
buf(as_secs(ONE_SECOND), true, 0),
);
let gateway_route =
Ipv6DiscoveredRoute { subnet: IPV6_DEFAULT_SUBNET, gateway: Some(src_ip) };
check_event(
non_sync_ctx,
Some(Ipv6RouteDiscoveryEvent {
device_id,
route: gateway_route,
action: Ipv6RouteDiscoverAction::Discovered,
}),
);
non_sync_ctx.timer_ctx().assert_timers_installed([(
timer_id(gateway_route),
DummyInstant::from(ONE_SECOND.get()),
)]);
// Discover an on-link prefix and update valid lifetime for default
// router.
receive_ipv6_packet(
sync_ctx,
non_sync_ctx,
device_id,
FrameDestination::Unicast,
buf(as_secs(TWO_SECONDS), true, as_secs(ONE_SECOND).into()),
);
let on_link_route = Ipv6DiscoveredRoute { subnet, gateway: None };
check_event(
non_sync_ctx,
Some(Ipv6RouteDiscoveryEvent {
device_id,
route: on_link_route,
action: Ipv6RouteDiscoverAction::Discovered,
}),
);
non_sync_ctx.timer_ctx().assert_timers_installed([
(timer_id(gateway_route), DummyInstant::from(TWO_SECONDS.get())),
(timer_id(on_link_route), DummyInstant::from(ONE_SECOND.get())),
]);
// Invalidate default router and update valid lifetime for on-link
// prefix.
receive_ipv6_packet(
sync_ctx,
non_sync_ctx,
device_id,
FrameDestination::Unicast,
buf(0, true, as_secs(TWO_SECONDS).into()),
);
check_event(
non_sync_ctx,
Some(Ipv6RouteDiscoveryEvent {
device_id,
route: gateway_route,
action: Ipv6RouteDiscoverAction::Invalidated,
}),
);
non_sync_ctx.timer_ctx().assert_timers_installed([(
timer_id(on_link_route),
DummyInstant::from(TWO_SECONDS.get()),
)]);
// Do nothing as prefix does not make on-link determination and router
// with valid lifetime is not discovered.
receive_ipv6_packet(
sync_ctx,
non_sync_ctx,
device_id,
FrameDestination::Unicast,
buf(0, false, 0),
);
check_event(non_sync_ctx, None);
non_sync_ctx.timer_ctx().assert_timers_installed([(
timer_id(on_link_route),
DummyInstant::from(TWO_SECONDS.get()),
)]);
// Invalidate on-link prefix.
receive_ipv6_packet(
sync_ctx,
non_sync_ctx,
device_id,
FrameDestination::Unicast,
buf(0, true, 0),
);
check_event(
non_sync_ctx,
Some(Ipv6RouteDiscoveryEvent {
device_id,
route: on_link_route,
action: Ipv6RouteDiscoverAction::Invalidated,
}),
);
non_sync_ctx.timer_ctx().assert_no_timers_installed();
}
#[test]
fn discovery_integration_infinite_to_finite_to_infinite_lifetime() {
let (
mut ctx,
device_id,
DummyEventDispatcherConfig {
local_mac: _,
remote_mac,
local_ip: _,
remote_ip: _,
subnet,
},
) = setup();
let Ctx { sync_ctx, non_sync_ctx } = &mut ctx;
let src_ip = remote_mac.to_ipv6_link_local().addr();
let buf = |router_lifetime_secs, on_link_prefix_flag, prefix_valid_lifetime_secs| {
router_advertisement_buf(
src_ip,
router_lifetime_secs,
subnet,
on_link_prefix_flag,
prefix_valid_lifetime_secs,
)
};
let timer_id = |route| timer_id(route, device_id);
let gateway_route =
Ipv6DiscoveredRoute { subnet: IPV6_DEFAULT_SUBNET, gateway: Some(src_ip) };
let on_link_route = Ipv6DiscoveredRoute { subnet, gateway: None };
// Router with finite lifetime and on-link prefix with infinite
// lifetime.
let router_lifetime_secs = u16::MAX;
let prefix_lifetime_secs = u32::MAX;
receive_ipv6_packet(
sync_ctx,
non_sync_ctx,
device_id,
FrameDestination::Unicast,
buf(router_lifetime_secs, true, prefix_lifetime_secs),
);
assert_eq!(
AsMut::<DummyEventCtx<_>>::as_mut(non_sync_ctx)
.take()
.into_iter()
.collect::<HashSet<_>>(),
HashSet::from([
Ipv6RouteDiscoveryEvent {
device_id,
route: gateway_route,
action: Ipv6RouteDiscoverAction::Discovered,
}
.into(),
Ipv6RouteDiscoveryEvent {
device_id,
route: on_link_route,
action: Ipv6RouteDiscoverAction::Discovered,
}
.into(),
]),
);
non_sync_ctx.timer_ctx().assert_timers_installed([(
timer_id(gateway_route),
DummyInstant::from(Duration::from_secs(router_lifetime_secs.into())),
)]);
// Router and prefix with finite lifetimes.
let router_lifetime_secs = u16::MAX - 1;
let prefix_lifetime_secs = u32::MAX - 1;
receive_ipv6_packet(
sync_ctx,
non_sync_ctx,
device_id,
FrameDestination::Unicast,
buf(router_lifetime_secs, true, prefix_lifetime_secs),
);
non_sync_ctx.timer_ctx().assert_timers_installed([
(
timer_id(gateway_route),
DummyInstant::from(Duration::from_secs(router_lifetime_secs.into())),
),
(
timer_id(on_link_route),
DummyInstant::from(Duration::from_secs(prefix_lifetime_secs.into())),
),
]);
// Router with finite lifetime and on-link prefix with infinite
// lifetime.
let router_lifetime_secs = u16::MAX;
let prefix_lifetime_secs = u32::MAX;
receive_ipv6_packet(
sync_ctx,
non_sync_ctx,
device_id,
FrameDestination::Unicast,
buf(router_lifetime_secs, true, prefix_lifetime_secs),
);
non_sync_ctx.timer_ctx().assert_timers_installed([(
timer_id(gateway_route),
DummyInstant::from(Duration::from_secs(router_lifetime_secs.into())),
)]);
// Router and prefix invalidated.
let router_lifetime_secs = 0;
let prefix_lifetime_secs = 0;
receive_ipv6_packet(
sync_ctx,
non_sync_ctx,
device_id,
FrameDestination::Unicast,
buf(router_lifetime_secs, true, prefix_lifetime_secs),
);
assert_eq!(
AsMut::<DummyEventCtx<_>>::as_mut(non_sync_ctx)
.take()
.into_iter()
.collect::<HashSet<_>>(),
HashSet::from([
Ipv6RouteDiscoveryEvent {
device_id,
route: gateway_route,
action: Ipv6RouteDiscoverAction::Invalidated,
}
.into(),
Ipv6RouteDiscoveryEvent {
device_id,
route: on_link_route,
action: Ipv6RouteDiscoverAction::Invalidated,
}
.into(),
]),
);
non_sync_ctx.timer_ctx().assert_no_timers_installed();
}
#[test]
fn flush_routes_on_interface_disabled_integration() {
let (
mut ctx,
device_id,
DummyEventDispatcherConfig {
local_mac: _,
remote_mac,
local_ip: _,
remote_ip: _,
subnet,
},
) = setup();
let Ctx { sync_ctx, non_sync_ctx } = &mut ctx;
let src_ip = remote_mac.to_ipv6_link_local().addr();
let gateway_route =
Ipv6DiscoveredRoute { subnet: IPV6_DEFAULT_SUBNET, gateway: Some(src_ip) };
let on_link_route = Ipv6DiscoveredRoute { subnet, gateway: None };
let timer_id = |route| timer_id(route, device_id);
// Discover both an on-link prefix and default router.
receive_ipv6_packet(
sync_ctx,
non_sync_ctx,
device_id,
FrameDestination::Unicast,
router_advertisement_buf(
src_ip,
as_secs(TWO_SECONDS),
subnet,
true,
as_secs(ONE_SECOND).into(),
),
);
assert_eq!(
AsMut::<DummyEventCtx<_>>::as_mut(non_sync_ctx)
.take()
.into_iter()
.collect::<HashSet<_>>(),
HashSet::from([
Ipv6RouteDiscoveryEvent {
device_id,
route: gateway_route,
action: Ipv6RouteDiscoverAction::Discovered,
}
.into(),
Ipv6RouteDiscoveryEvent {
device_id,
route: on_link_route,
action: Ipv6RouteDiscoverAction::Discovered,
}
.into(),
]),
);
non_sync_ctx.timer_ctx().assert_timers_installed([
(timer_id(gateway_route), DummyInstant::from(TWO_SECONDS.get())),
(timer_id(on_link_route), DummyInstant::from(ONE_SECOND.get())),
]);
// Disable the interface.
crate::ip::device::update_ipv6_configuration(sync_ctx, non_sync_ctx, device_id, |config| {
config.ip_config.ip_enabled = false;
});
assert_eq!(
AsMut::<DummyEventCtx<_>>::as_mut(non_sync_ctx)
.take()
.into_iter()
.collect::<HashSet<_>>(),
HashSet::from([
Ipv6RouteDiscoveryEvent {
device_id,
route: gateway_route,
action: Ipv6RouteDiscoverAction::Invalidated,
}
.into(),
Ipv6RouteDiscoveryEvent {
device_id,
route: on_link_route,
action: Ipv6RouteDiscoverAction::Invalidated,
}
.into(),
]),
);
non_sync_ctx.timer_ctx().assert_no_timers_installed();
}
}