blob: d0d6e98521dfb3e6130a396da006959b268e5d33 [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! Neighbor unreachability detection.
use alloc::{
collections::{
hash_map::{Entry, HashMap},
BinaryHeap, VecDeque,
},
vec::Vec,
};
use core::{
fmt::Debug,
hash::Hash,
marker::PhantomData,
num::{NonZeroU16, NonZeroU32},
};
use assert_matches::assert_matches;
use derivative::Derivative;
use net_types::{
ip::{GenericOverIp, Ip, IpMarked, Ipv4, Ipv6},
SpecifiedAddr,
};
use packet::{
Buf, BufferMut, GrowBuffer as _, ParsablePacket as _, ParseBufferMut as _, Serializer,
};
use packet_formats::{
ip::IpPacket as _,
ipv4::{Ipv4FragmentType, Ipv4Header as _, Ipv4Packet},
ipv6::Ipv6Packet,
utils::NonZeroDuration,
};
use zerocopy::ByteSlice;
use crate::{
context::{
CounterContext, EventContext, InstantBindingsTypes, TimerBindingsTypes, TimerContext2,
TimerHandler,
},
counters::Counter,
device::{
self,
link::{LinkAddress, LinkDevice, LinkUnicastAddress},
AnyDevice, DeviceIdContext, StrongId, WeakId as _,
},
error::AddressResolutionFailed,
socket::address::SocketIpAddr,
time::LocalTimerHeap,
Instant,
};
pub(crate) mod api;
/// The default 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 DEFAULT_MAX_MULTICAST_SOLICIT: NonZeroU16 =
const_unwrap::const_unwrap_option(NonZeroU16::new(3));
/// The default maximum number of unicast solicitations as defined in [RFC 4861
/// section 10].
///
/// [RFC 4861 section 10]: https://tools.ietf.org/html/rfc4861#section-10
const DEFAULT_MAX_UNICAST_SOLICIT: NonZeroU16 =
const_unwrap::const_unwrap_option(NonZeroU16::new(3));
/// The maximum amount of time between retransmissions of neighbor probe
/// messages as defined in [RFC 7048 section 4].
///
/// [RFC 7048 section 4]: https://tools.ietf.org/html/rfc7048#section-4
const MAX_RETRANS_TIMER: NonZeroDuration =
const_unwrap::const_unwrap_option(NonZeroDuration::from_secs(60));
/// The exponential backoff factor for retransmissions of multicast neighbor
/// probe messages as defined in [RFC 7048 section 4].
///
/// [RFC 7048 section 4]: https://tools.ietf.org/html/rfc7048#section-4
const BACKOFF_MULTIPLE: NonZeroU32 = const_unwrap::const_unwrap_option(NonZeroU32::new(3));
const MAX_PENDING_FRAMES: usize = 10;
/// The time a neighbor is considered reachable after receiving a reachability
/// confirmation, as defined in [RFC 4861 section 6.3.2].
///
/// [RFC 4861 section 6.3.2]: https://tools.ietf.org/html/rfc4861#section-6.3.2
const DEFAULT_BASE_REACHABLE_TIME: NonZeroDuration =
const_unwrap::const_unwrap_option(NonZeroDuration::from_secs(30));
/// The time after which a neighbor in the DELAY state transitions to PROBE, as
/// defined in [RFC 4861 section 10].
///
/// [RFC 4861 section 10]: https://tools.ietf.org/html/rfc4861#section-10
const DELAY_FIRST_PROBE_TIME: NonZeroDuration =
const_unwrap::const_unwrap_option(NonZeroDuration::from_secs(5));
/// The maximum number of neighbor entries in the neighbor table for a given
/// device. When the number of entries is above this number and an entry
/// transitions into a discardable state, a garbage collection task will be
/// scheduled to remove any entries that are not in use.
pub const MAX_ENTRIES: usize = 512;
/// The minimum amount of time between garbage collection passes when the
/// neighbor table grows beyond `MAX_SIZE`.
const MIN_GARBAGE_COLLECTION_INTERVAL: NonZeroDuration =
const_unwrap::const_unwrap_option(NonZeroDuration::from_secs(30));
/// NUD counters.
#[derive(Default)]
pub struct NudCountersInner {
/// Count of ICMP destination unreachable errors that could not be sent.
pub icmp_dest_unreachable_dropped: Counter,
}
/// NUD counters.
pub type NudCounters<I> = IpMarked<I, NudCountersInner>;
#[derive(Debug, Copy, Clone)]
pub(crate) struct ConfirmationFlags {
pub(crate) solicited_flag: bool,
pub(crate) override_flag: bool,
}
/// The type of message with a dynamic neighbor update.
#[derive(Debug, Copy, Clone)]
pub(crate) enum DynamicNeighborUpdateSource {
/// Indicates an update from a neighbor probe message.
///
/// E.g. NDP Neighbor Solicitation.
Probe,
/// Indicates an update from a neighbor confirmation message.
///
/// E.g. NDP Neighbor Advertisement.
Confirmation(ConfirmationFlags),
}
#[derive(Derivative)]
#[derivative(Debug(bound = ""))]
#[cfg_attr(test, derivative(Clone, PartialEq(bound = ""), Eq))]
enum NeighborState<D: LinkDevice, BT: NudBindingsTypes<D>> {
Dynamic(DynamicNeighborState<D, BT>),
Static(D::Address),
}
/// The state of a dynamic entry in the neighbor cache within the Neighbor
/// Unreachability Detection state machine, defined in [RFC 4861 section 7.3.2]
/// and [RFC 7048 section 3].
///
/// [RFC 4861 section 7.3.2]: https://tools.ietf.org/html/rfc4861#section-7.3.2
/// [RFC 7048 section 3]: https://tools.ietf.org/html/rfc7048#section-3
#[derive(Derivative)]
#[derivative(Debug(bound = ""))]
#[cfg_attr(test, derivative(Clone(bound = ""), PartialEq(bound = ""), Eq))]
pub(crate) enum DynamicNeighborState<D: LinkDevice, BT: NudBindingsTypes<D>> {
/// Address resolution is being performed on the entry.
///
/// Specifically, a probe has been sent to the solicited-node multicast
/// address of the target, but the corresponding confirmation has not yet
/// been received.
Incomplete(Incomplete<D, BT::Notifier>),
/// 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(Reachable<D, BT::Instant>),
/// 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
/// 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(Stale<D>),
/// A packet has been recently sent to the neighbor, which has stale
/// reachability information (i.e. we have not received recent positive
/// confirmation that the forward path is functioning properly).
///
/// 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(Delay<D>),
/// A reachability confirmation is actively sought by retransmitting probes
/// every RetransTimer milliseconds until a reachability confirmation is
/// received.
Probe(Probe<D>),
/// Similarly to the `Probe` state, a reachability confirmation is actively
/// sought by retransmitting probes; however, probes are multicast to the
/// solicited-node multicast address, using a timeout with exponential
/// backoff, rather than unicast to the cached link address. Also, probes
/// are only transmitted as long as packets continue to be sent to the
/// neighbor.
Unreachable(Unreachable<D>),
}
/// The state of dynamic neighbor table entries as published via events.
///
/// Note that this is not how state is held in the neighbor table itself,
/// see [`DynamicNeighborState`].
///
/// Modeled after RFC 4861 section 7.3.2. Descriptions are kept
/// implementation-independent by using a set of generic terminology.
///
/// ,------------------------------------------------------------------.
/// | Generic Term | ARP Term | NDP Term |
/// |---------------------------+-------------+------------------------|
/// | Reachability Probe | ARP Request | Neighbor Solicitation |
/// | Reachability Confirmation | ARP Reply | Neighbor Advertisement |
/// `---------------------------+-------------+------------------------'
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum EventDynamicState<L: LinkUnicastAddress> {
/// Reachability is in the process of being confirmed for a newly
/// created entry.
Incomplete,
/// Forward reachability has been confirmed; the path to the neighbor
/// is functioning properly.
Reachable(L),
/// Reachability is considered unknown.
///
/// Occurs in one of two ways:
/// 1. Too much time has elapsed since the last positive reachability
/// confirmation was received.
/// 2. Received a reachability confirmation from a neighbor with a
/// different MAC address than the one cached.
Stale(L),
/// A packet was recently sent while reachability was considered
/// unknown.
///
/// This state is an optimization that gives non-Neighbor-Discovery
/// related protocols time to confirm reachability after the last
/// confirmation of reachability has expired due to lack of recent
/// traffic.
Delay(L),
/// A reachability confirmation is actively sought by periodically
/// retransmitting unicast reachability probes until a reachability
/// confirmation is received, or until the maximum number of probes has
/// been sent.
Probe(L),
/// Target is considered unreachable. A reachability confirmation was not
/// received after transmitting the maximum number of reachability
/// probes.
Unreachable(L),
}
/// Neighbor state published via events.
///
/// Note that this is not how state is held in the neighbor table itself,
/// see [`NeighborState`].
///
/// Either a dynamic state within the Neighbor Unreachability Detection (NUD)
/// state machine, or a static entry that never expires.
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
pub enum EventState<L: LinkUnicastAddress> {
/// Dynamic neighbor state.
Dynamic(EventDynamicState<L>),
/// Static neighbor state.
Static(L),
}
/// Neighbor event kind.
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
pub enum EventKind<L: LinkUnicastAddress> {
/// A neighbor entry was added.
Added(EventState<L>),
/// A neighbor entry has changed.
Changed(EventState<L>),
/// A neighbor entry was removed.
Removed,
}
/// Neighbor event.
#[derive(Debug, Eq, Hash, PartialEq, GenericOverIp)]
#[generic_over_ip(I, Ip)]
pub struct Event<L: LinkUnicastAddress, DeviceId, I: Ip, Instant> {
/// The device.
pub device: DeviceId,
/// The neighbor's address.
pub addr: SpecifiedAddr<I::Addr>,
/// The kind of this neighbor event.
pub kind: EventKind<L>,
/// Time of this event.
pub at: Instant,
}
impl<L: LinkUnicastAddress, DeviceId: Clone, I: Ip, Instant> Event<L, DeviceId, I, Instant> {
fn changed(
device: &DeviceId,
event_state: EventState<L>,
addr: SpecifiedAddr<I::Addr>,
at: Instant,
) -> Self {
Self { device: device.clone(), kind: EventKind::Changed(event_state), addr, at }
}
fn added(
device: &DeviceId,
event_state: EventState<L>,
addr: SpecifiedAddr<I::Addr>,
at: Instant,
) -> Self {
Self { device: device.clone(), kind: EventKind::Added(event_state), addr, at }
}
fn removed(device: &DeviceId, addr: SpecifiedAddr<I::Addr>, at: Instant) -> Self {
Self { device: device.clone(), kind: EventKind::Removed, addr, at }
}
}
fn schedule_timer_if_should_retransmit<I, D, DeviceId, CC, BC>(
core_ctx: &mut CC,
bindings_ctx: &mut BC,
timers: &mut TimerHeap<I, BC>,
neighbor: SpecifiedAddr<I::Addr>,
event: NudEvent,
counter: &mut Option<NonZeroU16>,
) -> bool
where
I: Ip,
D: LinkDevice,
DeviceId: StrongId,
BC: NudBindingsContext<I, D, DeviceId>,
CC: NudConfigContext<I>,
{
match counter {
Some(c) => {
*counter = NonZeroU16::new(c.get() - 1);
let retransmit_timeout = core_ctx.retransmit_timeout();
timers.schedule_neighbor(bindings_ctx, retransmit_timeout, neighbor, event);
true
}
None => false,
}
}
#[derive(Debug, Derivative)]
#[cfg_attr(test, derivative(PartialEq, Eq))]
pub(crate) struct Incomplete<D: LinkDevice, N: LinkResolutionNotifier<D>> {
transmit_counter: Option<NonZeroU16>,
pending_frames: VecDeque<Buf<Vec<u8>>>,
#[derivative(PartialEq = "ignore")]
notifiers: Vec<N>,
_marker: PhantomData<D>,
}
#[cfg(test)]
impl<D: LinkDevice, N: LinkResolutionNotifier<D>> Clone for Incomplete<D, N> {
fn clone(&self) -> Self {
// Do not clone `notifiers` since the LinkResolutionNotifier type is not
// required to implement `Clone` and notifiers are not used in equality
// checks in tests.
let Self { transmit_counter, pending_frames, notifiers: _, _marker } = self;
Self {
transmit_counter: transmit_counter.clone(),
pending_frames: pending_frames.clone(),
notifiers: Vec::new(),
_marker: PhantomData,
}
}
}
impl<D: LinkDevice, N: LinkResolutionNotifier<D>> Drop for Incomplete<D, N> {
fn drop(&mut self) {
let Self { transmit_counter: _, pending_frames: _, notifiers, _marker } = self;
for notifier in notifiers.drain(..) {
notifier.notify(Err(AddressResolutionFailed));
}
}
}
impl<D: LinkDevice, N: LinkResolutionNotifier<D>> Incomplete<D, N> {
fn new_with_pending_frame<I, CC, BC, DeviceId>(
core_ctx: &mut CC,
bindings_ctx: &mut BC,
timers: &mut TimerHeap<I, BC>,
neighbor: SpecifiedAddr<I::Addr>,
frame: Buf<Vec<u8>>,
) -> Self
where
I: Ip,
D: LinkDevice,
BC: NudBindingsContext<I, D, DeviceId>,
CC: NudConfigContext<I>,
DeviceId: StrongId,
{
let mut this = Incomplete {
transmit_counter: Some(core_ctx.max_multicast_solicit()),
pending_frames: [frame].into(),
notifiers: Vec::new(),
_marker: PhantomData,
};
// NB: transmission of a neighbor probe on entering INCOMPLETE (and subsequent
// retransmissions) is done by `handle_timer`, as it need not be done with the
// neighbor table lock held.
assert!(this.schedule_timer_if_should_retransmit(core_ctx, bindings_ctx, timers, neighbor));
this
}
fn new_with_notifier<I, CC, BC, DeviceId>(
core_ctx: &mut CC,
bindings_ctx: &mut BC,
timers: &mut TimerHeap<I, BC>,
neighbor: SpecifiedAddr<I::Addr>,
notifier: BC::Notifier,
) -> Self
where
I: Ip,
D: LinkDevice,
BC: NudBindingsContext<I, D, DeviceId, Notifier = N>,
CC: NudConfigContext<I>,
DeviceId: StrongId,
{
let mut this = Incomplete {
transmit_counter: Some(core_ctx.max_multicast_solicit()),
pending_frames: VecDeque::new(),
notifiers: [notifier].into(),
_marker: PhantomData,
};
// NB: transmission of a neighbor probe on entering INCOMPLETE (and subsequent
// retransmissions) is done by `handle_timer`, as it need not be done with the
// neighbor table lock held.
assert!(this.schedule_timer_if_should_retransmit(core_ctx, bindings_ctx, timers, neighbor));
this
}
fn schedule_timer_if_should_retransmit<I, DeviceId, CC, BC>(
&mut self,
core_ctx: &mut CC,
bindings_ctx: &mut BC,
timers: &mut TimerHeap<I, BC>,
neighbor: SpecifiedAddr<I::Addr>,
) -> bool
where
I: Ip,
D: LinkDevice,
DeviceId: StrongId,
BC: NudBindingsContext<I, D, DeviceId>,
CC: NudConfigContext<I>,
{
let Self { transmit_counter, pending_frames: _, notifiers: _, _marker } = self;
schedule_timer_if_should_retransmit(
core_ctx,
bindings_ctx,
timers,
neighbor,
NudEvent::RetransmitMulticastProbe,
transmit_counter,
)
}
fn queue_packet<B, S>(&mut self, body: S) -> Result<(), S>
where
B: BufferMut,
S: Serializer<Buffer = B>,
{
let Self { pending_frames, transmit_counter: _, notifiers: _, _marker } = self;
// We don't accept new packets when the queue is full because earlier packets
// are more likely to initiate connections whereas later packets are more likely
// to carry data. E.g. A TCP SYN/SYN-ACK is likely to appear before a TCP
// segment with data and dropping the SYN/SYN-ACK may result in the TCP peer not
// processing the segment with data since the segment completing the handshake
// has not been received and handled yet.
if pending_frames.len() < MAX_PENDING_FRAMES {
pending_frames.push_back(
body.serialize_vec_outer()
.map_err(|(_err, s)| s)?
.map_a(|b| Buf::new(b.as_ref().to_vec(), ..))
.into_inner(),
);
}
Ok(())
}
/// Flush pending packets to the resolved link address and notify any observers
/// that link address resolution is complete.
fn complete_resolution<I, CC, BC>(
&mut self,
core_ctx: &mut CC,
bindings_ctx: &mut BC,
link_address: D::Address,
) where
I: Ip,
D: LinkDevice,
BC: NudBindingsContext<I, D, CC::DeviceId>,
CC: NudSenderContext<I, D, BC>,
{
let Self { pending_frames, notifiers, transmit_counter: _, _marker } = self;
// Send out pending packets while holding the NUD lock to prevent a potential
// ordering violation.
//
// If we drop the NUD lock before sending out these queued packets, another
// thread could take the NUD lock, observe that neighbor resolution is complete,
// and send a packet *before* these pending packets are sent out, resulting in
// out-of-order transmission to the device.
for body in pending_frames.drain(..) {
// Ignore any errors on sending the IP packet, because a failure at this point
// is not actionable for the caller: failing to send a previously-queued packet
// doesn't mean that updating the neighbor entry should fail.
core_ctx
.send_ip_packet_to_neighbor_link_addr(bindings_ctx, link_address, body)
.unwrap_or_else(|_: Buf<Vec<u8>>| {
tracing::error!("failed to send pending IP packet to neighbor {link_address:?}")
})
}
for notifier in notifiers.drain(..) {
notifier.notify(Ok(link_address));
}
}
}
#[derive(Debug, Derivative)]
#[cfg_attr(test, derivative(Clone, PartialEq, Eq))]
pub(crate) struct Reachable<D: LinkDevice, I: Instant> {
pub(crate) link_address: D::Address,
pub(crate) last_confirmed_at: I,
}
#[derive(Debug, Derivative)]
#[cfg_attr(test, derivative(Clone, PartialEq, Eq))]
pub(crate) struct Stale<D: LinkDevice> {
pub(crate) link_address: D::Address,
}
impl<D: LinkDevice> Stale<D> {
fn enter_delay<I, BC, DeviceId: Clone>(
&mut self,
bindings_ctx: &mut BC,
timers: &mut TimerHeap<I, BC>,
neighbor: SpecifiedAddr<I::Addr>,
) -> Delay<D>
where
I: Ip,
BC: NudBindingsContext<I, D, DeviceId>,
{
let Self { link_address } = *self;
// Start a timer to transition into PROBE after DELAY_FIRST_PROBE seconds if no
// packets are sent to this neighbor.
timers.schedule_neighbor(
bindings_ctx,
DELAY_FIRST_PROBE_TIME,
neighbor,
NudEvent::DelayFirstProbe,
);
Delay { link_address }
}
}
#[derive(Debug, Derivative)]
#[cfg_attr(test, derivative(Clone, PartialEq, Eq))]
pub(crate) struct Delay<D: LinkDevice> {
link_address: D::Address,
}
impl<D: LinkDevice> Delay<D> {
fn enter_probe<I, DeviceId, CC, BC>(
&mut self,
core_ctx: &mut CC,
bindings_ctx: &mut BC,
timers: &mut TimerHeap<I, BC>,
neighbor: SpecifiedAddr<I::Addr>,
) -> Probe<D>
where
I: Ip,
DeviceId: StrongId,
BC: NudBindingsContext<I, D, DeviceId>,
CC: NudConfigContext<I>,
{
let Self { link_address } = *self;
// NB: transmission of a neighbor probe on entering PROBE (and subsequent
// retransmissions) is done by `handle_timer`, as it need not be done with the
// neighbor table lock held.
let retransmit_timeout = core_ctx.retransmit_timeout();
timers.schedule_neighbor(
bindings_ctx,
retransmit_timeout,
neighbor,
NudEvent::RetransmitUnicastProbe,
);
Probe {
link_address,
transmit_counter: NonZeroU16::new(core_ctx.max_unicast_solicit().get() - 1),
}
}
}
#[derive(Debug, Derivative)]
#[cfg_attr(test, derivative(Clone, PartialEq, Eq))]
pub(crate) struct Probe<D: LinkDevice> {
link_address: D::Address,
transmit_counter: Option<NonZeroU16>,
}
impl<D: LinkDevice> Probe<D> {
fn schedule_timer_if_should_retransmit<I, DeviceId, CC, BC>(
&mut self,
core_ctx: &mut CC,
bindings_ctx: &mut BC,
timers: &mut TimerHeap<I, BC>,
neighbor: SpecifiedAddr<I::Addr>,
) -> bool
where
I: Ip,
DeviceId: StrongId,
BC: NudBindingsContext<I, D, DeviceId>,
CC: NudConfigContext<I>,
{
let Self { link_address: _, transmit_counter } = self;
schedule_timer_if_should_retransmit(
core_ctx,
bindings_ctx,
timers,
neighbor,
NudEvent::RetransmitUnicastProbe,
transmit_counter,
)
}
fn enter_unreachable<I, BC, DeviceId>(
&mut self,
bindings_ctx: &mut BC,
timers: &mut TimerHeap<I, BC>,
num_entries: usize,
last_gc: &mut Option<BC::Instant>,
) -> Unreachable<D>
where
I: Ip,
BC: NudBindingsContext<I, D, DeviceId>,
DeviceId: Clone,
{
// This entry is deemed discardable now that it is not in active use; schedule
// garbage collection for the neighbor table if we are currently over the
// maximum amount of entries.
timers.maybe_schedule_gc(bindings_ctx, num_entries, last_gc);
let Self { link_address, transmit_counter: _ } = self;
Unreachable { link_address: *link_address, mode: UnreachableMode::WaitingForPacketSend }
}
}
#[derive(Debug, Derivative)]
#[cfg_attr(test, derivative(Clone, PartialEq, Eq))]
pub(crate) struct Unreachable<D: LinkDevice> {
link_address: D::Address,
mode: UnreachableMode,
}
/// The dynamic neighbor state specific to the UNREACHABLE state as defined in
/// [RFC 7048].
///
/// When a neighbor entry transitions to UNREACHABLE, the netstack will stop
/// actively retransmitting probes if no packets are being sent to the neighbor.
///
/// If packets are sent through the neighbor, the netstack will continue to
/// retransmit multicast probes, but with exponential backoff on the timer,
/// based on the `BACKOFF_MULTIPLE` and clamped at `MAX_RETRANS_TIMER`.
///
/// [RFC 7048]: https://tools.ietf.org/html/rfc7048
#[derive(Debug, Clone, Copy, Derivative)]
#[cfg_attr(test, derivative(PartialEq, Eq))]
pub(crate) enum UnreachableMode {
WaitingForPacketSend,
Backoff { probes_sent: NonZeroU32, packet_sent: bool },
}
impl UnreachableMode {
/// The amount of time to wait before transmitting another multicast probe
/// to the cached link address, based on how many probes we have transmitted
/// so far, as defined in [RFC 7048 section 4].
///
/// [RFC 7048 section 4]: https://tools.ietf.org/html/rfc7048#section-4
fn next_backoff_retransmit_timeout<I, CC>(&self, core_ctx: &mut CC) -> NonZeroDuration
where
I: Ip,
CC: NudConfigContext<I>,
{
let probes_sent = match self {
UnreachableMode::Backoff { probes_sent, packet_sent: _ } => probes_sent,
UnreachableMode::WaitingForPacketSend => {
panic!("cannot calculate exponential backoff in state {self:?}")
}
};
// TODO(https://fxbug.dev/42083368): vary this retransmit timeout by some random
// "jitter factor" to avoid synchronization of transmissions from different
// hosts.
(core_ctx.retransmit_timeout() * BACKOFF_MULTIPLE.saturating_pow(probes_sent.get()))
.min(MAX_RETRANS_TIMER)
}
}
impl<D: LinkDevice> Unreachable<D> {
fn handle_timer<I, DeviceId, CC, BC>(
&mut self,
core_ctx: &mut CC,
bindings_ctx: &mut BC,
timers: &mut TimerHeap<I, BC>,
device_id: &DeviceId,
neighbor: SpecifiedAddr<I::Addr>,
) -> Option<TransmitProbe<D::Address>>
where
I: Ip,
DeviceId: StrongId,
BC: NudBindingsContext<I, D, DeviceId>,
CC: NudConfigContext<I>,
{
let Self { link_address: _, mode } = self;
match mode {
UnreachableMode::WaitingForPacketSend => {
panic!(
"timer should not have fired in UNREACHABLE while waiting for packet send; got \
a retransmit multicast probe event for {neighbor} on {device_id:?}",
);
}
UnreachableMode::Backoff { probes_sent, packet_sent } => {
if *packet_sent {
// It is all but guaranteed that we will never end up transmitting u32::MAX
// probes, given the retransmit timeout backs off to MAX_RETRANS_TIMER (1 minute
// by default), and u32::MAX minutes is over 8,000 years. By then we almost
// certainly would have garbage-collected the neighbor entry.
//
// But we do a saturating add just to be safe.
*probes_sent = probes_sent.saturating_add(1);
*packet_sent = false;
let duration = mode.next_backoff_retransmit_timeout(core_ctx);
timers.schedule_neighbor(
bindings_ctx,
duration,
neighbor,
NudEvent::RetransmitMulticastProbe,
);
Some(TransmitProbe::Multicast)
} else {
*mode = UnreachableMode::WaitingForPacketSend;
None
}
}
}
}
/// Advance the UNREACHABLE state machine based on a packet being queued for
/// transmission.
///
/// Returns whether a multicast neighbor probe should be sent as a result.
fn handle_packet_queued_to_send<I, DeviceId, CC, BC>(
&mut self,
core_ctx: &mut CC,
bindings_ctx: &mut BC,
timers: &mut TimerHeap<I, BC>,
neighbor: SpecifiedAddr<I::Addr>,
) -> bool
where
I: Ip,
DeviceId: StrongId,
BC: NudBindingsContext<I, D, DeviceId>,
CC: NudConfigContext<I>,
{
let Self { link_address: _, mode } = self;
match mode {
UnreachableMode::WaitingForPacketSend => {
// We already transmitted MAX_MULTICAST_SOLICIT probes to the neighbor
// without confirmation, but now a packet is being sent to that neighbor, so
// we are resuming transmission of probes for as long as packets continue to
// be sent to the neighbor. Instead of retransmitting on a fixed timeout,
// use exponential backoff per [RFC 7048 section 4]:
//
// If an implementation transmits more than MAX_UNICAST_SOLICIT/
// MAX_MULTICAST_SOLICIT packets, then it SHOULD use the exponential
// backoff of the retransmit timer. This is to avoid any significant
// load due to a steady background level of retransmissions from
// implementations that retransmit a large number of Neighbor
// Solicitations (NS) before discarding the NCE.
//
// [RFC 7048 section 4]: https://tools.ietf.org/html/rfc7048#section-4
let probes_sent = NonZeroU32::new(1).unwrap();
*mode = UnreachableMode::Backoff { probes_sent, packet_sent: false };
let duration = mode.next_backoff_retransmit_timeout(core_ctx);
timers.schedule_neighbor(
bindings_ctx,
duration,
neighbor,
NudEvent::RetransmitMulticastProbe,
);
// Transmit a multicast probe.
true
}
UnreachableMode::Backoff { probes_sent: _, packet_sent } => {
// We are in the exponential backoff phase of sending probes. Make a note
// that a packet was sent since the last transmission so that we will send
// another when the timer fires.
*packet_sent = true;
false
}
}
}
}
impl<D: LinkDevice, BT: NudBindingsTypes<D>> NeighborState<D, BT> {
fn to_event_state(&self) -> EventState<D::Address> {
match self {
NeighborState::Dynamic(dynamic_state) => {
EventState::Dynamic(dynamic_state.to_event_dynamic_state())
}
NeighborState::Static(addr) => EventState::Static(*addr),
}
}
}
impl<D: LinkDevice, BT: NudBindingsTypes<D>> DynamicNeighborState<D, BT> {
fn cancel_timer<I, BC, DeviceId>(
&mut self,
bindings_ctx: &mut BC,
timers: &mut TimerHeap<I, BC>,
neighbor: SpecifiedAddr<I::Addr>,
) where
I: Ip,
DeviceId: StrongId,
BC: NudBindingsContext<I, D, DeviceId>,
{
let expected_event = match self {
DynamicNeighborState::Incomplete(Incomplete {
transmit_counter: _,
pending_frames: _,
notifiers: _,
_marker,
}) => Some(NudEvent::RetransmitMulticastProbe),
DynamicNeighborState::Reachable(Reachable {
link_address: _,
last_confirmed_at: _,
}) => Some(NudEvent::ReachableTime),
DynamicNeighborState::Stale(Stale { link_address: _ }) => None,
DynamicNeighborState::Delay(Delay { link_address: _ }) => {
Some(NudEvent::DelayFirstProbe)
}
DynamicNeighborState::Probe(Probe { link_address: _, transmit_counter: _ }) => {
Some(NudEvent::RetransmitUnicastProbe)
}
DynamicNeighborState::Unreachable(Unreachable { link_address: _, mode }) => {
// A timer should be scheduled iff a packet was recently sent to the neighbor
// and we are retransmitting probes with exponential backoff.
match mode {
UnreachableMode::WaitingForPacketSend => None,
UnreachableMode::Backoff { probes_sent: _, packet_sent: _ } => {
Some(NudEvent::RetransmitMulticastProbe)
}
}
}
};
assert_eq!(
timers.cancel_neighbor(bindings_ctx, neighbor),
expected_event,
"neighbor {neighbor} ({self:?}) had unexpected timer installed"
);
}
fn cancel_timer_and_complete_resolution<I, CC, BC>(
mut self,
core_ctx: &mut CC,
bindings_ctx: &mut BC,
timers: &mut TimerHeap<I, BC>,
neighbor: SpecifiedAddr<I::Addr>,
link_address: D::Address,
) where
I: Ip,
BC: NudBindingsContext<I, D, CC::DeviceId>,
CC: NudSenderContext<I, D, BC>,
{
self.cancel_timer(bindings_ctx, timers, neighbor);
match self {
DynamicNeighborState::Incomplete(mut incomplete) => {
incomplete.complete_resolution(core_ctx, bindings_ctx, link_address);
}
DynamicNeighborState::Reachable(_)
| DynamicNeighborState::Stale(_)
| DynamicNeighborState::Delay(_)
| DynamicNeighborState::Probe(_)
| DynamicNeighborState::Unreachable(_) => {}
}
}
fn to_event_dynamic_state(&self) -> EventDynamicState<D::Address> {
match self {
Self::Incomplete(_) => EventDynamicState::Incomplete,
Self::Reachable(Reachable { link_address, last_confirmed_at: _ }) => {
EventDynamicState::Reachable(*link_address)
}
Self::Stale(Stale { link_address }) => EventDynamicState::Stale(*link_address),
Self::Delay(Delay { link_address }) => EventDynamicState::Delay(*link_address),
Self::Probe(Probe { link_address, transmit_counter: _ }) => {
EventDynamicState::Probe(*link_address)
}
Self::Unreachable(Unreachable { link_address, mode: _ }) => {
EventDynamicState::Unreachable(*link_address)
}
}
}
// Enters reachable state.
fn enter_reachable<I, CC, BC>(
&mut self,
core_ctx: &mut CC,
bindings_ctx: &mut BC,
timers: &mut TimerHeap<I, BC>,
device_id: &CC::DeviceId,
neighbor: SpecifiedAddr<I::Addr>,
link_address: D::Address,
) where
I: Ip,
BC: NudBindingsContext<I, D, CC::DeviceId, Instant = BT::Instant>,
CC: NudSenderContext<I, D, BC>,
{
// TODO(https://fxbug.dev/42075782): if the new state matches the current state,
// update the link address as necessary, but do not cancel + reschedule timers.
let now = bindings_ctx.now();
match self {
// If this neighbor entry is already in REACHABLE, rather than proactively
// rescheduling the timer (which can be a relatively expensive operation
// especially in the hot path), simply update `last_confirmed_at` so that when
// the timer does eventually fire, we can reschedule it accordingly.
DynamicNeighborState::Reachable(Reachable {
link_address: current,
last_confirmed_at,
}) if *current == link_address => {
*last_confirmed_at = now;
return;
}
DynamicNeighborState::Incomplete(_)
| DynamicNeighborState::Reachable(_)
| DynamicNeighborState::Stale(_)
| DynamicNeighborState::Delay(_)
| DynamicNeighborState::Probe(_)
| DynamicNeighborState::Unreachable(_) => {}
}
let previous = core::mem::replace(
self,
DynamicNeighborState::Reachable(Reachable { link_address, last_confirmed_at: now }),
);
let event_dynamic_state = self.to_event_dynamic_state();
debug_assert_ne!(previous.to_event_dynamic_state(), event_dynamic_state);
let event_state = EventState::Dynamic(event_dynamic_state);
bindings_ctx.on_event(Event::changed(device_id, event_state, neighbor, bindings_ctx.now()));
previous.cancel_timer_and_complete_resolution(
core_ctx,
bindings_ctx,
timers,
neighbor,
link_address,
);
timers.schedule_neighbor(
bindings_ctx,
core_ctx.base_reachable_time(),
neighbor,
NudEvent::ReachableTime,
);
}
// Enters the Stale state.
//
// # Panics
//
// Panics if `self` is already in Stale with a link address equal to
// `link_address`, i.e. this function should only be called when state
// actually changes.
fn enter_stale<I, CC, BC>(
&mut self,
core_ctx: &mut CC,
bindings_ctx: &mut BC,
timers: &mut TimerHeap<I, BC>,
device_id: &CC::DeviceId,
neighbor: SpecifiedAddr<I::Addr>,
link_address: D::Address,
num_entries: usize,
last_gc: &mut Option<BC::Instant>,
) where
I: Ip,
BC: NudBindingsContext<I, D, CC::DeviceId>,
CC: NudSenderContext<I, D, BC>,
{
// TODO(https://fxbug.dev/42075782): if the new state matches the current state,
// update the link address as necessary, but do not cancel + reschedule timers.
let previous =
core::mem::replace(self, DynamicNeighborState::Stale(Stale { link_address }));
let event_dynamic_state = self.to_event_dynamic_state();
debug_assert_ne!(previous.to_event_dynamic_state(), event_dynamic_state);
let event_state = EventState::Dynamic(event_dynamic_state);
bindings_ctx.on_event(Event::changed(device_id, event_state, neighbor, bindings_ctx.now()));
previous.cancel_timer_and_complete_resolution(
core_ctx,
bindings_ctx,
timers,
neighbor,
link_address,
);
// This entry is deemed discardable now that it is not in active use; schedule
// garbage collection for the neighbor table if we are currently over the
// maximum amount of entries.
timers.maybe_schedule_gc(bindings_ctx, num_entries, last_gc);
// Stale entries don't do anything until an outgoing packet is queued for
// transmission.
}
/// Resolve the cached link address for this neighbor entry, or return an
/// observer for an unresolved neighbor, and advance the NUD state machine
/// accordingly (as if a packet had been sent to the neighbor).
///
/// Also returns whether a multicast neighbor probe should be sent as a result.
fn resolve_link_addr<I, DeviceId, BC, CC>(
&mut self,
core_ctx: &mut CC,
bindings_ctx: &mut BC,
timers: &mut TimerHeap<I, BC>,
device_id: &DeviceId,
neighbor: SpecifiedAddr<I::Addr>,
) -> (
LinkResolutionResult<
D::Address,
<<BC as LinkResolutionContext<D>>::Notifier as LinkResolutionNotifier<D>>::Observer,
>,
bool,
)
where
I: Ip,
DeviceId: StrongId,
BC: NudBindingsContext<I, D, DeviceId, Notifier = BT::Notifier>,
CC: NudConfigContext<I>,
{
match self {
DynamicNeighborState::Incomplete(Incomplete {
notifiers,
transmit_counter: _,
pending_frames: _,
_marker,
}) => {
let (notifier, observer) = BC::Notifier::new();
notifiers.push(notifier);
(LinkResolutionResult::Pending(observer), false)
}
DynamicNeighborState::Stale(entry) => {
// Advance the state machine as if a packet had been sent to this neighbor.
//
// This is not required by the RFC, and it may result in neighbor probes going
// out for this neighbor that would not have otherwise (the only other way a
// STALE entry moves to DELAY is due to a packet being sent to it). However,
// sending neighbor probes to confirm reachability is likely to be useful given
// a client is attempting to resolve this neighbor. Additionally, this maintains
// consistency with Netstack2's behavior.
let delay @ Delay { link_address } =
entry.enter_delay(bindings_ctx, timers, neighbor);
*self = DynamicNeighborState::Delay(delay);
let event_state = EventState::Dynamic(self.to_event_dynamic_state());
bindings_ctx.on_event(Event::changed(
device_id,
event_state,
neighbor,
bindings_ctx.now(),
));
(LinkResolutionResult::Resolved(link_address), false)
}
DynamicNeighborState::Reachable(Reachable { link_address, last_confirmed_at: _ })
| DynamicNeighborState::Delay(Delay { link_address })
| DynamicNeighborState::Probe(Probe { link_address, transmit_counter: _ }) => {
(LinkResolutionResult::Resolved(*link_address), false)
}
DynamicNeighborState::Unreachable(unreachable) => {
let Unreachable { link_address, mode: _ } = unreachable;
let link_address = *link_address;
// Advance the state machine as if a packet had been sent to this neighbor.
let do_multicast_solicit = unreachable.handle_packet_queued_to_send(
core_ctx,
bindings_ctx,
timers,
neighbor,
);
(LinkResolutionResult::Resolved(link_address), do_multicast_solicit)
}
}
}
/// Handle a packet being queued for transmission: either queue it as a
/// pending packet for an unresolved neighbor, or send it to the cached link
/// address, and advance the NUD state machine accordingly.
///
/// Returns whether a multicast neighbor probe should be sent as a result.
fn handle_packet_queued_to_send<I, BC, CC, S>(
&mut self,
core_ctx: &mut CC,
bindings_ctx: &mut BC,
timers: &mut TimerHeap<I, BC>,
device_id: &CC::DeviceId,
neighbor: SpecifiedAddr<I::Addr>,
body: S,
) -> Result<bool, S>
where
I: Ip,
BC: NudBindingsContext<I, D, CC::DeviceId>,
CC: NudSenderContext<I, D, BC>,
S: Serializer,
S::Buffer: BufferMut,
{
match self {
DynamicNeighborState::Incomplete(incomplete) => {
incomplete.queue_packet(body)?;
Ok(false)
}
// Send the IP packet while holding the NUD lock to prevent a potential
// ordering violation.
//
// If we drop the NUD lock before sending out this packet, another thread
// could take the NUD lock and send a packet *before* this packet is sent
// out, resulting in out-of-order transmission to the device.
DynamicNeighborState::Stale(entry) => {
// Per [RFC 4861 section 7.3.3]:
//
// The first time a node sends a packet to a neighbor whose entry is
// STALE, the sender changes the state to DELAY and sets a timer to
// expire in DELAY_FIRST_PROBE_TIME seconds.
//
// [RFC 4861 section 7.3.3]: https://tools.ietf.org/html/rfc4861#section-7.3.3
let delay @ Delay { link_address } =
entry.enter_delay(bindings_ctx, timers, neighbor);
*self = DynamicNeighborState::Delay(delay);
let event_state = EventState::Dynamic(self.to_event_dynamic_state());
bindings_ctx.on_event(Event::changed(
device_id,
event_state,
neighbor,
bindings_ctx.now(),
));
core_ctx.send_ip_packet_to_neighbor_link_addr(bindings_ctx, link_address, body)?;
Ok(false)
}
DynamicNeighborState::Reachable(Reachable { link_address, last_confirmed_at: _ })
| DynamicNeighborState::Delay(Delay { link_address })
| DynamicNeighborState::Probe(Probe { link_address, transmit_counter: _ }) => {
core_ctx.send_ip_packet_to_neighbor_link_addr(bindings_ctx, *link_address, body)?;
Ok(false)
}
DynamicNeighborState::Unreachable(unreachable) => {
let Unreachable { link_address, mode: _ } = unreachable;
core_ctx.send_ip_packet_to_neighbor_link_addr(bindings_ctx, *link_address, body)?;
let do_multicast_solicit = unreachable.handle_packet_queued_to_send(
core_ctx,
bindings_ctx,
timers,
neighbor,
);
Ok(do_multicast_solicit)
}
}
}
fn handle_probe<I, CC, BC>(
&mut self,
core_ctx: &mut CC,
bindings_ctx: &mut BC,
timers: &mut TimerHeap<I, BC>,
device_id: &CC::DeviceId,
neighbor: SpecifiedAddr<I::Addr>,
link_address: D::Address,
num_entries: usize,
last_gc: &mut Option<BC::Instant>,
) where
I: Ip,
BC: NudBindingsContext<I, D, CC::DeviceId>,
CC: NudSenderContext<I, D, BC>,
{
// Per [RFC 4861 section 7.2.3] ("Receipt of Neighbor Solicitations"):
//
// If an entry already exists, and the cached link-layer address
// differs from the one in the received Source Link-Layer option, the
// cached address should be replaced by the received address, and the
// entry's reachability state MUST be set to STALE.
//
// [RFC 4861 section 7.2.3]: https://tools.ietf.org/html/rfc4861#section-7.2.3
let transition_to_stale = match self {
DynamicNeighborState::Incomplete(_) => true,
DynamicNeighborState::Reachable(Reachable {
link_address: current,
last_confirmed_at: _,
})
| DynamicNeighborState::Stale(Stale { link_address: current })
| DynamicNeighborState::Delay(Delay { link_address: current })
| DynamicNeighborState::Probe(Probe { link_address: current, transmit_counter: _ })
| DynamicNeighborState::Unreachable(Unreachable { link_address: current, mode: _ }) => {
current != &link_address
}
};
if transition_to_stale {
self.enter_stale(
core_ctx,
bindings_ctx,
timers,
device_id,
neighbor,
link_address,
num_entries,
last_gc,
);
}
}
fn handle_confirmation<I, CC, BC>(
&mut self,
core_ctx: &mut CC,
bindings_ctx: &mut BC,
timers: &mut TimerHeap<I, BC>,
device_id: &CC::DeviceId,
neighbor: SpecifiedAddr<I::Addr>,
link_address: D::Address,
flags: ConfirmationFlags,
num_entries: usize,
last_gc: &mut Option<BC::Instant>,
) where
I: Ip,
BC: NudBindingsContext<I, D, CC::DeviceId, Instant = BT::Instant>,
CC: NudSenderContext<I, D, BC>,
{
let ConfirmationFlags { solicited_flag, override_flag } = flags;
enum NewState<A> {
Reachable { link_address: A },
Stale { link_address: A },
}
let new_state = match self {
DynamicNeighborState::Incomplete(Incomplete {
transmit_counter: _,
pending_frames: _,
notifiers: _,
_marker,
}) => {
// Per RFC 4861 section 7.2.5:
//
// If the advertisement's Solicited flag is set, the state of the
// entry is set to REACHABLE; otherwise, it is set to STALE.
//
// Note that the Override flag is ignored if the entry is in the
// INCOMPLETE state.
if solicited_flag {
Some(NewState::Reachable { link_address })
} else {
Some(NewState::Stale { link_address })
}
}
DynamicNeighborState::Reachable(Reachable {
link_address: current,
last_confirmed_at: _,
})
| DynamicNeighborState::Stale(Stale { link_address: current })
| DynamicNeighborState::Delay(Delay { link_address: current })
| DynamicNeighborState::Probe(Probe { link_address: current, transmit_counter: _ })
| DynamicNeighborState::Unreachable(Unreachable { link_address: current, mode: _ }) => {
let updated_link_address = current != &link_address;
match (solicited_flag, updated_link_address, override_flag) {
// Per RFC 4861 section 7.2.5:
//
// If [either] the Override flag is set, or the supplied link-layer address is
// the same as that in the cache, [and] ... the Solicited flag is set, the
// entry MUST be set to REACHABLE.
(true, _, true) | (true, false, _) => {
Some(NewState::Reachable { link_address })
}
// 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 place:
//
// 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 the cache.
(_, true, false) => match self {
// NB: do not update the link address.
DynamicNeighborState::Reachable(Reachable {
link_address,
last_confirmed_at: _,
}) => Some(NewState::Stale { link_address: *link_address }),
// Ignore the advertisement and do not update the cache.
DynamicNeighborState::Stale(_)
| DynamicNeighborState::Delay(_)
| DynamicNeighborState::Probe(_)
| DynamicNeighborState::Unreachable(_) => None,
// The INCOMPLETE state was already handled in the outer match.
DynamicNeighborState::Incomplete(_) => unreachable!(),
},
// Per RFC 4861 section 7.2.5:
//
// If the Override flag is set [and] ... the Solicited flag is zero and the
// link-layer address was updated with a different address, the state MUST be
// set to STALE.
(false, true, true) => Some(NewState::Stale { link_address }),
// Per RFC 4861 section 7.2.5:
//
// There is no need to update the state for unsolicited advertisements that do
// not change the contents of the cache.
(false, false, _) => None,
}
}
};
match new_state {
Some(NewState::Reachable { link_address }) => self.enter_reachable(
core_ctx,
bindings_ctx,
timers,
device_id,
neighbor,
link_address,
),
Some(NewState::Stale { link_address }) => self.enter_stale(
core_ctx,
bindings_ctx,
timers,
device_id,
neighbor,
link_address,
num_entries,
last_gc,
),
None => {}
}
}
}
#[cfg(test)]
pub(crate) mod testutil {
use super::*;
pub(crate) fn assert_dynamic_neighbor_with_addr<
I: Ip,
D: LinkDevice,
BC: NudBindingsContext<I, D, CC::DeviceId>,
CC: NudContext<I, D, BC>,
>(
core_ctx: &mut CC,
device_id: CC::DeviceId,
neighbor: SpecifiedAddr<I::Addr>,
expected_link_addr: D::Address,
) {
core_ctx.with_nud_state_mut(&device_id, |NudState { neighbors, .. }, _config| {
assert_matches!(
neighbors.get(&neighbor),
Some(NeighborState::Dynamic(
DynamicNeighborState::Reachable(Reachable{ link_address, last_confirmed_at: _ })
| DynamicNeighborState::Stale(Stale{ link_address })
)) => {
assert_eq!(link_address, &expected_link_addr)
}
)
})
}
pub(crate) fn assert_dynamic_neighbor_state<I, D, BC, CC>(
core_ctx: &mut CC,
device_id: CC::DeviceId,
neighbor: SpecifiedAddr<I::Addr>,
expected_state: DynamicNeighborState<D, BC>,
) where
I: Ip,
D: LinkDevice + PartialEq,
BC: NudBindingsContext<I, D, CC::DeviceId>,
CC: NudContext<I, D, BC>,
{
core_ctx.with_nud_state_mut(&device_id, |NudState { neighbors, .. }, _config| {
assert_matches!(
neighbors.get(&neighbor),
Some(NeighborState::Dynamic(state)) => {
assert_eq!(state, &expected_state)
}
)
})
}
pub(crate) fn assert_neighbor_unknown<
I: Ip,
D: LinkDevice,
BC: NudBindingsContext<I, D, CC::DeviceId>,
CC: NudContext<I, D, BC>,
>(
core_ctx: &mut CC,
device_id: CC::DeviceId,
neighbor: SpecifiedAddr<I::Addr>,
) {
core_ctx.with_nud_state_mut(&device_id, |NudState { neighbors, .. }, _config| {
assert_matches!(neighbors.get(&neighbor), None)
})
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
enum NudEvent {
RetransmitMulticastProbe,
ReachableTime,
DelayFirstProbe,
RetransmitUnicastProbe,
}
#[derive(GenericOverIp, Copy, Clone, Debug, Eq, PartialEq, Hash)]
#[generic_over_ip(I, Ip)]
pub struct NudTimerId<I: Ip, L: LinkDevice, D: device::WeakId> {
device_id: D,
timer_type: NudTimerType,
_marker: PhantomData<(I, L)>,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
enum NudTimerType {
Neighbor,
GarbageCollection,
}
/// A wrapper for [`LocalTimerHeap`] that we can attach NUD helpers to.
#[cfg_attr(test, derive(Debug))]
struct TimerHeap<I: Ip, BT: TimerBindingsTypes + InstantBindingsTypes> {
gc: BT::Timer,
neighbor: LocalTimerHeap<SpecifiedAddr<I::Addr>, NudEvent, BT>,
}
impl<I: Ip, BC: TimerContext2> TimerHeap<I, BC> {
fn new<
DeviceId: device::WeakId,
L: LinkDevice,
F: Fn(NudTimerId<I, L, DeviceId>) -> BC::DispatchId,
>(
bindings_ctx: &mut BC,
device_id: DeviceId,
convert: F,
) -> Self {
Self {
neighbor: LocalTimerHeap::new(
bindings_ctx,
convert(NudTimerId {
device_id: device_id.clone(),
timer_type: NudTimerType::Neighbor,
_marker: PhantomData,
}),
),
gc: bindings_ctx.new_timer(convert(NudTimerId {
device_id,
timer_type: NudTimerType::GarbageCollection,
_marker: PhantomData,
})),
}
}
fn schedule_neighbor(
&mut self,
bindings_ctx: &mut BC,
after: NonZeroDuration,
neighbor: SpecifiedAddr<I::Addr>,
event: NudEvent,
) {
let Self { neighbor: heap, gc: _ } = self;
assert_eq!(heap.schedule_after(bindings_ctx, neighbor, event, after.get()), None);
}
fn schedule_neighbor_at(
&mut self,
bindings_ctx: &mut BC,
at: BC::Instant,
neighbor: SpecifiedAddr<I::Addr>,
event: NudEvent,
) {
let Self { neighbor: heap, gc: _ } = self;
assert_eq!(heap.schedule_instant(bindings_ctx, neighbor, event, at), None);
}
/// Cancels a neighbor timer.
fn cancel_neighbor(
&mut self,
bindings_ctx: &mut BC,
neighbor: SpecifiedAddr<I::Addr>,
) -> Option<NudEvent> {
let Self { neighbor: heap, gc: _ } = self;
heap.cancel(bindings_ctx, &neighbor).map(|(_instant, v)| v)
}
fn pop_neighbor(
&mut self,
bindings_ctx: &mut BC,
) -> Option<(SpecifiedAddr<I::Addr>, NudEvent)> {
let Self { neighbor: heap, gc: _ } = self;
heap.pop(bindings_ctx)
}
/// Schedules a garbage collection IFF we hit the entries threshold and it's
/// not already scheduled.
fn maybe_schedule_gc(
&mut self,
bindings_ctx: &mut BC,
num_entries: usize,
last_gc: &Option<BC::Instant>,
) {
let Self { gc, neighbor: _ } = self;
if num_entries > MAX_ENTRIES && bindings_ctx.scheduled_instant2(gc).is_none() {
let instant = if let Some(last_gc) = last_gc {
last_gc.add(MIN_GARBAGE_COLLECTION_INTERVAL.get())
} else {
bindings_ctx.now()
};
// Scheduling a timer requires a mutable borrow and we're
// currently holding it exclusively. We just checked that the timer
// is not scheduled, so this assertion always holds.
assert_eq!(bindings_ctx.schedule_timer_instant2(instant, gc), None);
}
}
}
#[cfg_attr(test, derive(Debug))]
pub struct NudState<I: Ip, D: LinkDevice, BT: NudBindingsTypes<D>> {
// TODO(https://fxbug.dev/42076887): Key neighbors by `UnicastAddr`.
neighbors: HashMap<SpecifiedAddr<I::Addr>, NeighborState<D, BT>>,
last_gc: Option<BT::Instant>,
timer_heap: TimerHeap<I, BT>,
}
impl<I: Ip, D: LinkDevice, BC: NudBindingsTypes<D> + TimerContext2> NudState<I, D, BC> {
pub fn new<DeviceId: device::WeakId, F: Fn(NudTimerId<I, D, DeviceId>) -> BC::DispatchId>(
bindings_ctx: &mut BC,
device_id: DeviceId,
convert: F,
) -> Self {
Self {
neighbors: Default::default(),
last_gc: None,
timer_heap: TimerHeap::new(bindings_ctx, device_id, convert),
}
}
}
/// The bindings context for NUD.
pub trait NudBindingsContext<I: Ip, D: LinkDevice, DeviceId>:
TimerContext2
+ LinkResolutionContext<D>
+ EventContext<Event<D::Address, DeviceId, I, <Self as InstantBindingsTypes>::Instant>>
{
}
impl<
I: Ip,
D: LinkDevice,
DeviceId,
BC: TimerContext2
+ LinkResolutionContext<D>
+ EventContext<Event<D::Address, DeviceId, I, <Self as InstantBindingsTypes>::Instant>>,
> NudBindingsContext<I, D, DeviceId> for BC
{
}
/// A marker trait for types provided by bindings to NUD.
pub trait NudBindingsTypes<D: LinkDevice>:
LinkResolutionContext<D> + InstantBindingsTypes + TimerBindingsTypes
{
}
impl<BT, D> NudBindingsTypes<D> for BT
where
D: LinkDevice,
BT: LinkResolutionContext<D> + InstantBindingsTypes + TimerBindingsTypes,
{
}
/// An execution context that allows creating link resolution notifiers.
pub trait LinkResolutionContext<D: LinkDevice> {
/// A notifier held by core that can be used to inform interested parties of
/// the result of link address resolution.
type Notifier: LinkResolutionNotifier<D>;
}
/// A notifier held by core that can be used to inform interested parties of the
/// result of link address resolution.
pub trait LinkResolutionNotifier<D: LinkDevice>: Debug + Sized + Send {
/// The corresponding observer that can be used to observe the result of
/// link address resolution.
type Observer;
/// Create a connected (notifier, observer) pair.
fn new() -> (Self, Self::Observer);
/// Signal to Bindings that link address resolution has completed for a
/// neighbor.
fn notify(self, result: Result<D::Address, AddressResolutionFailed>);
}
/// The execution context for NUD for a link device.
pub trait NudContext<I: Ip, D: LinkDevice, BC: NudBindingsContext<I, D, Self::DeviceId>>:
DeviceIdContext<D>
{
type ConfigCtx<'a>: NudConfigContext<I>;
type SenderCtx<'a>: NudSenderContext<I, D, BC, DeviceId = Self::DeviceId>;
/// Calls the function with a mutable reference to the NUD state and the
/// core sender context.
fn with_nud_state_mut_and_sender_ctx<
O,
F: FnOnce(&mut NudState<I, D, BC>, &mut Self::SenderCtx<'_>) -> O,
>(
&mut self,
device_id: &Self::DeviceId,
cb: F,
) -> O;
/// Calls the function with a mutable reference to the NUD state and NUD
/// configuration for the device.
fn with_nud_state_mut<O, F: FnOnce(&mut NudState<I, D, BC>, &mut Self::ConfigCtx<'_>) -> O>(
&mut self,
device_id: &Self::DeviceId,
cb: F,
) -> O;
/// Calls the function with an immutable reference to the NUD state.
fn with_nud_state<O, F: FnOnce(&NudState<I, D, BC>) -> O>(
&mut self,
device_id: &Self::DeviceId,
cb: F,
) -> O;
/// Sends a neighbor probe/solicitation message.
///
/// If `remote_link_addr` is provided, the message will be unicasted to that
/// address; if it is `None`, the message will be multicast.
fn send_neighbor_solicitation(
&mut self,
bindings_ctx: &mut BC,
device_id: &Self::DeviceId,
lookup_addr: SpecifiedAddr<I::Addr>,
remote_link_addr: Option<D::Address>,
);
}
/// IP extension trait to support [`NudIcmpContext`].
pub(crate) trait NudIcmpIpExt: packet_formats::ip::IpExt {
/// IP packet metadata needed when sending ICMP destination unreachable
/// errors as a result of link-layer address resolution failure.
type Metadata;
/// Extracts IP-version specific metadata from `packet`.
fn extract_metadata<B: ByteSlice>(packet: &Self::Packet<B>) -> Self::Metadata;
}
impl NudIcmpIpExt for Ipv4 {
type Metadata = (usize, Ipv4FragmentType);
fn extract_metadata<B: ByteSlice>(packet: &Ipv4Packet<B>) -> Self::Metadata {
(packet.header_len(), packet.fragment_type())
}
}
impl NudIcmpIpExt for Ipv6 {
type Metadata = ();
fn extract_metadata<B: ByteSlice>(_: &Ipv6Packet<B>) -> () {}
}
/// The execution context which allows sending ICMP destination unreachable
/// errors, which needs to happen when address resolution fails.
pub(crate) trait NudIcmpContext<I: Ip + NudIcmpIpExt, D: LinkDevice, BC>:
DeviceIdContext<D>
{
/// Send an ICMP destination unreachable error to `original_src_ip` as
/// a result of `frame` being unable to be sent/forwarded due to link
/// layer address resolution failure.
///
/// `original_src_ip`, `original_dst_ip`, and `header_len` are all IP
/// header fields from `frame`.
fn send_icmp_dest_unreachable(
&mut self,
bindings_ctx: &mut BC,
frame: Buf<Vec<u8>>,
device_id: Option<&Self::DeviceId>,
original_src_ip: SocketIpAddr<I::Addr>,
original_dst_ip: SocketIpAddr<I::Addr>,
metadata: I::Metadata,
);
}
/// NUD configurations.
#[derive(Clone, Debug)]
pub struct NudUserConfig {
/// The maximum number of unicast solicitations as defined in [RFC 4861
/// section 10].
///
/// [RFC 4861 section 10]: https://tools.ietf.org/html/rfc4861#section-10
pub max_unicast_solicitations: NonZeroU16,
/// 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
pub max_multicast_solicitations: NonZeroU16,
/// The base value used for computing the duration a neighbor is considered
/// reachable after receiving a reachability confirmation as defined in
/// [RFC 4861 section 6.3.2].
///
/// [RFC 4861 section 6.3.2]: https://tools.ietf.org/html/rfc4861#section-6.3.2
pub base_reachable_time: NonZeroDuration,
}
impl Default for NudUserConfig {
fn default() -> Self {
NudUserConfig {
max_unicast_solicitations: DEFAULT_MAX_UNICAST_SOLICIT,
max_multicast_solicitations: DEFAULT_MAX_MULTICAST_SOLICIT,
base_reachable_time: DEFAULT_BASE_REACHABLE_TIME,
}
}
}
/// An update structure for [`NudUserConfig`].
///
/// Only fields with variant `Some` are updated.
#[derive(Clone, Debug, Eq, PartialEq, Default)]
pub struct NudUserConfigUpdate {
/// The maximum number of unicast solicitations as defined in [RFC 4861
/// section 10].
pub max_unicast_solicitations: Option<NonZeroU16>,
/// The maximum number of multicast solicitations as defined in [RFC 4861
/// section 10].
pub max_multicast_solicitations: Option<NonZeroU16>,
/// The base value used for computing the duration a neighbor is considered
/// reachable after receiving a reachability confirmation as defined in
/// [RFC 4861 section 6.3.2].
///
/// [RFC 4861 section 6.3.2]: https://tools.ietf.org/html/rfc4861#section-6.3.2
pub base_reachable_time: Option<NonZeroDuration>,
}
impl NudUserConfigUpdate {
pub(crate) fn apply_and_take_previous(mut self, config: &mut NudUserConfig) -> Self {
fn swap_if_set<T>(opt: &mut Option<T>, target: &mut T) {
if let Some(opt) = opt.as_mut() {
core::mem::swap(opt, target)
}
}
let Self { max_unicast_solicitations, max_multicast_solicitations, base_reachable_time } =
&mut self;
swap_if_set(max_unicast_solicitations, &mut config.max_unicast_solicitations);
swap_if_set(max_multicast_solicitations, &mut config.max_multicast_solicitations);
swap_if_set(base_reachable_time, &mut config.base_reachable_time);
self
}
}
/// The execution context for NUD that allows accessing NUD configuration (such
/// as timer durations) for a particular device.
pub trait NudConfigContext<I: Ip> {
/// The amount of time between retransmissions of neighbor probe messages.
///
/// This corresponds to the configurable per-interface `RetransTimer` value
/// used in NUD as defined in [RFC 4861 section 6.3.2].
///
/// [RFC 4861 section 6.3.2]: https://datatracker.ietf.org/doc/html/rfc4861#section-6.3.2
fn retransmit_timeout(&mut self) -> NonZeroDuration;
/// Calls the callback with an immutable reference to NUD configurations.
fn with_nud_user_config<O, F: FnOnce(&NudUserConfig) -> O>(&mut self, cb: F) -> O;
/// Returns the maximum number of unicast solicitations.
fn max_unicast_solicit(&mut self) -> NonZeroU16 {
self.with_nud_user_config(|NudUserConfig { max_unicast_solicitations, .. }| {
*max_unicast_solicitations
})
}
/// Returns the maximum number of multicast solicitations.
fn max_multicast_solicit(&mut self) -> NonZeroU16 {
self.with_nud_user_config(|NudUserConfig { max_multicast_solicitations, .. }| {
*max_multicast_solicitations
})
}
/// Returns the base reachable time, the duration a neighbor is considered
/// reachable after receiving a reachability confirmation.
fn base_reachable_time(&mut self) -> NonZeroDuration {
self.with_nud_user_config(|NudUserConfig { base_reachable_time, .. }| *base_reachable_time)
}
}
/// The execution context for NUD for a link device that allows sending IP
/// packets to specific neighbors.
pub trait NudSenderContext<I: Ip, D: LinkDevice, BC: NudBindingsContext<I, D, Self::DeviceId>>:
NudConfigContext<I> + DeviceIdContext<D>
{
/// Send an IP frame to the neighbor with the specified link address.
fn send_ip_packet_to_neighbor_link_addr<S>(
&mut self,
bindings_ctx: &mut BC,
neighbor_link_addr: D::Address,
body: S,
) -> Result<(), S>
where
S: Serializer,
S::Buffer: BufferMut;
}
/// An implementation of NUD for the IP layer.
pub trait NudIpHandler<I: Ip, BC>: DeviceIdContext<AnyDevice> {
/// Handles an incoming neighbor probe message.
///
/// For IPv6, this can be an NDP Neighbor Solicitation or an NDP Router
/// Advertisement message.
fn handle_neighbor_probe(
&mut self,
bindings_ctx: &mut BC,
device_id: &Self::DeviceId,
neighbor: SpecifiedAddr<I::Addr>,
link_addr: &[u8],
);
/// Handles an incoming neighbor confirmation message.
///
/// For IPv6, this can be an NDP Neighbor Advertisement.
fn handle_neighbor_confirmation(
&mut self,
bindings_ctx: &mut BC,
device_id: &Self::DeviceId,
neighbor: SpecifiedAddr<I::Addr>,
link_addr: &[u8],
flags: ConfirmationFlags,
);
/// Clears the neighbor table.
fn flush_neighbor_table(&mut self, bindings_ctx: &mut BC, device_id: &Self::DeviceId);
}
/// Specifies the link-layer address of a neighbor.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum LinkResolutionResult<A: LinkAddress, Observer> {
/// The destination is a known neighbor with the given link-layer address.
Resolved(A),
/// The destination is pending neighbor resolution.
Pending(Observer),
}
/// An implementation of NUD for a link device.
pub(crate) trait NudHandler<I: Ip, D: LinkDevice, BC: LinkResolutionContext<D>>:
DeviceIdContext<D>
{
/// Sets a dynamic neighbor's entry state to the specified values in
/// response to the source packet.
fn handle_neighbor_update(
&mut self,
bindings_ctx: &mut BC,
device_id: &Self::DeviceId,
// TODO(https://fxbug.dev/42076887): Use IPv4 subnet information to
// disallow the address with all host bits equal to 0, and the
// subnet broadcast addresses with all host bits equal to 1.
// TODO(https://fxbug.dev/42083952): Use NeighborAddr when available.
neighbor: SpecifiedAddr<I::Addr>,
// TODO(https://fxbug.dev/42083958): Wrap in `UnicastAddr`.
link_addr: D::Address,
source: DynamicNeighborUpdateSource,
);
/// Clears the neighbor table.
fn flush(&mut self, bindings_ctx: &mut BC, device_id: &Self::DeviceId);
/// Send an IP packet to the neighbor.
///
/// If the neighbor's link address is not known, link address resolution
/// is performed.
fn send_ip_packet_to_neighbor<S>(
&mut self,
bindings_ctx: &mut BC,
device_id: &Self::DeviceId,
neighbor: SpecifiedAddr<I::Addr>,
body: S,
) -> Result<(), S>
where
S: Serializer,
S::Buffer: BufferMut;
}
enum TransmitProbe<A> {
Multicast,
Unicast(A),
}
impl<
I: Ip + NudIcmpIpExt,
D: LinkDevice,
BC: NudBindingsContext<I, D, CC::DeviceId>,
CC: NudContext<I, D, BC> + NudIcmpContext<I, D, BC> + CounterContext<NudCounters<I>>,
> TimerHandler<BC, NudTimerId<I, D, CC::WeakDeviceId>> for CC
{
fn handle_timer(
&mut self,
bindings_ctx: &mut BC,
NudTimerId { device_id, timer_type, _marker: PhantomData }: NudTimerId<
I,
D,
CC::WeakDeviceId,
>,
) {
let Some(device_id) = device_id.upgrade() else {
return;
};
match timer_type {
NudTimerType::Neighbor => handle_neighbor_timer(self, bindings_ctx, device_id),
NudTimerType::GarbageCollection => collect_garbage(self, bindings_ctx, device_id),
}
}
}
fn handle_neighbor_timer<I, D, CC, BC>(
core_ctx: &mut CC,
bindings_ctx: &mut BC,
device_id: CC::DeviceId,
) where
I: Ip + NudIcmpIpExt,
D: LinkDevice,
BC: NudBindingsContext<I, D, CC::DeviceId>,
CC: NudContext<I, D, BC> + NudIcmpContext<I, D, BC> + CounterContext<NudCounters<I>>,
{
enum Action<L, A> {
TransmitProbe { probe: TransmitProbe<L>, to: A },
SendIcmpDestUnreachable(VecDeque<Buf<Vec<u8>>>),
}
let action = core_ctx.with_nud_state_mut(
&device_id,
|NudState { neighbors, last_gc, timer_heap }, core_ctx| {
let (lookup_addr, event) = timer_heap.pop_neighbor(bindings_ctx)?;
let num_entries = neighbors.len();
let mut entry = match neighbors.entry(lookup_addr) {
Entry::Occupied(entry) => entry,
Entry::Vacant(_) => panic!("timer fired for invalid entry"),
};
match entry.get_mut() {
NeighborState::Dynamic(DynamicNeighborState::Incomplete(incomplete)) => {
assert_eq!(event, NudEvent::RetransmitMulticastProbe);
if incomplete.schedule_timer_if_should_retransmit(
core_ctx,
bindings_ctx,
timer_heap,
lookup_addr,
) {
Some(Action::TransmitProbe {
probe: TransmitProbe::Multicast,
to: lookup_addr,
})
} else {
// Failed to complete neighbor resolution and no more probes to send.
// Subsequent traffic to this neighbor will recreate the entry and restart
// address resolution.
//
// TODO(https://fxbug.dev/42082448): consider retaining this neighbor entry in
// a sentinel `Failed` state, equivalent to its having been discarded except
// for debugging/observability purposes.
tracing::debug!(
"neighbor resolution failed for {lookup_addr}; removing entry"
);
let Incomplete {
transmit_counter: _,
ref mut pending_frames,
notifiers: _,
_marker,
} = assert_matches!(
entry.remove(),
NeighborState::Dynamic(DynamicNeighborState::Incomplete(incomplete))
=> incomplete
);
let pending_frames = core::mem::take(pending_frames);
bindings_ctx.on_event(Event::removed(
&device_id,
lookup_addr,
bindings_ctx.now(),
));
Some(Action::SendIcmpDestUnreachable(pending_frames))
}
}
NeighborState::Dynamic(DynamicNeighborState::Probe(probe)) => {
assert_eq!(event, NudEvent::RetransmitUnicastProbe);
let Probe { link_address, transmit_counter: _ } = probe;
let link_address = *link_address;
if probe.schedule_timer_if_should_retransmit(
core_ctx,
bindings_ctx,
timer_heap,
lookup_addr,
) {
Some(Action::TransmitProbe {
probe: TransmitProbe::Unicast(link_address),
to: lookup_addr,
})
} else {
let unreachable =
probe.enter_unreachable(bindings_ctx, timer_heap, num_entries, last_gc);
*entry.get_mut() =
NeighborState::Dynamic(DynamicNeighborState::Unreachable(unreachable));
let event_state = entry.get_mut().to_event_state();
let event = Event::changed(
&device_id,
event_state,
lookup_addr,
bindings_ctx.now(),
);
bindings_ctx.on_event(event);
None
}
}
NeighborState::Dynamic(DynamicNeighborState::Unreachable(unreachable)) => {
assert_eq!(event, NudEvent::RetransmitMulticastProbe);
unreachable
.handle_timer(core_ctx, bindings_ctx, timer_heap, &device_id, lookup_addr)
.map(|probe| Action::TransmitProbe { probe, to: lookup_addr })
}
NeighborState::Dynamic(DynamicNeighborState::Reachable(Reachable {
link_address,
last_confirmed_at,
})) => {
assert_eq!(event, NudEvent::ReachableTime);
let link_address = *link_address;
let expiration = last_confirmed_at.add(core_ctx.base_reachable_time().get());
if expiration > bindings_ctx.now() {
timer_heap.schedule_neighbor_at(
bindings_ctx,
expiration,
lookup_addr,
NudEvent::ReachableTime,
);
} else {
// Per [RFC 4861 section 7.3.3]:
//
// When ReachableTime milliseconds have passed since receipt of the last
// reachability confirmation for a neighbor, the Neighbor Cache entry's
// state changes from REACHABLE to STALE.
//
// [RFC 4861 section 7.3.3]: https://tools.ietf.org/html/rfc4861#section-7.3.3
*entry.get_mut() =
NeighborState::Dynamic(DynamicNeighborState::Stale(Stale {
link_address,
}));
let event_state = entry.get_mut().to_event_state();
let event = Event::changed(
&device_id,
event_state,
lookup_addr,
bindings_ctx.now(),
);
bindings_ctx.on_event(event);
// This entry is deemed discardable now that it is not in active use;
// schedule garbage collection for the neighbor table if we are currently
// over the maximum amount of entries.
timer_heap.maybe_schedule_gc(bindings_ctx, num_entries, last_gc);
}
None
}
NeighborState::Dynamic(DynamicNeighborState::Delay(delay)) => {
assert_eq!(event, NudEvent::DelayFirstProbe);
// Per [RFC 4861 section 7.3.3]:
//
// If the entry is still in the DELAY state when the timer expires, the
// entry's state changes to PROBE.
//
// [RFC 4861 section 7.3.3]: https://tools.ietf.org/html/rfc4861#section-7.3.3
let probe @ Probe { link_address, transmit_counter: _ } =
delay.enter_probe(core_ctx, bindings_ctx, timer_heap, lookup_addr);
*entry.get_mut() = NeighborState::Dynamic(DynamicNeighborState::Probe(probe));
let event_state = entry.get_mut().to_event_state();
bindings_ctx.on_event(Event::changed(
&device_id,
event_state,
lookup_addr,
bindings_ctx.now(),
));
Some(Action::TransmitProbe {
probe: TransmitProbe::Unicast(link_address),
to: lookup_addr,
})
}
state @ (NeighborState::Static(_)
| NeighborState::Dynamic(DynamicNeighborState::Stale(_))) => {
panic!("timer unexpectedly fired in state {state:?}")
}
}
},
);
match action {
Some(Action::SendIcmpDestUnreachable(mut pending_frames)) => {
for mut frame in pending_frames.drain(..) {
// TODO(https://fxbug.dev/323585811): Avoid needing to parse the packet to get
// IP header fields by extracting them from the serializer passed into the NUD
// layer and storing them alongside the pending frames instead.
let Some((packet, original_src_ip, original_dst_ip)) = frame
.parse_mut::<I::Packet<_>>()
.map_err(|e| {
tracing::warn!(
"not sending ICMP dest unreachable due to parsing error: {:?}",
e
);
})
.ok()
.and_then(|packet| {
let original_src_ip = SocketIpAddr::new(packet.src_ip())?;
let original_dst_ip = SocketIpAddr::new(packet.dst_ip())?;
Some((packet, original_src_ip, original_dst_ip))
})
.or_else(|| {
core_ctx.increment(|counters| &counters.icmp_dest_unreachable_dropped);
None
})
else {
continue;
};
let header_metadata = I::extract_metadata(&packet);
let metadata = packet.parse_metadata();
core::mem::drop(packet);
frame.undo_parse(metadata);
core_ctx.send_icmp_dest_unreachable(
bindings_ctx,
frame,
// Provide the device ID if `original_src_ip`, the address the ICMP error
// is destined for, is link-local. Note that if this address is link-local,
// it should be an address assigned to one of our own interfaces, because the
// link-local subnet should always be on-link according to RFC 5942 Section 3:
//
// The link-local prefix is effectively considered a permanent entry on the
// Prefix List.
//
// Even if the link-local subnet is off-link, passing the device ID is never
// incorrect because link-local traffic will never be forwarded, and
// there is only ever one link and thus interface involved.
crate::socket::must_have_zone(original_src_ip.as_ref()).then_some(&device_id),
original_src_ip,
original_dst_ip,
header_metadata,
);
}
}
Some(Action::TransmitProbe { probe, to }) => {
let remote_link_addr = match probe {
TransmitProbe::Multicast => None,
TransmitProbe::Unicast(link_addr) => Some(link_addr),
};
core_ctx.send_neighbor_solicitation(bindings_ctx, &device_id, to, remote_link_addr);
}
None => {}
}
}
impl<
I: Ip,
D: LinkDevice,
BC: NudBindingsContext<I, D, CC::DeviceId>,
CC: NudContext<I, D, BC>,
> NudHandler<I, D, BC> for CC
{
fn handle_neighbor_update(
&mut self,
bindings_ctx: &mut BC,
device_id: &CC::DeviceId,
neighbor: SpecifiedAddr<I::Addr>,
link_address: D::Address,
source: DynamicNeighborUpdateSource,
) {
tracing::debug!("received neighbor {:?} from {}", source, neighbor);
self.with_nud_state_mut_and_sender_ctx(
device_id,
|NudState { neighbors, last_gc, timer_heap }, core_ctx| {
let num_entries = neighbors.len();
match neighbors.entry(neighbor) {
Entry::Vacant(e) => match source {
DynamicNeighborUpdateSource::Probe => {
// Per [RFC 4861 section 7.2.3] ("Receipt of Neighbor Solicitations"):
//
// If an entry does not already exist, the node SHOULD create a new
// one and set its reachability state to STALE as specified in Section
// 7.3.3.
//
// [RFC 4861 section 7.2.3]: https://tools.ietf.org/html/rfc4861#section-7.2.3
let state = e.insert(NeighborState::Dynamic(
DynamicNeighborState::Stale(Stale { link_address }),
));
let event = Event::added(
device_id,
state.to_event_state(),
neighbor,
bindings_ctx.now(),
);
bindings_ctx.on_event(event);
// This entry is not currently in active use; if we are currently over
// the maximum amount of entries, schedule garbage collection.
timer_heap.maybe_schedule_gc(bindings_ctx, neighbors.len(), last_gc);
}
// Per [RFC 4861 section 7.2.5] ("Receipt of Neighbor Advertisements"):
//
// If no entry exists, the advertisement SHOULD be silently discarded.
// There is no need to create an entry if none exists, since the
// recipient has apparently not initiated any communication with the
// target.
//
// [RFC 4861 section 7.2.5]: https://tools.ietf.org/html/rfc4861#section-7.2.5
DynamicNeighborUpdateSource::Confirmation(_) => {}
},
Entry::Occupied(e) => match e.into_mut() {
NeighborState::Dynamic(e) => match source {
DynamicNeighborUpdateSource::Probe => e.handle_probe(
core_ctx,
bindings_ctx,
timer_heap,
device_id,
neighbor,
link_address,
num_entries,
last_gc,
),
DynamicNeighborUpdateSource::Confirmation(flags) => e
.handle_confirmation(
core_ctx,
bindings_ctx,
timer_heap,
device_id,
neighbor,
link_address,
flags,
num_entries,
last_gc,
),
},
NeighborState::Static(_) => {}
},
}
},
);
}
fn flush(&mut self, bindings_ctx: &mut BC, device_id: &Self::DeviceId) {
self.with_nud_state_mut(
device_id,
|NudState { neighbors, last_gc: _, timer_heap }, _config| {
neighbors.drain().for_each(|(neighbor, state)| {
match state {
NeighborState::Dynamic(mut entry) => {
entry.cancel_timer(bindings_ctx, timer_heap, neighbor);
}
NeighborState::Static(_) => {}
}
bindings_ctx.on_event(Event::removed(device_id, neighbor, bindings_ctx.now()));
});
},
);
}
fn send_ip_packet_to_neighbor<S>(
&mut self,
bindings_ctx: &mut BC,
device_id: &Self::DeviceId,
lookup_addr: SpecifiedAddr<I::Addr>,
body: S,
) -> Result<(), S>
where
S: Serializer,
S::Buffer: BufferMut,
{
let do_multicast_solicit = self.with_nud_state_mut_and_sender_ctx(
device_id,
|NudState { neighbors, last_gc: _, timer_heap }, core_ctx| {
match neighbors.entry(lookup_addr) {
Entry::Vacant(e) => {
let state = e.insert(NeighborState::Dynamic(
DynamicNeighborState::Incomplete(Incomplete::new_with_pending_frame(
core_ctx,
bindings_ctx,
timer_heap,
lookup_addr,
body.serialize_vec_outer()
.map_err(|(_err, s)| s)?
.map_a(|b| Buf::new(b.as_ref().to_vec(), ..))
.into_inner(),
)),
));
let event = Event::added(
device_id,
state.to_event_state(),
lookup_addr,
bindings_ctx.now(),
);
bindings_ctx.on_event(event);
Ok(true)
}
Entry::Occupied(e) => {
match e.into_mut() {
NeighborState::Static(link_address) => {
// Send the IP packet while holding the NUD lock to prevent a
// potential ordering violation.
//
// If we drop the NUD lock before sending out this packet, another
// thread could take the NUD lock and send a packet *before* this
// packet is sent out, resulting in out-of-order transmission to the
// device.
core_ctx.send_ip_packet_to_neighbor_link_addr(
bindings_ctx,
*link_address,
body,
)?;
Ok(false)
}
NeighborState::Dynamic(e) => {
let do_multicast_solicit = e.handle_packet_queued_to_send(
core_ctx,
bindings_ctx,
timer_heap,
device_id,
lookup_addr,
body,
)?;
Ok(do_multicast_solicit)
}
}
}
}
},
)?;
if do_multicast_solicit {
self.send_neighbor_solicitation(
bindings_ctx,
&device_id,
lookup_addr,
/* multicast */ None,
);
}
Ok(())
}
}
/// Confirm upper-layer forward reachability to the specified neighbor through
/// the specified device.
pub(crate) fn confirm_reachable<I, D, CC, BC>(
core_ctx: &mut CC,
bindings_ctx: &mut BC,
device_id: &CC::DeviceId,
neighbor: SpecifiedAddr<I::Addr>,
) where
I: Ip,
D: LinkDevice,
BC: NudBindingsContext<I, D, CC::DeviceId>,
CC: NudContext<I, D, BC>,
{
core_ctx.with_nud_state_mut_and_sender_ctx(
device_id,
|NudState { neighbors, last_gc: _, timer_heap }, core_ctx| {
match neighbors.entry(neighbor) {
Entry::Vacant(_) => {
tracing::debug!(
"got an upper-layer confirmation for non-existent neighbor entry {}",
neighbor
);
}
Entry::Occupied(e) => match e.into_mut() {
NeighborState::Static(_) => {}
NeighborState::Dynamic(e) => {
// Per [RFC 4861 section 7.3.3]:
//
// When a reachability confirmation is received (either through upper-
// layer advice or a solicited Neighbor Advertisement), an entry's state
// changes to REACHABLE. The one exception is that upper-layer advice has
// no effect on entries in the INCOMPLETE state (e.g., for which no link-
// layer address is cached).
//
// [RFC 4861 section 7.3.3]: https://tools.ietf.org/html/rfc4861#section-7.3.3
let link_address = match e {
DynamicNeighborState::Incomplete(_) => return,
DynamicNeighborState::Reachable(Reachable {
link_address,
last_confirmed_at: _,
})
| DynamicNeighborState::Stale(Stale { link_address })
| DynamicNeighborState::Delay(Delay { link_address })
| DynamicNeighborState::Probe(Probe {
link_address,
transmit_counter: _,
})
| DynamicNeighborState::Unreachable(Unreachable {
link_address,
mode: _,
}) => *link_address,
};
e.enter_reachable(
core_ctx,
bindings_ctx,
timer_heap,
device_id,
neighbor,
link_address,
);
}
},
}
},
);
}
/// Performs a linear scan of the neighbor table, discarding enough entries to
/// bring the total size under `MAX_ENTRIES` if possible.
///
/// Static neighbor entries are never discarded, nor are any entries that are
/// considered to be in use, which is defined as an entry in REACHABLE,
/// INCOMPLETE, DELAY, or PROBE. In other words, the only entries eligible to be
/// discarded are those in STALE or UNREACHABLE. This is reasonable because all
/// other states represent entries to which we have either recently sent packets
/// (REACHABLE, DELAY, PROBE), or which we are actively trying to resolve and
/// for which we have recently queued outgoing packets (INCOMPLETE).
fn collect_garbage<I, D, CC, BC>(core_ctx: &mut CC, bindings_ctx: &mut BC, device_id: CC::DeviceId)
where
I: Ip,
D: LinkDevice,
BC: NudBindingsContext<I, D, CC::DeviceId>,
CC: NudContext<I, D, BC>,
{
core_ctx.with_nud_state_mut(&device_id, |NudState { neighbors, last_gc, timer_heap }, _| {
let max_to_remove = neighbors.len().saturating_sub(MAX_ENTRIES);
if max_to_remove == 0 {
return;
}
*last_gc = Some(bindings_ctx.now());
// Define an ordering by priority for garbage collection, such that lower
// numbers correspond to higher usefulness and therefore lower likelihood of
// being discarded.
//
// TODO(https://fxbug.dev/42075782): once neighbor entries hold a timestamp
// tracking when they were last updated, consider using this timestamp to break
// ties between entries in the same state, so that we discard less recently
// updated entries before more recently updated ones.
fn gc_priority<D: LinkDevice, BT: NudBindingsTypes<D>>(
state: &DynamicNeighborState<D, BT>,
) -> usize {
match state {
DynamicNeighborState::Incomplete(_)
| DynamicNeighborState::Reachable(_)
| DynamicNeighborState::Delay(_)
| DynamicNeighborState::Probe(_) => unreachable!(
"the netstack should only ever discard STALE or UNREACHABLE entries; \
found {:?}",
state,
),
DynamicNeighborState::Stale(_) => 0,
DynamicNeighborState::Unreachable(Unreachable {
link_address: _,
mode: UnreachableMode::Backoff { probes_sent: _, packet_sent: _ },
}) => 1,
DynamicNeighborState::Unreachable(Unreachable {
link_address: _,
mode: UnreachableMode::WaitingForPacketSend,
}) => 2,
}
}
struct SortEntry<'a, K: Eq, D: LinkDevice, BT: NudBindingsTypes<D>> {
key: K,
state: &'a mut DynamicNeighborState<D, BT>,
}
impl<K: Eq, D: LinkDevice, BT: NudBindingsTypes<D>> PartialEq for SortEntry<'_, K, D, BT> {
fn eq(&self, other: &Self) -> bool {
self.key == other.key && gc_priority(self.state) == gc_priority(other.state)
}
}
impl<K: Eq, D: LinkDevice, BT: NudBindingsTypes<D>> Eq for SortEntry<'_, K, D, BT> {}
impl<K: Eq, D: LinkDevice, BT: NudBindingsTypes<D>> Ord for SortEntry<'_, K, D, BT> {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
// Sort in reverse order so `BinaryHeap` will function as a min-heap rather than
// a max-heap. This means it will maintain the minimum (i.e. most useful) entry
// at the top of the heap.
gc_priority(self.state).cmp(&gc_priority(other.state)).reverse()
}
}
impl<K: Eq, D: LinkDevice, BT: NudBindingsTypes<D>> PartialOrd for SortEntry<'_, K, D, BT> {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(&other))
}
}
let mut entries_to_remove = BinaryHeap::with_capacity(max_to_remove);
for (ip, neighbor) in neighbors.iter_mut() {
match neighbor {
NeighborState::Static(_) => {
// Don't discard static entries.
continue;
}
NeighborState::Dynamic(state) => {
match state {
DynamicNeighborState::Incomplete(_)
| DynamicNeighborState::Reachable(_)
| DynamicNeighborState::Delay(_)
| DynamicNeighborState::Probe(_) => {
// Don't discard in-use entries.
continue;
}
DynamicNeighborState::Stale(_) | DynamicNeighborState::Unreachable(_) => {
// Unconditionally insert the first `max_to_remove` entries.
if entries_to_remove.len() < max_to_remove {
entries_to_remove.push(SortEntry { key: ip, state });
continue;
}
// Check if this neighbor is greater than (i.e. less useful than) the
// minimum (i.e. most useful) entry that is currently set to be removed.
// If it is, replace that entry with this one.
let minimum = entries_to_remove
.peek()
.expect("heap should have at least 1 entry");
let candidate = SortEntry { key: ip, state };
if &candidate > minimum {
let _: SortEntry<'_, _, _, _> = entries_to_remove.pop().unwrap();
entries_to_remove.push(candidate);
}
}
}
}
}
}
let entries_to_remove = entries_to_remove
.into_iter()
.map(|SortEntry { key: neighbor, state }| {
state.cancel_timer(bindings_ctx, timer_heap, *neighbor);
*neighbor
})
.collect::<Vec<_>>();
for neighbor in entries_to_remove {
assert_matches!(neighbors.remove(&neighbor), Some(_));
bindings_ctx.on_event(Event::removed(&device_id, neighbor, bindings_ctx.now()));
}
})
}
#[cfg(test)]
mod tests {
use alloc::collections::HashSet;
use alloc::vec;
use core::num::{NonZeroU16, NonZeroUsize};
use ip_test_macro::ip_test;
use net_declare::{net_ip_v4, net_ip_v6};
use net_types::{
ip::{AddrSubnet, IpAddress as _, IpInvariant, Ipv4Addr, Ipv6Addr, Subnet},
UnicastAddr, Witness as _,
};
use packet::{InnerPacketBuilder as _, Serializer as _};
use packet_formats::{
ethernet::{EtherType, EthernetFrameLengthCheck},
icmp::{
ndp::{
options::NdpOptionBuilder, NeighborAdvertisement, NeighborSolicitation,
OptionSequenceBuilder, RouterAdvertisement,
},
IcmpDestUnreachable, IcmpPacketBuilder, IcmpUnusedCode,
},
ip::{IpProto, Ipv4Proto, Ipv6Proto},
ipv4::Ipv4PacketBuilder,
ipv6::Ipv6PacketBuilder,
testutil::{parse_ethernet_frame, parse_icmp_packet_in_ip_packet_in_ethernet_frame},
udp::UdpPacketBuilder,
};
use test_case::test_case;
use super::*;
use crate::{
context::{
testutil::{
FakeBindingsCtx, FakeCoreCtx, FakeCtxWithCoreCtx, FakeInstant, FakeNetwork,
FakeNetworkContext as _, FakeNetworkLinks, FakeTimerCtxExt as _,
WithFakeFrameContext, WrappedFakeCoreCtx,
},
CtxPair, InstantContext, SendFrameContext as _,
},
device::{
ethernet::{EthernetCreationProperties, EthernetLinkDevice},
link::testutil::{FakeLinkAddress, FakeLinkDevice, FakeLinkDeviceId},
loopback::{LoopbackCreationProperties, LoopbackDevice},
ndp::testutil::{neighbor_advertisement_ip_packet, neighbor_solicitation_ip_packet},
testutil::FakeWeakDeviceId,
EthernetDeviceId, FrameDestination, WeakDeviceId,
},
ip::{
device::{
nud::api::NeighborApi, slaac::SlaacConfiguration, testutil::set_ip_device_enabled,
Ipv6DeviceConfigurationUpdate, Mtu,
},
icmp::REQUIRED_NDP_IP_PACKET_HOP_LIMIT,
},
routes::{AddableEntry, AddableMetric},
testutil::{
self, DispatchedFrame, FakeEventDispatcherConfig, TestIpExt as _,
DEFAULT_INTERFACE_METRIC, IPV6_MIN_IMPLIED_MAX_FRAME_SIZE,
},
transport::tcp,
UnlockedCoreCtx,
};
struct FakeNudContext<I: Ip, D: LinkDevice> {
nud: NudState<I, D, FakeBindingsCtxImpl<I>>,
counters: NudCounters<I>,
}
struct FakeConfigContext {
retrans_timer: NonZeroDuration,
nud_config: NudUserConfig,
}
type FakeCoreCtxImpl<I> = WrappedFakeCoreCtx<
FakeNudContext<I, FakeLinkDevice>,
FakeConfigContext,
FakeNudMessageMeta<I>,
FakeLinkDeviceId,
>;
type FakeInnerCtxImpl<I> =
FakeCoreCtx<FakeConfigContext, FakeNudMessageMeta<I>, FakeLinkDeviceId>;
#[derive(Debug, PartialEq, Eq)]
enum FakeNudMessageMeta<I: Ip> {
NeighborSolicitation {
lookup_addr: SpecifiedAddr<I::Addr>,
remote_link_addr: Option<FakeLinkAddress>,
},
IpFrame {
dst_link_address: FakeLinkAddress,
},
IcmpDestUnreachable,
}
type FakeBindingsCtxImpl<I> = FakeBindingsCtx<
NudTimerId<I, FakeLinkDevice, FakeWeakDeviceId<FakeLinkDeviceId>>,
Event<FakeLinkAddress, FakeLinkDeviceId, I, FakeInstant>,
(),
(),
>;
impl<I: Ip> FakeCoreCtxImpl<I> {
fn new(bindings_ctx: &mut FakeBindingsCtxImpl<I>) -> Self {
Self::with_inner_and_outer_state(
FakeConfigContext {
retrans_timer: ONE_SECOND,
// Use different values from the defaults in tests so we get
// coverage that the config is used everywhere and not the
// defaults.
nud_config: NudUserConfig {
max_unicast_solicitations: NonZeroU16::new(4).unwrap(),
max_multicast_solicitations: NonZeroU16::new(5).unwrap(),
base_reachable_time: NonZeroDuration::from_secs(23).unwrap(),
},
},
FakeNudContext {
nud: NudState::new(bindings_ctx, FakeWeakDeviceId(FakeLinkDeviceId), |t| t),
counters: Default::default(),
},
)
}
}
fn new_context<I: Ip>(
) -> crate::testutil::ContextPair<FakeCoreCtxImpl<I>, FakeBindingsCtxImpl<I>> {
FakeCtxWithCoreCtx::with_default_bindings_ctx(|bindings_ctx| {
FakeCoreCtxImpl::<I>::new(bindings_ctx)
})
}
impl<I: Ip> DeviceIdContext<FakeLinkDevice> for FakeCoreCtxImpl<I> {
type DeviceId = FakeLinkDeviceId;
type WeakDeviceId = FakeWeakDeviceId<FakeLinkDeviceId>;
}
impl<I: Ip> NudContext<I, FakeLinkDevice, FakeBindingsCtxImpl<I>> for FakeCoreCtxImpl<I> {
type ConfigCtx<'a> = FakeConfigContext;
type SenderCtx<'a> = FakeInnerCtxImpl<I>;
fn with_nud_state_mut_and_sender_ctx<
O,
F: FnOnce(
&mut NudState<I, FakeLinkDevice, FakeBindingsCtxImpl<I>>,
&mut Self::SenderCtx<'_>,
) -> O,
>(
&mut self,
_device_id: &Self::DeviceId,
cb: F,
) -> O {
let Self { outer, inner } = self;
cb(&mut outer.nud, inner)
}
fn with_nud_state_mut<
O,
F: FnOnce(
&mut NudState<I, FakeLinkDevice, FakeBindingsCtxImpl<I>>,
&mut Self::ConfigCtx<'_>,
) -> O,
>(
&mut self,
&FakeLinkDeviceId: &FakeLinkDeviceId,
cb: F,
) -> O {
cb(&mut self.outer.nud, self.inner.get_mut())
}
fn with_nud_state<
O,
F: FnOnce(&NudState<I, FakeLinkDevice, FakeBindingsCtxImpl<I>>) -> O,
>(
&mut self,
&FakeLinkDeviceId: &FakeLinkDeviceId,
cb: F,
) -> O {
cb(&self.outer.nud)
}
fn send_neighbor_solicitation(
&mut self,
bindings_ctx: &mut FakeBindingsCtxImpl<I>,
&FakeLinkDeviceId: &FakeLinkDeviceId,
lookup_addr: SpecifiedAddr<I::Addr>,
remote_link_addr: Option<FakeLinkAddress>,
) {
self.inner
.send_frame(
bindings_ctx,
FakeNudMessageMeta::NeighborSolicitation { lookup_addr, remote_link_addr },
Buf::new(Vec::new(), ..),
)
.unwrap()
}
}
impl<I: Ip + NudIcmpIpExt> NudIcmpContext<I, FakeLinkDevice, FakeBindingsCtxImpl<I>>
for FakeCoreCtxImpl<I>
{
fn send_icmp_dest_unreachable(
&mut self,
bindings_ctx: &mut FakeBindingsCtxImpl<I>,
frame: Buf<Vec<u8>>,
_device_id: Option<&Self::DeviceId>,
_original_src_ip: SocketIpAddr<I::Addr>,
_original_dst_ip: SocketIpAddr<I::Addr>,
_header_len: I::Metadata,
) {
self.inner
.send_frame(bindings_ctx, FakeNudMessageMeta::IcmpDestUnreachable, frame)
.unwrap()
}
}
impl<I: Ip> CounterContext<NudCounters<I>> for FakeCoreCtxImpl<I> {
fn with_counters<O, F: FnOnce(&NudCounters<I>) -> O>(&self, cb: F) -> O {
cb(&self.outer.counters)
}
}
impl<I: Ip> NudConfigContext<I> for FakeConfigContext {
fn retransmit_timeout(&mut self) -> NonZeroDuration {
self.retrans_timer
}
fn with_nud_user_config<O, F: FnOnce(&NudUserConfig) -> O>(&mut self, cb: F) -> O {
cb(&self.nud_config)
}
}
impl<I: Ip> NudSenderContext<I, FakeLinkDevice, FakeBindingsCtxImpl<I>> for FakeInnerCtxImpl<I> {
fn send_ip_packet_to_neighbor_link_addr<S>(
&mut self,
bindings_ctx: &mut FakeBindingsCtxImpl<I>,
dst_link_address: FakeLinkAddress,
body: S,
) -> Result<(), S>
where
S: Serializer,
S::Buffer: BufferMut,
{
self.send_frame(bindings_ctx, FakeNudMessageMeta::IpFrame { dst_link_address }, body)
}
}
impl<I: Ip> NudConfigContext<I> for FakeInnerCtxImpl<I> {
fn retransmit_timeout(&mut self) -> NonZeroDuration {
<FakeConfigContext as NudConfigContext<I>>::retransmit_timeout(self.get_mut())
}
fn with_nud_user_config<O, F: FnOnce(&NudUserConfig) -> O>(&mut self, cb: F) -> O {
<FakeConfigContext as NudConfigContext<I>>::with_nud_user_config(self.get_mut(), cb)
}
}
const ONE_SECOND: NonZeroDuration =
const_unwrap::const_unwrap_option(NonZeroDuration::from_secs(1));
#[track_caller]
fn check_lookup_has<I: Ip>(
core_ctx: &mut FakeCoreCtxImpl<I>,
bindings_ctx: &mut FakeBindingsCtxImpl<I>,
lookup_addr: SpecifiedAddr<I::Addr>,
expected_link_addr: FakeLinkAddress,
) {
let entry = assert_matches!(
core_ctx.outer.nud.neighbors.get(&lookup_addr),
Some(entry @ (
NeighborState::Dynamic(
DynamicNeighborState::Reachable (Reachable { link_address, last_confirmed_at: _ })
| DynamicNeighborState::Stale (Stale { link_address })
| DynamicNeighborState::Delay (Delay { link_address })
| DynamicNeighborState::Probe (Probe { link_address, transmit_counter: _ })
| DynamicNeighborState::Unreachable (Unreachable { link_address, mode: _ })
)
| NeighborState::Static(link_address)
)) => {
assert_eq!(link_address, &expected_link_addr);
entry
}
);
match entry {
NeighborState::Dynamic(DynamicNeighborState::Incomplete { .. }) => {
unreachable!("entry must be static, REACHABLE, or STALE")
}
NeighborState::Dynamic(DynamicNeighborState::Reachable { .. }) => {
core_ctx.outer.nud.timer_heap.neighbor.assert_timers_after(
bindings_ctx,
[(
lookup_addr,
NudEvent::ReachableTime,
core_ctx.inner.base_reachable_time().get(),
)],
);
}
NeighborState::Dynamic(DynamicNeighborState::Delay { .. }) => {
core_ctx.outer.nud.timer_heap.neighbor.assert_timers_after(
bindings_ctx,
[(lookup_addr, NudEvent::DelayFirstProbe, DELAY_FIRST_PROBE_TIME.get())],
);
}
NeighborState::Dynamic(DynamicNeighborState::Probe { .. }) => {
core_ctx.outer.nud.timer_heap.neighbor.assert_timers_after(
bindings_ctx,
[(
lookup_addr,
NudEvent::RetransmitUnicastProbe,
core_ctx.inner.get_ref().retrans_timer.get(),
)],
);
}
NeighborState::Dynamic(DynamicNeighborState::Unreachable(Unreachable {
link_address: _,
mode,
})) => {
let instant = match mode {
UnreachableMode::WaitingForPacketSend => None,
mode @ UnreachableMode::Backoff { .. } => {
let duration =
mode.next_backoff_retransmit_timeout::<I, _>(core_ctx.inner.get_mut());
Some(bindings_ctx.now() + duration.get())
}
};
if let Some(instant) = instant {
core_ctx.outer.nud.timer_heap.neighbor.assert_timers([(
lookup_addr,
NudEvent::RetransmitUnicastProbe,
instant,
)]);
}
}
NeighborState::Dynamic(DynamicNeighborState::Stale { .. })
| NeighborState::Static(_) => bindings_ctx.timer_ctx().assert_no_timers_installed(),
}
}
trait TestIpExt: NudIcmpIpExt {
const LOOKUP_ADDR1: SpecifiedAddr<Self::Addr>;
const LOOKUP_ADDR2: SpecifiedAddr<Self::Addr>;
const LOOKUP_ADDR3: SpecifiedAddr<Self::Addr>;
}
impl TestIpExt for Ipv4 {
// Safe because the address is non-zero.
const LOOKUP_ADDR1: SpecifiedAddr<Ipv4Addr> =
unsafe { SpecifiedAddr::new_unchecked(net_ip_v4!("192.168.0.1")) };
const LOOKUP_ADDR2: SpecifiedAddr<Ipv4Addr> =
unsafe { SpecifiedAddr::new_unchecked(net_ip_v4!("192.168.0.2")) };
const LOOKUP_ADDR3: SpecifiedAddr<Ipv4Addr> =
unsafe { SpecifiedAddr::new_unchecked(net_ip_v4!("192.168.0.3")) };
}
impl TestIpExt for Ipv6 {
// Safe because the address is non-zero.
const LOOKUP_ADDR1: SpecifiedAddr<Ipv6Addr> =
unsafe { SpecifiedAddr::new_unchecked(net_ip_v6!("fe80::1")) };
const LOOKUP_ADDR2: SpecifiedAddr<Ipv6Addr> =
unsafe { SpecifiedAddr::new_unchecked(net_ip_v6!("fe80::2")) };
const LOOKUP_ADDR3: SpecifiedAddr<Ipv6Addr> =
unsafe { SpecifiedAddr::new_unchecked(net_ip_v6!("fe80::3")) };
}
const LINK_ADDR1: FakeLinkAddress = FakeLinkAddress([1]);
const LINK_ADDR2: FakeLinkAddress = FakeLinkAddress([2]);
const LINK_ADDR3: FakeLinkAddress = FakeLinkAddress([3]);
impl<I: Ip, L: LinkDevice> NudTimerId<I, L, FakeWeakDeviceId<FakeLinkDeviceId>> {
fn neighbor() -> Self {
Self {
device_id: FakeWeakDeviceId(FakeLinkDeviceId),
timer_type: NudTimerType::Neighbor,
_marker: PhantomData,
}
}
fn garbage_collection() -> Self {
Self {
device_id: FakeWeakDeviceId(FakeLinkDeviceId),
timer_type: NudTimerType::GarbageCollection,
_marker: PhantomData,
}
}
}
fn queue_ip_packet_to_unresolved_neighbor<I: Ip + TestIpExt>(
core_ctx: &mut FakeCoreCtxImpl<I>,
bindings_ctx: &mut FakeBindingsCtxImpl<I>,
neighbor: SpecifiedAddr<I::Addr>,
pending_frames: &mut VecDeque<Buf<Vec<u8>>>,
body: u8,
expect_event: bool,
) {
let body = [body];
assert_eq!(
NudHandler::send_ip_packet_to_neighbor(
core_ctx,
bindings_ctx,
&FakeLinkDeviceId,
neighbor,
Buf::new(body, ..),
),
Ok(())
);
let max_multicast_solicit = core_ctx.inner.max_multicast_solicit().get();
pending_frames.push_back(Buf::new(body.to_vec(), ..));
assert_neighbor_state_with_ip(
core_ctx,
bindings_ctx,
neighbor,
DynamicNeighborState::Incomplete(Incomplete {
transmit_counter: NonZeroU16::new(max_multicast_solicit - 1),
pending_frames: pending_frames.clone(),
notifiers: Vec::new(),
_marker: PhantomData,
}),
expect_event.then_some(ExpectedEvent::Added),
);
core_ctx.outer.nud.timer_heap.neighbor.assert_timers_after(
bindings_ctx,
[(neighbor, NudEvent::RetransmitMulticastProbe, ONE_SECOND.get())],
);
}
fn init_incomplete_neighbor_with_ip<I: Ip + TestIpExt>(
core_ctx: &mut FakeCoreCtxImpl<I>,
bindings_ctx: &mut FakeBindingsCtxImpl<I>,
ip_address: SpecifiedAddr<I::Addr>,
take_probe: bool,
) -> VecDeque<Buf<Vec<u8>>> {
let mut pending_frames = VecDeque::new();
queue_ip_packet_to_unresolved_neighbor(
core_ctx,
bindings_ctx,
ip_address,
&mut pending_frames,
1,
true, /* expect_event */
);
if take_probe {
assert_neighbor_probe_sent_for_ip(core_ctx, ip_address, None);
}
pending_frames
}
fn init_incomplete_neighbor<I: Ip + TestIpExt>(
core_ctx: &mut FakeCoreCtxImpl<I>,
bindings_ctx: &mut FakeBindingsCtxImpl<I>,
take_probe: bool,
) -> VecDeque<Buf<Vec<u8>>> {
init_incomplete_neighbor_with_ip(core_ctx, bindings_ctx, I::LOOKUP_ADDR1, take_probe)
}
fn init_stale_neighbor_with_ip<I: Ip + TestIpExt>(
core_ctx: &mut FakeCoreCtxImpl<I>,
bindings_ctx: &mut FakeBindingsCtxImpl<I>,
ip_address: SpecifiedAddr<I::Addr>,
link_address: FakeLinkAddress,
) {
NudHandler::handle_neighbor_update(
core_ctx,
bindings_ctx,
&FakeLinkDeviceId,
ip_address,
link_address,
DynamicNeighborUpdateSource::Probe,
);
assert_neighbor_state_with_ip(
core_ctx,
bindings_ctx,
ip_address,
DynamicNeighborState::Stale(Stale { link_address }),
Some(ExpectedEvent::Added),
);
}
fn init_stale_neighbor<I: Ip + TestIpExt>(
core_ctx: &mut FakeCoreCtxImpl<I>,
bindings_ctx: &mut FakeBindingsCtxImpl<I>,
link_address: FakeLinkAddress,
) {
init_stale_neighbor_with_ip(core_ctx, bindings_ctx, I::LOOKUP_ADDR1, link_address);
}
fn init_reachable_neighbor_with_ip<I: Ip + TestIpExt>(
core_ctx: &mut FakeCoreCtxImpl<I>,
bindings_ctx: &mut FakeBindingsCtxImpl<I>,
ip_address: SpecifiedAddr<I::Addr>,
link_address: FakeLinkAddress,
) {
let queued_frame =
init_incomplete_neighbor_with_ip(core_ctx, bindings_ctx, ip_address, true);
NudHandler::handle_neighbor_update(
core_ctx,
bindings_ctx,
&FakeLinkDeviceId,
ip_address,
link_address,
DynamicNeighborUpdateSource::Confirmation(ConfirmationFlags {
solicited_flag: true,
override_flag: false,
}),
);
assert_neighbor_state_with_ip(
core_ctx,
bindings_ctx,
ip_address,
DynamicNeighborState::Reachable(Reachable {
link_address,
last_confirmed_at: bindings_ctx.now(),
}),
Some(ExpectedEvent::Changed),
);
assert_pending_frame_sent(core_ctx, queued_frame, link_address);
}
fn init_reachable_neighbor<I: Ip + TestIpExt>(
core_ctx: &mut FakeCoreCtxImpl<I>,
bindings_ctx: &mut FakeBindingsCtxImpl<I>,
link_address: FakeLinkAddress,
) {
init_reachable_neighbor_with_ip(core_ctx, bindings_ctx, I::LOOKUP_ADDR1, link_address);
}
fn init_delay_neighbor_with_ip<I: Ip + TestIpExt>(
core_ctx: &mut FakeCoreCtxImpl<I>,
bindings_ctx: &mut FakeBindingsCtxImpl<I>,
ip_address: SpecifiedAddr<I::Addr>,
link_address: FakeLinkAddress,
) {
init_stale_neighbor_with_ip(core_ctx, bindings_ctx, ip_address, link_address);
assert_eq!(
NudHandler::send_ip_packet_to_neighbor(
core_ctx,
bindings_ctx,
&FakeLinkDeviceId,
ip_address,
Buf::new([1], ..),
),
Ok(())
);
assert_neighbor_state_with_ip(
core_ctx,
bindings_ctx,
ip_address,
DynamicNeighborState::Delay(Delay { link_address }),
Some(ExpectedEvent::Changed),
);
assert_eq!(
core_ctx.inner.take_frames(),
vec![(FakeNudMessageMeta::IpFrame { dst_link_address: LINK_ADDR1 }, vec![1])],
);
}
fn init_delay_neighbor<I: Ip + TestIpExt>(
core_ctx: &mut FakeCoreCtxImpl<I>,
bindings_ctx: &mut FakeBindingsCtxImpl<I>,
link_address: FakeLinkAddress,
) {
init_delay_neighbor_with_ip(core_ctx, bindings_ctx, I::LOOKUP_ADDR1, link_address);
}
fn init_probe_neighbor_with_ip<I: Ip + TestIpExt>(
core_ctx: &mut FakeCoreCtxImpl<I>,
bindings_ctx: &mut FakeBindingsCtxImpl<I>,
ip_address: SpecifiedAddr<I::Addr>,
link_address: FakeLinkAddress,
take_probe: bool,
) {
init_delay_neighbor_with_ip(core_ctx, bindings_ctx, ip_address, link_address);
let max_unicast_solicit = core_ctx.inner.max_unicast_solicit().get();
core_ctx.outer.nud.timer_heap.neighbor.assert_top(&ip_address, &NudEvent::DelayFirstProbe);
assert_eq!(
bindings_ctx.trigger_timers_for(DELAY_FIRST_PROBE_TIME.into(), core_ctx),
[NudTimerId::neighbor()]
);
assert_neighbor_state_with_ip(
core_ctx,
bindings_ctx,
ip_address,
DynamicNeighborState::Probe(Probe {
link_address,
transmit_counter: NonZeroU16::new(max_unicast_solicit - 1),
}),
Some(ExpectedEvent::Changed),
);
if take_probe {
assert_neighbor_probe_sent_for_ip(core_ctx, ip_address, Some(LINK_ADDR1));
}
}
fn init_probe_neighbor<I: Ip + TestIpExt>(
core_ctx: &mut FakeCoreCtxImpl<I>,
bindings_ctx: &mut FakeBindingsCtxImpl<I>,
link_address: FakeLinkAddress,
take_probe: bool,
) {
init_probe_neighbor_with_ip(
core_ctx,
bindings_ctx,
I::LOOKUP_ADDR1,
link_address,
take_probe,
);
}
fn init_unreachable_neighbor_with_ip<I: Ip + TestIpExt>(
core_ctx: &mut FakeCoreCtxImpl<I>,
bindings_ctx: &mut FakeBindingsCtxImpl<I>,
ip_address: SpecifiedAddr<I::Addr>,
link_address: FakeLinkAddress,
) {
init_probe_neighbor_with_ip(core_ctx, bindings_ctx, ip_address, link_address, false);
let retransmit_timeout = core_ctx.inner.retransmit_timeout();
let max_unicast_solicit = core_ctx.inner.max_unicast_solicit().get();
for _ in 0..max_unicast_solicit {
assert_neighbor_probe_sent_for_ip(core_ctx, ip_address, Some(LINK_ADDR1));
assert_eq!(
bindings_ctx.trigger_timers_for(retransmit_timeout.into(), core_ctx),
[NudTimerId::neighbor()]
);
}
assert_neighbor_state_with_ip(
core_ctx,
bindings_ctx,
ip_address,
DynamicNeighborState::Unreachable(Unreachable {
link_address,
mode: UnreachableMode::WaitingForPacketSend,
}),
Some(ExpectedEvent::Changed),
);
}
fn init_unreachable_neighbor<I: Ip + TestIpExt>(
core_ctx: &mut FakeCoreCtxImpl<I>,
bindings_ctx: &mut FakeBindingsCtxImpl<I>,
link_address: FakeLinkAddress,
) {
init_unreachable_neighbor_with_ip(core_ctx, bindings_ctx, I::LOOKUP_ADDR1, link_address);
}
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
enum InitialState {
Incomplete,
Stale,
Reachable,
Delay,
Probe,
Unreachable,
}
fn init_neighbor_in_state<I: Ip + TestIpExt>(
core_ctx: &mut FakeCoreCtxImpl<I>,
bindings_ctx: &mut FakeBindingsCtxImpl<I>,
state: InitialState,
) -> DynamicNeighborState<FakeLinkDevice, FakeBindingsCtxImpl<I>> {
match state {
InitialState::Incomplete => {
let _: VecDeque<Buf<Vec<u8>>> =
init_incomplete_neighbor(core_ctx, bindings_ctx, true);
}
InitialState::Reachable => {
init_reachable_neighbor(core_ctx, bindings_ctx, LINK_ADDR1);
}
InitialState::Stale => {
init_stale_neighbor(core_ctx, bindings_ctx, LINK_ADDR1);
}
InitialState::Delay => {
init_delay_neighbor(core_ctx, bindings_ctx, LINK_ADDR1);
}
InitialState::Probe => {
init_probe_neighbor(core_ctx, bindings_ctx, LINK_ADDR1, true);
}
InitialState::Unreachable => {
init_unreachable_neighbor(core_ctx, bindings_ctx, LINK_ADDR1);
}
}
assert_matches!(core_ctx.outer.nud.neighbors.get(&I::LOOKUP_ADDR1),
Some(NeighborState::Dynamic(state)) => state.clone()
)
}
#[track_caller]
fn init_static_neighbor_with_ip<I: Ip + TestIpExt>(
core_ctx: &mut FakeCoreCtxImpl<I>,
bindings_ctx: &mut FakeBindingsCtxImpl<I>,
ip_address: SpecifiedAddr<I::Addr>,
link_address: FakeLinkAddress,
expected_event: ExpectedEvent,
) {
let mut ctx = CtxPair { core_ctx, bindings_ctx };
NeighborApi::new(&mut ctx)
.insert_static_entry(&FakeLinkDeviceId, *ip_address, link_address)
.unwrap();
assert_eq!(
ctx.bindings_ctx.take_events(),
[Event {
device: FakeLinkDeviceId,
addr: ip_address,
kind: match expected_event {
ExpectedEvent::Added => EventKind::Added(EventState::Static(link_address)),
ExpectedEvent::Changed => EventKind::Changed(EventState::Static(link_address)),
},
at: ctx.bindings_ctx.now(),
}],
);
}
#[track_caller]
fn init_static_neighbor<I: Ip + TestIpExt>(
core_ctx: &mut FakeCoreCtxImpl<I>,
bindings_ctx: &mut FakeBindingsCtxImpl<I>,
link_address: FakeLinkAddress,
expected_event: ExpectedEvent,
) {
init_static_neighbor_with_ip(
core_ctx,
bindings_ctx,
I::LOOKUP_ADDR1,
link_address,
expected_event,
);
}
#[track_caller]
fn delete_neighbor<I: Ip + TestIpExt>(
core_ctx: &mut FakeCoreCtxImpl<I>,
bindings_ctx: &mut FakeBindingsCtxImpl<I>,
) {
let mut ctx = CtxPair { core_ctx, bindings_ctx };
NeighborApi::new(&mut ctx)
.remove_entry(&FakeLinkDeviceId, *I::LOOKUP_ADDR1)
.expect("neighbor entry should exist");
assert_eq!(
ctx.bindings_ctx.take_events(),
[Event::removed(&FakeLinkDeviceId, I::LOOKUP_ADDR1, ctx.bindings_ctx.now())],
);
}
#[track_caller]
fn assert_neighbor_state<I: Ip + TestIpExt>(
core_ctx: &FakeCoreCtxImpl<I>,
bindings_ctx: &mut FakeBindingsCtxImpl<I>,
state: DynamicNeighborState<FakeLinkDevice, FakeBindingsCtxImpl<I>>,
event_kind: Option<ExpectedEvent>,
) {
assert_neighbor_state_with_ip(core_ctx, bindings_ctx, I::LOOKUP_ADDR1, state, event_kind);
}
#[derive(Clone, Copy, Debug)]
enum ExpectedEvent {
Added,
Changed,
}
#[track_caller]
fn assert_neighbor_state_with_ip<I: Ip + TestIpExt>(
core_ctx: &FakeCoreCtxImpl<I>,
bindings_ctx: &mut FakeBindingsCtxImpl<I>,
neighbor: SpecifiedAddr<I::Addr>,
state: DynamicNeighborState<FakeLinkDevice, FakeBindingsCtxImpl<I>>,
expected_event: Option<ExpectedEvent>,
) {
if let Some(expected_event) = expected_event {
let event_state = EventState::Dynamic(state.to_event_dynamic_state());
assert_eq!(
bindings_ctx.take_events(),
[Event {
device: FakeLinkDeviceId,
addr: neighbor,
kind: match expected_event {
ExpectedEvent::Added => EventKind::Added(event_state),
ExpectedEvent::Changed => EventKind::Changed(event_state),
},
at: bindings_ctx.now(),
}],
);
}
let FakeNudContext { nud, counters: _ } = &core_ctx.outer;
assert_eq!(nud.neighbors.get(&neighbor), Some(&NeighborState::Dynamic(state)));
}
#[track_caller]
fn assert_pending_frame_sent<I: Ip + TestIpExt>(
core_ctx: &mut FakeCoreCtxImpl<I>,
pending_frames: VecDeque<Buf<Vec<u8>>>,
link_address: FakeLinkAddress,
) {
assert_eq!(
core_ctx.inner.take_frames(),
pending_frames
.into_iter()
.map(|f| (
FakeNudMessageMeta::IpFrame { dst_link_address: link_address },
f.as_ref().to_vec(),
))
.collect::<Vec<_>>()
);
}
#[track_caller]
fn assert_neighbor_probe_sent_for_ip<I: Ip + TestIpExt>(
core_ctx: &mut FakeCoreCtxImpl<I>,
ip_address: SpecifiedAddr<I::Addr>,
link_address: Option<FakeLinkAddress>,
) {
assert_eq!(
core_ctx.inner.take_frames(),
[(
FakeNudMessageMeta::NeighborSolicitation {
lookup_addr: ip_address,
remote_link_addr: link_address,
},
Vec::new()
)]
);
}
#[track_caller]
fn assert_neighbor_probe_sent<I: Ip + TestIpExt>(
core_ctx: &mut FakeCoreCtxImpl<I>,
link_address: Option<FakeLinkAddress>,
) {
assert_neighbor_probe_sent_for_ip(core_ctx, I::LOOKUP_ADDR1, link_address);
}
#[track_caller]
fn assert_neighbor_removed_with_ip<I: Ip + TestIpExt>(
core_ctx: &mut FakeCoreCtxImpl<I>,
bindings_ctx: &mut FakeBindingsCtxImpl<I>,
neighbor: SpecifiedAddr<I::Addr>,
) {
super::testutil::assert_neighbor_unknown(core_ctx, FakeLinkDeviceId, neighbor);
assert_eq!(
bindings_ctx.take_events(),
[Event::removed(&FakeLinkDeviceId, neighbor, bindings_ctx.now())],
);
}
#[ip_test]
fn incomplete_to_stale_on_probe<I: Ip + TestIpExt>() {
let FakeCtxWithCoreCtx { mut core_ctx, mut bindings_ctx } = new_context::<I>();
// Initialize a neighbor in INCOMPLETE.
let queued_frame = init_incomplete_neighbor(&mut core_ctx, &mut bindings_ctx, true);
// Handle an incoming probe from that neighbor.
NudHandler::handle_neighbor_update(
&mut core_ctx,
&mut bindings_ctx,
&FakeLinkDeviceId,
I::LOOKUP_ADDR1,
LINK_ADDR1,
DynamicNeighborUpdateSource::Probe,
);
// Neighbor should now be in STALE, per RFC 4861 section 7.2.3.
assert_neighbor_state(
&core_ctx,
&mut bindings_ctx,
DynamicNeighborState::Stale(Stale { link_address: LINK_ADDR1 }),
Some(ExpectedEvent::Changed),
);
assert_pending_frame_sent(&mut core_ctx, queued_frame, LINK_ADDR1);
}
#[ip_test]
#[test_case(true, true; "solicited override")]
#[test_case(true, false; "solicited non-override")]
#[test_case(false, true; "unsolicited override")]
#[test_case(false, false; "unsolicited non-override")]
fn incomplete_on_confirmation<I: Ip + TestIpExt>(solicited_flag: bool, override_flag: bool) {
let FakeCtxWithCoreCtx { mut core_ctx, mut bindings_ctx } = new_context::<I>();
// Initialize a neighbor in INCOMPLETE.
let queued_frame = init_incomplete_neighbor(&mut core_ctx, &mut bindings_ctx, true);
// Handle an incoming confirmation from that neighbor.
NudHandler::handle_neighbor_update(
&mut core_ctx,
&mut bindings_ctx,
&FakeLinkDeviceId,
I::LOOKUP_ADDR1,
LINK_ADDR1,
DynamicNeighborUpdateSource::Confirmation(ConfirmationFlags {
solicited_flag,
override_flag,
}),
);
let expected_state = if solicited_flag {
DynamicNeighborState::Reachable(Reachable {
link_address: LINK_ADDR1,
last_confirmed_at: bindings_ctx.now(),
})
} else {
DynamicNeighborState::Stale(Stale { link_address: LINK_ADDR1 })
};
assert_neighbor_state(
&core_ctx,
&mut bindings_ctx,
expected_state,
Some(ExpectedEvent::Changed),
);
assert_pending_frame_sent(&mut core_ctx, queued_frame, LINK_ADDR1);
}
#[ip_test]
fn reachable_to_stale_on_timeout<I: Ip + TestIpExt>() {
let FakeCtxWithCoreCtx { mut core_ctx, mut bindings_ctx } = new_context::<I>();
// Initialize a neighbor in REACHABLE.
init_reachable_neighbor(&mut core_ctx, &mut bindings_ctx, LINK_ADDR1);
// After reachable time, neighbor should transition to STALE.
assert_eq!(
bindings_ctx
.trigger_timers_for(core_ctx.inner.base_reachable_time().into(), &mut core_ctx,),
[NudTimerId::neighbor()]
);
assert_neighbor_state(
&core_ctx,
&mut bindings_ctx,
DynamicNeighborState::Stale(Stale { link_address: LINK_ADDR1 }),
Some(ExpectedEvent::Changed),
);
}
#[ip_test]
#[test_case(InitialState::Reachable, true; "reachable with different address")]
#[test_case(InitialState::Reachable, false; "reachable with same address")]
#[test_case(InitialState::Stale, true; "stale with different address")]
#[test_case(InitialState::Stale, false; "stale with same address")]
#[test_case(InitialState::Delay, true; "delay with different address")]
#[test_case(InitialState::Delay, false; "delay with same address")]
#[test_case(InitialState::Probe, true; "probe with different address")]
#[test_case(InitialState::Probe, false; "probe with same address")]
#[test_case(InitialState::Unreachable, true; "unreachable with different address")]
#[test_case(InitialState::Unreachable, false; "unreachable with same address")]
fn transition_to_stale_on_probe_with_different_address<I: Ip + TestIpExt>(
initial_state: InitialState,
update_link_address: bool,
) {
let FakeCtxWithCoreCtx { mut core_ctx, mut bindings_ctx } = new_context::<I>();
// Initialize a neighbor.
let initial_state = init_neighbor_in_state(&mut core_ctx, &mut bindings_ctx, initial_state);
// Handle an incoming probe, possibly with an updated link address.
NudHandler::handle_neighbor_update(
&mut core_ctx,
&mut bindings_ctx,
&FakeLinkDeviceId,
I::LOOKUP_ADDR1,
if update_link_address { LINK_ADDR2 } else { LINK_ADDR1 },
DynamicNeighborUpdateSource::Probe,
);
// If the link address was updated, the neighbor should now be in STALE with the
// new link address, per RFC 4861 section 7.2.3.
//
// If the link address is the same, the entry should remain in its initial
// state.
let expected_state = if update_link_address {
DynamicNeighborState::Stale(Stale { link_address: LINK_ADDR2 })
} else {
initial_state
};
assert_neighbor_state(
&core_ctx,
&mut bindings_ctx,
expected_state,
update_link_address.then_some(ExpectedEvent::Changed),
);
}
#[ip_test]
#[test_case(InitialState::Reachable, true; "reachable with override flag set")]
#[test_case(InitialState::Reachable, false; "reachable with override flag not set")]
#[test_case(InitialState::Stale, true; "stale with override flag set")]
#[test_case(InitialState::Stale, false; "stale with override flag not set")]
#[test_case(InitialState::Delay, true; "delay with override flag set")]
#[test_case(InitialState::Delay, false; "delay with override flag not set")]
#[test_case(InitialState::Probe, true; "probe with override flag set")]
#[test_case(InitialState::Probe, false; "probe with override flag not set")]
#[test_case(InitialState::Unreachable, true; "unreachable with override flag set")]
#[test_case(InitialState::Unreachable, false; "unreachable with override flag not set")]
fn transition_to_reachable_on_solicited_confirmation_same_address<I: Ip + TestIpExt>(
initial_state: InitialState,
override_flag: bool,
) {
let FakeCtxWithCoreCtx { mut core_ctx, mut bindings_ctx } = new_context::<I>();
// Initialize a neighbor.
let _ = init_neighbor_in_state(&mut core_ctx, &mut bindings_ctx, initial_state);
// Handle an incoming solicited confirmation.
NudHandler::handle_neighbor_update(
&mut core_ctx,
&mut bindings_ctx,
&FakeLinkDeviceId,
I::LOOKUP_ADDR1,
LINK_ADDR1,
DynamicNeighborUpdateSource::Confirmation(ConfirmationFlags {
solicited_flag: true,
override_flag,
}),
);
// Neighbor should now be in REACHABLE, per RFC 4861 section 7.2.5.
let now = bindings_ctx.now();
assert_neighbor_state(
&core_ctx,
&mut bindings_ctx,
DynamicNeighborState::Reachable(Reachable {
link_address: LINK_ADDR1,
last_confirmed_at: now,
}),
(initial_state != InitialState::Reachable).then_some(ExpectedEvent::Changed),
);
}
#[ip_test]
#[test_case(InitialState::Reachable; "reachable")]
#[test_case(InitialState::Stale; "stale")]
#[test_case(InitialState::Delay; "delay")]
#[test_case(InitialState::Probe; "probe")]
#[test_case(InitialState::Unreachable; "unreachable")]
fn transition_to_stale_on_unsolicited_override_confirmation_with_different_address<
I: Ip + TestIpExt,
>(
initial_state: InitialState,
) {
let FakeCtxWithCoreCtx { mut core_ctx, mut bindings_ctx } = new_context::<I>();
// Initialize a neighbor.
let _ = init_neighbor_in_state(&mut core_ctx, &mut bindings_ctx, initial_state);
// Handle an incoming unsolicited override confirmation with a different link address.
NudHandler::handle_neighbor_update(
&mut core_ctx,
&mut bindings_ctx,
&FakeLinkDeviceId,
I::LOOKUP_ADDR1,
LINK_ADDR2,
DynamicNeighborUpdateSource::Confirmation(ConfirmationFlags {
solicited_flag: false,
override_flag: true,
}),
);
// Neighbor should now be in STALE, per RFC 4861 section 7.2.5.
assert_neighbor_state(
&core_ctx,
&mut bindings_ctx,
DynamicNeighborState::Stale(Stale { link_address: LINK_ADDR2 }),
Some(ExpectedEvent::Changed),
);
}
#[ip_test]
#[test_case(InitialState::Reachable, true; "reachable with override flag set")]
#[test_case(InitialState::Reachable, false; "reachable with override flag not set")]
#[test_case(InitialState::Stale, true; "stale with override flag set")]
#[test_case(InitialState::Stale, false; "stale with override flag not set")]
#[test_case(InitialState::Delay, true; "delay with override flag set")]
#[test_case(InitialState::Delay, false; "delay with override flag not set")]
#[test_case(InitialState::Probe, true; "probe with override flag set")]
#[test_case(InitialState::Probe, false; "probe with override flag not set")]
#[test_case(InitialState::Unreachable, true; "unreachable with override flag set")]
#[test_case(InitialState::Unreachable, false; "unreachable with override flag not set")]
fn noop_on_unsolicited_confirmation_with_same_address<I: Ip + TestIpExt>(
initial_state: InitialState,
override_flag: bool,
) {
let FakeCtxWithCoreCtx { mut core_ctx, mut bindings_ctx } = new_context::<I>();
// Initialize a neighbor.
let expected_state =
init_neighbor_in_state(&mut core_ctx, &mut bindings_ctx, initial_state);
// Handle an incoming unsolicited confirmation with the same link address.
NudHandler::handle_neighbor_update(
&mut core_ctx,
&mut bindings_ctx,
&FakeLinkDeviceId,
I::LOOKUP_ADDR1,
LINK_ADDR1,
DynamicNeighborUpdateSource::Confirmation(ConfirmationFlags {
solicited_flag: false,
override_flag,
}),
);
// Neighbor should not have been updated.
assert_neighbor_state(&core_ctx, &mut bindings_ctx, expected_state, None);
}
#[ip_test]
#[test_case(InitialState::Reachable; "reachable")]
#[test_case(InitialState::Stale; "stale")]
#[test_case(InitialState::Delay; "delay")]
#[test_case(InitialState::Probe; "probe")]
#[test_case(InitialState::Unreachable; "unreachable")]
fn transition_to_reachable_on_solicited_override_confirmation_with_different_address<
I: Ip + TestIpExt,
>(
initial_state: InitialState,
) {
let FakeCtxWithCoreCtx { mut core_ctx, mut bindings_ctx } = new_context::<I>();
// Initialize a neighbor.
let _ = init_neighbor_in_state(&mut core_ctx, &mut bindings_ctx, initial_state);
// Handle an incoming solicited override confirmation with a different link address.
NudHandler::handle_neighbor_update(
&mut core_ctx,
&mut bindings_ctx,
&FakeLinkDeviceId,
I::LOOKUP_ADDR1,
LINK_ADDR2,
DynamicNeighborUpdateSource::Confirmation(ConfirmationFlags {
solicited_flag: true,
override_flag: true,
}),
);
// Neighbor should now be in REACHABLE, per RFC 4861 section 7.2.5.
let now = bindings_ctx.now();
assert_neighbor_state(
&core_ctx,
&mut bindings_ctx,
DynamicNeighborState::Reachable(Reachable {
link_address: LINK_ADDR2,
last_confirmed_at: now,
}),
Some(ExpectedEvent::Changed),
);
}
#[ip_test]
fn reachable_to_reachable_on_probe_with_same_address<I: Ip + TestIpExt>() {
let FakeCtxWithCoreCtx { mut core_ctx, mut bindings_ctx } = new_context::<I>();
// Initialize a neighbor in REACHABLE.
init_reachable_neighbor(&mut core_ctx, &mut bindings_ctx, LINK_ADDR1);
// Handle an incoming probe with the same link address.
NudHandler::handle_neighbor_update(
&mut core_ctx,
&mut bindings_ctx,
&FakeLinkDeviceId,
I::LOOKUP_ADDR1,
LINK_ADDR1,
DynamicNeighborUpdateSource::Probe,
);
// Neighbor should still be in REACHABLE with the same link address.
let now = bindings_ctx.now();
assert_neighbor_state(
&core_ctx,
&mut bindings_ctx,
DynamicNeighborState::Reachable(Reachable {
link_address: LINK_ADDR1,
last_confirmed_at: now,
}),
None,
);
}
#[ip_test]
#[test_case(true; "solicited")]
#[test_case(false; "unsolicited")]
fn reachable_to_stale_on_non_override_confirmation_with_different_address<I: Ip + TestIpExt>(
solicited_flag: bool,
) {
let FakeCtxWithCoreCtx { mut core_ctx, mut bindings_ctx } = new_context::<I>();
// Initialize a neighbor in REACHABLE.
init_reachable_neighbor(&mut core_ctx, &mut bindings_ctx, LINK_ADDR1);
// Handle an incoming non-override confirmation with a different link address.
NudHandler::handle_neighbor_update(
&mut core_ctx,
&mut bindings_ctx,
&FakeLinkDeviceId,
I::LOOKUP_ADDR1,
LINK_ADDR2,
DynamicNeighborUpdateSource::Confirmation(ConfirmationFlags {
override_flag: false,
solicited_flag,
}),
);
// Neighbor should now be in STALE, with the *same* link address as was
// previously cached, per RFC 4861 section 7.2.5.
assert_neighbor_state(
&core_ctx,
&mut bindings_ctx,
DynamicNeighborState::Stale(Stale { link_address: LINK_ADDR1 }),
Some(ExpectedEvent::Changed),
);
}
#[ip_test]
#[test_case(InitialState::Stale, true; "stale solicited")]
#[test_case(InitialState::Stale, false; "stale unsolicited")]
#[test_case(InitialState::Delay, true; "delay solicited")]
#[test_case(InitialState::Delay, false; "delay unsolicited")]
#[test_case(InitialState::Probe, true; "probe solicited")]
#[test_case(InitialState::Probe, false; "probe unsolicited")]
#[test_case(InitialState::Unreachable, true; "unreachable solicited")]
#[test_case(InitialState::Unreachable, false; "unreachable unsolicited")]
fn noop_on_non_override_confirmation_with_different_address<I: Ip + TestIpExt>(
initial_state: InitialState,
solicited_flag: bool,
) {
let FakeCtxWithCoreCtx { mut core_ctx, mut bindings_ctx } = new_context::<I>();
// Initialize a neighbor.
let initial_state = init_neighbor_in_state(&mut core_ctx, &mut bindings_ctx, initial_state);
// Handle an incoming non-override confirmation with a different link address.
NudHandler::handle_neighbor_update(
&mut core_ctx,
&mut bindings_ctx,
&FakeLinkDeviceId,
I::LOOKUP_ADDR1,
LINK_ADDR2,
DynamicNeighborUpdateSource::Confirmation(ConfirmationFlags {
override_flag: false,
solicited_flag,
}),
);
// Neighbor should still be in the original state; the link address should *not*
// have been updated.
assert_neighbor_state(&core_ctx, &mut bindings_ctx, initial_state, None);
}
#[ip_test]
fn stale_to_delay_on_packet_sent<I: Ip + TestIpExt>() {
let FakeCtxWithCoreCtx { mut core_ctx, mut bindings_ctx } = new_context::<I>();
// Initialize a neighbor in STALE.
init_stale_neighbor(&mut core_ctx, &mut bindings_ctx, LINK_ADDR1);
// Send a packet to the neighbor.
let body = 1;
assert_eq!(
NudHandler::send_ip_packet_to_neighbor(
&mut core_ctx,
&mut bindings_ctx,
&FakeLinkDeviceId,
I::LOOKUP_ADDR1,
Buf::new([body], ..),
),
Ok(())
);
// Neighbor should be in DELAY.
assert_neighbor_state(
&core_ctx,
&mut bindings_ctx,
DynamicNeighborState::Delay(Delay { link_address: LINK_ADDR1 }),
Some(ExpectedEvent::Changed),
);
core_ctx.outer.nud.timer_heap.neighbor.assert_timers_after(
&mut bindings_ctx,
[(I::LOOKUP_ADDR1, NudEvent::DelayFirstProbe, DELAY_FIRST_PROBE_TIME.get())],
);
assert_pending_frame_sent(
&mut core_ctx,
VecDeque::from([Buf::new(vec![body], ..)]),
LINK_ADDR1,
);
}
#[ip_test]
#[test_case(InitialState::Delay,
NudEvent::DelayFirstProbe;
"delay to probe")]
#[test_case(InitialState::Probe,
NudEvent::RetransmitUnicastProbe;
"probe retransmit unicast probe")]
fn delay_or_probe_to_probe_on_timeout<I: Ip + TestIpExt>(
initial_state: InitialState,
expected_initial_event: NudEvent,
) {
let FakeCtxWithCoreCtx { mut core_ctx, mut bindings_ctx } = new_context::<I>();
// Initialize a neighbor.
let _ = init_neighbor_in_state(&mut core_ctx, &mut bindings_ctx, initial_state);
let max_unicast_solicit = core_ctx.inner.max_unicast_solicit().get();
// If the neighbor started in DELAY, then after DELAY_FIRST_PROBE_TIME, the
// neighbor should transition to PROBE and send out a unicast probe.
//
// If the neighbor started in PROBE, then after RetransTimer expires, the
// neighbor should remain in PROBE and retransmit a unicast probe.
let (time, transmit_counter) = match initial_state {
InitialState::Delay => {
(DELAY_FIRST_PROBE_TIME, NonZeroU16::new(max_unicast_solicit - 1))
}
InitialState::Probe => {
(core_ctx.inner.get_ref().retrans_timer, NonZeroU16::new(max_unicast_solicit - 2))
}
other => unreachable!("test only covers DELAY and PROBE, got {:?}", other),
};
core_ctx.outer.nud.timer_heap.neighbor.assert_timers_after(
&mut bindings_ctx,
[(I::LOOKUP_ADDR1, expected_initial_event, time.get())],
);
assert_eq!(
bindings_ctx.trigger_timers_for(time.into(), &mut core_ctx,),
[NudTimerId::neighbor()]
);
assert_neighbor_state(
&core_ctx,
&mut bindings_ctx,
DynamicNeighborState::Probe(Probe { link_address: LINK_ADDR1, transmit_counter }),
(initial_state != InitialState::Probe).then_some(ExpectedEvent::Changed),
);
core_ctx.outer.nud.timer_heap.neighbor.assert_timers_after(
&mut bindings_ctx,
[(
I::LOOKUP_ADDR1,
NudEvent::RetransmitUnicastProbe,
core_ctx.inner.get_ref().retrans_timer.get(),
)],
);
assert_neighbor_probe_sent(&mut core_ctx, Some(LINK_ADDR1));
}
#[ip_test]
fn unreachable_probes_with_exponential_backoff_while_packets_sent<I: Ip + TestIpExt>() {
let FakeCtxWithCoreCtx { mut core_ctx, mut bindings_ctx } = new_context::<I>();
init_unreachable_neighbor(&mut core_ctx, &mut bindings_ctx, LINK_ADDR1);
let retrans_timer = core_ctx.inner.retransmit_timeout().get();
let timer_id = NudTimerId::neighbor();
// No multicast probes should be transmitted even after the retransmit timeout.
assert_eq!(bindings_ctx.trigger_timers_for(retrans_timer, &mut core_ctx,), []);
assert_eq!(core_ctx.inner.take_frames(), []);
// Send a packet and ensure that we also transmit a multicast probe.
const BODY: u8 = 0x33;
assert_eq!(
NudHandler::send_ip_packet_to_neighbor(
&mut core_ctx,
&mut bindings_ctx,
&FakeLinkDeviceId,
I::LOOKUP_ADDR1,
Buf::new([BODY], ..),
),
Ok(())
);
assert_eq!(
core_ctx.inner.take_frames(),
[
(FakeNudMessageMeta::IpFrame { dst_link_address: LINK_ADDR1 }, vec![BODY]),
(
FakeNudMessageMeta::NeighborSolicitation {
lookup_addr: I::LOOKUP_ADDR1,
remote_link_addr: /* multicast */ None,
},
Vec::new()
)
]
);
let next_backoff_timer = |core_ctx: &mut FakeCoreCtxImpl<I>, probes_sent| {
UnreachableMode::Backoff {
probes_sent: NonZeroU32::new(probes_sent).unwrap(),
packet_sent: /* unused */ false,
}
.next_backoff_retransmit_timeout::<I, _>(core_ctx.inner.get_mut())
.get()
};
const ITERATIONS: u8 = 2;
for i in 1..ITERATIONS {
let probes_sent = u32::from(i);
// Send another packet before the retransmit timer expires: only the packet
// should be sent (not a probe), and the `packet_sent` flag should be set.
assert_eq!(
NudHandler::send_ip_packet_to_neighbor(
&mut core_ctx,
&mut bindings_ctx,
&FakeLinkDeviceId,
I::LOOKUP_ADDR1,
Buf::new([BODY + i], ..),
),
Ok(())
);
assert_eq!(
core_ctx.inner.take_frames(),
[(FakeNudMessageMeta::IpFrame { dst_link_address: LINK_ADDR1 }, vec![BODY + i])]
);
// Fast forward until the current retransmit timer should fire, taking
// exponential backoff into account. Another multicast probe should be
// transmitted and a new timer should be scheduled (backing off further) because
// a packet was recently sent.
assert_eq!(
bindings_ctx.trigger_timers_for(
next_backoff_timer(&mut core_ctx, probes_sent),
&mut core_ctx,
),
[timer_id]
);
assert_neighbor_probe_sent(&mut core_ctx, /* multicast */ None);
bindings_ctx.timer_ctx().assert_timers_installed([(
timer_id,
bindings_ctx.now() + next_backoff_timer(&mut core_ctx, probes_sent + 1),
)]);
}
// If no more packets are sent, no multicast probes should be transmitted even
// after the next backoff timer expires.
let current_timer = next_backoff_timer(&mut core_ctx, u32::from(ITERATIONS));
assert_eq!(bindings_ctx.trigger_timers_for(current_timer, &mut core_ctx,), [timer_id]);
assert_eq!(core_ctx.inner.take_frames(), []);
bindings_ctx.timer_ctx().assert_no_timers_installed();
// Finally, if another packet is sent, we resume transmitting multicast probes
// and "reset" the exponential backoff.
assert_eq!(
NudHandler::send_ip_packet_to_neighbor(
&mut core_ctx,
&mut bindings_ctx,
&FakeLinkDeviceId,
I::LOOKUP_ADDR1,
Buf::new([BODY], ..),
),
Ok(())
);
assert_eq!(
core_ctx.inner.take_frames(),
[
(FakeNudMessageMeta::IpFrame { dst_link_address: LINK_ADDR1 }, vec![BODY]),
(
FakeNudMessageMeta::NeighborSolicitation {
lookup_addr: I::LOOKUP_ADDR1,
remote_link_addr: /* multicast */ None,
},
Vec::new()
)
]
);
bindings_ctx.timer_ctx().assert_timers_installed([(
timer_id,
bindings_ctx.now() + next_backoff_timer(&mut core_ctx, 1),
)]);
}
#[ip_test]
#[test_case(true; "solicited confirmation")]
#[test_case(false; "unsolicited confirmation")]
fn confirmation_should_not_create_entry<I: Ip + TestIpExt>(solicited_flag: bool) {
let FakeCtxWithCoreCtx { mut core_ctx, mut bindings_ctx } = new_context::<I>();
let link_addr = FakeLinkAddress([1]);
NudHandler::handle_neighbor_update(
&mut core_ctx,
&mut bindings_ctx,
&FakeLinkDeviceId,
I::LOOKUP_ADDR1,
link_addr,
DynamicNeighborUpdateSource::Confirmation(ConfirmationFlags {
solicited_flag,
override_flag: false,
}),
);
assert_eq!(core_ctx.outer.nud.neighbors, HashMap::new());
}
#[ip_test]
#[test_case(true; "set_with_dynamic")]
#[test_case(false; "set_with_static")]
fn pending_frames<I: Ip + TestIpExt>(dynamic: bool) {
let FakeCtxWithCoreCtx { mut core_ctx, mut bindings_ctx } = new_context::<I>();
assert_eq!(core_ctx.inner.take_frames(), []);
// Send up to the maximum number of pending frames to some neighbor
// which requires resolution. This should cause all frames to be queued
// pending resolution completion.
const MAX_PENDING_FRAMES_U8: u8 = MAX_PENDING_FRAMES as u8;
let expected_pending_frames =
(0..MAX_PENDING_FRAMES_U8).map(|i| Buf::new(vec![i], ..)).collect::<VecDeque<_>>();
for body in expected_pending_frames.iter() {
assert_eq!(
NudHandler::send_ip_packet_to_neighbor(
&mut core_ctx,
&mut bindings_ctx,
&FakeLinkDeviceId,
I::LOOKUP_ADDR1,
body.clone()
),
Ok(())
);
}
let max_multicast_solicit = core_ctx.inner.max_multicast_solicit().get();
// Should have only sent out a single neighbor probe message.
assert_neighbor_probe_sent(&mut core_ctx, None);
assert_neighbor_state(
&core_ctx,
&mut bindings_ctx,
DynamicNeighborState::Incomplete(Incomplete {
transmit_counter: NonZeroU16::new(max_multicast_solicit - 1),
pending_frames: expected_pending_frames.clone(),
notifiers: Vec::new(),
_marker: PhantomData,
}),
Some(ExpectedEvent::Added),
);
// The next frame should be dropped.
assert_eq!(
NudHandler::send_ip_packet_to_neighbor(
&mut core_ctx,
&mut bindings_ctx,
&FakeLinkDeviceId,
I::LOOKUP_ADDR1,
Buf::new([123], ..),
),
Ok(())
);
assert_eq!(core_ctx.inner.take_frames(), []);
assert_neighbor_state(
&core_ctx,
&mut bindings_ctx,
DynamicNeighborState::Incomplete(Incomplete {
transmit_counter: NonZeroU16::new(max_multicast_solicit - 1),
pending_frames: expected_pending_frames.clone(),
notifiers: Vec::new(),
_marker: PhantomData,
}),
None,
);
// Completing resolution should result in all queued packets being sent.
if dynamic {
NudHandler::handle_neighbor_update(
&mut core_ctx,
&mut bindings_ctx,
&FakeLinkDeviceId,
I::LOOKUP_ADDR1,
LINK_ADDR1,
DynamicNeighborUpdateSource::Confirmation(ConfirmationFlags {
solicited_flag: true,
override_flag: false,
}),
);
core_ctx.outer.nud.timer_heap.neighbor.assert_timers_after(
&mut bindings_ctx,
[(
I::LOOKUP_ADDR1,
NudEvent::ReachableTime,
core_ctx.inner.base_reachable_time().get(),
)],
);
let last_confirmed_at = bindings_ctx.now();
assert_neighbor_state(
&core_ctx,
&mut bindings_ctx,
DynamicNeighborState::Reachable(Reachable {
link_address: LINK_ADDR1,
last_confirmed_at,
}),
Some(ExpectedEvent::Changed),
);
} else {
init_static_neighbor(
&mut core_ctx,
&mut bindings_ctx,
LINK_ADDR1,
ExpectedEvent::Changed,
);
bindings_ctx.timer_ctx().assert_no_timers_installed();
}
assert_eq!(
core_ctx.inner.take_frames(),
expected_pending_frames
.into_iter()
.map(|p| (
FakeNudMessageMeta::IpFrame { dst_link_address: LINK_ADDR1 },
p.as_ref().to_vec()
))
.collect::<Vec<_>>()
);
}
#[ip_test]
fn static_neighbor<I: Ip + TestIpExt>() {
let FakeCtxWithCoreCtx { mut core_ctx, mut bindings_ctx } = new_context::<I>();
init_static_neighbor(&mut core_ctx, &mut bindings_ctx, LINK_ADDR1, ExpectedEvent::Added);
bindings_ctx.timer_ctx().assert_no_timers_installed();
assert_eq!(core_ctx.inner.take_frames(), []);
check_lookup_has(&mut core_ctx, &mut bindings_ctx, I::LOOKUP_ADDR1, LINK_ADDR1);
// Dynamic entries should not overwrite static entries.
NudHandler::handle_neighbor_update(
&mut core_ctx,
&mut bindings_ctx,
&FakeLinkDeviceId,
I::LOOKUP_ADDR1,
LINK_ADDR2,
DynamicNeighborUpdateSource::Probe,
);
check_lookup_has(&mut core_ctx, &mut bindings_ctx, I::LOOKUP_ADDR1, LINK_ADDR1);
delete_neighbor(&mut core_ctx, &mut bindings_ctx);
let FakeNudContext { nud: NudState { neighbors, .. }, .. } = &core_ctx.outer;
assert!(neighbors.is_empty(), "neighbor table should be empty: {neighbors:?}");
}
#[ip_test]
fn dynamic_neighbor<I: Ip + TestIpExt>() {
let FakeCtxWithCoreCtx { mut core_ctx, mut bindings_ctx } = new_context::<I>();
init_stale_neighbor(&mut core_ctx, &mut bindings_ctx, LINK_ADDR1);
bindings_ctx.timer_ctx().assert_no_timers_installed();
assert_eq!(core_ctx.inner.take_frames(), []);
check_lookup_has(&mut core_ctx, &mut bindings_ctx, I::LOOKUP_ADDR1, LINK_ADDR1);
// Dynamic entries may be overwritten by new dynamic entries.
NudHandler::handle_neighbor_update(
&mut core_ctx,
&mut bindings_ctx,
&FakeLinkDeviceId,
I::LOOKUP_ADDR1,
LINK_ADDR2,
DynamicNeighborUpdateSource::Probe,
);
check_lookup_has(&mut core_ctx, &mut bindings_ctx, I::LOOKUP_ADDR1, LINK_ADDR2);
assert_eq!(core_ctx.inner.take_frames(), []);
assert_neighbor_state(
&core_ctx,
&mut bindings_ctx,
DynamicNeighborState::Stale(Stale { link_address: LINK_ADDR2 }),
Some(ExpectedEvent::Changed),
);
// A static entry may overwrite a dynamic entry.
init_static_neighbor_with_ip(
&mut core_ctx,
&mut bindings_ctx,
I::LOOKUP_ADDR1,
LINK_ADDR3,
ExpectedEvent::Changed,
);
check_lookup_has(&mut core_ctx, &mut bindings_ctx, I::LOOKUP_ADDR1, LINK_ADDR3);
assert_eq!(core_ctx.inner.take_frames(), []);
}
#[ip_test]
fn send_solicitation_on_lookup<I: Ip + TestIpExt>() {
let FakeCtxWithCoreCtx { mut core_ctx, mut bindings_ctx } = new_context::<I>();
bindings_ctx.timer_ctx().assert_no_timers_installed();
assert_eq!(core_ctx.inner.take_frames(), []);
let mut pending_frames = VecDeque::new();
queue_ip_packet_to_unresolved_neighbor(
&mut core_ctx,
&mut bindings_ctx,
I::LOOKUP_ADDR1,
&mut pending_frames,
1,
true, /* expect_event */
);
assert_neighbor_probe_sent(&mut core_ctx, None);
queue_ip_packet_to_unresolved_neighbor(
&mut core_ctx,
&mut bindings_ctx,
I::LOOKUP_ADDR1,
&mut pending_frames,
2,
false, /* expect_event */
);
assert_eq!(core_ctx.inner.take_frames(), []);
// Complete link resolution.
NudHandler::handle_neighbor_update(
&mut core_ctx,
&mut bindings_ctx,
&FakeLinkDeviceId,
I::LOOKUP_ADDR1,
LINK_ADDR1,
DynamicNeighborUpdateSource::Confirmation(ConfirmationFlags {
solicited_flag: true,
override_flag: false,
}),
);
check_lookup_has(&mut core_ctx, &mut bindings_ctx, I::LOOKUP_ADDR1, LINK_ADDR1);
let now = bindings_ctx.now();
assert_neighbor_state(
&core_ctx,
&mut bindings_ctx,
DynamicNeighborState::Reachable(Reachable {
link_address: LINK_ADDR1,
last_confirmed_at: now,
}),
Some(ExpectedEvent::Changed),
);
assert_eq!(
core_ctx.inner.take_frames(),
pending_frames
.into_iter()
.map(|f| (
FakeNudMessageMeta::IpFrame { dst_link_address: LINK_ADDR1 },
f.as_ref().to_vec(),
))
.collect::<Vec<_>>()
);
}
#[ip_test]
fn solicitation_failure_in_incomplete<I: Ip + TestIpExt>() {
let FakeCtxWithCoreCtx { mut core_ctx, mut bindings_ctx } = new_context::<I>();
bindings_ctx.timer_ctx().assert_no_timers_installed();
assert_eq!(core_ctx.inner.take_frames(), []);
let pending_frames = init_incomplete_neighbor(&mut core_ctx, &mut bindings_ctx, false);
let timer_id = NudTimerId::neighbor();
let retrans_timer = core_ctx.inner.retransmit_timeout().get();
let max_multicast_solicit = core_ctx.inner.max_multicast_solicit().get();
for i in 1..=max_multicast_solicit {
assert_neighbor_state(
&core_ctx,
&mut bindings_ctx,
DynamicNeighborState::Incomplete(Incomplete {
transmit_counter: NonZeroU16::new(max_multicast_solicit - i),
pending_frames: pending_frames.clone(),
notifiers: Vec::new(),
_marker: PhantomData,
}),
None,
);
bindings_ctx
.timer_ctx()
.assert_timers_installed([(timer_id, bindings_ctx.now() + ONE_SECOND.get())]);
assert_neighbor_probe_sent(&mut core_ctx, /* multicast */ None);
assert_eq!(bindings_ctx.trigger_timers_for(retrans_timer, &mut core_ctx,), [timer_id]);
}
// The neighbor entry should have been removed.
assert_neighbor_removed_with_ip(&mut core_ctx, &mut bindings_ctx, I::LOOKUP_ADDR1);
bindings_ctx.timer_ctx().assert_no_timers_installed();
// The ICMP destination unreachable error sent as a result of solicitation failure
// will be dropped because the packets pending address resolution in this test
// is not a valid IP packet.
assert_eq!(core_ctx.inner.take_frames(), []);
core_ctx.with_counters(|counters| {
assert_eq!(counters.as_ref().icmp_dest_unreachable_dropped.get(), 1)
});
}
#[ip_test]
fn solicitation_failure_in_probe<I: Ip + TestIpExt>() {
let FakeCtxWithCoreCtx { mut core_ctx, mut bindings_ctx } = new_context::<I>();
bindings_ctx.timer_ctx().assert_no_timers_installed();
assert_eq!(core_ctx.inner.take_frames(), []);
init_probe_neighbor(&mut core_ctx, &mut bindings_ctx, LINK_ADDR1, false);
let timer_id = NudTimerId::neighbor();
let retrans_timer = core_ctx.inner.retransmit_timeout().get();
let max_unicast_solicit = core_ctx.inner.max_unicast_solicit().get();
for i in 1..=max_unicast_solicit {
assert_neighbor_state(
&core_ctx,
&mut bindings_ctx,
DynamicNeighborState::Probe(Probe {
transmit_counter: NonZeroU16::new(max_unicast_solicit - i),
link_address: LINK_ADDR1,
}),
None,
);
bindings_ctx
.timer_ctx()
.assert_timers_installed([(timer_id, bindings_ctx.now() + ONE_SECOND.get())]);
assert_neighbor_probe_sent(&mut core_ctx, Some(LINK_ADDR1));
assert_eq!(bindings_ctx.trigger_timers_for(retrans_timer, &mut core_ctx,), [timer_id]);
}
assert_neighbor_state(
&core_ctx,
&mut bindings_ctx,
DynamicNeighborState::Unreachable(Unreachable {
link_address: LINK_ADDR1,
mode: UnreachableMode::WaitingForPacketSend,
}),
Some(ExpectedEvent::Changed),
);
bindings_ctx.timer_ctx().assert_no_timers_installed();
assert_eq!(core_ctx.inner.take_frames(), []);
}
#[ip_test]
fn flush_entries<I: Ip + TestIpExt>() {
let FakeCtxWithCoreCtx { mut core_ctx, mut bindings_ctx } = new_context::<I>();
bindings_ctx.timer_ctx().assert_no_timers_installed();
assert_eq!(core_ctx.inner.take_frames(), []);
init_static_neighbor(&mut core_ctx, &mut bindings_ctx, LINK_ADDR1, ExpectedEvent::Added);
init_stale_neighbor_with_ip(&mut core_ctx, &mut bindings_ctx, I::LOOKUP_ADDR2, LINK_ADDR2);
let pending_frames = init_incomplete_neighbor_with_ip(
&mut core_ctx,
&mut bindings_ctx,
I::LOOKUP_ADDR3,
true,
);
let max_multicast_solicit = core_ctx.inner.max_multicast_solicit().get();
let FakeNudContext { nud, counters: _ } = &core_ctx.outer;
assert_eq!(
nud.neighbors,
HashMap::from([
(I::LOOKUP_ADDR1, NeighborState::Static(LINK_ADDR1)),
(
I::LOOKUP_ADDR2,
NeighborState::Dynamic(DynamicNeighborState::Stale(Stale {
link_address: LINK_ADDR2,
})),
),
(
I::LOOKUP_ADDR3,
NeighborState::Dynamic(DynamicNeighborState::Incomplete(Incomplete {
transmit_counter: NonZeroU16::new(max_multicast_solicit - 1),
pending_frames: pending_frames,
notifiers: Vec::new(),
_marker: PhantomData,
})),
),
]),
);
core_ctx.outer.nud.timer_heap.neighbor.assert_timers_after(
&mut bindings_ctx,
[(I::LOOKUP_ADDR3, NudEvent::RetransmitMulticastProbe, ONE_SECOND.get())],
);
// Flushing the table should clear all entries (dynamic and static) and timers.
NudHandler::flush(&mut core_ctx, &mut bindings_ctx, &FakeLinkDeviceId);
let FakeNudContext { nud, counters: _ } = &core_ctx.outer;
assert!(nud.neighbors.is_empty(), "neighbor table should be empty: {:?}", nud.neighbors);
assert_eq!(
bindings_ctx.take_events().into_iter().collect::<HashSet<_>>(),
[I::LOOKUP_ADDR1, I::LOOKUP_ADDR2, I::LOOKUP_ADDR3]
.into_iter()
.map(|addr| { Event::removed(&FakeLinkDeviceId, addr, bindings_ctx.now()) })
.collect(),
);
bindings_ctx.timer_ctx().assert_no_timers_installed();
}
#[ip_test]
fn delete_dynamic_entry<I: Ip + TestIpExt>() {
let FakeCtxWithCoreCtx { mut core_ctx, mut bindings_ctx } = new_context::<I>();
bindings_ctx.timer_ctx().assert_no_timers_installed();
assert_eq!(core_ctx.inner.take_frames(), []);
init_reachable_neighbor(&mut core_ctx, &mut bindings_ctx, LINK_ADDR1);
check_lookup_has(&mut core_ctx, &mut bindings_ctx, I::LOOKUP_ADDR1, LINK_ADDR1);
delete_neighbor(&mut core_ctx, &mut bindings_ctx);
// Entry should be removed and timer cancelled.
let FakeNudContext { nud: NudState { neighbors, .. }, .. } = &core_ctx.outer;
assert!(neighbors.is_empty(), "neighbor table should be empty: {neighbors:?}");
bindings_ctx.timer_ctx().assert_no_timers_installed();
}
fn assert_neighbors<
'a,
I: Ip,
CC: NudContext<I, EthernetLinkDevice, BC>,
BC: NudBindingsContext<I, EthernetLinkDevice, CC::DeviceId>,
>(
core_ctx: &mut CC,
device_id: &CC::DeviceId,
expected: HashMap<SpecifiedAddr<I::Addr>, NeighborState<EthernetLinkDevice, BC>>,
) {
NudContext::<I, EthernetLinkDevice, _>::with_nud_state_mut(
core_ctx,
device_id,
|NudState { neighbors, .. }, _config| assert_eq!(*neighbors, expected),
)
}
#[ip_test]
#[test_case(InitialState::Reachable; "reachable neighbor")]
#[test_case(InitialState::Stale; "stale neighbor")]
#[test_case(InitialState::Delay; "delay neighbor")]
#[test_case(InitialState::Probe; "probe neighbor")]
#[test_case(InitialState::Unreachable; "unreachable neighbor")]
fn resolve_cached_linked_addr<I: Ip + TestIpExt>(initial_state: InitialState) {
let mut ctx = new_context::<I>();
ctx.bindings_ctx.timer_ctx().assert_no_timers_installed();
assert_eq!(ctx.core_ctx.inner.take_frames(), []);
let _ = init_neighbor_in_state(&mut ctx.core_ctx, &mut ctx.bindings_ctx, initial_state);
let link_addr = assert_matches!(
NeighborApi::new(ctx.as_mut()).resolve_link_addr(
&FakeLinkDeviceId,
&I::LOOKUP_ADDR1,
),
LinkResolutionResult::Resolved(addr) => addr
);
assert_eq!(link_addr, LINK_ADDR1);
if initial_state == InitialState::Stale {
assert_eq!(
ctx.bindings_ctx.take_events(),
[Event::changed(
&FakeLinkDeviceId,
EventState::Dynamic(EventDynamicState::Delay(LINK_ADDR1)),
I::LOOKUP_ADDR1,
ctx.bindings_ctx.now(),
)],
);
}
}
enum ResolutionSuccess {
Confirmation,
StaticEntryAdded,
}
#[ip_test]
#[test_case(ResolutionSuccess::Confirmation; "incomplete entry timed out")]
#[test_case(ResolutionSuccess::StaticEntryAdded; "incomplete entry removed from table")]
fn dynamic_neighbor_resolution_success<I: Ip + TestIpExt>(reason: ResolutionSuccess) {
let mut ctx = new_context::<I>();
let observers = (0..10)
.map(|_| {
let observer = assert_matches!(
NeighborApi::new(ctx.as_mut()).resolve_link_addr(
&FakeLinkDeviceId,
&I::LOOKUP_ADDR1,
),
LinkResolutionResult::Pending(observer) => observer
);
assert_eq!(*observer.lock(), None);
observer
})
.collect::<Vec<_>>();
let FakeCtxWithCoreCtx { core_ctx, bindings_ctx } = &mut ctx;
let max_multicast_solicit = core_ctx.inner.max_multicast_solicit().get();
// We should have initialized an incomplete neighbor and sent a neighbor probe
// to attempt resolution.
assert_neighbor_state(
core_ctx,
bindings_ctx,
DynamicNeighborState::Incomplete(Incomplete {
transmit_counter: NonZeroU16::new(max_multicast_solicit - 1),
pending_frames: VecDeque::new(),
// NB: notifiers is not checked for equality.
notifiers: Vec::new(),
_marker: PhantomData,
}),
Some(ExpectedEvent::Added),
);
assert_neighbor_probe_sent(core_ctx, /* multicast */ None);
match reason {
ResolutionSuccess::Confirmation => {
// Complete neighbor resolution with an incoming neighbor confirmation.
NudHandler::handle_neighbor_update(
core_ctx,
bindings_ctx,
&FakeLinkDeviceId,
I::LOOKUP_ADDR1,
LINK_ADDR1,
DynamicNeighborUpdateSource::Confirmation(ConfirmationFlags {
solicited_flag: true,
override_flag: false,
}),
);
let now = bindings_ctx.now();
assert_neighbor_state(
core_ctx,
bindings_ctx,
DynamicNeighborState::Reachable(Reachable {
link_address: LINK_ADDR1,
last_confirmed_at: now,
}),
Some(ExpectedEvent::Changed),
);
}
ResolutionSuccess::StaticEntryAdded => {
init_static_neighbor(core_ctx, bindings_ctx, LINK_ADDR1, ExpectedEvent::Changed);
assert_eq!(
core_ctx.outer.nud.neighbors.get(&I::LOOKUP_ADDR1),
Some(&NeighborState::Static(LINK_ADDR1))
);
}
}
// Each observer should have been notified of successful link resolution.
for observer in observers {
assert_eq!(*observer.lock(), Some(Ok(LINK_ADDR1)));
}
}
enum ResolutionFailure {
Timeout,
Removed,
}
#[ip_test]
#[test_case(ResolutionFailure::Timeout; "incomplete entry timed out")]
#[test_case(ResolutionFailure::Removed; "incomplete entry removed from table")]
fn dynamic_neighbor_resolution_failure<I: Ip + TestIpExt>(reason: ResolutionFailure) {
let mut ctx = new_context::<I>();
let observers = (0..10)
.map(|_| {
let observer = assert_matches!(
NeighborApi::new(ctx.as_mut()).resolve_link_addr(
&FakeLinkDeviceId,
&I::LOOKUP_ADDR1,
),
LinkResolutionResult::Pending(observer) => observer
);
assert_eq!(*observer.lock(), None);
observer
})
.collect::<Vec<_>>();
let FakeCtxWithCoreCtx { core_ctx, bindings_ctx } = &mut ctx;
let max_multicast_solicit = core_ctx.inner.max_multicast_solicit().get();
// We should have initialized an incomplete neighbor and sent a neighbor probe
// to attempt resolution.
assert_neighbor_state(
core_ctx,
bindings_ctx,
DynamicNeighborState::Incomplete(Incomplete {
transmit_counter: NonZeroU16::new(max_multicast_solicit - 1),
pending_frames: VecDeque::new(),
// NB: notifiers is not checked for equality.
notifiers: Vec::new(),
_marker: PhantomData,
}),
Some(ExpectedEvent::Added),
);
assert_neighbor_probe_sent(core_ctx, /* multicast */ None);
match reason {
ResolutionFailure::Timeout => {
// Wait until neighbor resolution exceeds its maximum probe retransmits and
// times out.
for _ in 1..=max_multicast_solicit {
let retrans_timer = core_ctx.inner.retransmit_timeout().get();
assert_eq!(
bindings_ctx.trigger_timers_for(retrans_timer, core_ctx),
[NudTimerId::neighbor()]
);
}
}
ResolutionFailure::Removed => {
// Flush the neighbor table so the entry is removed.
NudHandler::flush(core_ctx, bindings_ctx, &FakeLinkDeviceId);
}
}
assert_neighbor_removed_with_ip(core_ctx, bindings_ctx, I::LOOKUP_ADDR1);
// Each observer should have been notified of link resolution failure.
for observer in observers {
assert_eq!(*observer.lock(), Some(Err(AddressResolutionFailed)));
}
}
#[ip_test]
#[test_case(InitialState::Incomplete, false; "incomplete neighbor")]
#[test_case(InitialState::Reachable, true; "reachable neighbor")]
#[test_case(InitialState::Stale, true; "stale neighbor")]
#[test_case(InitialState::Delay, true; "delay neighbor")]
#[test_case(InitialState::Probe, true; "probe neighbor")]
#[test_case(InitialState::Unreachable, true; "unreachable neighbor")]
fn upper_layer_confirmation<I: Ip + TestIpExt>(
initial_state: InitialState,
should_transition_to_reachable: bool,
) {
let FakeCtxWithCoreCtx { mut core_ctx, mut bindings_ctx } = new_context::<I>();
let base_reachable_time = core_ctx.inner.base_reachable_time().get();
let initial = init_neighbor_in_state(&mut core_ctx, &mut bindings_ctx, initial_state);
confirm_reachable(&mut core_ctx, &mut bindings_ctx, &FakeLinkDeviceId, I::LOOKUP_ADDR1);
if !should_transition_to_reachable {
assert_neighbor_state(&core_ctx, &mut bindings_ctx, initial, None);
return;
}
// Neighbor should have transitioned to REACHABLE and scheduled a timer.
let now = bindings_ctx.now();
assert_neighbor_state(
&core_ctx,
&mut bindings_ctx,
DynamicNeighborState::Reachable(Reachable {
link_address: LINK_ADDR1,
last_confirmed_at: now,
}),
(initial_state != InitialState::Reachable).then_some(ExpectedEvent::Changed),
);
core_ctx.outer.nud.timer_heap.neighbor.assert_timers_after(
&mut bindings_ctx,
[(I::LOOKUP_ADDR1, NudEvent::ReachableTime, base_reachable_time)],
);
// Advance the clock by less than ReachableTime and confirm reachability again.
// The existing timer should not have been rescheduled; only the entry's
// `last_confirmed_at` timestamp should have been updated.
bindings_ctx.timer_ctx_mut().instant.sleep(base_reachable_time / 2);
confirm_reachable(&mut core_ctx, &mut bindings_ctx, &FakeLinkDeviceId, I::LOOKUP_ADDR1);
let now = bindings_ctx.now();
assert_neighbor_state(
&core_ctx,
&mut bindings_ctx,
DynamicNeighborState::Reachable(Reachable {
link_address: LINK_ADDR1,
last_confirmed_at: now,
}),
None,
);
core_ctx.outer.nud.timer_heap.neighbor.assert_timers_after(
&mut bindings_ctx,
[(I::LOOKUP_ADDR1, NudEvent::ReachableTime, base_reachable_time / 2)],
);
// When the original timer eventually does expire, a new timer should be
// scheduled based on when the entry was last confirmed.
assert_eq!(
bindings_ctx.trigger_timers_for(base_reachable_time / 2, &mut core_ctx,),
[NudTimerId::neighbor()]
);
let now = bindings_ctx.now();
assert_neighbor_state(
&core_ctx,
&mut bindings_ctx,
DynamicNeighborState::Reachable(Reachable {
link_address: LINK_ADDR1,
last_confirmed_at: now - base_reachable_time / 2,
}),
None,
);
core_ctx.outer.nud.timer_heap.neighbor.assert_timers_after(
&mut bindings_ctx,
[(I::LOOKUP_ADDR1, NudEvent::ReachableTime, base_reachable_time / 2)],
);
// When *that* timer fires, if the entry has not been confirmed since it was
// scheduled, it should move into STALE.
assert_eq!(
bindings_ctx.trigger_timers_for(base_reachable_time / 2, &mut core_ctx,),
[NudTimerId::neighbor()]
);
assert_neighbor_state(
&core_ctx,
&mut bindings_ctx,
DynamicNeighborState::Stale(Stale { link_address: LINK_ADDR1 }),
Some(ExpectedEvent::Changed),
);
bindings_ctx.timer_ctx().assert_no_timers_installed();
}
fn generate_ip_addr<I: Ip>(i: usize) -> SpecifiedAddr<I::Addr> {
I::map_ip(
IpInvariant(i),
|IpInvariant(i)| {
let start = u32::from_be_bytes(net_ip_v4!("192.168.0.1").ipv4_bytes());
let bytes = (start + u32::try_from(i).unwrap()).to_be_bytes();
SpecifiedAddr::new(Ipv4Addr::new(bytes)).unwrap()
},
|IpInvariant(i)| {
let start = u128::from_be_bytes(net_ip_v6!("fe80::1").ipv6_bytes());
let bytes = (start + u128::try_from(i).unwrap()).to_be_bytes();
SpecifiedAddr::new(Ipv6Addr::from_bytes(bytes)).unwrap()
},
)
}
#[ip_test]
fn garbage_collection_retains_static_entries<I: Ip + TestIpExt>() {
let FakeCtxWithCoreCtx { mut core_ctx, mut bindings_ctx } = new_context::<I>();
// Add `MAX_ENTRIES` STALE dynamic neighbors and `MAX_ENTRIES` static
// neighbors to the neighbor table, interleaved to avoid accidental
// behavior re: insertion order.
for i in 0..MAX_ENTRIES * 2 {
if i % 2 == 0 {
init_stale_neighbor_with_ip(
&mut core_ctx,
&mut bindings_ctx,
generate_ip_addr::<I>(i),
LINK_ADDR1,
);
} else {
init_static_neighbor_with_ip(
&mut core_ctx,
&mut bindings_ctx,
generate_ip_addr::<I>(i),
LINK_ADDR1,
ExpectedEvent::Added,
);
}
}
assert_eq!(core_ctx.outer.nud.neighbors.len(), MAX_ENTRIES * 2);
// Perform GC, and ensure that only the dynamic entries are discarded.
collect_garbage(&mut core_ctx, &mut bindings_ctx, FakeLinkDeviceId);
for event in bindings_ctx.take_events() {
assert_matches!(event, Event {
device,
addr: _,
kind,
at,
} => {
assert_eq!(kind, EventKind::Removed);
assert_eq!(device, FakeLinkDeviceId);
assert_eq!(at, bindings_ctx.now());
});
}
assert_eq!(core_ctx.outer.nud.neighbors.len(), MAX_ENTRIES);
for (_, neighbor) in core_ctx.outer.nud.neighbors {
assert_matches!(neighbor, NeighborState::Static(_));
}
}
#[ip_test]
fn garbage_collection_retains_in_use_entries<I: Ip + TestIpExt>() {
let FakeCtxWithCoreCtx { mut core_ctx, mut bindings_ctx } = new_context::<I>();
// Add enough static entries that the NUD table is near maximum capacity.
for i in 0..MAX_ENTRIES - 1 {
init_static_neighbor_with_ip(
&mut core_ctx,
&mut bindings_ctx,
generate_ip_addr::<I>(i),
LINK_ADDR1,
ExpectedEvent::Added,
);
}
// Add a STALE entry...
let stale_entry = generate_ip_addr::<I>(MAX_ENTRIES - 1);
init_stale_neighbor_with_ip(&mut core_ctx, &mut bindings_ctx, stale_entry, LINK_ADDR1);
// ...and a REACHABLE entry.
let reachable_entry = generate_ip_addr::<I>(MAX_ENTRIES);
init_reachable_neighbor_with_ip(
&mut core_ctx,
&mut bindings_ctx,
reachable_entry,
LINK_ADDR1,
);
// Perform GC, and ensure that the REACHABLE entry was retained.
collect_garbage(&mut core_ctx, &mut bindings_ctx, FakeLinkDeviceId);
super::testutil::assert_dynamic_neighbor_state(
&mut core_ctx,
FakeLinkDeviceId,
reachable_entry,
DynamicNeighborState::Reachable(Reachable {
link_address: LINK_ADDR1,
last_confirmed_at: bindings_ctx.now(),
}),
);
assert_neighbor_removed_with_ip(&mut core_ctx, &mut bindings_ctx, stale_entry);
}
#[ip_test]
fn garbage_collection_triggered_on_new_stale_entry<I: Ip + TestIpExt>() {
let FakeCtxWithCoreCtx { mut core_ctx, mut bindings_ctx } = new_context::<I>();
// Pretend we just ran GC so the next pass will be scheduled after a delay.
core_ctx.outer.nud.last_gc = Some(bindings_ctx.now());
// Fill the neighbor table to maximum capacity with static entries.
for i in 0..MAX_ENTRIES {
init_static_neighbor_with_ip(
&mut core_ctx,
&mut bindings_ctx,
generate_ip_addr::<I>(i),
LINK_ADDR1,
ExpectedEvent::Added,
);
}
// Add a STALE neighbor entry to the table, which should trigger a GC run
// because it pushes the size of the table over the max.
init_stale_neighbor_with_ip(
&mut core_ctx,
&mut bindings_ctx,
generate_ip_addr::<I>(MAX_ENTRIES + 1),
LINK_ADDR1,
);
let expected_gc_time = bindings_ctx.now() + MIN_GARBAGE_COLLECTION_INTERVAL.get();
bindings_ctx
.timer_ctx()
.assert_some_timers_installed([(NudTimerId::garbage_collection(), expected_gc_time)]);
// Advance the clock by less than the GC interval and add another STALE entry to
// trigger GC again. The existing GC timer should not have been rescheduled
// given a GC pass is already pending.
bindings_ctx.timer_ctx_mut().instant.sleep(ONE_SECOND.get());
init_stale_neighbor_with_ip(
&mut core_ctx,
&mut bindings_ctx,
generate_ip_addr::<I>(MAX_ENTRIES + 2),
LINK_ADDR1,
);
bindings_ctx
.timer_ctx()
.assert_some_timers_installed([(NudTimerId::garbage_collection(), expected_gc_time)]);
}
#[ip_test]
fn garbage_collection_triggered_on_transition_to_unreachable<I: Ip + TestIpExt>() {
let FakeCtxWithCoreCtx { mut core_ctx, mut bindings_ctx } = new_context::<I>();
// Pretend we just ran GC so the next pass will be scheduled after a delay.
core_ctx.outer.nud.last_gc = Some(bindings_ctx.now());
// Fill the neighbor table to maximum capacity.
for i in 0..MAX_ENTRIES {
init_static_neighbor_with_ip(
&mut core_ctx,
&mut bindings_ctx,
generate_ip_addr::<I>(i),
LINK_ADDR1,
ExpectedEvent::Added,
);
}
assert_eq!(core_ctx.outer.nud.neighbors.len(), MAX_ENTRIES);
// Add a dynamic neighbor entry to the table and transition it to the
// UNREACHABLE state. This should trigger a GC run.
init_unreachable_neighbor_with_ip(
&mut core_ctx,
&mut bindings_ctx,
generate_ip_addr::<I>(MAX_ENTRIES),
LINK_ADDR1,
);
let expected_gc_time =
core_ctx.outer.nud.last_gc.unwrap() + MIN_GARBAGE_COLLECTION_INTERVAL.get();
bindings_ctx
.timer_ctx()
.assert_some_timers_installed([(NudTimerId::garbage_collection(), expected_gc_time)]);
// Add a new entry and transition it to UNREACHABLE. The existing GC timer
// should not have been rescheduled given a GC pass is already pending.
init_unreachable_neighbor_with_ip(
&mut core_ctx,
&mut bindings_ctx,
generate_ip_addr::<I>(MAX_ENTRIES + 1),
LINK_ADDR1,
);
bindings_ctx
.timer_ctx()
.assert_some_timers_installed([(NudTimerId::garbage_collection(), expected_gc_time)]);
}
#[ip_test]
fn garbage_collection_not_triggered_on_new_incomplete_entry<I: Ip + TestIpExt>() {
let FakeCtxWithCoreCtx { mut core_ctx, mut bindings_ctx } = new_context::<I>();
// Fill the neighbor table to maximum capacity with static entries.
for i in 0..MAX_ENTRIES {
init_static_neighbor_with_ip(
&mut core_ctx,
&mut bindings_ctx,
generate_ip_addr::<I>(i),
LINK_ADDR1,
ExpectedEvent::Added,
);
}
assert_eq!(core_ctx.outer.nud.neighbors.len(), MAX_ENTRIES);
let _: VecDeque<Buf<Vec<u8>>> = init_incomplete_neighbor_with_ip(
&mut core_ctx,
&mut bindings_ctx,
generate_ip_addr::<I>(MAX_ENTRIES),
true,
);
assert_eq!(
bindings_ctx.timer_ctx().scheduled_instant2(&mut core_ctx.outer.nud.timer_heap.gc),
None
);
}
#[test]
fn router_advertisement_with_source_link_layer_option_should_add_neighbor() {
let FakeEventDispatcherConfig {
local_mac,
remote_mac,
local_ip: _,
remote_ip: _,
subnet: _,
} = Ipv6::FAKE_CONFIG;
let mut ctx = testutil::FakeCtx::default();
let device_id = ctx
.core_api()
.device::<EthernetLinkDevice>()
.add_device_with_default_state(
EthernetCreationProperties {
mac: local_mac,
max_frame_size: IPV6_MIN_IMPLIED_MAX_FRAME_SIZE,
},
DEFAULT_INTERFACE_METRIC,
)
.into();
set_ip_device_enabled::<Ipv6>(&mut ctx, &device_id, true, false);
let remote_mac_bytes = remote_mac.bytes();
let options = vec![NdpOptionBuilder::SourceLinkLayerAddress(&remote_mac_bytes[..])];
let src_ip = remote_mac.to_ipv6_link_local().addr();
let dst_ip = Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS.get();
let ra_packet_buf = |options: &[NdpOptionBuilder<'_>]| {
OptionSequenceBuilder::new(options.iter())
.into_serializer()
.encapsulate(IcmpPacketBuilder::<Ipv6, _>::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()
};
// First receive a Router Advertisement without the source link layer
// and make sure no new neighbor gets added.
ctx.test_api().receive_ip_packet::<Ipv6, _>(
&device_id,
Some(FrameDestination::Multicast),
ra_packet_buf(&[][..]),
);
let link_device_id = device_id.clone().try_into().unwrap();
assert_neighbors::<Ipv6, _, _>(&mut ctx.core_ctx(), &link_device_id, Default::default());
// RA with a source link layer option should create a new entry.
ctx.test_api().receive_ip_packet::<Ipv6, _>(
&device_id,
Some(FrameDestination::Multicast),
ra_packet_buf(&options[..]),
);
assert_neighbors::<Ipv6, _, _>(
&mut ctx.core_ctx(),
&link_device_id,
HashMap::from([(
{
let src_ip: UnicastAddr<_> = src_ip.into_addr();
src_ip.into_specified()
},
NeighborState::Dynamic(DynamicNeighborState::Stale(Stale {
link_address: remote_mac.get(),
})),
)]),
);
}
const LOCAL_IP: Ipv6Addr = net_ip_v6!("fe80::1");
const OTHER_IP: Ipv6Addr = net_ip_v6!("fe80::2");
const MULTICAST_IP: Ipv6Addr = net_ip_v6!("ff02::1234");
#[test_case(LOCAL_IP, None, true; "targeting assigned address")]
#[test_case(LOCAL_IP, NonZeroU16::new(1), false; "targeting tentative address")]
#[test_case(OTHER_IP, None, false; "targeting other host")]
#[test_case(MULTICAST_IP, None, false; "targeting multicast address")]
fn ns_response(target_addr: Ipv6Addr, dad_transmits: Option<NonZeroU16>, expect_handle: bool) {
let FakeEventDispatcherConfig {
local_mac,
remote_mac,
local_ip: _,
remote_ip: _,
subnet: _,
} = Ipv6::FAKE_CONFIG;
let mut ctx = testutil::FakeCtx::default();
let link_device_id =
ctx.core_api().device::<EthernetLinkDevice>().add_device_with_default_state(
EthernetCreationProperties {
mac: local_mac,
max_frame_size: IPV6_MIN_IMPLIED_MAX_FRAME_SIZE,
},
DEFAULT_INTERFACE_METRIC,
);
let device_id = link_device_id.clone().into();
set_ip_device_enabled::<Ipv6>(&mut ctx, &device_id, true, false);
// Set DAD config after enabling the device so that the default address
// does not perform DAD.
let _: Ipv6DeviceConfigurationUpdate = ctx
.core_api()
.device_ip::<Ipv6>()
.update_configuration(
&device_id,
Ipv6DeviceConfigurationUpdate {
dad_transmits: Some(dad_transmits),
..Default::default()
},
)
.unwrap();
ctx.core_api()
.device_ip::<Ipv6>()
.add_ip_addr_subnet(&device_id, AddrSubnet::new(LOCAL_IP, Ipv6Addr::BYTES * 8).unwrap())
.unwrap();
if let Some(NonZeroU16 { .. }) = dad_transmits {
// Take DAD message.
assert_matches!(
&ctx.bindings_ctx.take_ethernet_frames()[..],
[(got_device_id, got_frame)] => {
assert_eq!(got_device_id, &link_device_id);
let (src_mac, dst_mac, got_src_ip, got_dst_ip, ttl, message, code) =
parse_icmp_packet_in_ip_packet_in_ethernet_frame::<
Ipv6,
_,
NeighborSolicitation,
_,
>(got_frame, EthernetFrameLengthCheck::NoCheck, |_| {})
.unwrap();
let dst_ip = LOCAL_IP.to_solicited_node_address();
assert_eq!(src_mac, local_mac.get());
assert_eq!(dst_mac, dst_ip.into());
assert_eq!(got_src_ip, Ipv6::UNSPECIFIED_ADDRESS);
assert_eq!(got_dst_ip, dst_ip.get());
assert_eq!(ttl, REQUIRED_NDP_IP_PACKET_HOP_LIMIT);
assert_eq!(message.target_address(), &LOCAL_IP);
assert_eq!(code, IcmpUnusedCode);
}
);
}
// Send a neighbor solicitation with the test target address to the
// host.
let src_ip = remote_mac.to_ipv6_link_local().addr();
let snmc = target_addr.to_solicited_node_address();
let dst_ip = snmc.get();
ctx.test_api().receive_ip_packet::<Ipv6, _>(
&device_id,
Some(FrameDestination::Multicast),
neighbor_solicitation_ip_packet(**src_ip, dst_ip, target_addr, *remote_mac),
);
// Check if a neighbor advertisement was sent as a response and the
// new state of the neighbor table.
let expected_neighbors = if expect_handle {
assert_matches!(
&ctx.bindings_ctx.take_ethernet_frames()[..],
[(got_device_id, got_frame)] => {
assert_eq!(got_device_id, &link_device_id);
let (src_mac, dst_mac, got_src_ip, got_dst_ip, ttl, message, code) =
parse_icmp_packet_in_ip_packet_in_ethernet_frame::<
Ipv6,
_,
NeighborAdvertisement,
_,
>(got_frame, EthernetFrameLengthCheck::NoCheck, |_| {})
.unwrap();
assert_eq!(src_mac, local_mac.get());
assert_eq!(dst_mac, remote_mac.get());
assert_eq!(got_src_ip, target_addr);
assert_eq!(got_dst_ip, src_ip.into_addr());
assert_eq!(ttl, REQUIRED_NDP_IP_PACKET_HOP_LIMIT);
assert_eq!(message.target_address(), &target_addr);
assert_eq!(code, IcmpUnusedCode);
}
);
HashMap::from([(
{
let src_ip: UnicastAddr<_> = src_ip.into_addr();
src_ip.into_specified()
},
// TODO(https://fxbug.dev/42081683): expect STALE instead once we correctly do not
// go through NUD to send NDP packets.
NeighborState::Dynamic(DynamicNeighborState::Delay(Delay {
link_address: remote_mac.get(),
})),
)])
} else {
assert_matches!(&ctx.bindings_ctx.take_ethernet_frames()[..], []);
HashMap::default()
};
assert_neighbors::<Ipv6, _, _>(&mut ctx.core_ctx(), &link_device_id, expected_neighbors);
// Remove device to clear all dangling references.
core::mem::drop(device_id);
ctx.core_api().device().remove_device(link_device_id).into_removed();
}
#[test]
fn ipv6_integration() {
let FakeEventDispatcherConfig {
local_mac,
remote_mac,
local_ip: _,
remote_ip: _,
subnet: _,
} = Ipv6::FAKE_CONFIG;
let mut ctx = testutil::FakeCtx::default();
let eth_device_id =
ctx.core_api().device::<EthernetLinkDevice>().add_device_with_default_state(
EthernetCreationProperties {
mac: local_mac,
max_frame_size: IPV6_MIN_IMPLIED_MAX_FRAME_SIZE,
},
DEFAULT_INTERFACE_METRIC,
);
let device_id = eth_device_id.clone().into();
// Configure the device to generate a link-local address.
let _: Ipv6DeviceConfigurationUpdate = ctx
.core_api()
.device_ip::<Ipv6>()
.update_configuration(
&device_id,
Ipv6DeviceConfigurationUpdate {
slaac_config: Some(SlaacConfiguration {
enable_stable_addresses: true,
..Default::default()
}),
..Default::default()
},
)
.unwrap();
set_ip_device_enabled::<Ipv6>(&mut ctx, &device_id, true, false);
let neighbor_ip = remote_mac.to_ipv6_link_local().addr();
let neighbor_ip: UnicastAddr<_> = neighbor_ip.into_addr();
let dst_ip = Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS.get();
let na_packet_buf = |solicited_flag, override_flag| {
neighbor_advertisement_ip_packet(
*neighbor_ip,
dst_ip,
false, /* router_flag */
solicited_flag,
override_flag,
*remote_mac,
)
};
// NeighborAdvertisements should not create a new entry even if
// the advertisement has both the solicited and override flag set.
ctx.test_api().receive_ip_packet::<Ipv6, _>(
&device_id,
Some(FrameDestination::Multicast),
na_packet_buf(false, false),
);
let link_device_id = device_id.clone().try_into().unwrap();
assert_neighbors::<Ipv6, _, _>(&mut ctx.core_ctx(), &link_device_id, Default::default());
ctx.test_api().receive_ip_packet::<Ipv6, _>(
&device_id,
Some(FrameDestination::Multicast),
na_packet_buf(true, true),
);
assert_neighbors::<Ipv6, _, _>(&mut ctx.core_ctx(), &link_device_id, Default::default());
let testutil::Ctx { core_ctx, bindings_ctx } = &mut ctx;
assert_eq!(bindings_ctx.take_ethernet_frames(), []);
// Trigger a neighbor solicitation to be sent.
let body = [u8::MAX];
let pending_frames = VecDeque::from([Buf::new(body.to_vec(), ..)]);
assert_matches!(
NudHandler::<Ipv6, EthernetLinkDevice, _>::send_ip_packet_to_neighbor(
&mut core_ctx.context(),
bindings_ctx,
&eth_device_id,
neighbor_ip.into_specified(),
Buf::new(body, ..),
),
Ok(())
);
assert_matches!(
&bindings_ctx.take_ethernet_frames()[..],
[(got_device_id, got_frame)] => {
assert_eq!(got_device_id, &eth_device_id);
let (src_mac, dst_mac, got_src_ip, got_dst_ip, ttl, message, code) = parse_icmp_packet_in_ip_packet_in_ethernet_frame::<
Ipv6,
_,
NeighborSolicitation,
_,
>(got_frame, EthernetFrameLengthCheck::NoCheck, |_| {})
.unwrap();
let target = neighbor_ip;
let snmc = target.to_solicited_node_address();
assert_eq!(src_mac, local_mac.get());
assert_eq!(dst_mac, snmc.into());
assert_eq!(got_src_ip, local_mac.to_ipv6_link_local().addr().into());
assert_eq!(got_dst_ip, snmc.get());
assert_eq!(ttl, 255);
assert_eq!(message.target_address(), &target.get());
assert_eq!(code, IcmpUnusedCode);
}
);
let max_multicast_solicit = NudContext::<Ipv6, EthernetLinkDevice, _>::with_nud_state_mut(
&mut core_ctx.context(),
&link_device_id,
|_, nud_config| {
// NB: Because we're using the real core context here and it
// implements NudConfigContext for both Ipv4 and Ipv6 we need to
// nudge the compiler to the IPv6 implementation.
NudConfigContext::<Ipv6>::max_multicast_solicit(nud_config).get()
},
);
assert_neighbors::<Ipv6, _, _>(
&mut ctx.core_ctx(),
&link_device_id,
HashMap::from([(
neighbor_ip.into_specified(),
NeighborState::Dynamic(DynamicNeighborState::Incomplete(Incomplete {
transmit_counter: NonZeroU16::new(max_multicast_solicit - 1),
pending_frames,
notifiers: Vec::new(),
_marker: PhantomData,
})),
)]),
);
// A Neighbor advertisement should now update the entry.
ctx.test_api().receive_ip_packet::<Ipv6, _>(
&device_id,
Some(FrameDestination::Multicast),
na_packet_buf(true, true),
);
assert_neighbors::<Ipv6, _, _>(
&mut ctx.core_ctx(),
&link_device_id,
HashMap::from([(
neighbor_ip.into_specified(),
NeighborState::Dynamic(DynamicNeighborState::Reachable(Reachable {
link_address: remote_mac.get(),
last_confirmed_at: ctx.bindings_ctx.now(),
})),
)]),
);
let frames = ctx.bindings_ctx.take_ethernet_frames();
let (got_device_id, got_frame) = assert_matches!(&frames[..], [x] => x);
assert_eq!(got_device_id, &eth_device_id);
let (payload, src_mac, dst_mac, ether_type) =
parse_ethernet_frame(got_frame, EthernetFrameLengthCheck::NoCheck).unwrap();
assert_eq!(src_mac, local_mac.get());
assert_eq!(dst_mac, remote_mac.get());
assert_eq!(ether_type, Some(EtherType::Ipv6));
assert_eq!(payload, body);
// Disabling the device should clear the neighbor table.
set_ip_device_enabled::<Ipv6>(&mut ctx, &device_id, false, true);
assert_neighbors::<Ipv6, _, _>(&mut ctx.core_ctx(), &link_device_id, HashMap::new());
ctx.bindings_ctx.timer_ctx().assert_no_timers_installed();
}
type FakeNudNetwork<L> = FakeNetwork<&'static str, testutil::Ctx<testutil::FakeBindingsCtx>, L>;
fn new_test_net<I: Ip + testutil::TestIpExt>() -> (
FakeNudNetwork<
impl FakeNetworkLinks<
DispatchedFrame,
EthernetDeviceId<testutil::FakeBindingsCtx>,
&'static str,
>,
>,
EthernetDeviceId<testutil::FakeBindingsCtx>,
EthernetDeviceId<testutil::FakeBindingsCtx>,
) {
let build_ctx = |config: FakeEventDispatcherConfig<I::Addr>| {
let mut builder = testutil::FakeEventDispatcherBuilder::default();
let device =
builder.add_device_with_ip(config.local_mac, config.local_ip.get(), config.subnet);
let (ctx, device_ids) = builder.build();
(ctx, device_ids[device].clone())
};
let (local, local_device) = build_ctx(I::FAKE_CONFIG);
let (remote, remote_device) = build_ctx(I::FAKE_CONFIG.swap());
let net = crate::context::testutil::new_simple_fake_network(
"local",
local,
local_device.downgrade(),
"remote",
remote,
remote_device.downgrade(),
);
(net, local_device, remote_device)
}
#[netstack3_macros::context_ip_bounds(I, testutil::FakeBindingsCtx, crate)]
fn bind_and_connect_sockets<
I: testutil::TestIpExt + crate::IpExt,
L: FakeNetworkLinks<
DispatchedFrame,
EthernetDeviceId<testutil::FakeBindingsCtx>,
&'static str,
>,
>(
net: &mut FakeNudNetwork<L>,
local_buffers: tcp::buffer::testutil::ProvidedBuffers,
) -> tcp::socket::TcpSocketId<
I,
WeakDeviceId<testutil::FakeBindingsCtx>,
testutil::FakeBindingsCtx,
> {
const REMOTE_PORT: NonZeroU16 = const_unwrap::const_unwrap_option(NonZeroU16::new(33333));
net.with_context("remote", |ctx| {
let mut tcp_api = ctx.core_api().tcp::<I>();
let socket = tcp_api.create(tcp::buffer::testutil::ProvidedBuffers::default());
tcp_api
.bind(
&socket,
Some(net_types::ZonedAddr::Unzoned(I::FAKE_CONFIG.remote_ip)),
Some(REMOTE_PORT),
)
.unwrap();
tcp_api.listen(&socket, NonZeroUsize::new(1).unwrap()).unwrap();
});
net.with_context("local", |ctx| {
let mut tcp_api = ctx.core_api().tcp::<I>();
let socket = tcp_api.create(local_buffers);
tcp_api
.connect(
&socket,
Some(net_types::ZonedAddr::Unzoned(I::FAKE_CONFIG.remote_ip)),
REMOTE_PORT,
)
.unwrap();
socket
})
}
#[ip_test]
#[netstack3_macros::context_ip_bounds(I, testutil::FakeBindingsCtx, crate)]
fn upper_layer_confirmation_tcp_handshake<I: Ip + testutil::TestIpExt + crate::IpExt>()
where
for<'a> UnlockedCoreCtx<'a, testutil::FakeBindingsCtx>: DeviceIdContext<
EthernetLinkDevice,
DeviceId = EthernetDeviceId<testutil::FakeBindingsCtx>,
> + NudContext<I, EthernetLinkDevice, testutil::FakeBindingsCtx>,
{
let (mut net, local_device, remote_device) = new_test_net::<I>();
let FakeEventDispatcherConfig { local_ip, local_mac, remote_mac, remote_ip, .. } =
I::FAKE_CONFIG;
// Insert a STALE neighbor in each node's neighbor table so that they don't
// initiate neighbor resolution before performing the TCP handshake.
for (ctx, device, neighbor, link_addr) in [
("local", local_device.clone(), remote_ip, remote_mac),
("remote", remote_device.clone(), local_ip, local_mac),
] {
net.with_context(ctx, |testutil::FakeCtx { core_ctx, bindings_ctx }| {
NudHandler::handle_neighbor_update(
&mut core_ctx.context(),
bindings_ctx,
&device,
neighbor,
link_addr.get(),
DynamicNeighborUpdateSource::Probe,
);
super::testutil::assert_dynamic_neighbor_state(
&mut core_ctx.context(),
device.clone(),
neighbor,
DynamicNeighborState::Stale(Stale { link_address: link_addr.get() }),
);
});
}
// Initiate a TCP connection and make sure the SYN and resulting SYN/ACK are
// received by each context.
let _: tcp::socket::TcpSocketId<I, _, _> = bind_and_connect_sockets::<I, _>(
&mut net,
tcp::buffer::testutil::ProvidedBuffers::default(),
);
for _ in 0..2 {
assert_eq!(net.step().frames_sent, 1);
}
// The three-way handshake should now be complete, and the neighbor should have
// transitioned to REACHABLE.
net.with_context("local", |testutil::FakeCtx { core_ctx, bindings_ctx }| {
super::testutil::assert_dynamic_neighbor_state(
&mut core_ctx.context(),
local_device.clone(),
remote_ip,
DynamicNeighborState::Reachable(Reachable {
link_address: remote_mac.get(),
last_confirmed_at: bindings_ctx.now(),
}),
);
});
// Remove the devices so that existing NUD timers get cleaned up;
// otherwise, they would hold dangling references to the devices when
// the `StackState`s are dropped at the end of the test.
for (ctx, device) in [("local", local_device), ("remote", remote_device)] {
net.with_context(ctx, |ctx| {
ctx.test_api().clear_routes_and_remove_ethernet_device(device);
});
}
}
#[ip_test]
#[netstack3_macros::context_ip_bounds(I, testutil::FakeBindingsCtx, crate)]
fn upper_layer_confirmation_tcp_ack<I: Ip + testutil::TestIpExt + crate::IpExt>()
where
for<'a> UnlockedCoreCtx<'a, testutil::FakeBindingsCtx>: DeviceIdContext<
EthernetLinkDevice,
DeviceId = EthernetDeviceId<testutil::FakeBindingsCtx>,
> + NudContext<I, EthernetLinkDevice, testutil::FakeBindingsCtx>,
{
let (mut net, local_device, remote_device) = new_test_net::<I>();
let FakeEventDispatcherConfig { remote_mac, remote_ip, .. } = I::FAKE_CONFIG;
// Initiate a TCP connection, allow the handshake to complete, and wait until
// the neighbor entry goes STALE due to lack of traffic on the connection.
let client_ends = tcp::buffer::testutil::WriteBackClientBuffers::default();
let local_socket = bind_and_connect_sockets::<I, _>(
&mut net,
tcp::buffer::testutil::ProvidedBuffers::Buffers(client_ends.clone()),
);
net.run_until_idle();
net.with_context("local", |testutil::FakeCtx { core_ctx, bindings_ctx: _ }| {
super::testutil::assert_dynamic_neighbor_state(
&mut core_ctx.context(),
local_device.clone(),
remote_ip,
DynamicNeighborState::Stale(Stale { link_address: remote_mac.get() }),
);
});
// Send some data on the local socket and wait for it to be ACKed by the peer.
let tcp::buffer::testutil::ClientBuffers { send, receive: _ } =
client_ends.0.as_ref().lock().take().unwrap();
send.lock().extend_from_slice(b"hello");
net.with_context("local", |ctx| {
ctx.core_api().tcp().do_send(&local_socket);
});
for _ in 0..2 {
assert_eq!(net.step().frames_sent, 1);
}
// The ACK should have been processed, and the neighbor should have transitioned
// to REACHABLE.
net.with_context("local", |testutil::FakeCtx { core_ctx, bindings_ctx }| {
super::testutil::assert_dynamic_neighbor_state(
&mut core_ctx.context(),
local_device.clone(),
remote_ip,
DynamicNeighborState::Reachable(Reachable {
link_address: remote_mac.get(),
last_confirmed_at: bindings_ctx.now(),
}),
);
});
// Remove the devices so that existing NUD timers get cleaned up;
// otherwise, they would hold dangling references to the devices when
// the `StackState`s are dropped at the end of the test.
for (ctx, device) in [("local", local_device), ("remote", remote_device)] {
net.with_context(ctx, |ctx| {
ctx.test_api().clear_routes_and_remove_ethernet_device(device);
});
}
}
#[ip_test]
#[netstack3_macros::context_ip_bounds(I, testutil::FakeBindingsCtx, crate)]
fn icmp_error_on_address_resolution_failure_tcp_local<
I: Ip + testutil::TestIpExt + crate::IpExt,
>() {
let mut builder = testutil::FakeEventDispatcherBuilder::default();
let _device_id = builder.add_device_with_ip(
I::FAKE_CONFIG.local_mac,
I::FAKE_CONFIG.local_ip.get(),
I::FAKE_CONFIG.subnet,
);
let (mut ctx, _): (_, Vec<EthernetDeviceId<_>>) = builder.build();
// Add a loopback interface because local delivery of the ICMP error
// relies on loopback.
const MTU: Mtu = Mtu::new(65536);
let device = ctx
.core_api()
.device::<LoopbackDevice>()
.add_device_with_default_state(
LoopbackCreationProperties { mtu: MTU },
DEFAULT_INTERFACE_METRIC,
)
.into();
crate::device::testutil::enable_device(&mut ctx, &device);
let mut tcp_api = ctx.core_api().tcp::<I>();
let socket = tcp_api.create(tcp::buffer::testutil::ProvidedBuffers::default());
const REMOTE_PORT: NonZeroU16 = const_unwrap::const_unwrap_option(NonZeroU16::new(33333));
tcp_api
.connect(
&socket,
Some(net_types::ZonedAddr::Unzoned(I::FAKE_CONFIG.remote_ip)),
REMOTE_PORT,
)
.unwrap();
while ctx.process_queues() || ctx.trigger_next_timer().is_some() {}
let mut tcp_api = ctx.core_api().tcp::<I>();
assert_eq!(
tcp_api.get_socket_error(&socket),
Some(crate::transport::tcp::ConnectionError::HostUnreachable),
);
}
#[ip_test]
#[netstack3_macros::context_ip_bounds(I, testutil::FakeBindingsCtx, crate)]
fn icmp_error_on_address_resolution_failure_tcp_forwarding<
I: Ip + testutil::TestIpExt + crate::IpExt,
>() {
let (mut net, local_device, remote_device) = new_test_net::<I>();
let FakeEventDispatcherConfig { remote_ip, .. } = I::FAKE_CONFIG;
// These default routes mean that later when local tries to connect to an
// address not in the subnet on the network, it will send the SYN to remote,
// and remote will attempt to forward to the address as a neighbor (and
// link address resolution will fail here).
for (ctx, device, gateway) in
[("local", local_device, Some(remote_ip)), ("remote", remote_device.clone(), None)]
{
net.with_context(ctx, |ctx| {
let (mut core_ctx, bindings_ctx) = ctx.contexts();
crate::ip::forwarding::testutil::add_route::<I, _, _>(
&mut core_ctx,
bindings_ctx,
AddableEntry {
subnet: Subnet::new(I::UNSPECIFIED_ADDRESS, 0).unwrap(),
device: device.into(),
gateway: gateway,
metric: AddableMetric::MetricTracksInterface,
},
)
.expect("add default route");
});
}
net.with_context("remote", |ctx| {
crate::device::testutil::set_forwarding_enabled::<_, I>(
ctx,
&remote_device.into(),
true,
);
});
let socket = net.with_context("local", |ctx| {
let mut tcp_api = ctx.core_api().tcp::<I>();
let socket = tcp_api.create(tcp::buffer::testutil::ProvidedBuffers::default());
const REMOTE_PORT: NonZeroU16 =
const_unwrap::const_unwrap_option(NonZeroU16::new(33333));
tcp_api
.connect(
&socket,
Some(net_types::ZonedAddr::Unzoned(I::get_other_remote_ip_address(1))),
REMOTE_PORT,
)
.unwrap();
socket
});
net.run_until_idle();
net.with_context("local", |ctx| {
let mut tcp_api = ctx.core_api().tcp::<I>();
assert_eq!(
tcp_api.get_socket_error(&socket),
Some(crate::transport::tcp::ConnectionError::HostUnreachable),
);
});
}
#[test_case(1; "non_initial_fragment")]
#[test_case(0; "initial_fragment")]
fn icmp_error_fragment_offset(fragment_offset: u16) {
let mut builder = testutil::FakeEventDispatcherBuilder::default();
let _device_id = builder.add_device_with_ip(
Ipv4::FAKE_CONFIG.local_mac,
Ipv4::FAKE_CONFIG.local_ip.get(),
Ipv4::FAKE_CONFIG.subnet,
);
let (mut ctx, mut device_ids) = builder.build();
let device_id = device_ids.pop().unwrap();
// Add a static neighbor entry for `FROM_ADDR` so that NUD trivially
// succeeds if an ICMP dest unreachable message destined for the address
// is generated.
const FROM_ADDR: SpecifiedAddr<Ipv4Addr> = Ipv4::FAKE_CONFIG.remote_ip;
ctx.core_api()
.neighbor::<Ipv4, _>()
.insert_static_entry(&device_id, FROM_ADDR.get(), Ipv4::FAKE_CONFIG.remote_mac.get())
.expect("add static NUD entry for FROM_ADDR");
crate::device::testutil::set_forwarding_enabled::<_, Ipv4>(
&mut ctx,
&device_id.clone().into(),
true,
);
// Receive an IPv4 packet with the per test-case fragment offset value.
let to = Ipv4::get_other_ip_address(254);
let mut ipv4_packet_builder = Ipv4PacketBuilder::new(
FROM_ADDR,
to,
255, /* ttl */
Ipv4Proto::Proto(IpProto::Udp),
);
ipv4_packet_builder.fragment_offset(fragment_offset);
let non_initial_fragment_packet_buf = packet::Buf::new(&mut [], ..)
.encapsulate(UdpPacketBuilder::new(
FROM_ADDR.get(),
to.get(),
None,
NonZeroU16::new(12345).unwrap(),
))
.encapsulate(ipv4_packet_builder)
.serialize_vec_outer()
.unwrap()
.unwrap_b();
ctx.test_api().receive_ip_packet::<Ipv4, _>(
&device_id.into(),
Some(FrameDestination::Individual { local: false }),
non_initial_fragment_packet_buf,
);
// Should only see ICMP dest unreachable for initial fragments, i.e.
// fragment offset equal to 0.
while ctx.process_queues() || ctx.trigger_next_timer().is_some() {}
ctx.with_fake_frame_ctx_mut(|ctx| {
let found = ctx.take_frames().drain(..).find_map(|(_meta, buf)| {
packet_formats::testutil::parse_icmp_packet_in_ip_packet_in_ethernet_frame::<
Ipv4,
_,
IcmpDestUnreachable,
_,
>(
&buf, packet_formats::ethernet::EthernetFrameLengthCheck::NoCheck, |_| ()
)
.ok()
});
if fragment_offset == 0 {
assert!(found.is_some());
} else {
assert_eq!(found, None);
}
})
}
}