blob: 07d94d2484f4143aeb9edffb4ca942662d9552f7 [file] [log] [blame]
// Copyright 2019 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.
//! The Neighbor Discovery Protocol (NDP).
//!
//! Neighbor Discovery for IPv6 as defined in [RFC 4861] defines mechanisms for
//! solving the following problems:
//! - Router Discovery
//! - Prefix Discovery
//! - Parameter Discovery
//! - Address Autoconfiguration
//! - Address resolution
//! - Next-hop determination
//! - Neighbor Unreachability Detection
//! - Duplicate Address Detection
//! - Redirect
//!
//! [RFC 4861]: https://tools.ietf.org/html/rfc4861
use alloc::collections::HashMap;
use core::{fmt::Debug, marker::PhantomData, num::NonZeroU8, time::Duration};
use assert_matches::assert_matches;
use log::{debug, trace};
use net_types::{
ip::{Ip, Ipv6, Ipv6Addr, Ipv6Scope, Ipv6SourceAddr},
LinkLocalAddress, LinkLocalUnicastAddr, MulticastAddr, MulticastAddress, ScopeableAddress,
SpecifiedAddr, UnicastAddr, Witness,
};
use nonzero_ext::nonzero;
use packet::{EmptyBuf, InnerPacketBuilder, Serializer};
use packet_formats::{
icmp::{
ndp::{
self,
options::{NdpOption, NdpOptionBuilder},
NdpPacket, NeighborAdvertisement, NeighborSolicitation, Options, RouterSolicitation,
},
IcmpMessage, IcmpPacket, IcmpPacketBuilder, IcmpUnusedCode,
},
ip::Ipv6Proto,
ipv6::Ipv6PacketBuilder,
utils::NonZeroDuration,
};
use rand::{thread_rng, Rng};
use zerocopy::ByteSlice;
use crate::{
context::{CounterContext, StateContext, TimerContext},
device::{
link::{LinkAddress, LinkDevice},
DeviceIdContext,
},
ip::device::state::{AddressState, IpDeviceState, SlaacConfig},
};
/// The IP packet hop limit for all NDP packets.
///
/// See [RFC 4861 section 4.1], [RFC 4861 section 4.2], [RFC 4861 section 4.2],
/// [RFC 4861 section 4.3], [RFC 4861 section 4.4], and [RFC 4861 section 4.5]
/// for more information.
///
/// [RFC 4861 section 4.1]: https://tools.ietf.org/html/rfc4861#section-4.1
/// [RFC 4861 section 4.2]: https://tools.ietf.org/html/rfc4861#section-4.2
/// [RFC 4861 section 4.3]: https://tools.ietf.org/html/rfc4861#section-4.3
/// [RFC 4861 section 4.4]: https://tools.ietf.org/html/rfc4861#section-4.4
/// [RFC 4861 section 4.5]: https://tools.ietf.org/html/rfc4861#section-4.5
const REQUIRED_NDP_IP_PACKET_HOP_LIMIT: u8 = 255;
// Node Constants
/// The default value for the default hop limit to be used when sending IP
/// packets.
pub(crate) const HOP_LIMIT_DEFAULT: NonZeroU8 = nonzero!(64u8);
/// The default value for *BaseReachableTime* as defined in [RFC 4861 section
/// 10].
///
/// [RFC 4861 section 10]: https://tools.ietf.org/html/rfc4861#section-10
const REACHABLE_TIME_DEFAULT: Duration = Duration::from_secs(30);
/// The maximum number of multicast solicitations as defined in [RFC 4861
/// section 10].
///
/// [RFC 4861 section 10]: https://tools.ietf.org/html/rfc4861#section-10
const MAX_MULTICAST_SOLICIT: u8 = 3;
// NOTE(joshlf): The `LinkDevice` parameter may seem unnecessary. We only ever
// use the associated `Address` type, so why not just take that directly? By the
// same token, why have it as a parameter on `NdpState` and `NdpTimerId`? The
// answer is that, if we did, there would be no way to distinguish between
// different link device protocols that all happened to use the same hardware
// addressing scheme.
//
// Consider that the way that we implement context traits is via blanket impls.
// Even though each module's code _feels_ isolated from the rest of the system,
// in reality, all context impls end up on the same context type. In particular,
// all impls are of the form `impl<C: SomeContextTrait> SomeOtherContextTrait
// for C`. The `C` is the same throughout the whole stack.
//
// Thus, for two different link device protocols with the same hardware address
// type, if we used an `LinkAddress` parameter rather than a `LinkDevice`
// parameter, the `NdpContext` impls would conflict (in fact, the `StateContext`
// and `TimerContext` impls would conflict for similar reasons).
/// An NDP handler for NDP Events.
///
/// `NdpHandler<D>` is implemented for any type which implements
/// [`NdpContext<D>`], and it can also be mocked for use in testing.
pub(crate) trait NdpHandler<D: LinkDevice, C>: DeviceIdContext<D> {
/// Cleans up state associated with the device.
///
/// The contract is that after `deinitialize` is called, nothing else should
/// be done with the state.
fn deinitialize(&mut self, ctx: &mut C, device_id: Self::DeviceId);
/// Look up the link layer address.
///
/// Begins the address resolution process if the link layer address for
/// `lookup_addr` is not already known.
fn lookup(
&mut self,
ctx: &mut C,
device_id: Self::DeviceId,
lookup_addr: UnicastAddr<Ipv6Addr>,
) -> Option<D::Address>;
/// Handles a timer firing.
fn handle_timer(&mut self, ctx: &mut C, id: NdpTimerId<D, Self::DeviceId>);
/// Insert a neighbor to the known neighbors table.
///
/// This method only gets called when testing to force a neighbor so link
/// address lookups completes immediately without doing address resolution.
#[cfg(test)]
fn insert_static_neighbor(
&mut self,
ctx: &mut C,
device_id: Self::DeviceId,
net: UnicastAddr<Ipv6Addr>,
hw: D::Address,
);
}
impl<D: LinkDevice, C: NdpNonSyncContext<D, SC::DeviceId>, SC: NdpContext<D, C>> NdpHandler<D, C>
for SC
where
D::Address: for<'a> From<&'a MulticastAddr<Ipv6Addr>>,
{
fn deinitialize(&mut self, ctx: &mut C, device_id: Self::DeviceId) {
deinitialize(self, ctx, device_id)
}
fn lookup(
&mut self,
ctx: &mut C,
device_id: Self::DeviceId,
lookup_addr: UnicastAddr<Ipv6Addr>,
) -> Option<D::Address> {
lookup(self, ctx, device_id, lookup_addr)
}
fn handle_timer(&mut self, ctx: &mut C, id: NdpTimerId<D, Self::DeviceId>) {
handle_timer(self, ctx, id)
}
#[cfg(test)]
fn insert_static_neighbor(
&mut self,
ctx: &mut C,
device_id: Self::DeviceId,
net: UnicastAddr<Ipv6Addr>,
hw: D::Address,
) {
insert_neighbor(self, ctx, device_id, net, hw)
}
}
/// The non-synchronized execution context for NDP.
pub(crate) trait NdpNonSyncContext<D: LinkDevice, DeviceId>:
TimerContext<NdpTimerId<D, DeviceId>>
{
}
impl<DeviceId, D: LinkDevice, C: TimerContext<NdpTimerId<D, DeviceId>>>
NdpNonSyncContext<D, DeviceId> for C
{
}
/// The execution context for an NDP device.
pub(crate) trait NdpContext<D: LinkDevice, C: NdpNonSyncContext<D, Self::DeviceId>>:
Sized
+ DeviceIdContext<D>
+ CounterContext
+ StateContext<C, NdpState<D>, <Self as DeviceIdContext<D>>::DeviceId>
{
/// Returns the NDP retransmission timer configured on the device.
// TODO(https://fxbug.dev/72378): Remove this method once NUD operates in
// L3.
fn get_retrans_timer(&self, device_id: Self::DeviceId) -> NonZeroDuration;
/// Get the link layer address for a device.
fn get_link_layer_addr(&self, device_id: Self::DeviceId) -> UnicastAddr<D::Address>;
/// Gets the IP state for this device.
fn get_ip_device_state(&self, device_id: Self::DeviceId) -> &IpDeviceState<C::Instant, Ipv6>;
/// Gets the IP state for this device mutably.
fn get_ip_device_state_mut(
&mut self,
device_id: Self::DeviceId,
) -> &mut IpDeviceState<C::Instant, Ipv6>;
/// Gets a non-tentative global or link-local address.
///
/// Returns a non-tentative global address, if it is available. Otherwise,
/// returns a link-local address, if it is available. Otherwise, returns
/// `None`.
fn get_non_tentative_global_or_link_local_addr(
&self,
device_id: Self::DeviceId,
) -> Option<UnicastAddr<Ipv6Addr>> {
let mut non_tentative_addrs = self
.get_ip_device_state(device_id)
.iter_addrs()
.filter(|entry| match entry.state {
AddressState::Assigned => true,
AddressState::Tentative { dad_transmits_remaining: _ } => false,
})
.map(|entry| entry.addr_sub().addr());
non_tentative_addrs
.clone()
.find(|addr| addr.scope() == Ipv6Scope::Global)
.or_else(|| non_tentative_addrs.find(|addr| addr.is_link_local()))
}
// TODO(joshlf): Use `FrameContext` instead.
/// Send a packet in a device layer frame.
///
/// `send_ipv6_frame` accepts a device ID, a next hop IP address, and a
/// `Serializer`. Implementers must resolve the destination link-layer
/// address from the provided `next_hop` IPv6 address.
///
/// # Panics
///
/// May panic if `device_id` is not initialized. See
/// [`crate::device::testutil::enable_device`] for more information.
fn send_ipv6_frame<S: Serializer<Buffer = EmptyBuf>>(
&mut self,
ctx: &mut C,
device_id: Self::DeviceId,
next_hop: SpecifiedAddr<Ipv6Addr>,
body: S,
) -> Result<(), S>;
/// Notifies device layer that the link-layer address for the neighbor in
/// `address` has been resolved to `link_address`.
///
/// Implementers may use this signal to dispatch any packets that were
/// queued waiting for address resolution.
fn address_resolved(
&mut self,
ctx: &mut C,
device_id: Self::DeviceId,
address: &UnicastAddr<Ipv6Addr>,
link_address: D::Address,
);
/// Notifies the device layer that the link-layer address resolution for the
/// neighbor in `address` failed.
fn address_resolution_failed(
&mut self,
ctx: &mut C,
device_id: Self::DeviceId,
address: &UnicastAddr<Ipv6Addr>,
);
/// Set Link MTU.
///
/// `set_mtu` is used when a host receives a Router Advertisement with the
/// MTU option.
///
/// `set_mtu` MAY set the device's new MTU to a value less than `mtu` if the
/// device does not support using `mtu` as its new MTU. `set_mtu` MUST NOT
/// use a new MTU value that is greater than `mtu`.
///
/// See [RFC 4861 section 6.3.4] for more information.
///
/// # Panics
///
/// `set_mtu` is allowed to panic if `mtu` is less than the IPv6 minimum
/// link MTU, [`Ipv6::MINIMUM_LINK_MTU`].
///
/// [RFC 4861 section 6.3.4]: https://tools.ietf.org/html/rfc4861#section-6.3.4
fn set_mtu(&mut self, ctx: &mut C, device_id: Self::DeviceId, mtu: u32);
/// Can `device_id` route IP packets not destined for it?
///
/// If `is_router` returns `true`, we know that both the `device_id` and the
/// netstack (`ctx`) have routing enabled; if `is_router` returns false,
/// either `device_id` or the netstack (`ctx`) has routing disabled.
fn is_router_device(&self, device_id: Self::DeviceId) -> bool {
self.get_ip_device_state(device_id).routing_enabled
}
}
fn deinitialize<D: LinkDevice, C: NdpNonSyncContext<D, SC::DeviceId>, SC: NdpContext<D, C>>(
_sync_ctx: &mut SC,
ctx: &mut C,
device_id: SC::DeviceId,
) {
// Remove all timers associated with the device
ctx.cancel_timers_with(|timer_id| timer_id.get_device_id() == device_id);
// TODO(rheacock): Send any immediate packets, and potentially flag the
// state as uninitialized?
}
/// The state associated with an instance of the Neighbor Discovery Protocol
/// (NDP).
///
/// Each device will contain an `NdpState` object to keep track of discovery
/// operations.
pub(crate) struct NdpState<D: LinkDevice> {
//
// NDP operation data structures.
//
/// List of neighbors.
neighbors: NeighborTable<D::Address>,
//
// Interface parameters learned from Router Advertisements.
//
// See RFC 4861 section 6.3.2.
//
/// A base value used for computing the random `reachable_time` value.
///
/// Default: `REACHABLE_TIME_DEFAULT`.
///
/// See BaseReachableTime in [RFC 4861 section 6.3.2] for more details.
///
/// [RFC 4861 section 6.3.2]: https://tools.ietf.org/html/rfc4861#section-6.3.2
base_reachable_time: Duration,
/// The time a neighbor is considered reachable after receiving a
/// reachability confirmation.
///
/// This value should be uniformly distributed between MIN_RANDOM_FACTOR
/// (0.5) and MAX_RANDOM_FACTOR (1.5) times `base_reachable_time`
/// milliseconds. A new random should be calculated when
/// `base_reachable_time` changes (due to Router Advertisements) or at least
/// every few hours even if no Router Advertisements are received.
///
/// See ReachableTime in [RFC 4861 section 6.3.2] for more details.
///
/// [RFC 4861 section 6.3.2]: https://tools.ietf.org/html/rfc4861#section-6.3.2
// TODO(fxbug.dev/69490): Remove this or explain why it's here.
#[allow(dead_code)]
reachable_time: Duration,
}
impl<D: LinkDevice> NdpState<D> {
pub(crate) fn new() -> Self {
let mut ret = Self {
neighbors: NeighborTable::default(),
base_reachable_time: REACHABLE_TIME_DEFAULT,
reachable_time: REACHABLE_TIME_DEFAULT,
};
// Calculate an actually random `reachable_time` value instead of using
// a constant.
ret.recalculate_reachable_time();
ret
}
// Interface parameters learned from Router Advertisements.
/// Set the base value used for computing the random `reachable_time` value.
///
/// This method will also recalculate the `reachable_time` if the new base
/// value is different from the current value. If the new base value is the
/// same as the current value, `set_base_reachable_time` does nothing.
pub(crate) fn set_base_reachable_time(&mut self, v: Duration) {
assert_ne!(Duration::new(0, 0), v);
if self.base_reachable_time == v {
return;
}
self.base_reachable_time = v;
self.recalculate_reachable_time();
}
/// Recalculate `reachable_time`.
///
/// The new `reachable_time` will be a random value between a factor of
/// MIN_RANDOM_FACTOR and MAX_RANDOM_FACTOR, as per [RFC 4861 section
/// 6.3.2].
///
/// [RFC 4861 section 6.3.2]: https://tools.ietf.org/html/rfc4861#section-6.3.2
pub(crate) fn recalculate_reachable_time(&mut self) {
let base = self.base_reachable_time;
let half = base / 2;
let reachable_time = half + thread_rng().gen_range(Duration::new(0, 0)..base);
// Random value must between a factor of MIN_RANDOM_FACTOR (0.5) and
// MAX_RANDOM_FACTOR (1.5), as per RFC 4861 section 6.3.2.
assert!((reachable_time >= half) && (reachable_time <= (base + half)));
self.reachable_time = reachable_time;
}
}
/// The identifier for timer events in NDP operations.
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
pub(crate) struct NdpTimerId<D: LinkDevice, DeviceId> {
device_id: DeviceId,
inner: InnerNdpTimerId,
_marker: PhantomData<D>,
}
/// The types of NDP timers.
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
pub(crate) enum InnerNdpTimerId {
/// This is used to retry sending Neighbor Discovery Protocol requests.
LinkAddressResolution { neighbor_addr: UnicastAddr<Ipv6Addr> },
}
impl<D: LinkDevice, DeviceId: Copy> NdpTimerId<D, DeviceId> {
fn new(device_id: DeviceId, inner: InnerNdpTimerId) -> NdpTimerId<D, DeviceId> {
NdpTimerId { device_id, inner, _marker: PhantomData }
}
/// Creates a new `NdpTimerId` wrapped inside a `TimerId` with the provided
/// `device_id` and `neighbor_addr`.
pub(crate) fn new_link_address_resolution(
device_id: DeviceId,
neighbor_addr: UnicastAddr<Ipv6Addr>,
) -> NdpTimerId<D, DeviceId> {
NdpTimerId::new(device_id, InnerNdpTimerId::LinkAddressResolution { neighbor_addr })
}
pub(crate) fn get_device_id(&self) -> DeviceId {
self.device_id
}
}
fn handle_timer<D: LinkDevice, C: NdpNonSyncContext<D, SC::DeviceId>, SC: NdpContext<D, C>>(
sync_ctx: &mut SC,
ctx: &mut C,
id: NdpTimerId<D, SC::DeviceId>,
) {
match id.inner {
InnerNdpTimerId::LinkAddressResolution { neighbor_addr } => {
let ndp_state = sync_ctx.get_state_mut_with(id.device_id);
if let Some(NeighborState {
state: NeighborEntryState::Incomplete { transmit_counter },
..
}) = ndp_state.neighbors.get_neighbor_state_mut(&neighbor_addr)
{
if *transmit_counter < MAX_MULTICAST_SOLICIT {
// Increase the transmit counter and send the solicitation
// again
*transmit_counter += 1;
send_neighbor_solicitation(sync_ctx, ctx, id.device_id, neighbor_addr);
let retrans_timer = sync_ctx.get_retrans_timer(id.device_id);
let _: Option<C::Instant> = ctx.schedule_timer(
retrans_timer.get(),
NdpTimerId::new_link_address_resolution(id.device_id, neighbor_addr).into(),
);
} else {
// To make sure we don't get stuck in this neighbor
// unreachable state forever, remove the neighbor from the
// database:
ndp_state.neighbors.delete_neighbor_state(&neighbor_addr);
sync_ctx.increment_counter("ndp::neighbor_solicitation_timer");
sync_ctx.address_resolution_failed(ctx, id.device_id, &neighbor_addr);
}
} else {
unreachable!("handle_timer: timer for neighbor {:?} address resolution should not exist if no entry exists", neighbor_addr);
}
}
}
}
fn lookup<D: LinkDevice, C: NdpNonSyncContext<D, SC::DeviceId>, SC: NdpContext<D, C>>(
sync_ctx: &mut SC,
ctx: &mut C,
device_id: SC::DeviceId,
lookup_addr: UnicastAddr<Ipv6Addr>,
) -> Option<D::Address>
where
D::Address: for<'a> From<&'a MulticastAddr<Ipv6Addr>>,
{
trace!("ndp::lookup: {:?}", lookup_addr);
// TODO(brunodalbo): Figure out what to do if a frame can't be sent
let ndpstate = sync_ctx.get_state_mut_with(device_id);
let result = ndpstate.neighbors.get_neighbor_state(&lookup_addr);
match result {
// TODO(ghanan): As long as have ever received a link layer address for
// `lookup_addr` from any NDP packet with the source link
// layer option, we would have stored that address. Here
// we simply return that address without checking the
// actual state of the neighbor entry. We should make sure
// that the entry is not Stale before returning the
// address. If it is stale, we should make sure it is
// reachable first. See RFC 4861 section 7.3.2 for more
// information.
Some(NeighborState { link_address: Some(address), .. }) => Some(*address),
// We do not know about the neighbor and need to start address
// resolution.
None => {
trace!("ndp::lookup: starting address resolution process for {:?}", lookup_addr);
// If we're not already waiting for a neighbor solicitation
// response, mark it as Incomplete and send a neighbor solicitation,
// also setting the transmission count to 1.
ndpstate.neighbors.add_incomplete_neighbor_state(lookup_addr);
send_neighbor_solicitation(sync_ctx, ctx, device_id, lookup_addr);
// Also schedule a timer to retransmit in case we don't get neighbor
// advertisements back.
let retrans_timer = sync_ctx.get_retrans_timer(device_id);
let _: Option<C::Instant> = ctx.schedule_timer(
retrans_timer.get(),
NdpTimerId::new_link_address_resolution(device_id, lookup_addr).into(),
);
// Returning `None` as we do not have a link-layer address to give
// yet.
None
}
// Address resolution is currently in progress.
Some(NeighborState { state: NeighborEntryState::Incomplete { .. }, .. }) => {
trace!(
"ndp::lookup: still waiting for address resolution to complete for {:?}",
lookup_addr
);
None
}
// TODO(ghanan): Handle case where a neighbor entry exists for a
// `link_addr` but no link address as been discovered.
_ => unimplemented!("A neighbor entry exists but no link address is discovered"),
}
}
#[cfg(test)]
fn insert_neighbor<D: LinkDevice, C: NdpNonSyncContext<D, SC::DeviceId>, SC: NdpContext<D, C>>(
sync_ctx: &mut SC,
_ctx: &mut C,
device_id: SC::DeviceId,
net: UnicastAddr<Ipv6Addr>,
hw: D::Address,
) {
// Neighbor `net` should be marked as reachable.
sync_ctx.get_state_mut_with(device_id).neighbors.set_link_address(net, hw, true)
}
/// `NeighborState` keeps all state that NDP may want to keep about neighbors,
/// like link address resolution and reachability information, for example.
#[cfg_attr(test, derive(Debug, Eq, PartialEq))]
struct NeighborState<H> {
state: NeighborEntryState,
link_address: Option<H>,
}
impl<H> NeighborState<H> {
fn new() -> Self {
Self { state: NeighborEntryState::Incomplete { transmit_counter: 0 }, link_address: None }
}
/// Is the neighbor incomplete (waiting for address resolution)?
fn is_incomplete(&self) -> bool {
if let NeighborEntryState::Incomplete { .. } = self.state {
true
} else {
false
}
}
/// Is the neighbor reachable?
fn is_reachable(&self) -> bool {
self.state == NeighborEntryState::Reachable
}
}
/// The various states a Neighbor cache entry can be in.
///
/// See [RFC 4861 section 7.3.2].
///
/// [RFC 4861 section 7.3.2]: https://tools.ietf.org/html/rfc4861#section-7.3.2
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum NeighborEntryState {
/// Address resolution is being performed on the entry. Specifically, a
/// Neighbor Solicitation has been sent to the solicited-node multicast
/// address of the target, but the corresponding Neighbor Advertisement has
/// not yet been received.
///
/// `transmit_counter` is the count of Neighbor Solicitation messages sent
/// as part of the Address resolution process.
Incomplete { transmit_counter: u8 },
/// Positive confirmation was received within the last ReachableTime
/// milliseconds that the forward path to the neighbor was functioning
/// properly. While `Reachable`, no special action takes place as packets
/// are sent.
Reachable,
/// More than ReachableTime milliseconds have elapsed since the last
/// positive confirmation was received that the forward path was functioning
/// properly. While stale, no action takes place until a packet is sent.
///
/// The `Stale` state is entered upon receiving an unsolicited Neighbor
/// Discovery message that updates the cached link-layer address. Receipt
/// of such a message does not confirm reachability, and entering the
/// `Stale` state ensures reachability is verified quickly if the entry is
/// actually being used. However, reachability is not actually verified
/// until the entry is actually used.
Stale,
/// More than ReachableTime milliseconds have elapsed since the last
/// positive confirmation was received that the forward path was functioning
/// properly, and a packet was sent within the last DELAY_FIRST_PROBE_TIME
/// seconds. If no reachability confirmation is received within
/// DELAY_FIRST_PROBE_TIME seconds of entering the DELAY state, send a
/// Neighbor Solicitation and change the state to PROBE.
///
/// The DELAY state is an optimization that gives upper- layer protocols
/// additional time to provide reachability confirmation in those cases
/// where ReachableTime milliseconds have passed since the last confirmation
/// due to lack of recent traffic. Without this optimization, the opening
/// of a TCP connection after a traffic lull would initiate probes even
/// though the subsequent three-way handshake would provide a reachability
/// confirmation almost immediately.
_Delay,
/// A reachability confirmation is actively sought by retransmitting
/// Neighbor Solicitations every RetransTimer milliseconds until a
/// reachability confirmation is received.
_Probe,
}
struct NeighborTable<H> {
table: HashMap<UnicastAddr<Ipv6Addr>, NeighborState<H>>,
}
impl<H: PartialEq + Debug> NeighborTable<H> {
/// Sets the link address for a neighbor.
///
/// If `is_reachable` is `true`, the state of the neighbor will be set to
/// `NeighborEntryState::Reachable`. Otherwise, it will be set to
/// `NeighborEntryState::Stale` if the address was updated. A `false` value
/// for `is_reachable` does not mean that the neighbor is unreachable, it
/// just means that we do not know if it is reachable.
fn set_link_address(
&mut self,
neighbor: UnicastAddr<Ipv6Addr>,
address: H,
is_reachable: bool,
) {
let address = Some(address);
let neighbor_state = self.table.entry(neighbor).or_insert_with(NeighborState::new);
trace!("set_link_address: setting link address for neighbor {:?} to address", address);
if is_reachable {
trace!("set_link_address: reachability is known, so setting state for neighbor {:?} to Reachable", neighbor);
neighbor_state.state = NeighborEntryState::Reachable;
} else if neighbor_state.link_address != address {
trace!("set_link_address: new link addr different from old and reachability is unknown, so setting state for neighbor {:?} to Stale", neighbor);
neighbor_state.state = NeighborEntryState::Stale;
}
neighbor_state.link_address = address;
}
}
impl<H> NeighborTable<H> {
/// Create a new incomplete state of a neighbor, setting the transmit
/// counter to 1.
fn add_incomplete_neighbor_state(&mut self, neighbor: UnicastAddr<Ipv6Addr>) {
let mut state = NeighborState::new();
state.state = NeighborEntryState::Incomplete { transmit_counter: 1 };
let _: Option<_> = self.table.insert(neighbor, state);
}
/// Get the neighbor's state, if it exists.
fn get_neighbor_state(&self, neighbor: &UnicastAddr<Ipv6Addr>) -> Option<&NeighborState<H>> {
self.table.get(neighbor)
}
/// Get a the neighbor's mutable state, if it exists.
fn get_neighbor_state_mut(
&mut self,
neighbor: &UnicastAddr<Ipv6Addr>,
) -> Option<&mut NeighborState<H>> {
self.table.get_mut(neighbor)
}
/// Delete the neighbor's state, if it exists.
fn delete_neighbor_state(&mut self, neighbor: &UnicastAddr<Ipv6Addr>) {
let _: Option<_> = self.table.remove(neighbor);
}
}
impl<H> Default for NeighborTable<H> {
fn default() -> Self {
NeighborTable { table: HashMap::default() }
}
}
fn send_neighbor_solicitation<
D: LinkDevice,
C: NdpNonSyncContext<D, SC::DeviceId>,
SC: NdpContext<D, C>,
>(
sync_ctx: &mut SC,
ctx: &mut C,
device_id: SC::DeviceId,
lookup_addr: UnicastAddr<Ipv6Addr>,
) {
trace!("send_neighbor_solicitation: lookup_addr {:?}", lookup_addr);
// TODO(brunodalbo) when we send neighbor solicitations, we SHOULD set the
// source IP to the same IP as the packet that triggered the solicitation,
// so that when we hit the neighbor they'll have us in their cache,
// reducing overall burden on the network.
if let Some(src_ip) = sync_ctx.get_non_tentative_global_or_link_local_addr(device_id) {
assert!(src_ip.is_valid_unicast());
let src_ll = sync_ctx.get_link_layer_addr(device_id);
let dst_ip = lookup_addr.to_solicited_node_address();
// TODO(https://fxbug.dev/85055): Either panic or guarantee that this
// error can't happen statically?
let _ = send_ndp_packet::<_, _, _, &[u8], _>(
sync_ctx,
ctx,
device_id,
src_ip.get(),
dst_ip.into_specified(),
NeighborSolicitation::new(lookup_addr.get()),
&[NdpOptionBuilder::SourceLinkLayerAddress(src_ll.bytes())],
);
} else {
// Nothing can be done if we don't have any ipv6 addresses to send
// packets out to.
debug!("Not sending NDP request, since we don't know our IPv6 address");
}
}
fn send_neighbor_advertisement<
D: LinkDevice,
C: NdpNonSyncContext<D, SC::DeviceId>,
SC: NdpContext<D, C>,
>(
sync_ctx: &mut SC,
ctx: &mut C,
device_id: SC::DeviceId,
solicited: bool,
device_addr: SpecifiedAddr<Ipv6Addr>,
dst_ip: SpecifiedAddr<Ipv6Addr>,
) {
debug!("send_neighbor_advertisement from {:?} to {:?}", device_addr, dst_ip);
debug_assert!(device_addr.is_valid_unicast());
// We currently only allow the destination address to be:
// 1) a unicast address.
// 2) a multicast destination but the message should be a unsolicited
// neighbor advertisement.
// NOTE: this assertion may need change if more messages are to be allowed in the future.
debug_assert!(dst_ip.is_valid_unicast() || (!solicited && dst_ip.is_multicast()));
// We must call into the higher level send_ndp_packet function because it is
// not guaranteed that we have actually saved the link layer address of the
// destination IP. Typically, the solicitation request will carry that
// information, but it is not necessary. So it is perfectly valid that
// trying to send this advertisement will end up triggering a neighbor
// solicitation to be sent.
let src_ll = sync_ctx.get_link_layer_addr(device_id);
// TODO(https://fxbug.dev/85055): Either panic or guarantee that this error
// can't happen statically?
let device_addr = device_addr.get();
let is_router_device = sync_ctx.is_router_device(device_id);
let _ = send_ndp_packet::<_, _, _, &[u8], _>(
sync_ctx,
ctx,
device_id,
device_addr,
dst_ip,
NeighborAdvertisement::new(is_router_device, solicited, false, device_addr),
&[NdpOptionBuilder::TargetLinkLayerAddress(src_ll.bytes())],
);
}
/// Helper function to send MTU packet over an NdpDevice to `dst_ip`.
// TODO(https://fxbug.dev/85055): Is it possible to guarantee that some types of
// errors don't happen?
pub(super) fn send_ndp_packet<
D: LinkDevice,
C: NdpNonSyncContext<D, SC::DeviceId>,
SC: NdpContext<D, C>,
B: ByteSlice,
M,
>(
sync_ctx: &mut SC,
ctx: &mut C,
device_id: SC::DeviceId,
src_ip: Ipv6Addr,
dst_ip: SpecifiedAddr<Ipv6Addr>,
message: M,
options: &[NdpOptionBuilder<'_>],
) -> Result<(), ()>
where
M: IcmpMessage<Ipv6, B, Code = IcmpUnusedCode>,
{
trace!("send_ndp_packet: src_ip={:?} dst_ip={:?}", src_ip, dst_ip);
sync_ctx
.send_ipv6_frame(
ctx,
device_id,
dst_ip,
ndp::OptionSequenceBuilder::new(options.iter())
.into_serializer()
.encapsulate(IcmpPacketBuilder::<Ipv6, B, M>::new(
src_ip,
dst_ip,
IcmpUnusedCode,
message,
))
.encapsulate(Ipv6PacketBuilder::new(
src_ip,
dst_ip,
REQUIRED_NDP_IP_PACKET_HOP_LIMIT,
Ipv6Proto::Icmpv6,
)),
)
.map_err(|_| ())
}
/// A handler for incoming NDP packets.
///
/// An implementation of `NdpPacketHandler` is provided by the device layer (see
/// the `crate::device` module) to the IP layer so that it can pass incoming NDP
/// packets. It can also be mocked for use in testing.
pub(crate) trait NdpPacketHandler<C, DeviceId> {
/// Receive an NDP packet.
fn receive_ndp_packet<B: ByteSlice>(
&mut self,
ctx: &mut C,
device: DeviceId,
src_ip: Ipv6SourceAddr,
dst_ip: SpecifiedAddr<Ipv6Addr>,
packet: NdpPacket<B>,
);
}
pub(crate) fn receive_ndp_packet<
D: LinkDevice,
C: NdpNonSyncContext<D, SC::DeviceId>,
SC: NdpContext<D, C>,
B,
>(
sync_ctx: &mut SC,
ctx: &mut C,
device_id: SC::DeviceId,
src_ip: Ipv6SourceAddr,
_dst_ip: SpecifiedAddr<Ipv6Addr>,
packet: NdpPacket<B>,
) where
B: ByteSlice,
{
// TODO(ghanan): Make sure the IP packet's hop limit was set to 255 as per
// RFC 4861 sections 4.1, 4.2, 4.3, 4.4, and 4.5 (each type of
// NDP packet).
match packet {
NdpPacket::RouterSolicitation(p) => {
let _: IcmpPacket<Ipv6, B, RouterSolicitation> = p;
trace!("receive_ndp_packet: Received NDP RS");
if !sync_ctx.is_router_device(device_id) {
// Hosts MUST silently discard Router Solicitation messages as
// per RFC 4861 section 6.1.1.
trace!(
"receive_ndp_packet: device {:?} is not a router, discarding NDP RS",
device_id
);
return;
}
}
NdpPacket::RouterAdvertisement(p) => {
// Note that some parts of RFC 4861 w.r.t RAs are handled elsewhere.
//
// TODO(https://fxbug.dev/93817): Pull SLAAC handling into IP
// so this module doesn't handle RAs at all.
// Nodes MUST silently discard any received Router Advertisement
// message where the IP source address is not a link-local
// address as routers must use their link-local address as the
// source for Router Advertisements so hosts can uniquely
// identify routers, as per RFC 4861 section 6.1.2.
let src_ip = match match src_ip {
Ipv6SourceAddr::Unicast(ip) => LinkLocalUnicastAddr::new(ip),
Ipv6SourceAddr::Unspecified => None,
} {
Some(ip) => {
trace!("receive_ndp_packet: NDP RA source={:?}", ip);
ip
}
None => {
trace!(
"receive_ndp_packet: NDP RA source={:?} is not link-local; discarding",
src_ip
);
return;
}
};
// TODO(ghanan): Make sure IP's hop limit is set to 255 as per RFC
// 4861 section 6.1.2.
sync_ctx.increment_counter("ndp::rx_router_advertisement");
if sync_ctx.is_router_device(device_id) {
trace!("receive_ndp_packet: received NDP RA as a router, discarding NDP RA");
return;
}
let ra = p.message();
// Borrow again so that a) we shadow the original `ndp_state` and
// thus, b) the original is dropped before `ctx` is used mutably in
// various code above (namely, to schedule timers). Now that all of
// that mutation has happened, we can borrow `ctx` mutably again and
// not run afoul of the borrow checker.
let ndp_state = sync_ctx.get_state_mut_with(device_id);
// As per RFC 4861 section 6.3.4:
// If the received Reachable Time value is specified, the host
// SHOULD set its BaseReachableTime variable to the received value.
// If the new value differs from the previous value, the host SHOULD
// re-compute a new random ReachableTime value.
//
// TODO(ghanan): Make the updating of this field from the RA message
// configurable since the RFC does not say we MUST
// update the field.
//
// TODO(ghanan): In most cases, the advertised Reachable Time value
// will be the same in consecutive Router
// Advertisements, and a host's BaseReachableTime
// rarely changes. In such cases, an implementation
// SHOULD ensure that a new random value gets
// re-computed at least once every few hours.
if let Some(base_reachable_time) = ra.reachable_time() {
trace!("receive_ndp_packet: NDP RA: updating base_reachable_time to {:?} for router: {:?}", base_reachable_time, src_ip);
ndp_state.set_base_reachable_time(base_reachable_time.get());
}
// As per RFC 4861 section 6.3.4:
// If the received Cur Hop Limit value is specified, the host SHOULD
// set its CurHopLimit variable to the received value.
//
// TODO(ghanan): Make the updating of this field from the RA message
// configurable since the RFC does not say we MUST
// update the field.
if let Some(hop_limit) = ra.current_hop_limit() {
trace!("receive_ndp_packet: NDP RA: updating device's hop limit to {:?} for router: {:?}", ra.current_hop_limit(), src_ip);
sync_ctx.get_ip_device_state_mut(device_id).default_hop_limit = hop_limit;
}
for option in p.body().iter() {
match option {
// As per RFC 4861 section 6.3.4, if a Neighbor Cache entry
// is created for the router, its reachability state MUST be
// set to STALE as specified in Section 7.3.3. If a cache
// entry already exists and is updated with a different
// link-layer address, the reachability state MUST also be
// set to STALE.
//
// TODO(ghanan): Mark NDP state as STALE as per the RFC once
// we implement the RFC compliant states.
NdpOption::SourceLinkLayerAddress(a) => {
let ndp_state = sync_ctx.get_state_mut_with(device_id);
let link_addr = D::Address::from_bytes(&a[..D::Address::BYTES_LENGTH]);
trace!("receive_ndp_packet: NDP RA: setting link address for router {:?} to {:?}", src_ip, link_addr);
// Set the link address and mark it as stale if we
// either created the neighbor entry, or updated an
// existing one.
ndp_state.neighbors.set_link_address(src_ip.get(), link_addr, false);
}
NdpOption::Mtu(mtu) => {
trace!("receive_ndp_packet: mtu option with mtu = {:?}", mtu);
// TODO(ghanan): Make updating the MTU from an RA
// message configurable.
if mtu >= Ipv6::MINIMUM_LINK_MTU.into() {
// `set_mtu` may panic if `mtu` is less than
// `MINIMUM_LINK_MTU` but we just checked to make
// sure that `mtu` is at least `MINIMUM_LINK_MTU` so
// we know `set_mtu` will not panic.
sync_ctx.set_mtu(ctx, device_id, mtu);
} else {
trace!("receive_ndp_packet: NDP RA: not setting link MTU (from {:?}) to {:?} as it is less than Ipv6::MINIMUM_LINK_MTU", src_ip, mtu);
}
}
// These are handled elsewhere.
//
// TODO(https://fxbub.dev/99830): Move all of NDP handling
// to IP.
NdpOption::TargetLinkLayerAddress(_)
| NdpOption::RedirectedHeader { .. }
| NdpOption::RecursiveDnsServer(_)
| NdpOption::RouteInformation(_)
| NdpOption::PrefixInformation(_) => {}
}
}
}
NdpPacket::NeighborSolicitation(p) => {
let target_address = p.message().target_address();
let target_address = match UnicastAddr::new(*target_address) {
Some(addr) => {
trace!("receive_ndp_packet: NDP NS target={:?}", addr);
addr
}
None => {
trace!(
"receive_ndp_packet: NDP NS target={:?} is not unicast; discarding",
target_address
);
return;
}
};
// At this point, we guarantee the following is true because of the
// earlier checks (with 2 & 3 being done in IP):
//
// 1) The target address is a valid unicast address.
// 2) The target address is an address that is on our device,
// `device_id`.
// 3) The target address is not tentative.
//
// TODO(https://fxbub.dev/99830): Move all of NDP handling
// to IP.
sync_ctx.increment_counter("ndp::rx_neighbor_solicitation");
// If we have a source link layer address option, we take it and
// save to our cache.
if let Ipv6SourceAddr::Unicast(src_ip) = src_ip {
// We only update the cache if it is not from an unspecified
// address, i.e., it is not a DAD message. (RFC 4861)
if let Some(ll) = get_source_link_layer_option(p.body()) {
trace!("receive_ndp_packet: Received NDP NS from {:?} has source link layer option w/ link address {:?}", src_ip, ll);
// Set the link address and mark it as stale if we either
// create the neighbor entry, or updated an existing one, as
// per RFC 4861 section 7.2.3.
sync_ctx
.get_state_mut_with(device_id)
.neighbors
.set_link_address(src_ip, ll, false);
}
trace!(
"receive_ndp_packet: Received NDP NS: sending NA to source of NS {:?}",
src_ip
);
// Finally we ought to reply to the Neighbor Solicitation with a
// Neighbor Advertisement.
//
// TODO(https://fxbug.dev/99830): Move NUD to IP.
send_neighbor_advertisement(
sync_ctx,
ctx,
device_id,
true,
target_address.into_specified(),
src_ip.into_specified(),
);
} else {
// TODO(https://fxbub.dev/99830): Move all of NDP handling
// to IP.
unreachable!("Handled by caller")
}
}
NdpPacket::NeighborAdvertisement(p) => {
let message = p.message();
let target_address = p.message().target_address();
let src_ip = match src_ip {
Ipv6SourceAddr::Unicast(src_ip) => {
trace!(
"receive_ndp_packet: NDP NA source={:?} target={:?}",
src_ip,
target_address
);
src_ip
}
Ipv6SourceAddr::Unspecified => {
trace!("receive_ndp_packet: NDP NA source={:?} target={:?}; source is not specified; discarding", src_ip, target_address);
return;
}
};
sync_ctx.increment_counter("ndp::rx_neighbor_advertisement");
let ndp_state = sync_ctx.get_state_mut_with(device_id);
// TODO(https://fxbug.dev/99830): Move NUD to IP.
let neighbor_state = if let Some(state) =
ndp_state.neighbors.get_neighbor_state_mut(&src_ip)
{
state
} else {
// If the neighbor is not in the cache, we just ignore the
// advertisement, as we're not yet interested in communicating
// with it, as per RFC 4861 section 7.2.5.
trace!("receive_ndp_packet: Ignoring NDP NA from {:?} does not already exist in our list of neighbors, so discarding", src_ip);
return;
};
let target_ll = get_target_link_layer_option(p.body());
if neighbor_state.is_incomplete() {
// If we are in the Incomplete state, we should not have ever
// learned about a link-layer address.
assert_eq!(neighbor_state.link_address, None);
if let Some(address) = target_ll {
// Record the link-layer address.
//
// If the advertisement's Solicited flag is set, the state
// of the entry is set to REACHABLE; otherwise, it is set to
// STALE, as per RFC 4861 section 7.2.5.
//
// Note, since the neighbor's link address was `None`
// before, we will definitely update the address, so the
// state will be set to STALE if the solicited flag is
// unset.
trace!(
"receive_ndp_packet: Resolving link address of {:?} to {:?}",
src_ip,
address
);
ndp_state.neighbors.set_link_address(src_ip, address, message.solicited_flag());
// Cancel the resolution timeout.
let _: Option<C::Instant> = ctx.cancel_timer(
NdpTimerId::new_link_address_resolution(device_id, src_ip).into(),
);
// Send any packets queued for the neighbor awaiting address
// resolution.
sync_ctx.address_resolved(ctx, device_id, &src_ip, address);
} else {
trace!("receive_ndp_packet: Performing address resolution but the NDP NA from {:?} does not have a target link layer address option, so discarding", src_ip);
return;
}
return;
}
// If we are not in the Incomplete state, we should have (at some
// point) learned about a link-layer address.
assert_matches!(neighbor_state.link_address, Some(_));
if !message.override_flag() {
// As per RFC 4861 section 7.2.5:
//
// If the Override flag is clear and the supplied link-layer
// address differs from that in the cache, then one of two
// actions takes places:
//
// a) If the state of the entry is REACHABLE, set it to STALE,
// but do not update the entry in any other way.
//
// b) Otherwise, the received advertisement should be ignored
// and MUST NOT update cache.
if target_ll.map_or(false, |x| neighbor_state.link_address != Some(x)) {
if neighbor_state.is_reachable() {
trace!("receive_ndp_packet: NDP RS from known reachable neighbor {:?} does not have override set, but supplied link addr is different, setting state to stale", src_ip);
neighbor_state.state = NeighborEntryState::Stale;
} else {
trace!("receive_ndp_packet: NDP RS from known neighbor {:?} (with reachability unknown) does not have override set, but supplied link addr is different, ignoring", src_ip);
}
}
}
// Ignore this unless `target_ll` is `Some`.
let mut is_same = false;
// If override is set, the link-layer address MUST be inserted into
// the cache (if one is supplied and differs from the already
// recoded address).
if let Some(address) = target_ll {
let address = Some(address);
is_same = neighbor_state.link_address == address;
if !is_same && message.override_flag() {
neighbor_state.link_address = address;
}
}
// If the override flag is set, or the supplied link-layer address
// is the same as that in the cache, or no Target Link-Layer Address
// option was supplied:
if message.override_flag() || target_ll.is_none() || is_same {
// - If the solicited flag is set, the state of the entry MUST
// be set to REACHABLE.
// - Else, if it was unset, and the link address was updated,
// the state MUST be set to STALE.
// - Otherwise, the state remains the same.
if message.solicited_flag() {
trace!("receive_ndp_packet: NDP RS from {:?} is solicited and either has override set, link address isn't provided, or the provided address is not different, updating state to Reachable", src_ip);
neighbor_state.state = NeighborEntryState::Reachable;
} else if message.override_flag() && target_ll.is_some() && !is_same {
trace!("receive_ndp_packet: NDP RS from {:?} is unsolicited and the link address was updated, updating state to Stale", src_ip);
neighbor_state.state = NeighborEntryState::Stale;
} else {
trace!("receive_ndp_packet: NDP RS from {:?} is unsolicited and the link address was not updated, doing nothing", src_ip);
}
}
}
NdpPacket::Redirect(_) => log_unimplemented!((), "NDP Redirect not implemented"),
}
}
#[derive(PartialEq, Eq)]
enum SlaacType {
Static,
Temporary,
}
impl core::fmt::Debug for SlaacType {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
SlaacType::Static => f.write_str("static"),
SlaacType::Temporary => f.write_str("temporary"),
}
}
}
impl<'a, Instant> From<&'a SlaacConfig<Instant>> for SlaacType {
fn from(slaac_config: &'a SlaacConfig<Instant>) -> Self {
match slaac_config {
SlaacConfig::Static { .. } => SlaacType::Static,
SlaacConfig::Temporary { .. } => SlaacType::Temporary,
}
}
}
fn get_source_link_layer_option<L: LinkAddress, B>(options: &Options<B>) -> Option<L>
where
B: ByteSlice,
{
options.iter().find_map(|o| match o {
NdpOption::SourceLinkLayerAddress(a) => {
if a.len() >= L::BYTES_LENGTH {
Some(L::from_bytes(&a[..L::BYTES_LENGTH]))
} else {
None
}
}
_ => None,
})
}
fn get_target_link_layer_option<L: LinkAddress, B>(options: &Options<B>) -> Option<L>
where
B: ByteSlice,
{
options.iter().find_map(|o| match o {
NdpOption::TargetLinkLayerAddress(a) => {
if a.len() >= L::BYTES_LENGTH {
Some(L::from_bytes(&a[..L::BYTES_LENGTH]))
} else {
None
}
}
_ => None,
})
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::{collections::HashSet, vec, vec::Vec};
use core::convert::{TryFrom, TryInto as _};
use net_declare::net::{mac, subnet_v6};
use net_types::{
ethernet::Mac,
ip::{AddrSubnet, Subnet},
};
use packet::{Buf, ParseBuffer};
use packet_formats::{
icmp::{
ndp::{
options::PrefixInformation, OptionSequenceBuilder, RouterAdvertisement,
RouterSolicitation,
},
IcmpEchoRequest, Icmpv6Packet,
},
ip::IpProto,
testutil::{parse_ethernet_frame, parse_icmp_packet_in_ip_packet_in_ethernet_frame},
utils::NonZeroDuration,
};
use rand::RngCore;
use crate::{
algorithm::{
generate_opaque_interface_identifier, OpaqueIidNonce, STABLE_IID_SECRET_KEY_BYTES,
},
context::{
testutil::{DummyInstant, DummyTimerCtxExt as _, StepResult},
InstantContext as _, RngContext as _,
},
device::{
add_ip_addr_subnet, del_ip_addr,
ethernet::{EthernetLinkDevice, EthernetTimerId},
testutil::receive_frame_or_panic,
DeviceId, DeviceIdInner, DeviceLayerTimerId, DeviceLayerTimerIdInner, EthernetDeviceId,
FrameDestination,
},
ip::{
device::{
get_assigned_ipv6_addr_subnets, get_ipv6_device_state, get_ipv6_hop_limit,
is_ipv6_routing_enabled,
router_solicitation::{MAX_RTR_SOLICITATION_DELAY, RTR_SOLICITATION_INTERVAL},
set_ipv6_routing_enabled,
slaac::{SlaacConfiguration, SlaacTimerId, TemporarySlaacAddressConfiguration},
state::{
AddrConfig, Ipv6AddressEntry, Ipv6DeviceConfiguration, Lifetime,
TemporarySlaacConfig,
},
Ipv6DeviceHandler, Ipv6DeviceTimerId,
},
receive_ipv6_packet, SendIpPacketMeta,
},
testutil::{
assert_empty, get_counter_val, handle_timer, set_logger_for_test,
DummyEventDispatcherBuilder, TestIpExt, DUMMY_CONFIG_V6,
},
Ctx, Instant, StackStateBuilder, TimerId, TimerIdInner,
};
type IcmpParseArgs = packet_formats::icmp::IcmpParseArgs<Ipv6Addr>;
impl From<NdpTimerId<EthernetLinkDevice, EthernetDeviceId>> for TimerId {
fn from(id: NdpTimerId<EthernetLinkDevice, EthernetDeviceId>) -> Self {
TimerId(TimerIdInner::DeviceLayer(DeviceLayerTimerId(
DeviceLayerTimerIdInner::Ethernet(EthernetTimerId::Ndp(id)),
)))
}
}
// TODO(https://github.com/rust-lang/rust/issues/67441): Make these constants once const
// Option::unwrap is stablized
fn local_mac() -> UnicastAddr<Mac> {
UnicastAddr::new(Mac::new([0, 1, 2, 3, 4, 5])).unwrap()
}
fn remote_mac() -> UnicastAddr<Mac> {
UnicastAddr::new(Mac::new([6, 7, 8, 9, 10, 11])).unwrap()
}
fn local_ip() -> UnicastAddr<Ipv6Addr> {
UnicastAddr::from_witness(DUMMY_CONFIG_V6.local_ip).unwrap()
}
fn remote_ip() -> UnicastAddr<Ipv6Addr> {
UnicastAddr::from_witness(DUMMY_CONFIG_V6.remote_ip).unwrap()
}
fn router_advertisement_message(
src_ip: Ipv6Addr,
dst_ip: Ipv6Addr,
current_hop_limit: u8,
managed_flag: bool,
other_config_flag: bool,
router_lifetime: u16,
reachable_time: u32,
retransmit_timer: u32,
) -> Buf<Vec<u8>> {
Buf::new(Vec::new(), ..)
.encapsulate(IcmpPacketBuilder::<Ipv6, &[u8], _>::new(
src_ip,
dst_ip,
IcmpUnusedCode,
RouterAdvertisement::new(
current_hop_limit,
managed_flag,
other_config_flag,
router_lifetime,
reachable_time,
retransmit_timer,
),
))
.serialize_vec_outer()
.unwrap()
.into_inner()
}
fn neighbor_advertisement_message(
src_ip: Ipv6Addr,
dst_ip: Ipv6Addr,
router_flag: bool,
solicited_flag: bool,
override_flag: bool,
mac: Option<Mac>,
) -> Buf<Vec<u8>> {
let mac = mac.map(|x| x.bytes());
let mut options = Vec::new();
if let Some(ref mac) = mac {
options.push(NdpOptionBuilder::TargetLinkLayerAddress(mac));
}
OptionSequenceBuilder::new(options.iter())
.into_serializer()
.encapsulate(IcmpPacketBuilder::<Ipv6, &[u8], _>::new(
src_ip,
dst_ip,
IcmpUnusedCode,
NeighborAdvertisement::new(router_flag, solicited_flag, override_flag, src_ip),
))
.serialize_vec_outer()
.unwrap()
.unwrap_b()
}
impl TryFrom<DeviceId> for EthernetDeviceId {
type Error = DeviceId;
fn try_from(id: DeviceId) -> Result<EthernetDeviceId, DeviceId> {
match id.inner() {
DeviceIdInner::Ethernet(id) => Ok(id),
DeviceIdInner::Loopback => Err(id),
}
}
}
#[test]
fn test_send_neighbor_solicitation_on_cache_miss() {
set_logger_for_test();
let Ctx { mut sync_ctx, mut non_sync_ctx } = DummyEventDispatcherBuilder::default().build();
let dev_id = crate::add_ethernet_device(
&mut sync_ctx,
&mut non_sync_ctx,
local_mac(),
Ipv6::MINIMUM_LINK_MTU.into(),
);
crate::device::testutil::enable_device(&mut sync_ctx, &mut non_sync_ctx, dev_id);
// Now we have to manually assign the IP addresses, see
// `EthernetLinkDevice::get_ipv6_addr`
add_ip_addr_subnet(
&mut sync_ctx,
&mut non_sync_ctx,
dev_id,
AddrSubnet::new(local_ip().get(), 128).unwrap(),
)
.unwrap();
assert_eq!(
lookup::<EthernetLinkDevice, _, _>(
&mut sync_ctx,
&mut non_sync_ctx,
dev_id.try_into().expect("expected ethernet ID"),
remote_ip()
),
None
);
// Check that we send the original neighbor solicitation, then resend a
// few times if we don't receive a response.
for packet_num in 0..usize::from(MAX_MULTICAST_SOLICIT) {
assert_eq!(non_sync_ctx.frames_sent().len(), packet_num + 1);
assert_eq!(
non_sync_ctx.trigger_next_timer(&mut sync_ctx, crate::handle_timer).unwrap(),
NdpTimerId::new_link_address_resolution(
dev_id.try_into().expect("expected ethernet ID"),
remote_ip()
)
.into()
);
}
// Check that we hit the timeout after MAX_MULTICAST_SOLICIT.
assert_eq!(
get_counter_val(&sync_ctx, "ndp::neighbor_solicitation_timer"),
1,
"timeout counter at zero"
);
}
#[test]
fn test_address_resolution() {
set_logger_for_test();
let mut local = DummyEventDispatcherBuilder::default();
assert_eq!(local.add_device(local_mac()), 0);
let mut remote = DummyEventDispatcherBuilder::default();
assert_eq!(remote.add_device(remote_mac()), 0);
let device_id = DeviceId::new_ethernet(0);
let mut net = crate::context::testutil::new_legacy_simple_dummy_network(
"local",
local.build(),
"remote",
remote.build(),
);
// Let's try to ping the remote device from the local device:
let req = IcmpEchoRequest::new(0, 0);
let req_body = &[1, 2, 3, 4];
let body = Buf::new(req_body.to_vec(), ..).encapsulate(
IcmpPacketBuilder::<Ipv6, &[u8], _>::new(local_ip(), remote_ip(), IcmpUnusedCode, req),
);
// Manually assigning the addresses.
net.with_context("remote", |Ctx { sync_ctx, non_sync_ctx }| {
add_ip_addr_subnet(
sync_ctx,
non_sync_ctx,
device_id,
AddrSubnet::new(remote_ip().get(), 128).unwrap(),
)
.unwrap();
assert_empty(non_sync_ctx.frames_sent());
});
net.with_context("local", |Ctx { sync_ctx, non_sync_ctx }| {
add_ip_addr_subnet(
sync_ctx,
non_sync_ctx,
device_id,
AddrSubnet::new(local_ip().get(), 128).unwrap(),
)
.unwrap();
assert_empty(non_sync_ctx.frames_sent());
crate::ip::send_ipv6_packet_from_device(
sync_ctx,
non_sync_ctx,
SendIpPacketMeta {
device: device_id,
src_ip: Some(local_ip().into_specified()),
dst_ip: remote_ip().into_specified(),
next_hop: remote_ip().into_specified(),
proto: Ipv6Proto::Icmpv6,
ttl: None,
mtu: None,
},
body,
)
.unwrap();
// This should've triggered a neighbor solicitation to come out of
// local.
assert_eq!(non_sync_ctx.frames_sent().len(), 1);
// A timer should've been started.
assert_eq!(non_sync_ctx.timer_ctx().timers().len(), 1);
});
let _: StepResult = net.step(receive_frame_or_panic, handle_timer);
// Neighbor entry for remote should be marked as Incomplete.
assert_eq!(
StateContext::<_, NdpState<EthernetLinkDevice>, _>::get_state_mut_with(
net.sync_ctx("local"),
device_id.try_into().expect("expected ethernet ID")
)
.neighbors
.get_neighbor_state(&remote_ip())
.unwrap()
.state,
NeighborEntryState::Incomplete { transmit_counter: 1 }
);
assert_eq!(
get_counter_val(net.sync_ctx("remote"), "ndp::rx_neighbor_solicitation"),
1,
"remote received solicitation"
);
assert_eq!(net.non_sync_ctx("remote").frames_sent().len(), 1);
// Forward advertisement response back to local.
let _: StepResult = net.step(receive_frame_or_panic, handle_timer);
assert_eq!(
get_counter_val(net.sync_ctx("local"), "ndp::rx_neighbor_advertisement"),
1,
"local received advertisement"
);
// At the end of the exchange, both sides should have each other in
// their NDP tables.
let local_neighbor =
StateContext::<_, NdpState<EthernetLinkDevice>, _>::get_state_mut_with(
net.sync_ctx("local"),
device_id.try_into().expect("expected ethernet ID"),
)
.neighbors
.get_neighbor_state(&remote_ip())
.unwrap();
assert_eq!(local_neighbor.link_address.unwrap(), remote_mac().get(),);
// Remote must be reachable from local since it responded with an NA
// message with the solicited flag set.
assert_eq!(local_neighbor.state, NeighborEntryState::Reachable,);
let remote_neighbor =
StateContext::<_, NdpState<EthernetLinkDevice>, _>::get_state_mut_with(
net.sync_ctx("remote"),
device_id.try_into().expect("expected ethernet ID"),
)
.neighbors
.get_neighbor_state(&local_ip())
.unwrap();
assert_eq!(remote_neighbor.link_address.unwrap(), local_mac().get(),);
// Local must be marked as stale because remote got an NS from it but
// has not itself sent any packets to it and confirmed that local
// actually received it.
assert_eq!(remote_neighbor.state, NeighborEntryState::Stale);
// The local timer should've been unscheduled.
net.with_context("local", |Ctx { sync_ctx: _, non_sync_ctx }| {
assert_empty(non_sync_ctx.timer_ctx().timers());
// Upon link layer resolution, the original ping request should've been
// sent out.
assert_eq!(non_sync_ctx.frames_sent().len(), 1);
});
let _: StepResult = net.step(receive_frame_or_panic, handle_timer);
assert_eq!(
get_counter_val(net.sync_ctx("remote"), "<IcmpIpTransportContext as BufferIpTransportContext<Ipv6>>::receive_ip_packet::echo_request"),
1
);
// TODO(brunodalbo): We should be able to verify that remote also sends
// back an echo reply, but we're having some trouble with IPv6 link
// local addresses.
}
#[test]
fn test_deinitialize_cancels_timers() {
// Test that associated timers are cancelled when the NDP device
// is deinitialized.
set_logger_for_test();
let Ctx { mut sync_ctx, mut non_sync_ctx } = DummyEventDispatcherBuilder::default().build();
let dev_id = crate::add_ethernet_device(
&mut sync_ctx,
&mut non_sync_ctx,
local_mac(),
Ipv6::MINIMUM_LINK_MTU.into(),
);
crate::device::testutil::enable_device(&mut sync_ctx, &mut non_sync_ctx, dev_id);
// Now we have to manually assign the IP addresses, see
// `EthernetLinkDevice::get_ipv6_addr`
add_ip_addr_subnet(
&mut sync_ctx,
&mut non_sync_ctx,
dev_id,
AddrSubnet::new(local_ip().get(), 128).unwrap(),
)
.unwrap();
let device_id = dev_id.try_into().unwrap();
assert_eq!(
lookup::<EthernetLinkDevice, _, _>(
&mut sync_ctx,
&mut non_sync_ctx,
device_id,
remote_ip()
),
None
);
// This should have scheduled a timer
let timer_id = NdpTimerId::new_link_address_resolution(device_id, remote_ip()).into();
non_sync_ctx.timer_ctx().assert_timers_installed([(timer_id, ..)]);
// Deinitializing a different ID should not impact the current timer
let other_id = {
let EthernetDeviceId(id) = device_id;
EthernetDeviceId(id + 1).into()
};
deinitialize(&mut sync_ctx, &mut non_sync_ctx, other_id);
non_sync_ctx.timer_ctx().assert_timers_installed([(timer_id, ..)]);
// Deinitializing the correct ID should cancel the timer.
deinitialize(
&mut sync_ctx,
&mut non_sync_ctx,
dev_id.try_into().expect("expected ethernet ID"),
);
non_sync_ctx.timer_ctx().assert_no_timers_installed();
}
#[test]
fn test_dad_duplicate_address_detected_solicitation() {
// Tests whether a duplicate address will get detected by solicitation
// In this test, two nodes having the same MAC address will come up at
// the same time. And both of them will use the EUI address. Each of
// them should be able to detect each other is using the same address,
// so they will both give up using that address.
set_logger_for_test();
let mac = UnicastAddr::new(Mac::new([6, 5, 4, 3, 2, 1])).unwrap();
let ll_addr: Ipv6Addr = mac.to_ipv6_link_local().addr().get();
let multicast_addr = ll_addr.to_solicited_node_address();
let local = DummyEventDispatcherBuilder::default();
let remote = DummyEventDispatcherBuilder::default();
let device_id = DeviceId::new_ethernet(0);
let stack_builder = StackStateBuilder::default();
let mut net = crate::context::testutil::new_legacy_simple_dummy_network(
"local",
local.build_with(stack_builder.clone()),
"remote",
remote.build_with(stack_builder),
);
// Create the devices (will start DAD at the same time).
let update = |ipv6_config: &mut Ipv6DeviceConfiguration| {
ipv6_config.ip_config.ip_enabled = true;
// Doesn't matter as long as we perform DAD.
ipv6_config.dad_transmits = NonZeroU8::new(1);
};
net.with_context("local", |Ctx { sync_ctx, non_sync_ctx }| {
assert_eq!(
crate::add_ethernet_device(
sync_ctx,
non_sync_ctx,
mac,
Ipv6::MINIMUM_LINK_MTU.into(),
),
device_id
);
crate::device::update_ipv6_configuration(sync_ctx, non_sync_ctx, device_id, update);
assert_eq!(non_sync_ctx.frames_sent().len(), 1);
});
net.with_context("remote", |Ctx { sync_ctx, non_sync_ctx }| {
assert_eq!(
crate::add_ethernet_device(
sync_ctx,
non_sync_ctx,
mac,
Ipv6::MINIMUM_LINK_MTU.into(),
),
device_id
);
crate::device::update_ipv6_configuration(sync_ctx, non_sync_ctx, device_id, update);
assert_eq!(non_sync_ctx.frames_sent().len(), 1);
});
// Both devices should be in the solicited-node multicast group.
assert!(get_ipv6_device_state(net.sync_ctx("local"), device_id)
.multicast_groups
.contains(&multicast_addr));
assert!(get_ipv6_device_state(net.sync_ctx("remote"), device_id)
.multicast_groups
.contains(&multicast_addr));
let _: StepResult = net.step(receive_frame_or_panic, handle_timer);
// They should now realize the address they intend to use has a
// duplicate in the local network.
assert_empty(get_assigned_ipv6_addr_subnets(net.sync_ctx("local"), device_id));
assert_empty(get_assigned_ipv6_addr_subnets(net.sync_ctx("remote"), device_id));
// Both devices should not be in the multicast group
assert!(!get_ipv6_device_state(net.sync_ctx("local"), device_id)
.multicast_groups
.contains(&multicast_addr));
assert!(!get_ipv6_device_state(net.sync_ctx("remote"), device_id)
.multicast_groups
.contains(&multicast_addr));
}
fn dad_timer_id(id: EthernetDeviceId, addr: UnicastAddr<Ipv6Addr>) -> TimerId {
TimerId(TimerIdInner::Ipv6Device(Ipv6DeviceTimerId::Dad(
crate::ip::device::dad::DadTimerId { device_id: id.into(), addr },
)))
}
fn rs_timer_id(id: EthernetDeviceId) -> TimerId {
TimerId(TimerIdInner::Ipv6Device(Ipv6DeviceTimerId::Rs(
crate::ip::device::router_solicitation::RsTimerId { device_id: id.into() },
)))
}
#[test]
fn test_dad_duplicate_address_detected_advertisement() {
// Tests whether a duplicate address will get detected by advertisement
// In this test, one of the node first assigned itself the local_ip(),
// then the second node comes up and it should be able to find out that
// it cannot use the address because someone else has already taken that
// address.
set_logger_for_test();
let mut local = DummyEventDispatcherBuilder::default();
assert_eq!(local.add_device(local_mac()), 0);
let mut remote = DummyEventDispatcherBuilder::default();
assert_eq!(remote.add_device(remote_mac()), 0);
let device_id = DeviceId::new_ethernet(0);
let mut net = crate::context::testutil::new_legacy_simple_dummy_network(
"local",
local.build(),
"remote",
remote.build(),
);
// Enable DAD.
let update = |ipv6_config: &mut Ipv6DeviceConfiguration| {
ipv6_config.ip_config.ip_enabled = true;
// Doesn't matter as long as we perform DAD.
ipv6_config.dad_transmits = NonZeroU8::new(1);
};
let addr = AddrSubnet::new(local_ip().get(), 128).unwrap();
let multicast_addr = local_ip().to_solicited_node_address();
net.with_context("local", |Ctx { sync_ctx, non_sync_ctx }| {
crate::device::update_ipv6_configuration(sync_ctx, non_sync_ctx, device_id, update);
add_ip_addr_subnet(sync_ctx, non_sync_ctx, device_id, addr).unwrap();
});
net.with_context("remote", |Ctx { sync_ctx, non_sync_ctx }| {
crate::device::update_ipv6_configuration(sync_ctx, non_sync_ctx, device_id, update);
});
// Only local should be in the solicited node multicast group.
assert!(get_ipv6_device_state(net.sync_ctx("local"), device_id)
.multicast_groups
.contains(&multicast_addr));
assert!(!get_ipv6_device_state(net.sync_ctx("remote"), device_id)
.multicast_groups
.contains(&multicast_addr));
net.with_context("local", |Ctx { sync_ctx, non_sync_ctx }| {
assert_eq!(
non_sync_ctx.trigger_next_timer(sync_ctx, crate::handle_timer).unwrap(),
dad_timer_id(device_id.try_into().expect("expected ethernet ID"), local_ip())
);
});
assert!(NdpContext::<EthernetLinkDevice, _>::get_ip_device_state(
net.sync_ctx("local"),
device_id.try_into().expect("expected ethernet ID")
)
.find_addr(&local_ip())
.unwrap()
.state
.is_assigned());
net.with_context("remote", |Ctx { sync_ctx, non_sync_ctx }| {
add_ip_addr_subnet(sync_ctx, non_sync_ctx, device_id, addr).unwrap();
});
// Local & remote should be in the multicast group.
assert!(get_ipv6_device_state(net.sync_ctx("local"), device_id)
.multicast_groups
.contains(&multicast_addr));
assert!(get_ipv6_device_state(net.sync_ctx("remote"), device_id)
.multicast_groups
.contains(&multicast_addr));
let _: StepResult = net.step(receive_frame_or_panic, handle_timer);
assert_eq!(get_assigned_ipv6_addr_subnets(net.sync_ctx("remote"), device_id).count(), 1);
// Let's make sure that our local node still can use that address.
assert!(NdpContext::<EthernetLinkDevice, _>::get_ip_device_state(
net.sync_ctx("local"),
device_id.try_into().expect("expected ethernet ID")
)
.find_addr(&local_ip())
.unwrap()
.state
.is_assigned());
// Only local should be in the solicited node multicast group.
assert!(get_ipv6_device_state(net.sync_ctx("local"), device_id)
.multicast_groups
.contains(&multicast_addr));
assert!(!get_ipv6_device_state(net.sync_ctx("remote"), device_id)
.multicast_groups
.contains(&multicast_addr));
}
#[test]
fn test_dad_set_ipv6_address_when_ongoing() {
// Test that we can make our tentative address change when DAD is
// ongoing.
let Ctx { mut sync_ctx, mut non_sync_ctx } = DummyEventDispatcherBuilder::default().build();
let dev_id = crate::add_ethernet_device(
&mut sync_ctx,
&mut non_sync_ctx,
local_mac(),
Ipv6::MINIMUM_LINK_MTU.into(),
);
crate::device::update_ipv6_configuration(
&mut sync_ctx,
&mut non_sync_ctx,
dev_id,
|config| {
config.ip_config.ip_enabled = true;
config.dad_transmits = NonZeroU8::new(1);
},
);
let addr = local_ip();
add_ip_addr_subnet(
&mut sync_ctx,
&mut non_sync_ctx,
dev_id,
AddrSubnet::new(addr.get(), 128).unwrap(),
)
.unwrap();
assert_eq!(
NdpContext::<EthernetLinkDevice, _>::get_ip_device_state(
&sync_ctx,
dev_id.try_into().expect("expected ethernet ID")
)
.find_addr(&addr)
.unwrap()
.state,
AddressState::Tentative { dad_transmits_remaining: None },
);
let addr = remote_ip();
assert_eq!(
NdpContext::<EthernetLinkDevice, _>::get_ip_device_state(
&sync_ctx,
dev_id.try_into().expect("expected ethernet ID")
)
.find_addr(&addr),
None
);
add_ip_addr_subnet(
&mut sync_ctx,
&mut non_sync_ctx,
dev_id,
AddrSubnet::new(addr.get(), 128).unwrap(),
)
.unwrap();
assert_eq!(
NdpContext::<EthernetLinkDevice, _>::get_ip_device_state(
&sync_ctx,
dev_id.try_into().expect("expected ethernet ID")
)
.find_addr(&addr)
.unwrap()
.state,
AddressState::Tentative { dad_transmits_remaining: None },
);
}
#[test]
fn test_dad_three_transmits_no_conflicts() {
let Ctx { mut sync_ctx, mut non_sync_ctx } = crate::testutil::DummyCtx::default();
let dev_id = crate::add_ethernet_device(
&mut sync_ctx,
&mut non_sync_ctx,
local_mac(),
Ipv6::MINIMUM_LINK_MTU.into(),
);
crate::device::testutil::enable_device(&mut sync_ctx, &mut non_sync_ctx, dev_id);
// Enable DAD.
crate::device::update_ipv6_configuration(
&mut sync_ctx,
&mut non_sync_ctx,
dev_id,
|config| {
config.ip_config.ip_enabled = true;
config.dad_transmits = NonZeroU8::new(3);
},
);
add_ip_addr_subnet(
&mut sync_ctx,
&mut non_sync_ctx,
dev_id,
AddrSubnet::new(local_ip().get(), 128).unwrap(),
)
.unwrap();
for _ in 0..3 {
assert_eq!(
non_sync_ctx.trigger_next_timer(&mut sync_ctx, crate::handle_timer).unwrap(),
dad_timer_id(dev_id.try_into().expect("expected ethernet ID"), local_ip())
);
}
assert!(NdpContext::<EthernetLinkDevice, _>::get_ip_device_state(
&sync_ctx,
dev_id.try_into().expect("expected ethernet ID")
)
.find_addr(&local_ip())
.unwrap()
.state
.is_assigned());
}
#[test]
fn test_dad_three_transmits_with_conflicts() {
// Test if the implementation is correct when we have more than 1 NS
// packets to send.
set_logger_for_test();
let mac = UnicastAddr::new(Mac::new([6, 5, 4, 3, 2, 1])).unwrap();
let mut local = DummyEventDispatcherBuilder::default();
assert_eq!(local.add_device(mac), 0);
let mut remote = DummyEventDispatcherBuilder::default();
assert_eq!(remote.add_device(mac), 0);
let device_id = DeviceId::new_ethernet(0);
let mut net = crate::context::testutil::new_legacy_simple_dummy_network(
"local",
local.build(),
"remote",
remote.build(),
);
let update = |ipv6_config: &mut Ipv6DeviceConfiguration| {
ipv6_config.ip_config.ip_enabled = true;
ipv6_config.dad_transmits = NonZeroU8::new(3);
};
net.with_context("local", |Ctx { sync_ctx, non_sync_ctx }| {
crate::device::update_ipv6_configuration(sync_ctx, non_sync_ctx, device_id, update);
add_ip_addr_subnet(
sync_ctx,
non_sync_ctx,
device_id,
AddrSubnet::new(local_ip().get(), 128).unwrap(),
)
.unwrap();
});
net.with_context("remote", |Ctx { sync_ctx, non_sync_ctx }| {
crate::device::update_ipv6_configuration(sync_ctx, non_sync_ctx, device_id, update);
});
let expected_timer_id =
dad_timer_id(device_id.try_into().expect("expected ethernet ID"), local_ip());
// During the first and second period, the remote host is still down.
net.with_context("local", |Ctx { sync_ctx, non_sync_ctx }| {
assert_eq!(
non_sync_ctx.trigger_next_timer(sync_ctx, crate::handle_timer).unwrap(),
expected_timer_id
);
assert_eq!(
non_sync_ctx.trigger_next_timer(sync_ctx, crate::handle_timer).unwrap(),
expected_timer_id
);
});
net.with_context("remote", |Ctx { sync_ctx, non_sync_ctx }| {
add_ip_addr_subnet(
sync_ctx,
non_sync_ctx,
device_id,
AddrSubnet::new(local_ip().get(), 128).unwrap(),
)
.unwrap();
});
// The local host should have sent out 3 packets while the remote one
// should only have sent out 1.
assert_eq!(net.non_sync_ctx("local").frames_sent().len(), 3);
assert_eq!(net.non_sync_ctx("remote").frames_sent().len(), 1);
let _: StepResult = net.step(receive_frame_or_panic, handle_timer);
// Let's make sure that all timers are cancelled properly.
net.with_context("local", |Ctx { sync_ctx: _, non_sync_ctx }| {
assert_empty(non_sync_ctx.timer_ctx().timers());
});
net.with_context("remote", |Ctx { sync_ctx: _, non_sync_ctx }| {
assert_empty(non_sync_ctx.timer_ctx().timers());
});
// They should now realize the address they intend to use has a
// duplicate in the local network.
assert_eq!(get_assigned_ipv6_addr_subnets(net.sync_ctx("local"), device_id).count(), 1);
assert_eq!(get_assigned_ipv6_addr_subnets(net.sync_ctx("remote"), device_id).count(), 1);
}
#[test]
fn test_dad_multiple_ips_simultaneously() {
let Ctx { mut sync_ctx, mut non_sync_ctx } = DummyEventDispatcherBuilder::default().build();
let dev_id = crate::add_ethernet_device(
&mut sync_ctx,
&mut non_sync_ctx,
local_mac(),
Ipv6::MINIMUM_LINK_MTU.into(),
);
crate::device::testutil::enable_device(&mut sync_ctx, &mut non_sync_ctx, dev_id);
assert_empty(non_sync_ctx.frames_sent());
crate::device::update_ipv6_configuration(
&mut sync_ctx,
&mut non_sync_ctx,
dev_id,
|ipv6_config| {
ipv6_config.ip_config.ip_enabled = true;
ipv6_config.dad_transmits = NonZeroU8::new(3);
ipv6_config.max_router_solicitations = None;
},
);
// Add an IP.
add_ip_addr_subnet(
&mut sync_ctx,
&mut non_sync_ctx,
dev_id,
AddrSubnet::new(local_ip().get(), 128).unwrap(),
)
.unwrap();
assert!(get_ipv6_device_state(&sync_ctx, dev_id)
.find_addr(&local_ip())
.unwrap()
.state
.is_tentative());
assert_eq!(non_sync_ctx.frames_sent().len(), 1);
// Send another NS.
let local_timer_id =
dad_timer_id(dev_id.try_into().expect("expected ethernet ID"), local_ip());
assert_eq!(
non_sync_ctx.trigger_timers_for(
&mut sync_ctx,
Duration::from_secs(1),
crate::handle_timer
),
[local_timer_id]
);
assert_eq!(non_sync_ctx.frames_sent().len(), 2);
// Add another IP
add_ip_addr_subnet(
&mut sync_ctx,
&mut non_sync_ctx,
dev_id,
AddrSubnet::new(remote_ip().get(), 128).unwrap(),
)
.unwrap();
assert!(get_ipv6_device_state(&sync_ctx, dev_id)
.find_addr(&local_ip())
.unwrap()
.state
.is_tentative());
assert!(get_ipv6_device_state(&sync_ctx, dev_id)
.find_addr(&remote_ip())
.unwrap()
.state
.is_tentative());
assert_eq!(non_sync_ctx.frames_sent().len(), 3);
// Run to the end for DAD for local ip
let remote_timer_id =
dad_timer_id(dev_id.try_into().expect("expected ethernet ID"), remote_ip());
assert_eq!(
non_sync_ctx.trigger_timers_for(
&mut sync_ctx,
Duration::from_secs(2),
crate::handle_timer
),
[local_timer_id, remote_timer_id, local_timer_id, remote_timer_id]
);
assert!(get_ipv6_device_state(&sync_ctx, dev_id)
.find_addr(&local_ip())
.unwrap()
.state
.is_assigned());
assert!(get_ipv6_device_state(&sync_ctx, dev_id)
.find_addr(&remote_ip())
.unwrap()
.state
.is_tentative());
assert_eq!(non_sync_ctx.frames_sent().len(), 6);
// Run to the end for DAD for local ip
assert_eq!(
non_sync_ctx.trigger_timers_for(
&mut sync_ctx,
Duration::from_secs(1),
crate::handle_timer
),
[remote_timer_id]
);
assert!(get_ipv6_device_state(&sync_ctx, dev_id)
.find_addr(&local_ip())
.unwrap()
.state
.is_assigned());
assert!(get_ipv6_device_state(&sync_ctx, dev_id)
.find_addr(&remote_ip())
.unwrap()
.state
.is_assigned());
assert_eq!(non_sync_ctx.frames_sent().len(), 6);
// No more timers.
assert_eq!(non_sync_ctx.trigger_next_timer(&mut sync_ctx, crate::handle_timer), None);
}
#[test]
fn test_dad_cancel_when_ip_removed() {
let Ctx { mut sync_ctx, mut non_sync_ctx } = DummyEventDispatcherBuilder::default().build();
let dev_id = crate::add_ethernet_device(
&mut sync_ctx,
&mut non_sync_ctx,
local_mac(),
Ipv6::MINIMUM_LINK_MTU.into(),
);
crate::device::testutil::enable_device(&mut sync_ctx, &mut non_sync_ctx, dev_id);
// Enable DAD.
crate::device::update_ipv6_configuration(
&mut sync_ctx,
&mut non_sync_ctx,
dev_id,
|ipv6_config| {
ipv6_config.ip_config.ip_enabled = true;
ipv6_config.dad_transmits = NonZeroU8::new(3);
ipv6_config.max_router_solicitations = None;
},
);
assert_empty(non_sync_ctx.frames_sent());
// Add an IP.
add_ip_addr_subnet(
&mut sync_ctx,
&mut non_sync_ctx,
dev_id,
AddrSubnet::new(local_ip().get(), 128).unwrap(),
)
.unwrap();
assert!(get_ipv6_device_state(&sync_ctx, dev_id)
.find_addr(&local_ip())
.unwrap()
.state
.is_tentative());
assert_eq!(non_sync_ctx.frames_sent().len(), 1);
// Send another NS.
let local_timer_id =
dad_timer_id(dev_id.try_into().expect("expected ethernet ID"), local_ip());
assert_eq!(
non_sync_ctx.trigger_timers_for(
&mut sync_ctx,
Duration::from_secs(1),
crate::handle_timer
),
[local_timer_id]
);
assert_eq!(non_sync_ctx.frames_sent().len(), 2);
// Add another IP
add_ip_addr_subnet(
&mut sync_ctx,
&mut non_sync_ctx,
dev_id,
AddrSubnet::new(remote_ip().get(), 128).unwrap(),
)
.unwrap();
assert!(get_ipv6_device_state(&sync_ctx, dev_id)
.find_addr(&local_ip())
.unwrap()
.state
.is_tentative());
assert!(get_ipv6_device_state(&sync_ctx, dev_id)
.find_addr(&remote_ip())
.unwrap()
.state
.is_tentative());
assert_eq!(non_sync_ctx.frames_sent().len(), 3);
// Run 1s
let remote_timer_id =
dad_timer_id(dev_id.try_into().expect("expected ethernet ID"), remote_ip());
assert_eq!(
non_sync_ctx.trigger_timers_for(
&mut sync_ctx,
Duration::from_secs(1),
crate::handle_timer
),
[local_timer_id, remote_timer_id]
);
assert!(get_ipv6_device_state(&sync_ctx, dev_id)
.find_addr(&local_ip())
.unwrap()
.state
.is_tentative());
assert!(get_ipv6_device_state(&sync_ctx, dev_id)
.find_addr(&remote_ip())
.unwrap()
.state
.is_tentative());
assert_eq!(non_sync_ctx.frames_sent().len(), 5);
// Remove local ip
del_ip_addr(&mut sync_ctx, &mut non_sync_ctx, dev_id, &local_ip().into_specified())
.unwrap();
assert_eq!(get_ipv6_device_state(&sync_ctx, dev_id).find_addr(&local_ip()), None);
assert!(get_ipv6_device_state(&sync_ctx, dev_id)
.find_addr(&remote_ip())
.unwrap()
.state
.is_tentative());
assert_eq!(non_sync_ctx.frames_sent().len(), 5);
// Run to the end for DAD for local ip
assert_eq!(
non_sync_ctx.trigger_timers_for(
&mut sync_ctx,
Duration::from_secs(2),
crate::handle_timer
),
[remote_timer_id, remote_timer_id]
);
assert_eq!(get_ipv6_device_state(&sync_ctx, dev_id).find_addr(&local_ip()), None);
assert!(get_ipv6_device_state(&sync_ctx, dev_id)
.find_addr(&remote_ip())
.unwrap()
.state
.is_assigned());
assert_eq!(non_sync_ctx.frames_sent().len(), 6);
// No more timers.
assert_eq!(non_sync_ctx.trigger_next_timer(&mut sync_ctx, crate::handle_timer), None);
}
trait UnwrapNdp<B: ByteSlice> {
fn unwrap_ndp(self) -> NdpPacket<B>;
}
impl<B: ByteSlice> UnwrapNdp<B> for Icmpv6Packet<B> {
fn unwrap_ndp(self) -> NdpPacket<B> {
match self {
Icmpv6Packet::Ndp(ndp) => ndp,
_ => unreachable!(),
}
}
}
#[test]
fn test_receiving_router_solicitation_validity_check() {
let config = Ipv6::DUMMY_CONFIG;
let src_ip = Ipv6Addr::from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 168, 0, 10]);
let src_mac = [10, 11, 12, 13, 14, 15];
let options = vec![NdpOptionBuilder::SourceLinkLayerAddress(&src_mac[..])];
// Test receiving NDP RS when not a router (should not receive)
let Ctx { mut sync_ctx, mut non_sync_ctx } =
DummyEventDispatcherBuilder::from_config(config.clone()).build();
let device_id = DeviceId::new_ethernet(0);
let mut icmpv6_packet_buf = OptionSequenceBuilder::new(options.iter())
.into_serializer()
.encapsulate(IcmpPacketBuilder::<Ipv6, &[u8], _>::new(
src_ip,
config.local_ip,
IcmpUnusedCode,
RouterSolicitation::default(),
))
.serialize_vec_outer()
.unwrap();
let icmpv6_packet = icmpv6_packet_buf
.parse_with::<_, Icmpv6Packet<_>>(IcmpParseArgs::new(src_ip, config.local_ip))
.unwrap();
sync_ctx.receive_ndp_packet(
&mut non_sync_ctx,
device_id,
src_ip.try_into().unwrap(),
config.local_ip,
icmpv6_packet.unwrap_ndp(),
);
assert_eq!(get_counter_val(&mut sync_ctx, "ndp::rx_router_solicitation"), 0);
}
#[test]
fn test_receiving_router_advertisement_validity_check() {
let config = Ipv6::DUMMY_CONFIG;
let src_mac = [10, 11, 12, 13, 14, 15];
let src_ip = Ipv6Addr::from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 168, 0, 10]);
let Ctx { mut sync_ctx, mut non_sync_ctx } =
DummyEventDispatcherBuilder::from_config(config.clone()).build();
let device_id = DeviceId::new_ethernet(0);
// Test receiving NDP RA where source IP is not a link local address
// (should not receive).
let mut icmpv6_packet_buf = router_advertisement_message(
src_ip.into(),
config.local_ip.get(),
1,
false,
false,
3,
4,
5,
);
let icmpv6_packet = icmpv6_packet_buf
.parse_with::<_, Icmpv6Packet<_>>(IcmpParseArgs::new(src_ip, config.local_ip))
.unwrap();
sync_ctx.receive_ndp_packet(
&mut non_sync_ctx,
device_id,
src_ip.try_into().unwrap(),
config.local_ip,
icmpv6_packet.unwrap_ndp(),
);
assert_eq!(get_counter_val(&mut sync_ctx, "ndp::rx_router_advertisement"), 0);
// Test receiving NDP RA where source IP is a link local address (should
// receive).
let src_ip = Mac::new(src_mac).to_ipv6_link_local().addr().get();
let mut icmpv6_packet_buf =
router_advertisement_message(src_ip, config.local_ip.get(), 1, false, false, 3, 4, 5);
let icmpv6_packet = icmpv6_packet_buf
.parse_with::<_, Icmpv6Packet<_>>(IcmpParseArgs::new(src_ip, config.local_ip))
.unwrap();
sync_ctx.receive_ndp_packet(
&mut non_sync_ctx,
device_id,
src_ip.try_into().unwrap(),
config.local_ip,
icmpv6_packet.unwrap_ndp(),
);
assert_eq!(get_counter_val(&mut sync_ctx, "ndp::rx_router_advertisement"), 1);
}
#[test]
fn test_sending_ipv6_packet_after_hop_limit_change() {
// Sets the hop limit with a router advertisement and sends a packet to
// make sure the packet uses the new hop limit.
fn inner_test(
sync_ctx: &mut crate::testutil::DummySyncCtx,
ctx: &mut crate::testutil::DummyNonSyncCtx,
hop_limit: u8,
frame_offset: usize,
) {
let config = Ipv6::DUMMY_CONFIG;
let device_id = DeviceId::new_ethernet(0);
let src_ip = config.remote_mac.to_ipv6_link_local().addr();
let mut icmpv6_packet_buf = router_advertisement_message(
src_ip.get(),
config.local_ip.get(),
hop_limit,
false,
false,
0,
0,
0,
);
let icmpv6_packet = icmpv6_packet_buf
.parse_with::<_, Icmpv6Packet<_>>(IcmpParseArgs::new(src_ip, config.local_ip))
.unwrap();
sync_ctx.receive_ndp_packet(
ctx,
device_id,
Ipv6SourceAddr::from_witness(src_ip).unwrap(),
config.local_ip,
icmpv6_packet.unwrap_ndp(),
);
assert_eq!(get_ipv6_hop_limit(sync_ctx, device_id).get(), hop_limit);
crate::ip::send_ipv6_packet_from_device(
sync_ctx,
ctx,
SendIpPacketMeta {
device: device_id,
src_ip: Some(config.local_ip),
dst_ip: config.remote_ip,
next_hop: config.remote_ip,
proto: IpProto::Tcp.into(),
ttl: None,
mtu: None,
},
Buf::new(vec![0; 10], ..),
)
.unwrap();
let (buf, _, _, _) =
parse_ethernet_frame(&ctx.frames_sent()[frame_offset].1[..]).unwrap();
// Packet's hop limit should be 100.
assert_eq!(buf[7], hop_limit);
}
let Ctx { mut sync_ctx, mut non_sync_ctx } =
DummyEventDispatcherBuilder::from_config(Ipv6::DUMMY_CONFIG).build();
// Set hop limit to 100.
inner_test(&mut sync_ctx, &mut non_sync_ctx, 100, 0);
// Set hop limit to 30.
inner_test(&mut sync_ctx, &mut non_sync_ctx, 30, 1);
}
#[test]
fn test_receiving_router_advertisement_source_link_layer_option() {
let config = Ipv6::DUMMY_CONFIG;
let Ctx { mut sync_ctx, mut non_sync_ctx } =
DummyEventDispatcherBuilder::from_config(config.clone()).build();
let device_id = DeviceId::new_ethernet(0);
let src_mac = Mac::new([10, 11, 12, 13, 14, 15]);
let src_ip = src_mac.to_ipv6_link_local().addr();
let src_mac_bytes = src_mac.bytes();
let options = vec![NdpOptionBuilder::SourceLinkLayerAddress(&src_mac_bytes[..])];
// First receive a Router Advertisement without the source link layer
// and make sure no new neighbor gets added.
let mut icmpv6_packet_buf = router_advertisement_message(
src_ip.get(),
config.local_ip.get(),
1,
false,
false,
3,
4,
5,
);
let icmpv6_packet = icmpv6_packet_buf
.parse_with::<_, Icmpv6Packet<_>>(IcmpParseArgs::new(src_ip, config.local_ip))
.unwrap();
let ndp_state = StateContext::<_, NdpState<EthernetLinkDevice>, _>::get_state_mut_with(
&mut sync_ctx,
device_id.try_into().expect("expected ethernet ID"),
);
assert_eq!(ndp_state.neighbors.get_neighbor_state(&src_ip), None);
sync_ctx.receive_ndp_packet(
&mut non_sync_ctx,
device_id,
Ipv6SourceAddr::from_witness(src_ip).unwrap(),
config.local_ip,
icmpv6_packet.unwrap_ndp(),
);
assert_eq!(get_counter_val(&mut sync_ctx, "ndp::rx_router_advertisement"), 1);
let ndp_state = StateContext::<_, NdpState<EthernetLinkDevice>, _>::get_state_mut_with(
&mut sync_ctx,
device_id.try_into().expect("expected ethernet ID"),
);
// Should still not have a neighbor added.
assert_eq!(ndp_state.neighbors.get_neighbor_state(&src_ip), None);
// Receive a new RA but with the source link layer option
let mut icmpv6_packet_buf = OptionSequenceBuilder::new(options.iter())
.into_serializer()
.encapsulate(IcmpPacketBuilder::<Ipv6, &[u8], _>::new(
src_ip,
config.local_ip,
IcmpUnusedCode,
RouterAdvertisement::new(1, false, false, 3, 4, 5),
))
.serialize_vec_outer()
.unwrap();
let icmpv6_packet = icmpv6_packet_buf
.parse_with::<_, Icmpv6Packet<_>>(IcmpParseArgs::new(src_ip, config.local_ip))
.unwrap();
sync_ctx.receive_ndp_packet(
&mut non_sync_ctx,
device_id,
Ipv6SourceAddr::from_witness(src_ip).unwrap(),
config.local_ip,
icmpv6_packet.unwrap_ndp(),
);
assert_eq!(get_counter_val(&mut sync_ctx, "ndp::rx_router_advertisement"), 2);
let ndp_state = StateContext::<_, NdpState<EthernetLinkDevice>, _>::get_state_mut_with(
&mut sync_ctx,
device_id.try_into().expect("expected ethernet ID"),
);
let neighbor = ndp_state.neighbors.get_neighbor_state(&src_ip).unwrap();
assert_eq!(neighbor.link_address.unwrap(), src_mac);
// Router should be marked stale as a neighbor.
assert_eq!(neighbor.state, NeighborEntryState::Stale);
}
#[test]
fn test_receiving_router_advertisement_mtu_option() {
fn packet_buf(src_ip: Ipv6Addr, dst_ip: Ipv6Addr, mtu: u32) -> Buf<Vec<u8>> {
let options = &[NdpOptionBuilder::Mtu(mtu)];
OptionSequenceBuilder::new(options.iter())
.into_serializer()
.encapsulate(IcmpPacketBuilder::<Ipv6, &[u8], _>::new(
src_ip,
dst_ip,
IcmpUnusedCode,
RouterAdvertisement::new(1, false, false, 3, 4, 5),
))
.serialize_vec_outer()
.unwrap()
.unwrap_b()
}
let config = Ipv6::DUMMY_CONFIG;
let Ctx { mut sync_ctx, mut non_sync_ctx } = DummyEventDispatcherBuilder::default().build();
let hw_mtu = 5000;
let device =
crate::add_ethernet_device(&mut sync_ctx, &mut non_sync_ctx, local_mac(), hw_mtu);
let src_mac = Mac::new([10, 11, 12, 13, 14, 15]);
let src_ip = src_mac.to_ipv6_link_local().addr();
crate::device::testutil::enable_device(&mut sync_ctx, &mut non_sync_ctx, device);
// Receive a new RA with a valid MTU option (but the new MTU should only
// be 5000 as that is the max MTU of the device).
let mut icmpv6_packet_buf = packet_buf(src_ip.get(), config.local_ip.get(), 5781);
let icmpv6_packet = icmpv6_packet_buf
.parse_with::<_, Icmpv6Packet<_>>(IcmpParseArgs::new(src_ip, config.local_ip))
.unwrap();
sync_ctx.receive_ndp_packet(
&mut non_sync_ctx,
device,
Ipv6SourceAddr::from_witness(src_ip).unwrap(),
config.local_ip,
icmpv6_packet.unwrap_ndp(),
);
assert_eq!(get_counter_val(&mut sync_ctx, "ndp::rx_router_advertisement"), 1);
assert_eq!(crate::ip::IpDeviceContext::<Ipv6, _>::get_mtu(&sync_ctx, device), hw_mtu);
// Receive a new RA with an invalid MTU option (value is lower than IPv6
// min MTU).
let mut icmpv6_packet_buf =
packet_buf(src_ip.get(), config.local_ip.get(), u32::from(Ipv6::MINIMUM_LINK_MTU) - 1);
let icmpv6_packet = icmpv6_packet_buf
.parse_with::<_, Icmpv6Packet<_>>(IcmpParseArgs::new(src_ip, config.local_ip))
.unwrap();
sync_ctx.receive_ndp_packet(
&mut non_sync_ctx,
device,
Ipv6SourceAddr::from_witness(src_ip).unwrap(),
config.local_ip,
icmpv6_packet.unwrap_ndp(),
);
assert_eq!(get_counter_val(&mut sync_ctx, "ndp::rx_router_advertisement"), 2);
assert_eq!(crate::ip::IpDeviceContext::<Ipv6, _>::get_mtu(&sync_ctx, device), hw_mtu);
// Receive a new RA with a valid MTU option (value is exactly IPv6 min
// MTU).
let mut icmpv6_packet_buf =
packet_buf(src_ip.get(), config.local_ip.get(), Ipv6::MINIMUM_LINK_MTU.into());
let icmpv6_packet = icmpv6_packet_buf
.parse_with::<_, Icmpv6Packet<_>>(IcmpParseArgs::new(src_ip, config.local_ip))
.unwrap();
sync_ctx.receive_ndp_packet(
&mut non_sync_ctx,
device,
Ipv6SourceAddr::from_witness(src_ip).unwrap(),
config.local_ip,
icmpv6_packet.unwrap_ndp(),
);
assert_eq!(get_counter_val(&mut sync_ctx, "ndp::rx_router_advertisement"), 3);
assert_eq!(
crate::ip::IpDeviceContext::<Ipv6, _>::get_mtu(&sync_ctx, device),
Ipv6::MINIMUM_LINK_MTU.into()
);
}
#[test]
fn test_host_send_router_solicitations() {
fn validate_params(
src_mac: Mac,
src_ip: Ipv6Addr,
message: RouterSolicitation,
code: IcmpUnusedCode,
) {
let dummy_config = Ipv6::DUMMY_CONFIG;
assert_eq!(src_mac, dummy_config.local_mac.get());
assert_eq!(src_ip, dummy_config.local_mac.to_ipv6_link_local().addr().get());
assert_eq!(message, RouterSolicitation::default());
assert_eq!(code, IcmpUnusedCode);
}
let dummy_config = Ipv6::DUMMY_CONFIG;
let Ctx { mut sync_ctx, mut non_sync_ctx } = crate::testutil::DummyCtx::default();
assert_empty(non_sync_ctx.frames_sent());
let device_id = crate::add_ethernet_device(
&mut sync_ctx,
&mut non_sync_ctx,
dummy_config.local_mac,
Ipv6::MINIMUM_LINK_MTU.into(),
);
crate::device::update_ipv6_configuration(
&mut sync_ctx,
&mut non_sync_ctx,
device_id,
|config| {
config.ip_config.ip_enabled = true;
// Test expects to send 3 RSs.
config.max_router_solicitations = NonZeroU8::new(3);
},
);
assert_empty(non_sync_ctx.frames_sent());
let time = non_sync_ctx.now();
assert_eq!(
non_sync_ctx.trigger_next_timer(&mut sync_ctx, crate::handle_timer).unwrap(),
rs_timer_id(device_id.try_into().expect("expected ethernet ID")).into()
);
// Initial router solicitation should be a random delay between 0 and
// `MAX_RTR_SOLICITATION_DELAY`.
assert!(non_sync_ctx.now().duration_since(time) < MAX_RTR_SOLICITATION_DELAY);
assert_eq!(non_sync_ctx.frames_sent().len(), 1);
let (src_mac, _, src_ip, _, _, message, code) =
parse_icmp_packet_in_ip_packet_in_ethernet_frame::<Ipv6, _, RouterSolicitation, _>(
&non_sync_ctx.frames_sent()[0].1,
|_| {},
)
.unwrap();
validate_params(src_mac, src_ip, message, code);
// Should get 2 more router solicitation messages
let time = non_sync_ctx.now();
assert_eq!(
non_sync_ctx.trigger_next_timer(&mut sync_ctx, crate::handle_timer).unwrap(),
rs_timer_id(device_id.try_into().expect("expected ethernet ID")).into()
);
assert_eq!(non_sync_ctx.now().duration_since(time), RTR_SOLICITATION_INTERVAL);
let (src_mac, _, src_ip, _, _, message, code) =
parse_icmp_packet_in_ip_packet_in_ethernet_frame::<Ipv6, _, RouterSolicitation, _>(
&non_sync_ctx.frames_sent()[1].1,
|_| {},
)
.unwrap();
validate_params(src_mac, src_ip, message, code);
// Before the next one, lets assign an IP address (DAD won't be
// performed so it will be assigned immediately). The router solicitation
// message should continue to use the link-local address.
add_ip_addr_subnet(
&mut sync_ctx,
&mut non_sync_ctx,
device_id,
AddrSubnet::new(dummy_config.local_ip.get(), 128).unwrap(),
)
.unwrap();
let time = non_sync_ctx.now();
assert_eq!(
non_sync_ctx.trigger_next_timer(&mut sync_ctx, crate::handle_timer).unwrap(),
rs_timer_id(device_id.try_into().expect("expected ethernet ID")).into()
);
assert_eq!(non_sync_ctx.now().duration_since(time), RTR_SOLICITATION_INTERVAL);
let (src_mac, _, src_ip, _, _, message, code) =
parse_icmp_packet_in_ip_packet_in_ethernet_frame::<Ipv6, _, RouterSolicitation, _>(
&non_sync_ctx.frames_sent()[2].1,
|p| {
// We should have a source link layer option now because we
// have a source IP address set.
assert_eq!(p.body().iter().count(), 1);
if let Some(ll) = get_source_link_layer_option::<Mac, _>(p.body()) {
assert_eq!(ll, dummy_config.local_mac.get());
} else {
panic!("Should have a source link layer option");
}
},
)
.unwrap();
validate_params(src_mac, src_ip, message, code);
// No more timers.
assert_eq!(non_sync_ctx.trigger_next_timer(&mut sync_ctx, crate::handle_timer), None);
// Should have only sent 3 packets (Router solicitations).
assert_eq!(non_sync_ctx.frames_sent().len(), 3);
let Ctx { mut sync_ctx, mut non_sync_ctx } = crate::testutil::DummyCtx::default();
assert_empty(non_sync_ctx.frames_sent());
let device_id = crate::add_ethernet_device(
&mut sync_ctx,
&mut non_sync_ctx,
dummy_config.local_mac,
Ipv6::MINIMUM_LINK_MTU.into(),
);
crate::device::update_ipv6_configuration(
&mut sync_ctx,
&mut non_sync_ctx,
device_id,
|config| {
config.ip_config.ip_enabled = true;
config.max_router_solicitations = NonZeroU8::new(2);
},
);
assert_empty(non_sync_ctx.frames_sent());
let time = non_sync_ctx.now();
assert_eq!(
non_sync_ctx.trigger_next_timer(&mut sync_ctx, crate::handle_timer).unwrap(),
rs_timer_id(device_id.try_into().expect("expected ethernet ID")).into()
);
// Initial router solicitation should be a random delay between 0 and
// `MAX_RTR_SOLICITATION_DELAY`.
assert!(non_sync_ctx.now().duration_since(time) < MAX_RTR_SOLICITATION_DELAY);
assert_eq!(non_sync_ctx.frames_sent().len(), 1);
// Should trigger 1 more router solicitations
let time = non_sync_ctx.now();
assert_eq!(
non_sync_ctx.trigger_next_timer(&mut sync_ctx, crate::handle_timer).unwrap(),
rs_timer_id(device_id.try_into().expect("expected ethernet ID")).into()
);
assert_eq!(non_sync_ctx.now().duration_since(time), RTR_SOLICITATION_INTERVAL);
assert_eq!(non_sync_ctx.frames_sent().len(), 2);
// Each packet would be the same.
for f in non_sync_ctx.frames_sent() {
let (src_mac, _, src_ip, _, _, message, code) =
parse_icmp_packet_in_ip_packet_in_ethernet_frame::<Ipv6, _, RouterSolicitation, _>(
&f.1,
|_| {},
)
.unwrap();
validate_params(src_mac, src_ip, message, code);
}
// No more timers.
assert_eq!(non_sync_ctx.trigger_next_timer(&mut sync_ctx, crate::handle_timer), None);
}
#[test]
fn test_router_solicitation_on_routing_enabled_changes() {
// Make sure that when an interface goes from host -> router, it stops
// sending Router Solicitations, and starts sending them when it goes
// form router -> host as routers should not send Router Solicitation
// messages, but hosts should.
let dummy_config = Ipv6::DUMMY_CONFIG;
// If netstack is not set to forward packets, make sure router
// solicitations do not get cancelled when we enable forwarding on the
// device.
let Ctx { mut sync_ctx, mut non_sync_ctx } = DummyEventDispatcherBuilder::default().build();
assert_empty(non_sync_ctx.frames_sent());
assert_empty(non_sync_ctx.timer_ctx().timers());
let device = crate::add_ethernet_device(
&mut sync_ctx,
&mut non_sync_ctx,
dummy_config.local_mac,
Ipv6::MINIMUM_LINK_MTU.into(),
);
crate::device::update_ipv6_configuration(
&mut sync_ctx,
&mut non_sync_ctx,
device,
|config| {
config.ip_config.ip_enabled = true;
// Doesn't matter as long as we are configured to send at least 2
// solicitations.
config.max_router_solicitations = NonZeroU8::new(2);
},
);
let timer_id = rs_timer_id(device.try_into().expect("expected ethernet ID")).into();
// Send the first router solicitation.
assert_empty(non_sync_ctx.frames_sent());
non_sync_ctx.timer_ctx().assert_timers_installed([(timer_id, ..)]);
assert_eq!(
non_sync_ctx.trigger_next_timer(&mut sync_ctx, crate::handle_timer).unwrap(),
timer_id
);
// Should have sent a router solicitation and still have the timer
// setup.
assert_eq!(non_sync_ctx.frames_sent().len(), 1);
let (_, _dst_mac, _, _, _, _, _) =
parse_icmp_packet_in_ip_packet_in_ethernet_frame::<Ipv6, _, RouterSolicitation, _>(
&non_sync_ctx.frames_sent()[0].1,
|_| {},
)
.unwrap();
non_sync_ctx.timer_ctx().assert_timers_installed([(timer_id, ..)]);
// Enable routing on device.
set_ipv6_routing_enabled(&mut sync_ctx, &mut non_sync_ctx, device, true)
.expect("error setting routing enabled");
assert!(is_ipv6_routing_enabled(&sync_ctx, device));
// Should have not sent any new packets, but unset the router
// solicitation timer.
assert_eq!(non_sync_ctx.frames_sent().len(), 1);
assert_empty(non_sync_ctx.timer_ctx().timers().iter().filter(|x| x.1 == timer_id));
// Unsetting routing should succeed.
set_ipv6_routing_enabled(&mut sync_ctx, &mut non_sync_ctx, device, false)
.expect("error setting routing enabled");
assert!(!is_ipv6_routing_enabled(&sync_ctx, device));
assert_eq!(non_sync_ctx.frames_sent().len(), 1);
non_sync_ctx.timer_ctx().assert_timers_installed([(timer_id, ..)]);
// Send the first router solicitation after being turned into a host.
assert_eq!(
non_sync_ctx.trigger_next_timer(&mut sync_ctx, crate::handle_timer).unwrap(),
timer_id
);
// Should have sent a router solicitation.
assert_eq!(non_sync_ctx.frames_sent().len(), 2);
assert_matches::assert_matches!(
parse_icmp_packet_in_ip_packet_in_ethernet_frame::<Ipv6, _, RouterSolicitation, _>(
&non_sync_ctx.frames_sent()[1].1,
|_| {},
),
Ok((_, _, _, _, _, _, _))
);
non_sync_ctx.timer_ctx().assert_timers_installed([(timer_id, ..)]);
}
#[test]
fn test_set_ndp_config_dup_addr_detect_transmits() {
// Test that updating the DupAddrDetectTransmits parameter on an
// interface updates the number of DAD messages (NDP Neighbor
// Solicitations) sent before concluding that an address is not a
// duplicate.
let dummy_config = Ipv6::DUMMY_CONFIG;
let Ctx { mut sync_ctx, mut non_sync_ctx } = DummyEventDispatcherBuilder::default().build();
let device = crate::add_ethernet_device(
&mut sync_ctx,
&mut non_sync_ctx,
dummy_config.local_mac,
Ipv6::MINIMUM_LINK_MTU.into(),
);
crate::device::testutil::enable_device(&mut sync_ctx, &mut non_sync_ctx, device);
assert_empty(non_sync_ctx.frames_sent());
assert_empty(non_sync_ctx.timer_ctx().timers());
// Updating the IP should resolve immediately since DAD is turned off by
// `DummyEventDispatcherBuilder::build`.
add_ip_addr_subnet(
&mut sync_ctx,
&mut non_sync_ctx,
device,
AddrSubnet::new(dummy_config.local_ip.get(), 128).unwrap(),
)
.unwrap();
let device_id = device.try_into().unwrap();
assert_eq!(
NdpContext::<EthernetLinkDevice, _>::get_ip_device_state(&sync_ctx, device_id)
.find_addr(&dummy_config.local_ip.try_into().unwrap())
.unwrap()
.state,
AddressState::Assigned
);
assert_empty(non_sync_ctx.frames_sent());
assert_empty(non_sync_ctx.timer_ctx().timers());
// Enable DAD for the device.
const DUP_ADDR_DETECT_TRANSMITS: u8 = 3;
crate::device::update_ipv6_configuration(
&mut sync_ctx,
&mut non_sync_ctx,
device,
|ipv6_config| {
ipv6_config.ip_config.ip_enabled = true;
ipv6_config.dad_transmits = NonZeroU8::new(DUP_ADDR_DETECT_TRANSMITS);
},
);
// Updating the IP should start the DAD process.
add_ip_addr_subnet(
&mut sync_ctx,
&mut non_sync_ctx,
device,
AddrSubnet::new(dummy_config.remote_ip.get(), 128).unwrap(),
)
.unwrap();
assert_eq!(
NdpContext::<EthernetLinkDevice, _>::get_ip_device_state(&sync_ctx, device_id)
.find_addr(&dummy_config.local_ip.try_into().unwrap())
.unwrap()
.state,
AddressState::Assigned
);
assert_eq!(
NdpContext::<EthernetLinkDevice, _>::get_ip_device_state(&sync_ctx, device_id)
.find_addr(&dummy_config.remote_ip.try_into().unwrap())
.unwrap()
.state,
AddressState::Tentative {
dad_transmits_remaining: NonZeroU8::new(DUP_ADDR_DETECT_TRANSMITS - 1)
}
);
assert_eq!(non_sync_ctx.frames_sent().len(), 1);
assert_eq!(non_sync_ctx.timer_ctx().timers().len(), 1);
// Disable DAD during DAD.
crate::device::update_ipv6_configuration(
&mut sync_ctx,
&mut non_sync_ctx,
device,
|ipv6_config| {
ipv6_config.dad_transmits = None;
},
);
let expected_timer_id = dad_timer_id(device_id, dummy_config.remote_ip.try_into().unwrap());
// Allow already started DAD to complete (2 more more NS, 3 more timers).
assert_eq!(
non_sync_ctx.trigger_next_timer(&mut sync_ctx, crate::handle_timer).unwrap(),
expected_timer_id
);
assert_eq!(non_sync_ctx.frames_sent().len(), 2);
assert_eq!(
non_sync_ctx.trigger_next_timer(&mut sync_ctx, crate::handle_timer).unwrap(),
expected_timer_id
);
assert_eq!(non_sync_ctx.frames_sent().len(), 3);
assert_eq!(
non_sync_ctx.trigger_next_timer(&mut sync_ctx, crate::handle_timer).unwrap(),
expected_timer_id
);
assert_eq!(non_sync_ctx.frames_sent().len(), 3);
assert_eq!(
NdpContext::<EthernetLinkDevice, _>::get_ip_device_state(&sync_ctx, device_id)
.find_addr(&dummy_config.remote_ip.try_into().unwrap())
.unwrap()
.state,
AddressState::Assigned
);
// Updating the IP should resolve immediately since DAD has just been
// turned off.
let new_ip = Ipv6::get_other_ip_address(3);
add_ip_addr_subnet(
&mut sync_ctx,
&mut non_sync_ctx,
device,
AddrSubnet::new(new_ip.get(), 128).unwrap(),
)
.unwrap();
assert_eq!(
NdpContext::<EthernetLinkDevice, _>::get_ip_device_state(&sync_ctx, device_id)
.find_addr(&dummy_config.local_ip.try_into().unwrap())
.unwrap()
.state,
AddressState::Assigned
);
assert_eq!(
NdpContext::<EthernetLinkDevice, _>::get_ip_device_state(&sync_ctx, device_id)
.find_addr(&dummy_config.remote_ip.try_into().unwrap())
.unwrap()
.state,
AddressState::Assigned
);
assert_eq!(
NdpContext::<EthernetLinkDevice, _>::get_ip_device_state(&sync_ctx, device_id)
.find_addr(&new_ip.try_into().unwrap())
.unwrap()
.state,
AddressState::Assigned
);
}
#[test]
fn test_receiving_neighbor_advertisements() {
fn test_receiving_na_from_known_neighbor(
sync_ctx: &mut crate::testutil::DummySyncCtx,
ctx: &mut crate::testutil::DummyNonSyncCtx,
src_ip: Ipv6Addr,
dst_ip: SpecifiedAddr<Ipv6Addr>,
device: DeviceId,
router_flag: bool,
solicited_flag: bool,
override_flag: bool,
mac: Option<Mac>,
expected_state: NeighborEntryState,
expected_link_addr: Option<Mac>,
) {
let mut buf = neighbor_advertisement_message(
src_ip,
dst_ip.get(),
router_flag,
solicited_flag,
override_flag,
mac,
);
let packet =
buf.parse_with::<_, Icmpv6Packet<_>>(IcmpParseArgs::new(src_ip, dst_ip)).unwrap();
sync_ctx.receive_ndp_packet(
ctx,
device,
src_ip.try_into().unwrap(),
dst_ip,
packet.unwrap_ndp(),
);
let neighbor_state =
StateContext::<_, NdpState<EthernetLinkDevice>, _>::get_state_mut_with(
sync_ctx,
device.try_into().unwrap(),
)
.neighbors
.get_neighbor_state(&src_ip.try_into().unwrap())
.unwrap();
assert_eq!(neighbor_state.state, expected_state);
assert_eq!(neighbor_state.link_address, expected_link_addr);
}
let config = Ipv6::DUMMY_CONFIG;
let Ctx { mut sync_ctx, mut non_sync_ctx } = DummyEventDispatcherBuilder::default().build();
let device = crate::add_ethernet_device(
&mut sync_ctx,
&mut non_sync_ctx,
config.local_mac,
Ipv6::MINIMUM_LINK_MTU.into(),
);
crate::device::testutil::enable_device(&mut sync_ctx, &mut non_sync_ctx, device);
let neighbor_mac = config.remote_mac.get();
let neighbor_ip = neighbor_mac.to_ipv6_link_local().addr();
let all_nodes_addr = Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS.into_specified();
// Should not know about the neighbor yet.
let device_id = device.try_into().unwrap();
assert_eq!(
StateContext::<_, NdpState<EthernetLinkDevice>, _>::get_state_mut_with(
&mut sync_ctx,
device_id
)
.neighbors
.get_neighbor_state(&neighbor_ip.get()),
None
);
// Receiving unsolicited NA from a neighbor we don't care about yet
// should do nothing.
// Receive the NA.
let mut buf = neighbor_advertisement_message(
neighbor_ip.get(),
all_nodes_addr.get(),
false,
false,
false,
None,
);
let packet = buf
.parse_with::<_, Icmpv6Packet<_>>(IcmpParseArgs::new(neighbor_ip, all_nodes_addr))
.unwrap();
sync_ctx.receive_ndp_packet(
&mut non_sync_ctx,
device,
Ipv6SourceAddr::from_witness(neighbor_ip).unwrap(),
all_nodes_addr,
packet.unwrap_ndp(),
);
// We still do not know about the neighbor since the NA was unsolicited
// and we never were interested in the neighbor yet.
assert_eq!(
StateContext::<_, NdpState<EthernetLinkDevice>, _>::get_state_mut_with(
&mut sync_ctx,
device_id
)
.neighbors
.get_neighbor_state(&neighbor_ip),
None
);
// Receiving solicited NA from a neighbor we don't care about yet should
// do nothing (should never happen).
// Receive the NA.
let mut buf = neighbor_advertisement_message(
neighbor_ip.get(),
all_nodes_addr.get(),
false,
true,
false,
None,
);
let packet = buf
.parse_with::<_, Icmpv6Packet<_>>(IcmpParseArgs::new(neighbor_ip, all_nodes_addr))
.unwrap();
sync_ctx.receive_ndp_packet(
&mut non_sync_ctx,
device,
Ipv6SourceAddr::from_witness(neighbor_ip).unwrap(),
all_nodes_addr,
packet.unwrap_ndp(),
);
// We still do not know about the neighbor since the NA was unsolicited
// and we never were interested in the neighbor yet.
assert_eq!(
StateContext::<_, NdpState<EthernetLinkDevice>, _>::get_state_mut_with(
&mut sync_ctx,
device_id
)
.neighbors
.get_neighbor_state(&neighbor_ip),
None
);
// Receiving solicited NA from a neighbor we are trying to resolve, but
// no target link addr.
//
// Should do nothing (still INCOMPLETE).
// Create incomplete neighbor entry.
let neighbors =
&mut StateContext::<_, NdpState<EthernetLinkDevice>, _>::get_state_mut_with(
&mut sync_ctx,
device_id,
)
.neighbors;
neighbors.add_incomplete_neighbor_state(neighbor_ip.get());
test_receiving_na_from_known_neighbor(
&mut sync_ctx,
&mut non_sync_ctx,
neighbor_ip.get(),
config.local_ip,
device,
false,
true,
false,
None,
NeighborEntryState::Incomplete { transmit_counter: 1 },
None,
);
// Receiving solicited NA from a neighbor we are resolving, but with
// target link addr.
//
// Should update link layer address and set state to REACHABLE.
test_receiving_na_from_known_neighbor(
&mut sync_ctx,
&mut non_sync_ctx,
neighbor_ip.get(),
config.local_ip,
device,
false,
true,
false,
Some(neighbor_mac),
NeighborEntryState::Reachable,
Some(neighbor_mac),
);
// Receive unsolicited NA from a neighbor with router flag updated (no
// target link addr).
//
// Should update is_router to true.
test_receiving_na_from_known_neighbor(
&mut sync_ctx,
&mut non_sync_ctx,
neighbor_ip.get(),
config.local_ip,
device,
true,
false,
false,
None,
NeighborEntryState::Reachable,
Some(neighbor_mac),
);
// Receive unsolicited NA from a neighbor without router flag set and
// same target link addr.
//
// Should update is_router, state should be unchanged.
test_receiving_na_from_known_neighbor(
&mut sync_ctx,
&mut non_sync_ctx,
neighbor_ip.get(),
config.local_ip,
device,
false,
false,
false,
Some(neighbor_mac),
NeighborEntryState::Reachable,
Some(neighbor_mac),
);
// Receive unsolicited NA from a neighbor with new target link addr.
//
// Should NOT update link layer addr, but set state to STALE.
let new_mac = Mac::new([99, 98, 97, 96, 95, 94]);
test_receiving_na_from_known_neighbor(
&mut sync_ctx,
&mut non_sync_ctx,
neighbor_ip.get(),
config.local_ip,
device,
false,
false,
false,
Some(new_mac),
NeighborEntryState::Stale,
Some(neighbor_mac),
);
// Receive unsolicited NA from a neighbor with new target link addr and
// override set.
//
// Should update link layer addr and set state to STALE.
test_receiving_na_from_known_neighbor(
&mut sync_ctx,
&mut non_sync_ctx,
neighbor_ip.get(),
config.local_ip,
device,
false,
false,
true,
Some(new_mac),
NeighborEntryState::Stale,
Some(new_mac),
);
// Receive solicited NA from a neighbor with the same link layer addr.
//
// Should not update link layer addr, but set state to REACHABLE.
test_receiving_na_from_known_neighbor(
&mut sync_ctx,
&mut non_sync_ctx,
neighbor_ip.get(),
config.local_ip,
device,
false,
true,
false,
Some(new_mac),
NeighborEntryState::Reachable,
Some(new_mac),
);
// Receive unsolicited NA from a neighbor with new target link addr and
// override set.
//
// Should update link layer addr, and set state to Stale.
test_receiving_na_from_known_neighbor(
&mut sync_ctx,
&mut non_sync_ctx,
neighbor_ip.get(),
config.local_ip,
device,
false,
false,
true,
Some(neighbor_mac),
NeighborEntryState::Stale,
Some(neighbor_mac),
);
// Receive solicited NA from a neighbor with new target link addr and
// override set.
//
// Should set state to Reachable.
test_receiving_na_from_known_neighbor(
&mut sync_ctx,
&mut non_sync_ctx,
neighbor_ip.get(),
config.local_ip,
device,
false,
true,
true,
Some(neighbor_mac),
NeighborEntryState::Reachable,
Some(neighbor_mac),
);
// Receive unsolicited NA from a neighbor with no target link addr and
// override set.
//
// Should do nothing.
test_receiving_na_from_known_neighbor(
&mut sync_ctx,
&mut non_sync_ctx,
neighbor_ip.get(),
config.local_ip,
device,
false,
false,
true,
None,
NeighborEntryState::Reachable,
Some(neighbor_mac),
);
}
fn slaac_packet_buf(
src_ip: Ipv6Addr,
dst_ip: Ipv6Addr,
prefix: Ipv6Addr,
prefix_length: u8,
on_link_flag: bool,
autonomous_address_configuration_flag: bool,
valid_lifetime_secs: u32,
preferred_lifetime_secs: u32,
) -> Buf<Vec<u8>> {
let p = PrefixInformation::new(
prefix_length,
on_link_flag,
autonomous_address_configuration_flag,
valid_lifetime_secs,
preferred_lifetime_secs,
prefix,
);
let options = &[NdpOptionBuilder::PrefixInformation(p)];
OptionSequenceBuilder::new(options.iter())
.into_serializer()
.encapsulate(IcmpPacketBuilder::<Ipv6, &[u8], _>::new(
src_ip,
dst_ip,
IcmpUnusedCode,
RouterAdvertisement::new(0, false, false, 0, 0, 0),
))
.encapsulate(Ipv6PacketBuilder::new(
src_ip,
dst_ip,
REQUIRED_NDP_IP_PACKET_HOP_LIMIT,
Ipv6Proto::Icmpv6,
))
.serialize_vec_outer()
.unwrap()
.unwrap_b()
}
fn iter_global_ipv6_addrs<
'a,
D: LinkDevice,
C: NdpNonSyncContext<D, SC::DeviceId>,
SC: NdpContext<D, C>,
>(
sync_ctx: &'a SC,
device_id: SC::DeviceId,
) -> impl Iterator<Item = &'a Ipv6AddressEntry<C::Instant>> {
sync_ctx.get_ip_device_state(device_id).iter_addrs().filter(|entry| {
match entry.addr_sub.addr().scope() {
Ipv6Scope::Global => true,
Ipv6Scope::InterfaceLocal
| Ipv6Scope::LinkLocal
| Ipv6Scope::AdminLocal
| Ipv6Scope::SiteLocal
| Ipv6Scope::OrganizationLocal
| Ipv6Scope::Reserved(_)
| Ipv6Scope::Unassigned(_) => false,
}
})
}
#[test]
fn test_router_stateless_address_autoconfiguration() {
// Routers should not perform SLAAC for global addresses.
let config = Ipv6::DUMMY_CONFIG;
let Ctx { mut sync_ctx, mut non_sync_ctx } = DummyEventDispatcherBuilder::default().build();
let device = crate::add_ethernet_device(
&mut sync_ctx,
&mut non_sync_ctx,
config.local_mac,
Ipv6::MINIMUM_LINK_MTU.into(),
);
crate::device::testutil::enable_device(&mut sync_ctx, &mut non_sync_ctx, device);
set_ipv6_routing_enabled(&mut sync_ctx, &mut non_sync_ctx, device, true)
.expect("error setting routing enabled");
let src_mac = config.remote_mac;
let src_ip = src_mac.to_ipv6_link_local().addr().get();
let prefix = Ipv6Addr::from([1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0]);
let prefix_length = 64;
let mut expected_addr = [1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0];
expected_addr[8..].copy_from_slice(&src_mac.to_eui64()[..]);
// Receive a new RA with new prefix (autonomous).
//
// Should not get a new IP.
let icmpv6_packet_buf = slaac_packet_buf(
src_ip,
Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS.get(),
prefix,
prefix_length,
false,
false,
100,
0,
);
receive_ipv6_packet(
&mut sync_ctx,
&mut non_sync_ctx,
device,
FrameDestination::Multicast,
icmpv6_packet_buf,
);
assert_empty(iter_global_ipv6_addrs(&sync_ctx, device.try_into().unwrap()));
// No timers.
assert_eq!(non_sync_ctx.trigger_next_timer(&mut sync_ctx, crate::handle_timer), None);
}
impl From<SlaacTimerId<DeviceId>> for TimerId {
fn from(id: SlaacTimerId<DeviceId>) -> TimerId {
TimerId(TimerIdInner::Ipv6Device(Ipv6DeviceTimerId::Slaac(id)))
}
}
#[derive(Copy, Clone, Debug)]
struct TestSlaacPrefix {
prefix: Subnet<Ipv6Addr>,
valid_for: u32,
preferred_for: u32,
}
impl TestSlaacPrefix {
fn send_prefix_update(
&self,
sync_ctx: &mut crate::testutil::DummySyncCtx,
ctx: &mut crate::testutil::DummyNonSyncCtx,
device: DeviceId,
src_ip: Ipv6Addr,
) {
let Self { prefix, valid_for, preferred_for } = *self;
receive_prefix_update(sync_ctx, ctx, device, src_ip, prefix, preferred_for, valid_for);
}
fn valid_until<I: Instant>(&self, now: I) -> I {
now.checked_add(Duration::from_secs(self.valid_for.into())).unwrap()
}
}
fn slaac_address<I: Instant>(
entry: &Ipv6AddressEntry<I>,
) -> Option<(UnicastAddr<Ipv6Addr>, SlaacConfig<I>)> {
match entry.config {
AddrConfig::Manual => None,
AddrConfig::Slaac(s) => Some((entry.addr_sub().addr(), s)),
}
}
/// Extracts the single static and temporary address config from the provided iterator and
/// returns them as (static, temporary).
///
/// Panics
///
/// Panics if the iterator doesn't contain exactly one static and one temporary SLAAC entry.
fn single_static_and_temporary<
I: Copy + Debug,
A: Copy + Debug,
It: Iterator<Item = (A, SlaacConfig<I>)>,
>(
slaac_configs: It,
) -> ((A, SlaacConfig<I>), (A, SlaacConfig<I>)) {
{
let (static_addresses, temporary_addresses): (Vec<_>, Vec<_>) = slaac_configs
.partition(|(_, s)| if let SlaacConfig::Static { .. } = s { true } else { false });
let static_addresses: [_; 1] =
static_addresses.try_into().expect("expected a single static address");
let temporary_addresses: [_; 1] =
temporary_addresses.try_into().expect("expected a single temporary address");
(static_addresses[0], temporary_addresses[0])
}
}
/// Enables temporary addressing with the provided parameters.
///
/// `rng` is used to initialize the key that is used to generate new addresses.
fn enable_temporary_addresses<R: RngCore>(
config: &mut SlaacConfiguration,
rng: &mut R,
max_valid_lifetime: NonZeroDuration,
max_preferred_lifetime: NonZeroDuration,
max_generation_retries: u8,
) {
let mut secret_key = [0; STABLE_IID_SECRET_KEY_BYTES];
rng.fill_bytes(&mut secret_key);
config.temporary_address_configuration = Some(TemporarySlaacAddressConfiguration {
temp_valid_lifetime: max_valid_lifetime,
temp_preferred_lifetime: max_preferred_lifetime,
temp_idgen_retries: max_generation_retries,
secret_key,
})
}
fn initialize_with_temporary_addresses_enabled(
) -> (crate::testutil::DummyCtx, DeviceId, SlaacConfiguration) {
set_logger_for_test();
let config = Ipv6::DUMMY_CONFIG;
let mut ctx = DummyEventDispatcherBuilder::default().build();
let Ctx { sync_ctx, non_sync_ctx } = &mut ctx;
let device = crate::add_ethernet_device(
sync_ctx,
non_sync_ctx,
config.local_mac,
Ipv6::MINIMUM_LINK_MTU.into(),
);
crate::device::testutil::enable_device(sync_ctx, non_sync_ctx, device);
let max_valid_lifetime = Duration::from_secs(60 * 60);
let max_preferred_lifetime = Duration::from_secs(30 * 60);
let idgen_retries = 3;
let mut slaac_config = SlaacConfiguration::default();
enable_temporary_addresses(
&mut slaac_config,
non_sync_ctx.rng_mut(),
NonZeroDuration::new(max_valid_lifetime).unwrap(),
NonZeroDuration::new(max_preferred_lifetime).unwrap(),
idgen_retries,
);
crate::ip::device::update_ipv6_configuration(
sync_ctx,
non_sync_ctx,
device,
|ipv6_config| {
ipv6_config.slaac_config = slaac_config;
},
);
(ctx, device, slaac_config)
}
#[test]
fn test_host_stateless_address_autoconfiguration_multiple_prefixes() {
let (Ctx { mut sync_ctx, mut non_sync_ctx }, device, _): (_, _, SlaacConfiguration) =
initialize_with_temporary_addresses_enabled();
crate::device::update_ipv6_configuration(
&mut sync_ctx,
&mut non_sync_ctx,
device,
|config| {
config.slaac_config.enable_stable_addresses = true;
},
);
let prefix1 = TestSlaacPrefix {
prefix: subnet_v6!("1:2:3:4::/64"),
valid_for: 1500,
preferred_for: 900,
};
let prefix2 = TestSlaacPrefix {
prefix: subnet_v6!("5:6:7:8::/64"),
valid_for: 1200,
preferred_for: 600,
};
let config = Ipv6::DUMMY_CONFIG;
let src_mac = config.remote_mac;
let src_ip: Ipv6Addr = src_mac.to_ipv6_link_local().addr().get();
// After the RA for the first prefix, we should have two addresses, one
// static and one temporary.
prefix1.send_prefix_update(&mut sync_ctx, &mut non_sync_ctx, device, src_ip);
let (prefix_1_static, prefix_1_temporary) = {
let slaac_configs = iter_global_ipv6_addrs(&sync_ctx, device.try_into().unwrap())
.filter_map(slaac_address)
.filter(|(a, _)| prefix1.prefix.contains(a));
let (static_address, temporary_address) = single_static_and_temporary(slaac_configs);
let now = non_sync_ctx.now();
let prefix1_valid_until = prefix1.valid_until(now);
assert_matches!(static_address, (_addr,
SlaacConfig::Static { valid_until }) => {
assert_eq!(valid_until, Lifetime::Finite(prefix1_valid_until))
});
assert_matches!(temporary_address, (_addr,
SlaacConfig::Temporary(TemporarySlaacConfig {
valid_until,
creation_time,
desync_factor: _,
dad_counter: _ })) => {
assert_eq!(creation_time, now);
assert_eq!(valid_until, prefix1_valid_until);
});
(static_address.0, temporary_address.0)
};
// When the RA for the second prefix comes in, we should leave the entries for the addresses
// in the first prefix alone.
prefix2.send_prefix_update(&mut sync_ctx, &mut non_sync_ctx, device, src_ip);
{
// Check prefix 1 addresses again.
let slaac_configs = iter_global_ipv6_addrs(&sync_ctx, device.try_into().unwrap())
.filter_map(slaac_address)
.filter(|(a, _)| prefix1.prefix.contains(a));
let (static_address, temporary_address) = single_static_and_temporary(slaac_configs);
let now = non_sync_ctx.now();
let prefix1_valid_until = prefix1.valid_until(now);
assert_matches!(static_address, (addr, SlaacConfig::Static { valid_until }) => {
assert_eq!(addr, prefix_1_static);
assert_eq!(valid_until, Lifetime::Finite(prefix1_valid_until));
});
assert_matches!(temporary_address,
(addr, SlaacConfig::Temporary(TemporarySlaacConfig { valid_until, creation_time, desync_factor: _, dad_counter: 0 })) => {
assert_eq!(addr, prefix_1_temporary);
assert_eq!(creation_time, now);
assert_eq!(valid_until, prefix1_valid_until);
});
}
{
// Check prefix 2 addresses.
let slaac_configs = iter_global_ipv6_addrs(&sync_ctx, device.try_into().unwrap())
.filter_map(slaac_address)
.filter(|(a, _)| prefix2.prefix.contains(a));
let (static_address, temporary_address) = single_static_and_temporary(slaac_configs);
let now = non_sync_ctx.now();
let prefix2_valid_until = prefix2.valid_until(now);
assert_matches!(static_address, (_, SlaacConfig::Static { valid_until }) => {
assert_eq!(valid_until, Lifetime::Finite(prefix2_valid_until))
});
assert_matches!(temporary_address,
(_, SlaacConfig::Temporary(TemporarySlaacConfig {
valid_until, creation_time, desync_factor: _, dad_counter: 0 })) => {
assert_eq!(creation_time, now);
assert_eq!(valid_until, prefix2_valid_until);
});
}
}
fn test_host_generate_temporary_slaac_address(
valid_lifetime_in_ra: u32,
preferred_lifetime_in_ra: u32,
) -> (crate::testutil::DummyCtx, DeviceId, UnicastAddr<Ipv6Addr>) {
set_logger_for_test();
let (mut ctx, device, slaac_config) = initialize_with_temporary_addresses_enabled();
let Ctx { sync_ctx, non_sync_ctx } = &mut ctx;
let max_valid_lifetime =
slaac_config.temporary_address_configuration.unwrap().temp_valid_lifetime.get();
let config = Ipv6::DUMMY_CONFIG;
let src_mac = config.remote_mac;
let src_ip = src_mac.to_ipv6_link_local().addr().get();
let subnet = subnet_v6!("0102:0304:0506:0708::/64");
let interface_identifier = generate_opaque_interface_identifier(
subnet,
&config.local_mac.to_eui64()[..],
[],
// Clone the RNG so we can see what the next value (which will be
// used to generate the temporary address) will be.
OpaqueIidNonce::Random(non_sync_ctx.rng().clone().next_u64()),
&slaac_config.temporary_address_configuration.unwrap().secret_key,
);
let mut expected_addr = [1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0];
expected_addr[8..].copy_from_slice(&interface_identifier.to_be_bytes()[..8]);
let expected_addr = UnicastAddr::new(Ipv6Addr::from(expected_addr)).unwrap();
let expected_addr_sub = AddrSubnet::from_witness(expected_addr, subnet.prefix()).unwrap();
assert_eq!(expected_addr_sub.subnet(), subnet);
// Receive a new RA with new prefix (autonomous).
//
// Should get a new temporary IP.
receive_prefix_update(
sync_ctx,
non_sync_ctx,
device,
src_ip,
subnet,
preferred_lifetime_in_ra,
valid_lifetime_in_ra,
);
// Should have gotten a new temporary IP.
let temporary_slaac_addresses =
iter_global_ipv6_addrs(sync_ctx, device.try_into().unwrap())
.filter_map(|entry| match entry.config {
AddrConfig::Slaac(SlaacConfig::Static { .. }) => None,
AddrConfig::Slaac(SlaacConfig::Temporary(TemporarySlaacConfig {
creation_time: _,
desync_factor: _,
valid_until,
dad_counter: _,
})) => Some((entry.addr_sub(), entry.state, valid_until)),
AddrConfig::Manual => None,
})
.collect::<Vec<_>>();
assert_eq!(temporary_slaac_addresses.len(), 1);
let (addr_sub, state, valid_until) = temporary_slaac_addresses.into_iter().next().unwrap();
assert_eq!(addr_sub.subnet(), subnet);
assert_eq!(state, AddressState::Assigned);
assert!(valid_until <= non_sync_ctx.now().checked_add(max_valid_lifetime).unwrap());
(ctx, device, expected_addr)
}
const INFINITE_LIFETIME: u32 = u32::MAX;
#[test]
fn test_host_temporary_slaac_and_manual_addresses_conflict() {
// Verify that if the temporary SLAAC address generation picks an
// address that is already assigned, it tries again. The difficulty here
// is that the test uses an RNG to pick an address. To make sure we
// assign the address that the code _would_ pick, we run the code twice
// with the same RNG seed and parameters. The first time is lets us
// figure out the temporary address that is generated. Then, we run the
// same code with the address already assigned to verify the behavior.
const RNG_SEED: [u8; 16] = [1; 16];
let config = Ipv6::DUMMY_CONFIG;
let src_mac = config.remote_mac;
let src_ip = src_mac.to_ipv6_link_local().addr().get();
let subnet = subnet_v6!("0102:0304:0506:0708::/64");
// Receive an RA to figure out the temporary address that is assigned.
let conflicted_addr = {
let (Ctx { mut sync_ctx, mut non_sync_ctx }, device, _config) =
initialize_with_temporary_addresses_enabled();
*non_sync_ctx.rng_mut() = rand::SeedableRng::from_seed(RNG_SEED);
// Receive an RA and determine what temporary address was assigned, then return it.
receive_prefix_update(
&mut sync_ctx,
&mut non_sync_ctx,
device,
src_ip,
subnet,
9000,
10000,
);
*get_matching_slaac_address_entry(&sync_ctx, device, |entry| match entry.config {
AddrConfig::Slaac(SlaacConfig::Static { valid_until: _ }) => false,
AddrConfig::Slaac(SlaacConfig::Temporary(_)) => true,
AddrConfig::Manual => false,
})
.unwrap()
.addr_sub()
};
assert!(subnet.contains(&conflicted_addr.addr().get()));
// Now that we know what address will be assigned, create a new instance
// of the stack and assign that same address manually.
let (Ctx { mut sync_ctx, mut non_sync_ctx }, device, _config) =
initialize_with_temporary_addresses_enabled();
let device_id = device.try_into().unwrap();
crate::device::add_ip_addr_subnet(
&mut sync_ctx,
&mut non_sync_ctx,
device,
conflicted_addr.to_witness(),
)
.expect("adding address failed");
// Sanity check: `conflicted_addr` is already assigned on the device.
assert_matches!(
iter_global_ipv6_addrs(&sync_ctx, device_id)
.find(|entry| entry.addr_sub() == &conflicted_addr),
Some(_)
);
// Seed the RNG right before the RA is received, just like in our
// earlier run above.
*non_sync_ctx.rng_mut() = rand::SeedableRng::from_seed(RNG_SEED);
// Receive a new RA with new prefix (autonomous). The system will assign
// a temporary and static SLAAC address. The first temporary address
// tried will conflict with `conflicted_addr` assigned above, so a
// different one will be generated.
receive_prefix_update(
&mut sync_ctx,
&mut non_sync_ctx,
device,
src_ip,
subnet,
9000,
10000,
);
// Verify that `conflicted_addr` was generated and rejected.
assert_eq!(get_counter_val(&mut sync_ctx, "generated_slaac_addr_exists"), 1);
// Should have gotten a new temporary IP.
let temporary_slaac_addresses =
get_matching_slaac_address_entries(&sync_ctx, device, |entry| match entry.config {
AddrConfig::Slaac(SlaacConfig::Static { valid_until: _ }) => false,
AddrConfig::Slaac(SlaacConfig::Temporary(_)) => true,
AddrConfig::Manual => false,
})
.map(|entry| entry.addr_sub())
.collect::<Vec<_>>();
assert_matches!(&temporary_slaac_addresses[..], [&temporary_addr] => {
assert_eq!(temporary_addr.subnet(), conflicted_addr.subnet());
assert_ne!(temporary_addr, conflicted_addr);
});
}
#[test]
fn test_host_slaac_invalid_prefix_information() {
let config = Ipv6::DUMMY_CONFIG;
let Ctx { mut sync_ctx, mut non_sync_ctx } = DummyEventDispatcherBuilder::default().build();
let device = crate::add_ethernet_device(
&mut sync_ctx,
&mut non_sync_ctx,
config.local_mac,
Ipv6::MINIMUM_LINK_MTU.into(),
);
crate::device::testutil::enable_device(&mut sync_ctx, &mut non_sync_ctx, device);
let src_mac = config.remote_mac;
let src_ip = src_mac.to_ipv6_link_local().addr().get();
let prefix = Ipv6Addr::from([1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0]);
let prefix_length = 64;
let device_id = device.try_into().unwrap();
assert_empty(iter_global_ipv6_addrs(&sync_ctx, device_id));
// Receive a new RA with new prefix (autonomous), but preferred lifetime
// is greater than valid.
//
// Should not get a new IP.
let icmpv6_packet_buf = slaac_packet_buf(
src_ip,
Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS.get(),
prefix,
prefix_length,
false,
true,
9000,
10000,
);
receive_ipv6_packet(
&mut sync_ctx,
&mut non_sync_ctx,
device,
FrameDestination::Multicast,
icmpv6_packet_buf,
);
assert_empty(iter_global_ipv6_addrs(&sync_ctx, device_id));
// Address invalidation timers were added.
assert_empty(non_sync_ctx.timer_ctx().timers());
}
#[test]
fn test_host_slaac_address_deprecate_while_tentative() {
let config = Ipv6::DUMMY_CONFIG;
let Ctx { mut sync_ctx, mut non_sync_ctx } = DummyEventDispatcherBuilder::default().build();
let device = crate::add_ethernet_device(
&mut sync_ctx,
&mut non_sync_ctx,
config.local_mac,
Ipv6::MINIMUM_LINK_MTU.into(),
);
crate::device::testutil::enable_device(&mut sync_ctx, &mut non_sync_ctx, device);
let src_mac = config.remote_mac;
let src_ip = src_mac.to_ipv6_link_local().addr().get();
let prefix = subnet_v6!("0102:0304:0506:0708::/64");
let mut expected_addr = [1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0];
expected_addr[8..].copy_from_slice(&config.local_mac.to_eui64()[..]);
let expected_addr = UnicastAddr::new(Ipv6Addr::from(expected_addr)).unwrap();
let expected_addr_sub = AddrSubnet::from_witness(expected_addr, prefix.prefix()).unwrap();
// Have no addresses yet.
let device_id = device.try_into().unwrap();
assert_empty(iter_global_ipv6_addrs(&sync_ctx, device_id));
crate::device::update_ipv6_configuration(
&mut sync_ctx,
&mut non_sync_ctx,
device,
|config| {
config.ip_config.ip_enabled = true;
config.slaac_config.enable_stable_addresses = true;
// Doesn't matter as long as we perform DAD.
config.dad_transmits = NonZeroU8::new(1);
},
);
// Set the retransmit timer between neighbor solicitations to be greater
// than the preferred lifetime of the prefix.
Ipv6DeviceHandler::set_discovered_retrans_timer(
&mut sync_ctx,
&mut non_sync_ctx,
device,
NonZeroDuration::from_nonzero_secs(nonzero!(10u64)),
);
// Receive a new RA with new prefix (autonomous).
//
// Should get a new IP and set preferred lifetime to 1s.
let valid_lifetime = 2;
let preferred_lifetime = 1;
receive_prefix_update(
&mut sync_ctx,
&mut non_sync_ctx,
device,
src_ip,
prefix,
preferred_lifetime,
valid_lifetime,
);
// Should have gotten a new IP.
let now = non_sync_ctx.now();
let valid_until = now + Duration::from_secs(valid_lifetime.into());
let expected_address_entry = Ipv6AddressEntry {
addr_sub: expected_addr_sub,
state: AddressState::Tentative { dad_transmits_remaining: None },
config: AddrConfig::Slaac(SlaacConfig::Static {
valid_until: Lifetime::Finite(DummyInstant::from(valid_until)),
}),
deprecated: false,
};
assert_eq!(
iter_global_ipv6_addrs(&sync_ctx, device_id).collect::<Vec<_>>(),
[&expected_address_entry]
);
// Make sure deprecate and invalidation timers are set.
non_sync_ctx.timer_ctx().assert_some_timers_installed([
(
SlaacTimerId::new_deprecate_slaac_address(device, expected_addr).into(),
now + Duration::from_secs(preferred_lifetime.into()),
),
(SlaacTimerId::new_invalidate_slaac_address(device, expected_addr).into(), valid_until),
]);
// Trigger the deprecation timer.
assert_eq!(
non_sync_ctx.trigger_next_timer(&mut sync_ctx, crate::handle_timer).unwrap(),
SlaacTimerId::new_deprecate_slaac_address(device, expected_addr).into()
);
assert_eq!(
iter_global_ipv6_addrs(&sync_ctx, device_id).collect::<Vec<_>>(),
[&Ipv6AddressEntry { deprecated: true, ..expected_address_entry }]
);
}
fn receive_prefix_update(
sync_ctx: &mut crate::testutil::DummySyncCtx,
ctx: &mut crate::testutil::DummyNonSyncCtx,
device: DeviceId,
src_ip: Ipv6Addr,
subnet: Subnet<Ipv6Addr>,
preferred_lifetime: u32,
valid_lifetime: u32,
) {
let prefix = subnet.network();
let prefix_length = subnet.prefix();
let icmpv6_packet_buf = slaac_packet_buf(
src_ip,
Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS.get(),
prefix,
prefix_length,
false,
true,
valid_lifetime,
preferred_lifetime,
);
receive_ipv6_packet(sync_ctx, ctx, device, FrameDestination::Multicast, icmpv6_packet_buf);
}
fn get_matching_slaac_address_entries<F: FnMut(&&Ipv6AddressEntry<DummyInstant>) -> bool>(
sync_ctx: &crate::testutil::DummySyncCtx,
device: DeviceId,
filter: F,
) -> impl Iterator<Item = &Ipv6AddressEntry<DummyInstant>> {
iter_global_ipv6_addrs(sync_ctx, device.try_into().unwrap()).filter(filter)
}
fn get_matching_slaac_address_entry<F: FnMut(&&Ipv6AddressEntry<DummyInstant>) -> bool>(
sync_ctx: &crate::testutil::DummySyncCtx,
device: DeviceId,
filter: F,
) -> Option<&Ipv6AddressEntry<DummyInstant>> {
let mut matching_addrs = get_matching_slaac_address_entries(sync_ctx, device, filter);
let entry = matching_addrs.next();
assert_eq!(matching_addrs.next(), None);
entry
}
fn get_slaac_address_entry(
sync_ctx: &crate::testutil::DummySyncCtx,
device: DeviceId,
addr_sub: AddrSubnet<Ipv6Addr, UnicastAddr<Ipv6Addr>>,
) -> Option<&Ipv6AddressEntry<DummyInstant>> {
let mut matching_addrs = iter_global_ipv6_addrs(sync_ctx, device.try_into().unwrap())
.filter(|entry| *entry.addr_sub() == addr_sub);
let entry = matching_addrs.next();
assert_eq!(matching_addrs.next(), None);
entry
}
fn assert_slaac_lifetimes_enforced(
non_sync_ctx: &crate::testutil::DummyNonSyncCtx,
device: DeviceId,
entry: &Ipv6AddressEntry<DummyInstant>,
valid_until: DummyInstant,
preferred_until: DummyInstant,
) {
assert_eq!(entry.state, AddressState::Assigned);
assert_matches!(entry.config, AddrConfig::Slaac(_));
let entry_valid_until = match entry.config {
AddrConfig::Slaac(SlaacConfig::Static { valid_until }) => valid_until,
AddrConfig::Slaac(SlaacConfig::Temporary(TemporarySlaacConfig {
valid_until,
desync_factor: _,
creation_time: _,
dad_counter: _,
})) => Lifetime::Finite(valid_until),
AddrConfig::Manual => unreachable!(),
};
assert_eq!(entry_valid_until, Lifetime::Finite(valid_until));
non_sync_ctx.timer_ctx().assert_some_timers_installed([
(
SlaacTimerId::new_deprecate_slaac_address(device, entry.addr_sub().addr()).into(),
preferred_until,
),
(
SlaacTimerId::new_invalidate_slaac_address(device, entry.addr_sub().addr()).into(),
valid_until,
),
]);
}
#[test]
fn test_host_static_slaac_valid_lifetime_updates() {
// Make sure we update the valid lifetime only in certain scenarios
// to prevent denial-of-service attacks as outlined in RFC 4862 section
// 5.5.3.e. Note, the preferred lifetime should always be updated.
set_logger_for_test();
fn inner_test(
sync_ctx: &mut crate::testutil::DummySyncCtx,
ctx: &mut crate::testutil::DummyNonSyncCtx,
device: DeviceId,
src_ip: Ipv6Addr,
subnet: Subnet<Ipv6Addr>,
addr_sub: AddrSubnet<Ipv6Addr, UnicastAddr<Ipv6Addr>>,
preferred_lifetime: u32,
valid_lifetime: u32,
expected_valid_lifetime: u32,
) {
receive_prefix_update(
sync_ctx,
ctx,
device,
src_ip,
subnet,
preferred_lifetime,
valid_lifetime,
);
let entry =
get_slaac_address_entry(sync_ctx, device.try_into().unwrap(), addr_sub).unwrap();
let now = ctx.now();
let valid_until =
now.checked_add(Duration::from_secs(expected_valid_lifetime.into())).unwrap();
let preferred_until =
now.checked_add(Duration::from_secs(preferred_lifetime.into())).unwrap();
assert_slaac_lifetimes_enforced(ctx, device, entry, valid_until, preferred_until);
}
let config = Ipv6::DUMMY_CONFIG;
let Ctx { mut sync_ctx, mut non_sync_ctx } = DummyEventDispatcherBuilder::default().build();
let device = crate::add_ethernet_device(
&mut sync_ctx,
&mut non_sync_ctx,
config.local_mac,
Ipv6::MINIMUM_LINK_MTU.into(),
);
crate::device::update_ipv6_configuration(
&mut sync_ctx,
&mut non_sync_ctx,
device,
|config| {
config.ip_config.ip_enabled = true;
config.slaac_config.enable_stable_addresses = true;
},
);
let src_mac = config.remote_mac;
let src_ip = src_mac.to_ipv6_link_local().addr().get();
let prefix = Ipv6Addr::from([1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0]);
let prefix_length = 64;
let subnet = Subnet::new(prefix, prefix_length).unwrap();
let mut expected_addr = [1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0];
expected_addr[8..].copy_from_slice(&config.local_mac.to_eui64()[..]);
let expected_addr = UnicastAddr::new(Ipv6Addr::from(expected_addr)).unwrap();
let expected_addr_sub = AddrSubnet::from_witness(expected_addr, prefix_length).unwrap();
// Have no addresses yet.
assert_empty(iter_global_ipv6_addrs(&sync_ctx, device.try_into().unwrap()));
// Receive a new RA with new prefix (autonomous).
//
// Should get a new IP and set preferred lifetime to 1s.
// Make sure deprecate and invalidation timers are set.
inner_test(
&mut sync_ctx,
&mut non_sync_ctx,
device,
src_ip,
subnet,
expected_addr_sub,
30,
60,
60,
);
// If the valid lifetime is greater than the remaining lifetime, update
// the valid lifetime.
inner_test(
&mut sync_ctx,
&mut non_sync_ctx,
device,
src_ip,
subnet,
expected_addr_sub,
70,
70,
70,
);
// If the valid lifetime is greater than 2 hrs, update the valid
// lifetime.
inner_test(
&mut sync_ctx,
&mut non_sync_ctx,
device,
src_ip,
subnet,
expected_addr_sub,
1001,
7201,
7201,
);
// Make remaining lifetime < 2 hrs.
assert_eq!(
non_sync_ctx.trigger_timers_for(
&mut sync_ctx,
Duration::from_secs(1000),
crate::handle_timer
),
[]
);
// If the remaining lifetime is <= 2 hrs & valid lifetime is less than
// that, don't update valid lifetime.
inner_test(
&mut sync_ctx,
&mut non_sync_ctx,
device,
src_ip,
subnet,
expected_addr_sub,
1000,
2000,
6201,
);
// Make the remaining lifetime > 2 hours.
inner_test(
&mut sync_ctx,
&mut non_sync_ctx,
device,
src_ip,
subnet,
expected_addr_sub,
1000,
10800,
10800,
);
// If the remaining lifetime is > 2 hours, and new valid lifetime is < 2
// hours, set the valid lifetime to 2 hours.
inner_test(
&mut sync_ctx,
&mut non_sync_ctx,
device,
src_ip,
subnet,
expected_addr_sub,
1000,
1000,
7200,
);
// If the remaining lifetime is <= 2 hrs & valid lifetime is less than
// that, don't update valid lifetime.
inner_test(
&mut sync_ctx,
&mut non_sync_ctx,
device,
src_ip,
subnet,
expected_addr_sub,
1000,
2000,
7200,
);
// Increase valid lifetime twice while it is greater than 2 hours.
inner_test(
&mut sync_ctx,
&mut non_sync_ctx,
device,
src_ip,
subnet,
expected_addr_sub,
1001,
7201,
7201,
);
inner_test(
&mut sync_ctx,
&mut non_sync_ctx,
device,
src_ip,
subnet,
expected_addr_sub,
1001,
7202,
7202,
);
// Make remaining lifetime < 2 hrs.
assert_eq!(
non_sync_ctx.trigger_timers_for(
&mut sync_ctx,
Duration::from_secs(1000),
crate::handle_timer
),
[]
);
// If the remaining lifetime is <= 2 hrs & valid lifetime is less than
// that, don't update valid lifetime.
inner_test(
&mut sync_ctx,
&mut non_sync_ctx,
device,
src_ip,
subnet,
expected_addr_sub,
1001,
6202,
6202,
);
// Increase valid lifetime twice while it is less than 2 hours.
inner_test(
&mut sync_ctx,
&mut non_sync_ctx,
device,
src_ip,
subnet,
expected_addr_sub,
1001,
6203,
6203,
);
inner_test(
&mut sync_ctx,
&mut non_sync_ctx,
device,
src_ip,
subnet,
expected_addr_sub,
1001,
6204,
6204,
);
}
#[test]
fn test_host_temporary_slaac_regenerates_address_on_dad_failure() {
// Check that when a tentative temporary address is detected as a
// duplicate, a new address gets created.
set_logger_for_test();
let config = Ipv6::DUMMY_CONFIG;
let Ctx { mut sync_ctx, mut non_sync_ctx } = DummyEventDispatcherBuilder::default().build();
let device = crate::add_ethernet_device(
&mut sync_ctx,
&mut non_sync_ctx,
config.local_mac,
Ipv6::MINIMUM_LINK_MTU.into(),
);
let router_mac = config.remote_mac;
let router_ip = router_mac.to_ipv6_link_local().addr().get();
let prefix = Ipv6Addr::from([1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0]);
let prefix_length = 64;
let subnet = Subnet::new(prefix, prefix_length).unwrap();
const MAX_VALID_LIFETIME: Duration = Duration::from_secs(15000);
const MAX_PREFERRED_LIFETIME: Duration = Duration::from_secs(5000);
let idgen_retries = 3;
let mut slaac_config = SlaacConfiguration::default();
enable_temporary_addresses(
&mut slaac_config,
non_sync_ctx.rng_mut(),
NonZeroDuration::new(MAX_VALID_LIFETIME).unwrap(),
NonZeroDuration::new(MAX_PREFERRED_LIFETIME).unwrap(),
idgen_retries,
);
crate::device::update_ipv6_configuration(
&mut sync_ctx,
&mut non_sync_ctx,
device,
|ipv6_config| {
ipv6_config.slaac_config = slaac_config;
ipv6_config.ip_config.ip_enabled = true;
// Doesn't matter as long as we perform DAD.
ipv6_config.dad_transmits = NonZeroU8::new(1);
},
);
// Send an update with lifetimes that are smaller than the ones specified in the preferences.
let valid_lifetime = 10000;
let preferred_lifetime = 4000;
receive_prefix_update(
&mut sync_ctx,
&mut non_sync_ctx,
device,
router_ip,
subnet,
preferred_lifetime,
valid_lifetime,
);
let first_addr_entry = *get_matching_slaac_address_entry(&sync_ctx, device, |entry| {
entry.addr_sub().subnet() == subnet
&& match entry.config {
AddrConfig::Slaac(SlaacConfig::Temporary(_)) => true,
AddrConfig::Slaac(SlaacConfig::Static { valid_until: _ }) => false,
AddrConfig::Manual => false,
}
})
.unwrap();
assert_eq!(
first_addr_entry.state,
AddressState::Tentative { dad_transmits_remaining: None }
);
receive_neighbor_advertisement_for_duplicate_address(
&mut sync_ctx,
&mut non_sync_ctx,
device,
first_addr_entry.addr_sub().addr(),
);
// In response to the advertisement with the duplicate address, a
// different address should be selected.
let second_addr_entry = *get_matching_slaac_address_entry(&sync_ctx, device, |entry| {
entry.addr_sub().subnet() == subnet
&& match entry.config {
AddrConfig::Slaac(SlaacConfig::Temporary(_)) => true,
AddrConfig::Slaac(SlaacConfig::Static { valid_until: _ }) => false,
AddrConfig::Manual => false,
}
})
.unwrap();
let first_addr_entry_valid = assert_matches!(first_addr_entry.config,
AddrConfig::Slaac(SlaacConfig::Temporary(TemporarySlaacConfig {
valid_until, creation_time: _, desync_factor: _, dad_counter: 0})) => {valid_until});
let first_addr_sub = first_addr_entry.addr_sub();
let second_addr_sub = second_addr_entry.addr_sub();
assert_eq!(first_addr_sub.subnet(), second_addr_sub.subnet());
assert_ne!(first_addr_sub.addr(), second_addr_sub.addr());
assert_matches!(second_addr_entry.config, AddrConfig::Slaac(SlaacConfig::Temporary(
TemporarySlaacConfig {
valid_until,
creation_time,
desync_factor: _,
dad_counter: 1,
})) => {
assert_eq!(creation_time, non_sync_ctx.now());
assert_eq!(valid_until, first_addr_entry_valid);
});
}
fn receive_neighbor_advertisement_for_duplicate_address(
sync_ctx: &mut crate::testutil::DummySyncCtx,
ctx: &mut crate::testutil::DummyNonSyncCtx,
device: DeviceId,
source_ip: UnicastAddr<Ipv6Addr>,
) {
let peer_mac = mac!("00:11:22:33:44:55");
let dest_ip = Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS.get();
let router_flag = false;
let solicited_flag = false;
let override_flag = true;
let src_ip = source_ip.get();
receive_ipv6_packet(
sync_ctx,
ctx,
device,
FrameDestination::Multicast,
Buf::new(
neighbor_advertisement_message(
src_ip,
dest_ip,
router_flag,
solicited_flag,
override_flag,
Some(peer_mac),
),
..,
)
.encapsulate(Ipv6PacketBuilder::new(
src_ip,
dest_ip,
REQUIRED_NDP_IP_PACKET_HOP_LIMIT,
Ipv6Proto::Icmpv6,
))
.serialize_vec_outer()
.unwrap()
.unwrap_b(),
)
}
#[test]
fn test_host_temporary_slaac_gives_up_after_dad_failures() {
// Check that when the chosen tentative temporary addresses are detected
// as duplicates enough times, the system gives up.
set_logger_for_test();
let config = Ipv6::DUMMY_CONFIG;
let Ctx { mut sync_ctx, mut non_sync_ctx } = DummyEventDispatcherBuilder::default().build();
let device = crate::add_ethernet_device(
&mut sync_ctx,
&mut non_sync_ctx,
config.local_mac,
Ipv6::MINIMUM_LINK_MTU.into(),
);
let router_mac = config.remote_mac;
let router_ip = router_mac.to_ipv6_link_local().addr().get();
let prefix = Ipv6Addr::from([1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0]);
let prefix_length = 64;
let subnet = Subnet::new(prefix, prefix_length).unwrap();
const MAX_VALID_LIFETIME: Duration = Duration::from_secs(15000);
const MAX_PREFERRED_LIFETIME: Duration = Duration::from_secs(5000);
let idgen_retries = 3;
let mut slaac_config = SlaacConfiguration::default();
enable_temporary_addresses(
&mut slaac_config,
non_sync_ctx.rng_mut(),
NonZeroDuration::new(MAX_VALID_LIFETIME).unwrap(),
NonZeroDuration::new(MAX_PREFERRED_LIFETIME).unwrap(),
idgen_retries,
);
crate::device::update_ipv6_configuration(
&mut sync_ctx,
&mut non_sync_ctx,
device,
|ipv6_config| {
ipv6_config.slaac_config = slaac_config;
ipv6_config.ip_config.ip_enabled = true;
// Doesn't matter as long as we perform DAD.
ipv6_config.dad_transmits = NonZeroU8::new(1);
},
);
receive_prefix_update(
&mut sync_ctx,
&mut non_sync_ctx,
device,
router_ip,
subnet,
MAX_PREFERRED_LIFETIME.as_secs() as u32,
MAX_VALID_LIFETIME.as_secs() as u32,
);
let match_temporary_address = |entry: &&Ipv6AddressEntry<DummyInstant>| {
entry.addr_sub().subnet() == subnet
&& match entry.config {
AddrConfig::Slaac(SlaacConfig::Temporary(_)) => true,
AddrConfig::Slaac(SlaacConfig::Static { valid_until: _ }) => false,
AddrConfig::Manual => false,
}
};
// The system should try several (1 initial + # retries) times to
// generate an address. In the loop below, each generated address is
// detected as a duplicate.
let attempted_addresses: Vec<_> = (0..=idgen_retries)
.into_iter()
.map(|_| {
// An address should be selected. This must be checked using DAD
// against other hosts on the network.
let addr_entry =
*get_matching_slaac_address_entry(&sync_ctx, device, match_temporary_address)
.unwrap();
assert_eq!(
addr_entry.state,
AddressState::Tentative { dad_transmits_remaining: None }
);
// A response is received to the DAD request indicating that it
// is a duplicate.
receive_neighbor_advertisement_for_duplicate_address(
&mut sync_ctx,
&mut non_sync_ctx,
device,
addr_entry.addr_sub().addr(),
);
// The address should be unassigned from the device.
assert_eq!(
get_slaac_address_entry(&sync_ctx, device, *addr_entry.addr_sub()),
None
);
*addr_entry.addr_sub()
})
.collect();
// After the last failed try, the system should have given up, and there
// should be no temporary address for the subnet.
assert_eq!(
get_matching_slaac_address_entry(&sync_ctx, device, match_temporary_address),
None
);
// All the attempted addresses should be unique.
let unique_addresses = attempted_addresses.iter().collect::<HashSet<_>>();
assert_eq!(
unique_addresses.len(),
(1 + idgen_retries).into(),
"not all addresses are unique: {attempted_addresses:?}"
);
}
#[test]
fn test_host_temporary_slaac_deprecate_before_regen() {
// Check that if there are multiple non-deprecated addresses in a subnet
// and the regen timer goes off, no new address is generated. This tests
// the following scenario:
//
// 1. At time T, an address A is created for a subnet whose preferred
// lifetime is PA. This results in a regen timer set at T + PA - R.
// 2. At time T + PA - R, a new address B is created for the same
// subnet when the regen timer for A expires, with a preferred
// lifetime of PB (PA != PB because of the desync values).
// 3. Before T + PA, an advertisement is received for the prefix with
// preferred lifetime X. Address A is now preferred until T + PA + X
// and regenerated at T + PA + X - R and address B is preferred until
// (T + PA - R) + PB + X.
//
// Since both addresses are preferred, we expect that when the regen
// timer for address A goes off, it is ignored since there is already
// another preferred address (namely B) for the subnet.
set_logger_for_test();
let config = Ipv6::DUMMY_CONFIG;
let Ctx { mut sync_ctx, mut non_sync_ctx } = DummyEventDispatcherBuilder::default().build();
let device = crate::add_ethernet_device(
&mut sync_ctx,
&mut non_sync_ctx,
config.local_mac,
Ipv6::MINIMUM_LINK_MTU.into(),
);
let router_mac = config.remote_mac;
let router_ip = router_mac.to_ipv6_link_local().addr().get();
let prefix = Ipv6Addr::from([1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0]);
let prefix_length = 64;
let subnet = Subnet::new(prefix, prefix_length).unwrap();
const MAX_VALID_LIFETIME: Duration = Duration::from_secs(15000);
const MAX_PREFERRED_LIFETIME: Duration = Duration::from_secs(5000);
let mut slaac_config = SlaacConfiguration::default();
enable_temporary_addresses(
&mut slaac_config,
non_sync_ctx.rng_mut(),
NonZeroDuration::new(MAX_VALID_LIFETIME).unwrap(),
NonZeroDuration::new(MAX_PREFERRED_LIFETIME).unwrap(),
0,
);
crate::device::update_ipv6_configuration(
&mut sync_ctx,
&mut non_sync_ctx,
device,
|ipv6_config| {
ipv6_config.slaac_config = slaac_config;
ipv6_config.ip_config.ip_enabled = true;
},
);
// The prefix updates contains a shorter preferred lifetime than
// the preferences allow.
let prefix_preferred_for: Duration = MAX_PREFERRED_LIFETIME * 2 / 3;
receive_prefix_update(
&mut sync_ctx,
&mut non_sync_ctx,
device,
router_ip,
subnet,
prefix_preferred_for.as_secs().try_into().unwrap(),
MAX_VALID_LIFETIME.as_secs().try_into().unwrap(),
);
let first_addr_entry = *get_matching_slaac_address_entry(&sync_ctx, device, |entry| {
entry.addr_sub().subnet() == subnet
&& match entry.config {
AddrConfig::Slaac(SlaacConfig::Temporary(_)) => true,
AddrConfig::Slaac(SlaacConfig::Static { valid_until: _ }) => false,
AddrConfig::Manual => false,
}
})
.unwrap();
let regen_timer_id = SlaacTimerId::new_regenerate_temporary_slaac_address(
device,
*first_addr_entry.addr_sub(),
);
trace!("advancing to regen for first address");
assert_eq!(
non_sync_ctx.trigger_next_timer(&mut sync_ctx, crate::handle_timer),
Some(regen_timer_id.into())
);
// The regeneration timer should cause a new address to be created in
// the same subnet.
assert_matches!(
get_matching_slaac_address_entry(&sync_ctx, device, |entry| {
entry.addr_sub().subnet() == subnet
&& entry.addr_sub() != first_addr_entry.addr_sub()
&& match entry.config {
AddrConfig::Slaac(SlaacConfig::Temporary(_)) => true,
AddrConfig::Slaac(SlaacConfig::Static { valid_until: _ }) => false,
AddrConfig::Manual => false,
}
}),
Some(_)
);
// Now the router sends a new update that extends the preferred lifetime
// of addresses.
receive_prefix_update(
&mut sync_ctx,
&mut non_sync_ctx,
device,
router_ip,
subnet,
prefix_preferred_for.as_secs().try_into().unwrap(),
MAX_VALID_LIFETIME.as_secs().try_into().unwrap(),
);
let addresses = get_matching_slaac_address_entries(&sync_ctx, device, |entry| {
entry.addr_sub().subnet() == subnet
&& match entry.config {
AddrConfig::Slaac(SlaacConfig::Temporary(_)) => true,
AddrConfig::Slaac(SlaacConfig::Static { valid_until: _ }) => false,
AddrConfig::Manual => false,
}
})
.map(|entry| entry.addr_sub().addr())
.collect::<Vec<_>>();
for address in &addresses {
assert_matches!(
non_sync_ctx.scheduled_instant(SlaacTimerId::new_deprecate_slaac_address(
device,
*address,
)),
Some(deprecate_at) => {
let preferred_for = deprecate_at - non_sync_ctx.now();
assert!(preferred_for <= prefix_preferred_for, "{:?} <= {:?}", preferred_for, prefix_preferred_for);
}
);
}
trace!("advancing to new regen for first address");
// Running the context forward until the first address is again eligible
// for regeneration doesn't result in a new address being created.
assert_eq!(
non_sync_ctx.trigger_next_timer(&mut sync_ctx, crate::handle_timer),
Some(regen_timer_id.into())
);
assert_eq!(
get_matching_slaac_address_entries(&sync_ctx, device, |entry| entry
.addr_sub()
.subnet()
== subnet
&& match entry.config {
AddrConfig::Slaac(SlaacConfig::Temporary(_)) => true,
AddrConfig::Slaac(SlaacConfig::Static { valid_until: _ }) => false,
AddrConfig::Manual => false,
})
.map(|entry| entry.addr_sub().addr())
.collect::<HashSet<_>>(),
addresses.iter().cloned().collect()
);
trace!("advancing to deprecation for first address");
// If we continue on until the first address is deprecated, we still
// shouldn't regenerate since the second address is active.
assert_eq!(
non_sync_ctx.trigger_next_timer(&mut sync_ctx, crate::handle_timer),
Some(
SlaacTimerId::new_deprecate_slaac_address(
device,
first_addr_entry.addr_sub().addr()
)
.into()
)
);
let remaining_addresses = addresses
.into_iter()
.filter(|addr| addr != &first_addr_entry.addr_sub().addr())
.collect::<HashSet<_>>();
assert_eq!(
get_matching_slaac_address_entries(&sync_ctx, device, |entry| entry
.addr_sub()
.subnet()
== subnet
&& !entry.deprecated
&& match entry.config {
AddrConfig::Slaac(SlaacConfig::Temporary(_)) => true,
AddrConfig::Slaac(SlaacConfig::Static { valid_until: _ }) => false,
AddrConfig::Manual => false,
})
.map(|entry| entry.addr_sub().addr())
.collect::<HashSet<_>>(),
remaining_addresses
);
}
#[test]
fn test_host_temporary_slaac_config_update_skips_regen() {
// If the NDP configuration gets updated such that the target regen time
// for an address is moved earlier than the current time, the address
// should be regenerated immediately.
set_logger_for_test();
let config = Ipv6::DUMMY_CONFIG;
let Ctx { mut sync_ctx, mut non_sync_ctx } = DummyEventDispatcherBuilder::default().build();
let device = crate::add_ethernet_device(
&mut sync_ctx,
&mut non_sync_ctx,
config.local_mac,
Ipv6::MINIMUM_LINK_MTU.into(),
);
let device_id = device.try_into().unwrap();
// No DAD for the auto-generated link-local address.
crate::device::update_ipv6_configuration(
&mut sync_ctx,
&mut non_sync_ctx,
device,
|ipv6_config| {
ipv6_config.dad_transmits = None;
ipv6_config.ip_config.ip_enabled = true;
},
);
let router_mac = config.remote_mac;
let router_ip = router_mac.to_ipv6_link_local().addr().get();
let prefix = Ipv6Addr::from([1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0]);
let prefix_length = 64;
let subnet = Subnet::new(prefix, prefix_length).unwrap();
const MAX_VALID_LIFETIME: Duration = Duration::from_secs(15000);
let max_preferred_lifetime = Duration::from_secs(5000);
let mut slaac_config = SlaacConfiguration::default();
enable_temporary_addresses(
&mut slaac_config,
non_sync_ctx.rng_mut(),
NonZeroDuration::new(MAX_VALID_LIFETIME).unwrap(),
NonZeroDuration::new(max_preferred_lifetime).unwrap(),
1,
);
crate::device::update_ipv6_configuration(
&mut sync_ctx,
&mut non_sync_ctx,
device,
|ipv6_config| {
// Perform DAD for later addresses.
ipv6_config.dad_transmits = NonZeroU8::new(1);
ipv6_config.slaac_config = slaac_config;
},
);
// Set a large value for the retransmit period. This forces
// REGEN_ADVANCE to be large, which increases the window between when an
// address is regenerated and when it becomes deprecated.
Ipv6DeviceHandler::set_discovered_retrans_timer(
&mut sync_ctx,
&mut non_sync_ctx,
device,
NonZeroDuration::new(max_preferred_lifetime / 4).unwrap(),
);
receive_prefix_update(
&mut sync_ctx,
&mut non_sync_ctx,
device,
router_ip,
subnet,
max_preferred_lifetime.as_secs().try_into().unwrap(),
MAX_VALID_LIFETIME.as_secs().try_into().unwrap(),
);
let first_addr_entry = *get_matching_slaac_address_entry(&sync_ctx, device, |entry| {
entry.addr_sub().subnet() == subnet
&& match entry.config {
AddrConfig::Slaac(SlaacConfig::Temporary(_)) => true,
AddrConfig::Slaac(SlaacConfig::Static { valid_until: _ }) => false,
AddrConfig::Manual => false,
}
})
.unwrap();
let regen_at = non_sync_ctx
.scheduled_instant(SlaacTimerId::new_regenerate_temporary_slaac_address(
device,
*first_addr_entry.addr_sub(),
))
.unwrap();
let before_regen = regen_at - Duration::from_secs(10);
// The only events that run before regen should be the DAD timers for
// the static and temporary address that were created earlier.
let dad_timer_ids = get_matching_slaac_address_entries(&sync_ctx, device, |entry| {
entry.addr_sub().subnet() == subnet
})
.map(|entry| dad_timer_id(device_id, entry.addr_sub().addr()))
.collect::<Vec<_>>();
non_sync_ctx.trigger_timers_until_and_expect_unordered(
&mut sync_ctx,
before_regen,
dad_timer_ids,
crate::handle_timer,
);
let preferred_until = non_sync_ctx
.scheduled_instant(SlaacTimerId::new_deprecate_slaac_address(
device,
first_addr_entry.addr_sub().addr(),
))
.unwrap();
let max_preferred_lifetime = max_preferred_lifetime * 4 / 5;
let mut slaac_config = SlaacConfiguration::default();
enable_temporary_addresses(
&mut slaac_config,
non_sync_ctx.rng_mut(),
NonZeroDuration::new(MAX_VALID_LIFETIME).unwrap(),
NonZeroDuration::new(max_preferred_lifetime).unwrap(),
1,
);
crate::device::update_ipv6_configuration(
&mut sync_ctx,
&mut non_sync_ctx,
device,
|ipv6_config| {
ipv6_config.slaac_config = slaac_config;
},
);
// Receiving this update should result in requiring a regen time that is
// before the current time. The address should be regenerated
// immediately.
let prefix_preferred_for = preferred_until - non_sync_ctx.now();
receive_prefix_update(
&mut sync_ctx,
&mut non_sync_ctx,
device,
router_ip,
subnet,
prefix_preferred_for.as_secs().try_into().unwrap(),
MAX_VALID_LIFETIME.as_secs().try_into().unwrap(),
);
// The regeneration is still handled by timer, so handle any pending
// events.
assert_eq!(
non_sync_ctx.trigger_timers_for(&mut sync_ctx, Duration::ZERO, crate::handle_timer),
vec![SlaacTimerId::new_regenerate_temporary_slaac_address(
device,
*first_addr_entry.addr_sub()
)
.into()]
);
let addresses = get_matching_slaac_address_entries(&sync_ctx, device, |entry| {
entry.addr_sub().subnet() == subnet
&& match entry.config {
AddrConfig::Slaac(SlaacConfig::Temporary(_)) => true,
AddrConfig::Slaac(SlaacConfig::Static { valid_until: _ }) => false,
AddrConfig::Manual => false,
}
})
.map(|entry| entry.addr_sub().addr())
.collect::<HashSet<_>>();
assert!(addresses.contains(&first_addr_entry.addr_sub().addr()));
assert_eq!(addresses.len(), 2);
}
#[test]
fn test_host_temporary_slaac_lifetime_updates_respect_max() {
// Make sure that the preferred and valid lifetimes of the NDP
// configuration are respected.
let src_mac = Ipv6::DUMMY_CONFIG.remote_mac;
let src_ip = src_mac.to_ipv6_link_local().addr().get();
let subnet = subnet_v6!("0102:0304:0506:0708::/64");
let (Ctx { mut sync_ctx, mut non_sync_ctx }, device, config) =
initialize_with_temporary_addresses_enabled();
let now = non_sync_ctx.now();
let start = now;
let temporary_address_config = config.temporary_address_configuration.unwrap();
let max_valid_lifetime = temporary_address_config.temp_valid_lifetime;
let max_valid_until = now.checked_add(max_valid_lifetime.get()).unwrap();
let max_preferred_lifetime = temporary_address_config.temp_preferred_lifetime;
let max_preferred_until = now.checked_add(max_preferred_lifetime.get()).unwrap();
let secret_key = temporary_address_config.secret_key;
let interface_identifier = generate_opaque_interface_identifier(
subnet,
&Ipv6::DUMMY_CONFIG.local_mac.to_eui64()[..],
[],
// Clone the RNG so we can see what the next value (which will be
// used to generate the temporary address) will be.
OpaqueIidNonce::Random(non_sync_ctx.rng().clone().next_u64()),
&secret_key,
);
let mut expected_addr = [1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0];
expected_addr[8..].copy_from_slice(&interface_identifier.to_be_bytes()[..8]);
let expected_addr = UnicastAddr::new(Ipv6Addr::from(expected_addr)).unwrap();
let expected_addr_sub = AddrSubnet::from_witness(expected_addr, subnet.prefix()).unwrap();
// Send an update with lifetimes that are smaller than the ones specified in the preferences.
let valid_lifetime = 2000;
let preferred_lifetime = 1500;
assert!(u64::from(valid_lifetime) < max_valid_lifetime.get().as_secs());
assert!(u64::from(preferred_lifetime) < max_preferred_lifetime.get().as_secs());
receive_prefix_update(
&mut sync_ctx,
&mut non_sync_ctx,
device,
src_ip,
subnet,
preferred_lifetime,
valid_lifetime,
);
let entry = get_slaac_address_entry(&sync_ctx, device, expected_addr_sub).unwrap();
let expected_valid_until =
now.checked_add(Duration::from_secs(valid_lifetime.into())).unwrap();
let expected_preferred_until =
now.checked_add(Duration::from_secs(preferred_lifetime.into())).unwrap();
assert!(
expected_valid_until < max_valid_until,
"expected {:?} < {:?}",
expected_valid_until,
max_valid_until
);
assert!(expected_preferred_until < max_preferred_until);
assert_slaac_lifetimes_enforced(
&non_sync_ctx,
device,
entry,
expected_valid_until,
expected_preferred_until,
);
// After some time passes, another update is received with the same lifetimes for the
// prefix. Per RFC 8981 Section 3.4.1, the lifetimes for the address should obey the
// overall constraints expressed in the preferences.
assert_eq!(
non_sync_ctx.trigger_timers_for(
&mut sync_ctx,
Duration::from_secs(1000),
crate::handle_timer
),
[]
);
let now = non_sync_ctx.now();
let expected_valid_until =
now.checked_add(Duration::from_secs(valid_lifetime.into())).unwrap();
let expected_preferred_until =
now.checked_add(Duration::from_secs(preferred_lifetime.into())).unwrap();
// The preferred lifetime advertised by the router is now past the max allowed by
// the NDP configuration.
assert!(expected_preferred_until > max_preferred_until);
receive_prefix_update(
&mut sync_ctx,
&mut non_sync_ctx,
device,
src_ip,
subnet,
preferred_lifetime,
valid_lifetime,
);
let entry = get_matching_slaac_address_entry(&sync_ctx, device, |entry| {
entry.addr_sub().subnet() == subnet
&& match entry.config {
AddrConfig::Slaac(SlaacConfig::Temporary(_)) => true,
AddrConfig::Slaac(SlaacConfig::Static { valid_until: _ }) => false,
AddrConfig::Manual => false,
}
})
.unwrap();
let desync_factor = match entry.config {
AddrConfig::Slaac(SlaacConfig::Temporary(TemporarySlaacConfig {
desync_factor,
creation_time: _,
valid_until: _,
dad_counter: _,
})) => desync_factor,
AddrConfig::Slaac(SlaacConfig::Static { valid_until: _ }) => {
unreachable!("temporary address")
}
AddrConfig::Manual => unreachable!("temporary slaac address"),
};
assert_slaac_lifetimes_enforced(
&non_sync_ctx,
device,
entry,
expected_valid_until,
max_preferred_until - desync_factor,
);
// Update the max allowed lifetime in the NDP configuration. This won't take effect until
// the next router advertisement is reeived.
let max_valid_lifetime = max_preferred_lifetime;
let idgen_retries = 3;
let mut slaac_config = SlaacConfiguration::default();
enable_temporary_addresses(
&mut slaac_config,
non_sync_ctx.rng_mut(),
max_valid_lifetime,
max_preferred_lifetime,
idgen_retries,
);
crate::ip::device::update_ipv6_configuration(
&mut sync_ctx,
&mut non_sync_ctx,
device,
|config| {
config.slaac_config = slaac_config;
},
);
// The new valid time is measured from the time at which the address was created (`start`),
// not the current time (`now`). That means the max valid lifetime takes precedence over
// the router's advertised valid lifetime.
let max_valid_until = start.checked_add(max_valid_lifetime.get()).unwrap();
assert!(expected_valid_until > max_valid_until);
receive_prefix_update(
&mut sync_ctx,
&mut non_sync_ctx,
device.try_into().unwrap(),
src_ip,
subnet,
preferred_lifetime,
valid_lifetime,
);
let entry = get_matching_slaac_address_entry(&sync_ctx, device, |entry| {
entry.addr_sub().subnet() == subnet
&& match entry.config {
AddrConfig::Slaac(SlaacConfig::Temporary(_)) => true,
AddrConfig::Slaac(SlaacConfig::Static { valid_until: _ }) => false,
AddrConfig::Manual => false,
}
})
.unwrap();
assert_slaac_lifetimes_enforced(
&non_sync_ctx,
device,
entry,
max_valid_until,
max_preferred_until - desync_factor,
);
}
#[test]
fn test_remove_stable_slaac_address() {
let config = Ipv6::DUMMY_CONFIG;
let Ctx { mut sync_ctx, mut non_sync_ctx } = DummyEventDispatcherBuilder::default().build();
let device = crate::add_ethernet_device(
&mut sync_ctx,
&mut non_sync_ctx,
config.local_mac,
Ipv6::MINIMUM_LINK_MTU.into(),
);
crate::device::update_ipv6_configuration(
&mut sync_ctx,
&mut non_sync_ctx,
device,
|config| {
config.ip_config.ip_enabled = true;
config.slaac_config.enable_stable_addresses = true;
},
);
let src_mac = config.remote_mac;
let src_ip = src_mac.to_ipv6_link_local().addr().get();
let prefix = Ipv6Addr::from([1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0]);
let prefix_length = 64;
let mut expected_addr = [1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0];
expected_addr[8..].copy_from_slice(&config.local_mac.to_eui64()[..]);
let expected_addr = UnicastAddr::new(Ipv6Addr::from(expected_addr)).unwrap();
// Receive a new RA with new prefix (autonomous).
//
// Should get a new IP.
const VALID_LIFETIME_SECS: u32 = 10000;
const PREFERRED_LIFETIME_SECS: u32 = 9000;
let icmpv6_packet_buf = slaac_packet_buf(
src_ip,
Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS.get(),
prefix,
prefix_length,
false,
true,
VALID_LIFETIME_SECS,
PREFERRED_LIFETIME_SECS,
);
receive_ipv6_packet(
&mut sync_ctx,
&mut non_sync_ctx,
device,
FrameDestination::Multicast,
icmpv6_packet_buf,
);
// Should have gotten a new IP.
let now = non_sync_ctx.now();
let valid_until = now + Duration::from_secs(VALID_LIFETIME_SECS.into());
let expected_address_entry = Ipv6AddressEntry {
addr_sub: AddrSubnet::new(expected_addr.get(), prefix_length).unwrap(),
state: AddressState::Assigned,
config: AddrConfig::Slaac(SlaacConfig::Static {
valid_until: Lifetime::Finite(DummyInstant::from(valid_until)),
}),
deprecated: false,
};
let device_id = device.try_into().unwrap();
assert_eq!(
iter_global_ipv6_addrs(&sync_ctx, device_id).collect::<Vec<_>>(),
[&expected_address_entry]
);
// Make sure deprecate and invalidation timers are set.
non_sync_ctx.timer_ctx().assert_some_timers_installed([
(
SlaacTimerId::new_deprecate_slaac_address(device, expected_addr).into(),
now + Duration::from_secs(PREFERRED_LIFETIME_SECS.into()),
),
(SlaacTimerId::new_invalidate_slaac_address(device, expected_addr).into(), valid_until),
]);
// Deleting the address should cancel its SLAAC timers.
del_ip_addr(&mut sync_ctx, &mut non_sync_ctx, device, &expected_addr.into_specified())
.unwrap();
assert_empty(iter_global_ipv6_addrs(&sync_ctx, device_id));
non_sync_ctx.timer_ctx().assert_no_timers_installed();
}
#[test]
fn test_remove_temporary_slaac_address() {
// We use the infinite lifetime so that the stable address does not have
// any timers as it is valid and preferred forever. As a result, we will
// only observe timers for temporary addresses.
let (Ctx { mut sync_ctx, mut non_sync_ctx }, device, expected_addr) =
test_host_generate_temporary_slaac_address(INFINITE_LIFETIME, INFINITE_LIFETIME);
// Deleting the address should cancel its SLAAC timers.
del_ip_addr(&mut sync_ctx, &mut non_sync_ctx, device, &expected_addr.into_specified())
.unwrap();
assert_empty(iter_global_ipv6_addrs(&sync_ctx, device.try_into().unwrap()).filter(|e| {
match e.config {
AddrConfig::Slaac(SlaacConfig::Temporary(_)) => true,
AddrConfig::Slaac(SlaacConfig::Static { valid_until: _ }) => false,
AddrConfig::Manual => false,
}
}));
non_sync_ctx.timer_ctx().assert_no_timers_installed();
}
}