| // Copyright 2018 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. |
| |
| use alloc::{collections::HashMap, vec::Vec}; |
| use core::{ |
| fmt::{Debug, Display}, |
| marker::PhantomData, |
| num::NonZeroU64, |
| }; |
| |
| use derivative::Derivative; |
| use lock_order::{lock::UnlockedAccess, wrap::prelude::*}; |
| use net_types::{ |
| ethernet::Mac, |
| ip::{Ip, IpVersion, Ipv4, Ipv6}, |
| BroadcastAddr, MulticastAddr, |
| }; |
| use packet::Buf; |
| |
| use crate::{ |
| context::{CounterContext, InstantContext, TimerBindingsTypes, TimerHandler}, |
| counters::Counter, |
| device::{ |
| arp::ArpCounters, |
| ethernet::{EthernetLinkDevice, EthernetTimerId}, |
| id::{ |
| BaseDeviceId, BasePrimaryDeviceId, DeviceId, EthernetDeviceId, EthernetPrimaryDeviceId, |
| EthernetWeakDeviceId, StrongId, WeakId, |
| }, |
| loopback::{LoopbackDeviceId, LoopbackPrimaryDeviceId}, |
| pure_ip::{PureIpDeviceId, PureIpPrimaryDeviceId}, |
| socket::{self, HeldSockets}, |
| state::{DeviceStateSpec, IpLinkDeviceStateInner}, |
| }, |
| filter::FilterBindingsTypes, |
| inspect::Inspectable, |
| ip::{ |
| device::{ |
| nud::{LinkResolutionContext, NudCounters}, |
| state::IpDeviceFlags, |
| IpDeviceIpExt, IpDeviceStateContext, |
| }, |
| forwarding::IpForwardingDeviceContext, |
| types::RawMetric, |
| }, |
| sync::RwLock, |
| BindingsContext, CoreCtx, Inspector, StackState, |
| }; |
| |
| /// A device. |
| /// |
| /// `Device` is used to identify a particular device implementation. It |
| /// is only intended to exist at the type level, never instantiated at runtime. |
| pub trait Device: 'static {} |
| |
| /// Marker type for a generic device. |
| pub enum AnyDevice {} |
| |
| impl Device for AnyDevice {} |
| |
| /// An execution context which provides device ID types type for various |
| /// netstack internals to share. |
| pub trait DeviceIdContext<D: Device> { |
| /// The type of device IDs. |
| type DeviceId: StrongId<Weak = Self::WeakDeviceId> + 'static; |
| |
| /// The type of weakly referenced device IDs. |
| type WeakDeviceId: WeakId<Strong = Self::DeviceId> + 'static; |
| } |
| |
| /// A marker trait tying [`DeviceIdContext`] implementations. |
| /// |
| /// To call into the IP layer, we need to be able to represent device |
| /// identifiers in the [`AnyDevice`] domain. This trait is a statement that a |
| /// [`DeviceIdContext`] in some domain `D` has its identifiers convertible into |
| /// the [`AnyDevice`] domain with `From` bounds. |
| /// |
| /// It is provided as a blanket implementation for [`DeviceIdContext`]s that |
| /// fulfill the conversion. |
| pub trait DeviceIdAnyCompatContext<D: Device>: |
| DeviceIdContext<D> |
| + DeviceIdContext<AnyDevice, DeviceId = Self::DeviceId_, WeakDeviceId = Self::WeakDeviceId_> |
| { |
| type DeviceId_: StrongId<Weak = Self::WeakDeviceId_> |
| + From<<Self as DeviceIdContext<D>>::DeviceId>; |
| type WeakDeviceId_: WeakId<Strong = Self::DeviceId_> |
| + From<<Self as DeviceIdContext<D>>::WeakDeviceId>; |
| } |
| |
| impl<CC, D> DeviceIdAnyCompatContext<D> for CC |
| where |
| D: Device, |
| CC: DeviceIdContext<D> + DeviceIdContext<AnyDevice>, |
| <CC as DeviceIdContext<AnyDevice>>::WeakDeviceId: |
| From<<CC as DeviceIdContext<D>>::WeakDeviceId>, |
| <CC as DeviceIdContext<AnyDevice>>::DeviceId: From<<CC as DeviceIdContext<D>>::DeviceId>, |
| { |
| type DeviceId_ = <CC as DeviceIdContext<AnyDevice>>::DeviceId; |
| type WeakDeviceId_ = <CC as DeviceIdContext<AnyDevice>>::WeakDeviceId; |
| } |
| |
| pub(super) struct RecvIpFrameMeta<D, I: Ip> { |
| /// The device on which the IP frame was received. |
| pub(super) device: D, |
| /// The link-layer destination address from the link-layer frame, if any. |
| /// `None` if the IP frame originated above the link-layer (e.g. pure IP |
| /// devices). |
| // NB: In the future, this field may also be `None` to represent link-layer |
| // protocols without destination addresses (i.e. PPP), but at the moment no |
| // such protocols are supported. |
| pub(super) frame_dst: Option<FrameDestination>, |
| pub(super) _marker: PhantomData<I>, |
| } |
| |
| impl<D, I: Ip> RecvIpFrameMeta<D, I> { |
| pub(super) fn new(device: D, frame_dst: Option<FrameDestination>) -> RecvIpFrameMeta<D, I> { |
| RecvIpFrameMeta { device, frame_dst, _marker: PhantomData } |
| } |
| } |
| |
| /// Iterator over devices. |
| /// |
| /// Implements `Iterator<Item=DeviceId<C>>` by pulling from provided loopback |
| /// and ethernet device ID iterators. This struct only exists as a named type |
| /// so it can be an associated type on impls of the [`IpDeviceContext`] trait. |
| pub struct DevicesIter<'s, BC: BindingsContext> { |
| pub(super) ethernet: |
| alloc::collections::hash_map::Values<'s, EthernetDeviceId<BC>, EthernetPrimaryDeviceId<BC>>, |
| pub(super) pure_ip: |
| alloc::collections::hash_map::Values<'s, PureIpDeviceId<BC>, PureIpPrimaryDeviceId<BC>>, |
| pub(super) loopback: core::option::Iter<'s, LoopbackPrimaryDeviceId<BC>>, |
| } |
| |
| impl<'s, BC: BindingsContext> Iterator for DevicesIter<'s, BC> { |
| type Item = DeviceId<BC>; |
| |
| fn next(&mut self) -> Option<Self::Item> { |
| let Self { ethernet, pure_ip, loopback } = self; |
| ethernet |
| .map(|primary| primary.clone_strong().into()) |
| .chain(pure_ip.map(|primary| primary.clone_strong().into())) |
| .chain(loopback.map(|primary| primary.clone_strong().into())) |
| .next() |
| } |
| } |
| |
| impl<I: IpDeviceIpExt, BC: BindingsContext, L> IpForwardingDeviceContext<I> for CoreCtx<'_, BC, L> |
| where |
| Self: IpDeviceStateContext<I, BC, DeviceId = DeviceId<BC>>, |
| { |
| fn get_routing_metric(&mut self, device_id: &Self::DeviceId) -> RawMetric { |
| crate::device::integration::with_ip_device_state(self, device_id, |state| { |
| *state.unlocked_access::<crate::lock_ordering::RoutingMetric>() |
| }) |
| } |
| |
| fn is_ip_device_enabled(&mut self, device_id: &Self::DeviceId) -> bool { |
| IpDeviceStateContext::<I, _>::with_ip_device_flags( |
| self, |
| device_id, |
| |IpDeviceFlags { ip_enabled }| *ip_enabled, |
| ) |
| } |
| } |
| |
| pub enum Ipv6DeviceLinkLayerAddr { |
| Mac(Mac), |
| // Add other link-layer address types as needed. |
| } |
| |
| impl AsRef<[u8]> for Ipv6DeviceLinkLayerAddr { |
| fn as_ref(&self) -> &[u8] { |
| match self { |
| Ipv6DeviceLinkLayerAddr::Mac(a) => a.as_ref(), |
| } |
| } |
| } |
| |
| /// The identifier for timer events in the device layer. |
| #[derive(Derivative)] |
| #[derivative( |
| Clone(bound = ""), |
| Eq(bound = ""), |
| PartialEq(bound = ""), |
| Hash(bound = ""), |
| Debug(bound = "") |
| )] |
| pub(crate) struct DeviceLayerTimerId<BT: DeviceLayerTypes>(DeviceLayerTimerIdInner<BT>); |
| |
| #[derive(Derivative)] |
| #[derivative( |
| Clone(bound = ""), |
| Eq(bound = ""), |
| PartialEq(bound = ""), |
| Hash(bound = ""), |
| Debug(bound = "") |
| )] |
| enum DeviceLayerTimerIdInner<BT: DeviceLayerTypes> { |
| /// A timer event for an Ethernet device. |
| Ethernet(EthernetTimerId<EthernetWeakDeviceId<BT>>), |
| } |
| |
| impl<BT: DeviceLayerTypes> From<EthernetTimerId<EthernetWeakDeviceId<BT>>> |
| for DeviceLayerTimerId<BT> |
| { |
| fn from(id: EthernetTimerId<EthernetWeakDeviceId<BT>>) -> DeviceLayerTimerId<BT> { |
| DeviceLayerTimerId(DeviceLayerTimerIdInner::Ethernet(id)) |
| } |
| } |
| |
| impl<CC, BT> TimerHandler<BT, DeviceLayerTimerId<BT>> for CC |
| where |
| BT: DeviceLayerTypes, |
| CC: TimerHandler<BT, EthernetTimerId<EthernetWeakDeviceId<BT>>>, |
| { |
| fn handle_timer( |
| &mut self, |
| bindings_ctx: &mut BT, |
| DeviceLayerTimerId(id): DeviceLayerTimerId<BT>, |
| ) { |
| match id { |
| DeviceLayerTimerIdInner::Ethernet(id) => self.handle_timer(bindings_ctx, id), |
| } |
| } |
| } |
| |
| /// The type of address used as the destination address in a device-layer frame. |
| /// |
| /// `FrameDestination` is used to implement RFC 1122 section 3.2.2 and RFC 4443 |
| /// section 2.4.e, which govern when to avoid sending an ICMP error message for |
| /// ICMP and ICMPv6 respectively. |
| #[derive(Copy, Clone, Debug, Eq, PartialEq)] |
| pub enum FrameDestination { |
| /// A unicast address - one which is neither multicast nor broadcast. |
| Individual { |
| /// Whether the frame's destination address belongs to the receiver. |
| local: bool, |
| }, |
| /// A multicast address; if the addressing scheme supports overlap between |
| /// multicast and broadcast, then broadcast addresses should use the |
| /// `Broadcast` variant. |
| Multicast, |
| /// A broadcast address; if the addressing scheme supports overlap between |
| /// multicast and broadcast, then broadcast addresses should use the |
| /// `Broadcast` variant. |
| Broadcast, |
| } |
| |
| impl FrameDestination { |
| /// Is this `FrameDestination::Broadcast`? |
| pub(crate) fn is_broadcast(self) -> bool { |
| self == FrameDestination::Broadcast |
| } |
| |
| pub(crate) fn from_dest(destination: Mac, local_mac: Mac) -> Self { |
| BroadcastAddr::new(destination) |
| .map(Into::into) |
| .or_else(|| MulticastAddr::new(destination).map(Into::into)) |
| .unwrap_or_else(|| FrameDestination::Individual { local: destination == local_mac }) |
| } |
| } |
| |
| impl From<BroadcastAddr<Mac>> for FrameDestination { |
| fn from(_value: BroadcastAddr<Mac>) -> Self { |
| Self::Broadcast |
| } |
| } |
| |
| impl From<MulticastAddr<Mac>> for FrameDestination { |
| fn from(_value: MulticastAddr<Mac>) -> Self { |
| Self::Multicast |
| } |
| } |
| |
| #[derive(Derivative)] |
| #[derivative(Default(bound = ""))] |
| pub struct Devices<BT: DeviceLayerTypes> { |
| pub(super) ethernet: HashMap<EthernetDeviceId<BT>, EthernetPrimaryDeviceId<BT>>, |
| pub(super) pure_ip: HashMap<PureIpDeviceId<BT>, PureIpPrimaryDeviceId<BT>>, |
| pub(super) loopback: Option<LoopbackPrimaryDeviceId<BT>>, |
| } |
| |
| /// The state associated with the device layer. |
| pub(crate) struct DeviceLayerState<BT: DeviceLayerTypes> { |
| pub(super) devices: RwLock<Devices<BT>>, |
| pub(super) origin: OriginTracker, |
| pub(super) shared_sockets: HeldSockets<BT>, |
| pub(super) counters: DeviceCounters, |
| pub(super) ethernet_counters: EthernetDeviceCounters, |
| pub(super) pure_ip_counters: PureIpDeviceCounters, |
| pub(super) nud_v4_counters: NudCounters<Ipv4>, |
| pub(super) nud_v6_counters: NudCounters<Ipv6>, |
| pub(super) arp_counters: ArpCounters, |
| } |
| |
| impl<BT: DeviceLayerTypes> DeviceLayerState<BT> { |
| pub(crate) fn counters(&self) -> &DeviceCounters { |
| &self.counters |
| } |
| |
| pub(crate) fn ethernet_counters(&self) -> &EthernetDeviceCounters { |
| &self.ethernet_counters |
| } |
| |
| pub(crate) fn pure_ip_counters(&self) -> &PureIpDeviceCounters { |
| &self.pure_ip_counters |
| } |
| |
| pub(crate) fn nud_counters<I: Ip>(&self) -> &NudCounters<I> { |
| I::map_ip((), |()| &self.nud_v4_counters, |()| &self.nud_v6_counters) |
| } |
| |
| pub(crate) fn arp_counters(&self) -> &ArpCounters { |
| &self.arp_counters |
| } |
| } |
| |
| /// Counters for ethernet devices. |
| #[derive(Default)] |
| pub struct EthernetDeviceCounters { |
| /// Count of incoming frames dropped because the destination address was for |
| /// another device. |
| pub recv_ethernet_other_dest: Counter, |
| /// Count of incoming frames dropped due to an unsupported ethertype. |
| pub recv_unsupported_ethertype: Counter, |
| /// Count of incoming frames dropped due to an empty ethertype. |
| pub recv_no_ethertype: Counter, |
| } |
| |
| impl Inspectable for EthernetDeviceCounters { |
| fn record<I: Inspector>(&self, inspector: &mut I) { |
| inspector.record_child("Ethernet", |inspector| { |
| crate::counters::inspect_ethernet_device_counters(inspector, self) |
| }) |
| } |
| } |
| |
| /// Counters for pure IP devices. |
| #[derive(Default)] |
| pub struct PureIpDeviceCounters {} |
| |
| impl Inspectable for PureIpDeviceCounters { |
| fn record<I: Inspector>(&self, _inspector: &mut I) {} |
| } |
| |
| /// Device layer counters. |
| #[derive(Default)] |
| pub struct DeviceCounters { |
| /// Count of outgoing frames which enter the device layer (but may or may |
| /// not have been dropped prior to reaching the wire). |
| pub send_total_frames: Counter, |
| /// Count of frames sent. |
| pub send_frame: Counter, |
| /// Count of frames that failed to send because of a full Tx queue. |
| pub send_queue_full: Counter, |
| /// Count of frames that failed to send because of a serialization error. |
| pub send_serialize_error: Counter, |
| /// Count of frames received. |
| pub recv_frame: Counter, |
| /// Count of incoming frames dropped due to a parsing error. |
| pub recv_parse_error: Counter, |
| /// Count of incoming frames containing an IPv4 packet delivered. |
| pub recv_ipv4_delivered: Counter, |
| /// Count of incoming frames containing an IPv6 packet delivered. |
| pub recv_ipv6_delivered: Counter, |
| /// Count of sent frames containing an IPv4 packet. |
| pub send_ipv4_frame: Counter, |
| /// Count of sent frames containing an IPv6 packet. |
| pub send_ipv6_frame: Counter, |
| /// Count of frames that failed to send because there was no Tx queue. |
| pub send_dropped_no_queue: Counter, |
| } |
| |
| impl Inspectable for DeviceCounters { |
| fn record<I: Inspector>(&self, inspector: &mut I) { |
| crate::counters::inspect_device_counters(inspector, self) |
| } |
| } |
| |
| impl<BC: BindingsContext> UnlockedAccess<crate::lock_ordering::DeviceCounters> for StackState<BC> { |
| type Data = DeviceCounters; |
| type Guard<'l> = &'l DeviceCounters where Self: 'l; |
| |
| fn access(&self) -> Self::Guard<'_> { |
| self.device_counters() |
| } |
| } |
| |
| impl<T, BC: BindingsContext> UnlockedAccess<crate::lock_ordering::DeviceCounters> |
| for IpLinkDeviceStateInner<T, BC> |
| { |
| type Data = DeviceCounters; |
| type Guard<'l> = &'l DeviceCounters where Self: 'l; |
| |
| fn access(&self) -> Self::Guard<'_> { |
| &self.counters |
| } |
| } |
| |
| impl<BC: BindingsContext, L> CounterContext<DeviceCounters> for CoreCtx<'_, BC, L> { |
| fn with_counters<O, F: FnOnce(&DeviceCounters) -> O>(&self, cb: F) -> O { |
| cb(self.unlocked_access::<crate::lock_ordering::DeviceCounters>()) |
| } |
| } |
| |
| /// Light-weight tracker for recording the source of some instance. |
| /// |
| /// This should be held as a field in a parent type that is cloned into each |
| /// child instance. Then, the origin of a child instance can be verified by |
| /// asserting equality against the parent's field. |
| /// |
| /// This is only enabled in debug builds; in non-debug builds, all |
| /// `OriginTracker` instances are identical so all operations are no-ops. |
| // TODO(https://fxbug.dev/320078167): Move this and OriginTrackerContext out of |
| // the device module and apply to more places. |
| #[derive(Clone, Debug, PartialEq)] |
| pub struct OriginTracker(#[cfg(debug_assertions)] u64); |
| |
| impl OriginTracker { |
| /// Creates a new `OriginTracker` that isn't derived from any other |
| /// instance. |
| /// |
| /// In debug builds, this creates a unique `OriginTracker` that won't be |
| /// equal to any instances except those cloned from it. In non-debug builds |
| /// all `OriginTracker` instances are identical. |
| #[cfg_attr(not(debug_assertions), inline)] |
| fn new() -> Self { |
| Self( |
| #[cfg(debug_assertions)] |
| { |
| static COUNTER: core::sync::atomic::AtomicU64 = |
| core::sync::atomic::AtomicU64::new(0); |
| COUNTER.fetch_add(1, core::sync::atomic::Ordering::Relaxed) |
| }, |
| ) |
| } |
| } |
| |
| /// A trait abstracting a context containing an [`OriginTracker`]. |
| /// |
| /// This allows API structs to extract origin from contexts when creating |
| /// resources. |
| pub trait OriginTrackerContext { |
| /// Gets the origin tracker for this context. |
| fn origin_tracker(&mut self) -> OriginTracker; |
| } |
| |
| /// A context providing facilities to store and remove primary device IDs. |
| /// |
| /// This allows the device layer APIs to be written generically on `D`. |
| pub trait DeviceCollectionContext<D: Device + DeviceStateSpec, BT: DeviceLayerTypes>: |
| DeviceIdContext<D> |
| { |
| /// Adds `device` to the device collection. |
| fn insert(&mut self, device: BasePrimaryDeviceId<D, BT>); |
| |
| /// Removes `device` from the collection, if it exists. |
| fn remove(&mut self, device: &BaseDeviceId<D, BT>) -> Option<BasePrimaryDeviceId<D, BT>>; |
| } |
| |
| /// Provides abstractions over the frame metadata received from bindings for |
| /// implementers of [`Device`]. |
| /// |
| /// This trait allows [`api::DeviceApi`] to provide a single entrypoint for |
| /// frames from bindings. |
| pub trait DeviceReceiveFrameSpec { |
| /// The frame metadata for ingress frames, where `D` is a device identifier. |
| type FrameMetadata<D>; |
| } |
| |
| impl<BC: DeviceLayerTypes + socket::DeviceSocketBindingsContext<DeviceId<BC>>> |
| DeviceLayerState<BC> |
| { |
| /// Creates a new [`DeviceLayerState`] instance. |
| pub(crate) fn new() -> Self { |
| Self { |
| devices: Default::default(), |
| origin: OriginTracker::new(), |
| shared_sockets: Default::default(), |
| counters: Default::default(), |
| ethernet_counters: EthernetDeviceCounters::default(), |
| pure_ip_counters: PureIpDeviceCounters::default(), |
| nud_v4_counters: Default::default(), |
| nud_v6_counters: Default::default(), |
| arp_counters: Default::default(), |
| } |
| } |
| } |
| |
| /// Provides associated types used in the device layer. |
| pub trait DeviceLayerStateTypes: InstantContext + FilterBindingsTypes { |
| /// The state associated with loopback devices. |
| type LoopbackDeviceState: Send + Sync + DeviceClassMatcher<Self::DeviceClass>; |
| |
| /// The state associated with ethernet devices. |
| type EthernetDeviceState: Send + Sync + DeviceClassMatcher<Self::DeviceClass>; |
| |
| /// The state associated with pure IP devices. |
| type PureIpDeviceState: Send + Sync + DeviceClassMatcher<Self::DeviceClass>; |
| |
| /// An opaque identifier that is available from both strong and weak device |
| /// references. |
| type DeviceIdentifier: Send + Sync + Debug + Display + DeviceIdAndNameMatcher; |
| } |
| |
| /// Provides matching functionality for the device class of a device installed |
| /// in the netstack. |
| pub trait DeviceClassMatcher<DeviceClass> { |
| /// Returns whether the provided device class matches the class of the |
| /// device. |
| fn device_class_matches(&self, device_class: &DeviceClass) -> bool; |
| } |
| |
| /// Provides matching functionality for the ID and name of a device installed in |
| /// the netstack. |
| pub trait DeviceIdAndNameMatcher { |
| /// Returns whether the provided ID matches the ID of the device. |
| fn id_matches(&self, id: &NonZeroU64) -> bool; |
| |
| /// Returns whether the provided name matches the name of the device. |
| fn name_matches(&self, name: &str) -> bool; |
| } |
| |
| /// Provides associated types used in the device layer. |
| /// |
| /// This trait groups together state types used throughout the device layer. It |
| /// is blanket-implemented for all types that implement |
| /// [`socket::DeviceSocketTypes`] and [`DeviceLayerStateTypes`]. |
| pub trait DeviceLayerTypes: |
| DeviceLayerStateTypes |
| + socket::DeviceSocketTypes |
| + LinkResolutionContext<EthernetLinkDevice> |
| + TimerBindingsTypes |
| + 'static |
| { |
| } |
| impl< |
| BC: DeviceLayerStateTypes |
| + socket::DeviceSocketTypes |
| + LinkResolutionContext<EthernetLinkDevice> |
| + TimerBindingsTypes |
| + 'static, |
| > DeviceLayerTypes for BC |
| { |
| } |
| |
| /// An event dispatcher for the device layer. |
| /// |
| /// See the `EventDispatcher` trait in the crate root for more details. |
| pub trait DeviceLayerEventDispatcher: DeviceLayerTypes + Sized { |
| /// Signals to the dispatcher that RX frames are available and ready to be |
| /// handled by [`handle_queued_rx_packets`]. |
| /// |
| /// Implementations must make sure that [`handle_queued_rx_packets`] is |
| /// scheduled to be called as soon as possible so that enqueued RX frames |
| /// are promptly handled. |
| fn wake_rx_task(&mut self, device: &LoopbackDeviceId<Self>); |
| |
| /// Signals to the dispatcher that TX frames are available and ready to be |
| /// sent by [`transmit_queued_tx_frames`]. |
| /// |
| /// Implementations must make sure that [`transmit_queued_tx_frames`] is |
| /// scheduled to be called as soon as possible so that enqueued TX frames |
| /// are promptly sent. |
| fn wake_tx_task(&mut self, device: &DeviceId<Self>); |
| |
| /// Send a frame to an Ethernet device driver. |
| /// |
| /// See [`DeviceSendFrameError`] for the ways this call may fail; all other |
| /// errors are silently ignored and reported as success. Implementations are |
| /// expected to gracefully handle non-conformant but correctable input, e.g. |
| /// by padding too-small frames. |
| fn send_ethernet_frame( |
| &mut self, |
| device: &EthernetDeviceId<Self>, |
| frame: Buf<Vec<u8>>, |
| ) -> Result<(), DeviceSendFrameError<Buf<Vec<u8>>>>; |
| |
| /// Send an IP packet to an IP device driver. |
| /// |
| /// See [`DeviceSendFrameError`] for the ways this call may fail; all other |
| /// errors are silently ignored and reported as success. Implementations are |
| /// expected to gracefully handle non-conformant but correctable input, e.g. |
| /// by padding too-small frames. |
| fn send_ip_packet( |
| &mut self, |
| device: &PureIpDeviceId<Self>, |
| packet: Buf<Vec<u8>>, |
| ip_version: IpVersion, |
| ) -> Result<(), DeviceSendFrameError<Buf<Vec<u8>>>>; |
| } |
| |
| /// An error encountered when sending a frame. |
| #[derive(Debug, PartialEq, Eq)] |
| pub enum DeviceSendFrameError<T> { |
| /// The device is not ready to send frames. |
| DeviceNotReady(T), |
| } |
| |
| #[cfg(any(test, feature = "testutils"))] |
| pub(crate) mod testutil { |
| use super::*; |
| |
| #[cfg(test)] |
| use alloc::sync::Arc; |
| #[cfg(test)] |
| use core::sync::atomic::AtomicBool; |
| |
| use crate::ip::device::config::{ |
| IpDeviceConfigurationUpdate, Ipv4DeviceConfigurationUpdate, Ipv6DeviceConfigurationUpdate, |
| }; |
| #[cfg(test)] |
| use crate::testutil::Ctx; |
| |
| #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)] |
| pub struct FakeWeakDeviceId<D>(pub(crate) D); |
| |
| impl<D: PartialEq> PartialEq<D> for FakeWeakDeviceId<D> { |
| fn eq(&self, other: &D) -> bool { |
| let Self(this) = self; |
| this == other |
| } |
| } |
| |
| impl<D: FakeStrongDeviceId> WeakId for FakeWeakDeviceId<D> { |
| type Strong = D; |
| |
| fn upgrade(&self) -> Option<D> { |
| let Self(inner) = self; |
| inner.is_alive().then(|| inner.clone()) |
| } |
| } |
| |
| impl<D: crate::device::Id> crate::device::Id for FakeWeakDeviceId<D> { |
| fn is_loopback(&self) -> bool { |
| let Self(inner) = self; |
| inner.is_loopback() |
| } |
| } |
| |
| /// A fake device ID for use in testing. |
| #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)] |
| pub(crate) struct FakeDeviceId; |
| |
| impl StrongId for FakeDeviceId { |
| type Weak = FakeWeakDeviceId<Self>; |
| |
| fn downgrade(&self) -> Self::Weak { |
| FakeWeakDeviceId(self.clone()) |
| } |
| } |
| |
| impl crate::device::Id for FakeDeviceId { |
| fn is_loopback(&self) -> bool { |
| false |
| } |
| } |
| |
| impl crate::filter::InterfaceProperties<()> for FakeDeviceId { |
| fn id_matches(&self, _: &core::num::NonZeroU64) -> bool { |
| unimplemented!() |
| } |
| |
| fn name_matches(&self, _: &str) -> bool { |
| unimplemented!() |
| } |
| |
| fn device_class_matches(&self, _: &()) -> bool { |
| unimplemented!() |
| } |
| } |
| |
| impl FakeStrongDeviceId for FakeDeviceId { |
| fn is_alive(&self) -> bool { |
| true |
| } |
| } |
| |
| /// A fake device ID for use in testing. |
| /// |
| /// [`FakeReferencyDeviceId`] behaves like a referency device ID, each |
| /// constructed instance represents a new device. |
| #[derive(Clone, Debug, Default)] |
| #[cfg(test)] |
| pub(crate) struct FakeReferencyDeviceId { |
| removed: Arc<AtomicBool>, |
| } |
| |
| #[cfg(test)] |
| impl core::hash::Hash for FakeReferencyDeviceId { |
| fn hash<H: core::hash::Hasher>(&self, state: &mut H) { |
| let Self { removed } = self; |
| core::ptr::hash(alloc::sync::Arc::as_ptr(removed), state) |
| } |
| } |
| |
| #[cfg(test)] |
| impl core::cmp::Eq for FakeReferencyDeviceId {} |
| |
| #[cfg(test)] |
| impl core::cmp::PartialEq for FakeReferencyDeviceId { |
| fn eq(&self, Self { removed: other }: &Self) -> bool { |
| let Self { removed } = self; |
| alloc::sync::Arc::ptr_eq(removed, other) |
| } |
| } |
| |
| #[cfg(test)] |
| impl core::cmp::Ord for FakeReferencyDeviceId { |
| fn cmp(&self, Self { removed: other }: &Self) -> core::cmp::Ordering { |
| let Self { removed } = self; |
| alloc::sync::Arc::as_ptr(removed).cmp(&alloc::sync::Arc::as_ptr(other)) |
| } |
| } |
| |
| #[cfg(test)] |
| impl core::cmp::PartialOrd for FakeReferencyDeviceId { |
| fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> { |
| Some(self.cmp(other)) |
| } |
| } |
| |
| #[cfg(test)] |
| impl FakeReferencyDeviceId { |
| /// Marks this device as removed, all weak references will not be able |
| /// to upgrade anymore. |
| pub(crate) fn mark_removed(&self) { |
| self.removed.store(true, core::sync::atomic::Ordering::Relaxed); |
| } |
| } |
| |
| #[cfg(test)] |
| impl StrongId for FakeReferencyDeviceId { |
| type Weak = FakeWeakDeviceId<Self>; |
| |
| fn downgrade(&self) -> Self::Weak { |
| FakeWeakDeviceId(self.clone()) |
| } |
| } |
| |
| #[cfg(test)] |
| impl crate::device::Id for FakeReferencyDeviceId { |
| fn is_loopback(&self) -> bool { |
| false |
| } |
| } |
| |
| #[cfg(test)] |
| impl crate::filter::InterfaceProperties<()> for FakeReferencyDeviceId { |
| fn id_matches(&self, _: &core::num::NonZeroU64) -> bool { |
| unimplemented!() |
| } |
| |
| fn name_matches(&self, _: &str) -> bool { |
| unimplemented!() |
| } |
| |
| fn device_class_matches(&self, _: &()) -> bool { |
| unimplemented!() |
| } |
| } |
| |
| #[cfg(test)] |
| impl FakeStrongDeviceId for FakeReferencyDeviceId { |
| fn is_alive(&self) -> bool { |
| !self.removed.load(core::sync::atomic::Ordering::Relaxed) |
| } |
| } |
| |
| pub trait FakeStrongDeviceId: StrongId<Weak = FakeWeakDeviceId<Self>> + 'static + Ord { |
| /// Returns whether this ID is still alive. |
| /// |
| /// This is used by [`FakeWeakDeviceId`] to return `None` when trying to |
| /// upgrade back a `FakeStrongDeviceId`. |
| fn is_alive(&self) -> bool; |
| } |
| |
| pub fn enable_device<BC: BindingsContext>( |
| ctx: &mut crate::testutil::Ctx<BC>, |
| device: &DeviceId<BC>, |
| ) { |
| let ip_config = |
| IpDeviceConfigurationUpdate { ip_enabled: Some(true), ..Default::default() }; |
| let _: Ipv4DeviceConfigurationUpdate = ctx |
| .core_api() |
| .device_ip::<Ipv4>() |
| .update_configuration( |
| device, |
| Ipv4DeviceConfigurationUpdate { ip_config, ..Default::default() }, |
| ) |
| .unwrap(); |
| let _: Ipv6DeviceConfigurationUpdate = ctx |
| .core_api() |
| .device_ip::<Ipv6>() |
| .update_configuration( |
| device, |
| Ipv6DeviceConfigurationUpdate { ip_config, ..Default::default() }, |
| ) |
| .unwrap(); |
| } |
| |
| /// Enables or disables IP packet routing on `device`. |
| #[cfg(test)] |
| #[netstack3_macros::context_ip_bounds(I, BC, crate)] |
| pub(crate) fn set_forwarding_enabled<BC: BindingsContext, I: crate::IpExt>( |
| ctx: &mut Ctx<BC>, |
| device: &DeviceId<BC>, |
| enabled: bool, |
| ) { |
| let _config = ctx |
| .core_api() |
| .device_ip::<I>() |
| .update_configuration( |
| device, |
| IpDeviceConfigurationUpdate { |
| forwarding_enabled: Some(enabled), |
| ..Default::default() |
| } |
| .into(), |
| ) |
| .unwrap(); |
| } |
| |
| /// Returns whether IP packet routing is enabled on `device`. |
| #[cfg(test)] |
| #[netstack3_macros::context_ip_bounds(I, BC, crate)] |
| pub(crate) fn is_forwarding_enabled<BC: BindingsContext, I: crate::IpExt>( |
| ctx: &mut Ctx<BC>, |
| device: &DeviceId<BC>, |
| ) -> bool { |
| let configuration = ctx.core_api().device_ip::<I>().get_configuration(device); |
| let crate::ip::device::state::IpDeviceConfiguration { forwarding_enabled, .. } = |
| configuration.as_ref(); |
| *forwarding_enabled |
| } |
| |
| /// A device ID type that supports identifying more than one distinct |
| /// device. |
| #[cfg(test)] |
| #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Ord, PartialOrd)] |
| pub(crate) enum MultipleDevicesId { |
| A, |
| B, |
| C, |
| } |
| |
| #[cfg(test)] |
| impl MultipleDevicesId { |
| pub(crate) fn all() -> [Self; 3] { |
| [Self::A, Self::B, Self::C] |
| } |
| } |
| |
| #[cfg(test)] |
| impl crate::device::Id for MultipleDevicesId { |
| fn is_loopback(&self) -> bool { |
| false |
| } |
| } |
| |
| #[cfg(test)] |
| impl StrongId for MultipleDevicesId { |
| type Weak = FakeWeakDeviceId<Self>; |
| |
| fn downgrade(&self) -> Self::Weak { |
| FakeWeakDeviceId(self.clone()) |
| } |
| } |
| |
| #[cfg(test)] |
| impl FakeStrongDeviceId for MultipleDevicesId { |
| fn is_alive(&self) -> bool { |
| true |
| } |
| } |
| |
| #[cfg(test)] |
| impl crate::filter::InterfaceProperties<()> for MultipleDevicesId { |
| fn id_matches(&self, _: &core::num::NonZeroU64) -> bool { |
| unimplemented!() |
| } |
| |
| fn name_matches(&self, _: &str) -> bool { |
| unimplemented!() |
| } |
| |
| fn device_class_matches(&self, _: &()) -> bool { |
| unimplemented!() |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use core::{ |
| num::{NonZeroU16, NonZeroU8}, |
| time::Duration, |
| }; |
| |
| use const_unwrap::const_unwrap_option; |
| use net_declare::net_mac; |
| use net_types::{ |
| ip::{AddrSubnet, Mtu}, |
| SpecifiedAddr, UnicastAddr, Witness as _, |
| }; |
| use test_case::test_case; |
| |
| use super::*; |
| use crate::{ |
| context::testutil::FakeInstant, |
| device::{ |
| ethernet::{EthernetCreationProperties, MaxEthernetFrameSize}, |
| loopback::{LoopbackCreationProperties, LoopbackDevice}, |
| queue::tx::TransmitQueueConfiguration, |
| DeviceProvider, |
| }, |
| error, for_any_device_id, |
| ip::device::{ |
| api::AddIpAddrSubnetError, |
| config::{ |
| IpDeviceConfigurationUpdate, Ipv4DeviceConfigurationUpdate, |
| Ipv6DeviceConfigurationUpdate, |
| }, |
| slaac::SlaacConfiguration, |
| state::{Ipv4AddrConfig, Ipv6AddrManualConfig, Lifetime}, |
| }, |
| testutil::{TestIpExt, DEFAULT_INTERFACE_METRIC, IPV6_MIN_IMPLIED_MAX_FRAME_SIZE}, |
| work_queue::WorkQueueReport, |
| }; |
| |
| #[test] |
| fn test_origin_tracker() { |
| let tracker = OriginTracker::new(); |
| if cfg!(debug_assertions) { |
| assert_ne!(tracker, OriginTracker::new()); |
| } else { |
| assert_eq!(tracker, OriginTracker::new()); |
| } |
| assert_eq!(tracker.clone(), tracker); |
| } |
| |
| #[test] |
| fn frame_destination_from_dest() { |
| const LOCAL_ADDR: Mac = net_mac!("88:88:88:88:88:88"); |
| |
| assert_eq!( |
| FrameDestination::from_dest( |
| UnicastAddr::new(net_mac!("00:11:22:33:44:55")).unwrap().get(), |
| LOCAL_ADDR |
| ), |
| FrameDestination::Individual { local: false } |
| ); |
| assert_eq!( |
| FrameDestination::from_dest(LOCAL_ADDR, LOCAL_ADDR), |
| FrameDestination::Individual { local: true } |
| ); |
| assert_eq!( |
| FrameDestination::from_dest(Mac::BROADCAST, LOCAL_ADDR), |
| FrameDestination::Broadcast, |
| ); |
| assert_eq!( |
| FrameDestination::from_dest( |
| MulticastAddr::new(net_mac!("11:11:11:11:11:11")).unwrap().get(), |
| LOCAL_ADDR |
| ), |
| FrameDestination::Multicast |
| ); |
| } |
| |
| #[test] |
| fn test_no_default_routes() { |
| let mut ctx = crate::testutil::FakeCtx::default(); |
| let _loopback_device: LoopbackDeviceId<_> = |
| ctx.core_api().device::<LoopbackDevice>().add_device_with_default_state( |
| LoopbackCreationProperties { mtu: Mtu::new(55) }, |
| DEFAULT_INTERFACE_METRIC, |
| ); |
| |
| assert_eq!(ctx.core_api().routes_any().get_all_routes(), []); |
| let _ethernet_device: EthernetDeviceId<_> = |
| ctx.core_api().device::<EthernetLinkDevice>().add_device_with_default_state( |
| EthernetCreationProperties { |
| mac: UnicastAddr::new(net_mac!("aa:bb:cc:dd:ee:ff")).expect("MAC is unicast"), |
| max_frame_size: MaxEthernetFrameSize::MIN, |
| }, |
| DEFAULT_INTERFACE_METRIC, |
| ); |
| assert_eq!(ctx.core_api().routes_any().get_all_routes(), []); |
| } |
| |
| #[test] |
| fn remove_ethernet_device_disables_timers() { |
| let mut ctx = crate::testutil::FakeCtx::default(); |
| |
| let ethernet_device = |
| ctx.core_api().device::<EthernetLinkDevice>().add_device_with_default_state( |
| EthernetCreationProperties { |
| mac: UnicastAddr::new(net_mac!("aa:bb:cc:dd:ee:ff")).expect("MAC is unicast"), |
| max_frame_size: MaxEthernetFrameSize::from_mtu(Mtu::new(1500)).unwrap(), |
| }, |
| DEFAULT_INTERFACE_METRIC, |
| ); |
| |
| { |
| let device = ethernet_device.clone().into(); |
| // Enable the device, turning on a bunch of features that install |
| // timers. |
| let ip_config = IpDeviceConfigurationUpdate { |
| ip_enabled: Some(true), |
| gmp_enabled: Some(true), |
| ..Default::default() |
| }; |
| let _: Ipv4DeviceConfigurationUpdate = ctx |
| .core_api() |
| .device_ip::<Ipv4>() |
| .update_configuration(&device, ip_config.into()) |
| .unwrap(); |
| let _: Ipv6DeviceConfigurationUpdate = ctx |
| .core_api() |
| .device_ip::<Ipv6>() |
| .update_configuration( |
| &device, |
| Ipv6DeviceConfigurationUpdate { |
| max_router_solicitations: Some(Some(const_unwrap_option(NonZeroU8::new( |
| 2, |
| )))), |
| slaac_config: Some(SlaacConfiguration { |
| enable_stable_addresses: true, |
| ..Default::default() |
| }), |
| ip_config, |
| ..Default::default() |
| }, |
| ) |
| .unwrap(); |
| } |
| |
| ctx.core_api().device().remove_device(ethernet_device).into_removed(); |
| assert_eq!(ctx.bindings_ctx.timer_ctx().timers(), &[]); |
| } |
| |
| fn add_ethernet( |
| ctx: &mut crate::testutil::FakeCtx, |
| ) -> DeviceId<crate::testutil::FakeBindingsCtx> { |
| ctx.core_api() |
| .device::<EthernetLinkDevice>() |
| .add_device_with_default_state( |
| EthernetCreationProperties { |
| mac: Ipv6::FAKE_CONFIG.local_mac, |
| max_frame_size: IPV6_MIN_IMPLIED_MAX_FRAME_SIZE, |
| }, |
| DEFAULT_INTERFACE_METRIC, |
| ) |
| .into() |
| } |
| |
| fn add_loopback( |
| ctx: &mut crate::testutil::FakeCtx, |
| ) -> DeviceId<crate::testutil::FakeBindingsCtx> { |
| let device = ctx |
| .core_api() |
| .device::<LoopbackDevice>() |
| .add_device_with_default_state( |
| LoopbackCreationProperties { mtu: Ipv6::MINIMUM_LINK_MTU }, |
| DEFAULT_INTERFACE_METRIC, |
| ) |
| .into(); |
| ctx.core_api() |
| .device_ip::<Ipv6>() |
| .add_ip_addr_subnet( |
| &device, |
| AddrSubnet::from_witness(Ipv6::LOOPBACK_ADDRESS, Ipv6::LOOPBACK_SUBNET.prefix()) |
| .unwrap(), |
| ) |
| .unwrap(); |
| device |
| } |
| |
| fn check_transmitted_ethernet( |
| bindings_ctx: &mut crate::testutil::FakeBindingsCtx, |
| _device_id: &DeviceId<crate::testutil::FakeBindingsCtx>, |
| count: usize, |
| ) { |
| assert_eq!(bindings_ctx.take_ethernet_frames().len(), count); |
| } |
| |
| fn check_transmitted_loopback( |
| bindings_ctx: &mut crate::testutil::FakeBindingsCtx, |
| device_id: &DeviceId<crate::testutil::FakeBindingsCtx>, |
| count: usize, |
| ) { |
| // Loopback frames leave the stack; outgoing frames land in |
| // its RX queue. |
| let rx_available = core::mem::take(&mut bindings_ctx.state_mut().rx_available); |
| if count == 0 { |
| assert_eq!(rx_available, <[LoopbackDeviceId::<_>; 0]>::default()); |
| } else { |
| assert_eq!( |
| rx_available.into_iter().map(DeviceId::Loopback).collect::<Vec<_>>(), |
| [device_id.clone()] |
| ); |
| } |
| } |
| |
| #[test_case(add_ethernet, check_transmitted_ethernet, true; "ethernet with queue")] |
| #[test_case(add_ethernet, check_transmitted_ethernet, false; "ethernet without queue")] |
| #[test_case(add_loopback, check_transmitted_loopback, true; "loopback with queue")] |
| #[test_case(add_loopback, check_transmitted_loopback, false; "loopback without queue")] |
| fn tx_queue( |
| add_device: fn(&mut crate::testutil::FakeCtx) -> DeviceId<crate::testutil::FakeBindingsCtx>, |
| check_transmitted: fn( |
| &mut crate::testutil::FakeBindingsCtx, |
| &DeviceId<crate::testutil::FakeBindingsCtx>, |
| usize, |
| ), |
| with_tx_queue: bool, |
| ) { |
| let mut ctx = crate::testutil::FakeCtx::default(); |
| let device = add_device(&mut ctx); |
| |
| if with_tx_queue { |
| for_any_device_id!(DeviceId, DeviceProvider, D, &device, device => { |
| ctx.core_api().transmit_queue::<D>() |
| .set_configuration(device, TransmitQueueConfiguration::Fifo) |
| }) |
| } |
| |
| let _: Ipv6DeviceConfigurationUpdate = ctx |
| .core_api() |
| .device_ip::<Ipv6>() |
| .update_configuration( |
| &device, |
| Ipv6DeviceConfigurationUpdate { |
| // Enable DAD so that the auto-generated address triggers a DAD |
| // message immediately on interface enable. |
| dad_transmits: Some(Some(const_unwrap_option(NonZeroU16::new(1)))), |
| // Enable stable addresses so the link-local address is auto- |
| // generated. |
| slaac_config: Some(SlaacConfiguration { |
| enable_stable_addresses: true, |
| ..Default::default() |
| }), |
| ip_config: IpDeviceConfigurationUpdate { |
| ip_enabled: Some(true), |
| ..Default::default() |
| }, |
| ..Default::default() |
| }, |
| ) |
| .unwrap(); |
| |
| if with_tx_queue { |
| check_transmitted(&mut ctx.bindings_ctx, &device, 0); |
| assert_eq!( |
| core::mem::take(&mut ctx.bindings_ctx.state_mut().tx_available), |
| [device.clone()] |
| ); |
| let result = for_any_device_id!( |
| DeviceId, DeviceProvider, D, &device, device => { |
| ctx.core_api().transmit_queue::<D>().transmit_queued_frames(device) |
| } |
| ); |
| assert_eq!(result, Ok(WorkQueueReport::AllDone)); |
| } |
| |
| check_transmitted(&mut ctx.bindings_ctx, &device, 1); |
| assert_eq!(ctx.bindings_ctx.state_mut().tx_available, <[DeviceId::<_>; 0]>::default()); |
| for_any_device_id!( |
| DeviceId, |
| device, |
| device => ctx.core_api().device().remove_device(device).into_removed() |
| ) |
| } |
| |
| #[netstack3_macros::context_ip_bounds(I, crate::testutil::FakeBindingsCtx, crate)] |
| fn test_add_remove_ip_addresses<I: Ip + TestIpExt + crate::IpExt>( |
| addr_config: Option<I::ManualAddressConfig<FakeInstant>>, |
| ) { |
| let config = I::FAKE_CONFIG; |
| let mut ctx = crate::testutil::FakeCtx::default(); |
| let device = ctx |
| .core_api() |
| .device::<EthernetLinkDevice>() |
| .add_device_with_default_state( |
| EthernetCreationProperties { |
| mac: config.local_mac, |
| max_frame_size: IPV6_MIN_IMPLIED_MAX_FRAME_SIZE, |
| }, |
| DEFAULT_INTERFACE_METRIC, |
| ) |
| .into(); |
| |
| crate::device::testutil::enable_device(&mut ctx, &device); |
| |
| let ip = I::get_other_ip_address(1).get(); |
| let prefix = config.subnet.prefix(); |
| let addr_subnet = AddrSubnet::new(ip, prefix).unwrap(); |
| |
| let check_contains_addr = |ctx: &mut crate::testutil::FakeCtx| { |
| ctx.core_api() |
| .device_ip::<I>() |
| .get_assigned_ip_addr_subnets(&device) |
| .contains(&addr_subnet) |
| }; |
| |
| // IP doesn't exist initially. |
| assert_eq!(check_contains_addr(&mut ctx), false); |
| |
| // Add IP (OK). |
| ctx.core_api() |
| .device_ip::<I>() |
| .add_ip_addr_subnet_with_config(&device, addr_subnet, addr_config.unwrap_or_default()) |
| .unwrap(); |
| assert_eq!(check_contains_addr(&mut ctx), true); |
| |
| // Add IP again (already exists). |
| assert_eq!( |
| ctx.core_api().device_ip::<I>().add_ip_addr_subnet(&device, addr_subnet), |
| Err(AddIpAddrSubnetError::Exists), |
| ); |
| assert_eq!(check_contains_addr(&mut ctx), true); |
| |
| // Add IP with different subnet (already exists). |
| let wrong_addr_subnet = AddrSubnet::new(ip, prefix - 1).unwrap(); |
| assert_eq!( |
| ctx.core_api().device_ip::<I>().add_ip_addr_subnet(&device, wrong_addr_subnet), |
| Err(AddIpAddrSubnetError::Exists), |
| ); |
| assert_eq!(check_contains_addr(&mut ctx), true); |
| |
| let ip = SpecifiedAddr::new(ip).unwrap(); |
| // Del IP (ok). |
| let () = ctx.core_api().device_ip::<I>().del_ip_addr(&device, ip).unwrap(); |
| assert_eq!(check_contains_addr(&mut ctx), false); |
| |
| // Del IP again (not found). |
| assert_eq!( |
| ctx.core_api().device_ip::<I>().del_ip_addr(&device, ip), |
| Err(error::NotFoundError), |
| ); |
| |
| assert_eq!(check_contains_addr(&mut ctx), false); |
| } |
| |
| #[test_case(None; "with no AddressConfig specified")] |
| #[test_case(Some(Ipv4AddrConfig { |
| valid_until: Lifetime::Finite(FakeInstant::from(Duration::from_secs(1))) |
| }); "with AddressConfig specified")] |
| fn test_add_remove_ipv4_addresses(addr_config: Option<Ipv4AddrConfig<FakeInstant>>) { |
| test_add_remove_ip_addresses::<Ipv4>(addr_config); |
| } |
| |
| #[test_case(None; "with no AddressConfig specified")] |
| #[test_case(Some(Ipv6AddrManualConfig { |
| valid_until: Lifetime::Finite(FakeInstant::from(Duration::from_secs(1))) |
| }); "with AddressConfig specified")] |
| fn test_add_remove_ipv6_addresses(addr_config: Option<Ipv6AddrManualConfig<FakeInstant>>) { |
| test_add_remove_ip_addresses::<Ipv6>(addr_config); |
| } |
| } |