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