| // Copyright 2022 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| //! Duplicate Address Detection. |
| |
| use core::{num::NonZeroU8, time::Duration}; |
| |
| use net_types::{ |
| ip::{Ipv6, Ipv6Addr}, |
| MulticastAddr, UnicastAddr, Witness as _, |
| }; |
| use packet_formats::icmp::ndp::NeighborSolicitation; |
| |
| use crate::{ |
| context::{EventContext, TimerContext}, |
| ip::{device::state::AddressState, IpDeviceIdContext}, |
| }; |
| |
| /// A timer ID for duplicate address detection. |
| #[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)] |
| pub(crate) struct DadTimerId<DeviceId> { |
| pub(crate) device_id: DeviceId, |
| pub(crate) addr: UnicastAddr<Ipv6Addr>, |
| } |
| |
| /// The IP device context provided to DAD. |
| pub(super) trait Ipv6DeviceDadContext<C>: IpDeviceIdContext<Ipv6> { |
| /// Returns the address's state mutably, if it exists on the interface. |
| fn get_address_state_mut( |
| &mut self, |
| ctx: &mut C, |
| device_id: Self::DeviceId, |
| addr: UnicastAddr<Ipv6Addr>, |
| ) -> Option<&mut AddressState>; |
| |
| /// Returns the NDP retransmission timer configured on the device. |
| fn retrans_timer(&self, ctx: &mut C, device_id: Self::DeviceId) -> Duration; |
| } |
| |
| /// The IP layer context provided to DAD. |
| pub(super) trait Ipv6LayerDadContext<C>: IpDeviceIdContext<Ipv6> { |
| /// Sends an NDP Neighbor Solicitation message for DAD to the local-link. |
| /// |
| /// The message will be sent with the unspecified (all-zeroes) source |
| /// address. |
| fn send_dad_packet( |
| &mut self, |
| ctx: &mut C, |
| device_id: Self::DeviceId, |
| dst_ip: MulticastAddr<Ipv6Addr>, |
| message: NeighborSolicitation, |
| ) -> Result<(), ()>; |
| } |
| |
| #[derive(Debug, Eq, PartialEq)] |
| /// Events generated by duplicate address detection. |
| pub enum DadEvent<DeviceId> { |
| /// Duplicate address detection completed and the address is assigned. |
| AddressAssigned { |
| /// Device the address belongs to. |
| device: DeviceId, |
| /// The address that moved to the assigned state. |
| addr: UnicastAddr<Ipv6Addr>, |
| }, |
| } |
| |
| /// The non-synchronized execution context for DAD. |
| pub(super) trait DadNonSyncContext<DeviceId>: |
| TimerContext<DadTimerId<DeviceId>> + EventContext<DadEvent<DeviceId>> |
| { |
| } |
| impl<DeviceId, C: TimerContext<DadTimerId<DeviceId>> + EventContext<DadEvent<DeviceId>>> |
| DadNonSyncContext<DeviceId> for C |
| { |
| } |
| |
| /// The execution context for DAD. |
| pub(super) trait DadContext<C: DadNonSyncContext<Self::DeviceId>>: |
| Ipv6DeviceDadContext<C> + Ipv6LayerDadContext<C> |
| { |
| } |
| |
| impl<C: DadNonSyncContext<SC::DeviceId>, SC: Ipv6DeviceDadContext<C> + Ipv6LayerDadContext<C>> |
| DadContext<C> for SC |
| { |
| } |
| |
| /// An implementation for Duplicate Address Detection. |
| pub(crate) trait DadHandler<C>: IpDeviceIdContext<Ipv6> { |
| /// Do duplicate address detection. |
| /// |
| /// # Panics |
| /// |
| /// Panics if tentative state for the address is not found. |
| fn do_duplicate_address_detection( |
| &mut self, |
| ctx: &mut C, |
| device_id: Self::DeviceId, |
| addr: UnicastAddr<Ipv6Addr>, |
| ); |
| |
| /// Stops duplicate address detection. |
| /// |
| /// Does nothing if DAD is not being performed on the address. |
| fn stop_duplicate_address_detection( |
| &mut self, |
| ctx: &mut C, |
| device_id: Self::DeviceId, |
| addr: UnicastAddr<Ipv6Addr>, |
| ); |
| |
| /// Handles a timer. |
| // TODO(https://fxbug.dev/101399): Replace this with a `TimerHandler` bound. |
| fn handle_timer( |
| &mut self, |
| ctx: &mut C, |
| DadTimerId { device_id, addr }: DadTimerId<Self::DeviceId>, |
| ) { |
| self.do_duplicate_address_detection(ctx, device_id, addr) |
| } |
| } |
| |
| impl<C: DadNonSyncContext<SC::DeviceId>, SC: DadContext<C>> DadHandler<C> for SC { |
| fn do_duplicate_address_detection( |
| &mut self, |
| ctx: &mut C, |
| device_id: Self::DeviceId, |
| addr: UnicastAddr<Ipv6Addr>, |
| ) { |
| let state = self |
| .get_address_state_mut(ctx, device_id, addr) |
| .unwrap_or_else(|| panic!("expected address to exist; addr={}", addr)); |
| |
| let remaining = match state { |
| AddressState::Tentative { dad_transmits_remaining } => dad_transmits_remaining, |
| AddressState::Assigned => { |
| panic!("expected address to be tentative; addr={}", addr) |
| } |
| }; |
| |
| match remaining { |
| None => { |
| *state = AddressState::Assigned; |
| ctx.on_event(DadEvent::AddressAssigned { device: device_id, addr }); |
| } |
| Some(non_zero_remaining) => { |
| *remaining = NonZeroU8::new(non_zero_remaining.get() - 1); |
| |
| // Per RFC 4862 section 5.1, |
| // |
| // DupAddrDetectTransmits ... |
| // Autoconfiguration also assumes the presence of the variable |
| // RetransTimer as defined in [RFC4861]. For autoconfiguration |
| // purposes, RetransTimer specifies the delay between |
| // consecutive Neighbor Solicitation transmissions performed |
| // during Duplicate Address Detection (if |
| // DupAddrDetectTransmits is greater than 1), as well as the |
| // time a node waits after sending the last Neighbor |
| // Solicitation before ending the Duplicate Address Detection |
| // process. |
| let retrans_timer = self.retrans_timer(ctx, device_id); |
| |
| let dst_ip = addr.to_solicited_node_address(); |
| |
| // Do not include the source link-layer option when the NS |
| // message as DAD messages are sent with the unspecified source |
| // address which must not hold a source link-layer option. |
| // |
| // As per RFC 4861 section 4.3, |
| // |
| // Possible options: |
| // |
| // Source link-layer address |
| // The link-layer address for the sender. MUST NOT be |
| // included when the source IP address is the |
| // unspecified address. Otherwise, on link layers |
| // that have addresses this option MUST be included in |
| // multicast solicitations and SHOULD be included in |
| // unicast solicitations. |
| // |
| // TODO(https://fxbug.dev/85055): Either panic or guarantee that this error |
| // can't happen statically. |
| let _: Result<(), _> = self.send_dad_packet( |
| ctx, |
| device_id, |
| dst_ip, |
| NeighborSolicitation::new(addr.get()), |
| ); |
| |
| assert_eq!( |
| ctx.schedule_timer(retrans_timer, DadTimerId { device_id, addr }), |
| None, |
| "Should not have a DAD timer set when performing DAD work; addr={}, device_id={}", |
| addr, |
| device_id |
| ); |
| } |
| } |
| } |
| |
| fn stop_duplicate_address_detection( |
| &mut self, |
| ctx: &mut C, |
| device_id: Self::DeviceId, |
| addr: UnicastAddr<Ipv6Addr>, |
| ) { |
| let _: Option<C::Instant> = ctx.cancel_timer(DadTimerId { device_id, addr }); |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use packet::EmptyBuf; |
| use packet_formats::icmp::ndp::Options; |
| |
| use super::*; |
| use crate::{ |
| context::{ |
| testutil::{DummyCtx, DummyNonSyncCtx, DummySyncCtx, DummyTimerCtxExt as _}, |
| FrameContext as _, InstantContext as _, |
| }, |
| ip::DummyDeviceId, |
| }; |
| |
| struct MockDadContext { |
| addr: UnicastAddr<Ipv6Addr>, |
| state: AddressState, |
| retrans_timer: Duration, |
| } |
| |
| #[derive(Debug)] |
| struct DadMessageMeta { |
| dst_ip: MulticastAddr<Ipv6Addr>, |
| message: NeighborSolicitation, |
| } |
| |
| type MockNonSyncCtx = DummyNonSyncCtx<DadTimerId<DummyDeviceId>, DadEvent<DummyDeviceId>, ()>; |
| |
| type MockCtx = DummySyncCtx<MockDadContext, DadMessageMeta, DummyDeviceId>; |
| |
| impl Ipv6DeviceDadContext<MockNonSyncCtx> for MockCtx { |
| fn get_address_state_mut( |
| &mut self, |
| _ctx: &mut MockNonSyncCtx, |
| DummyDeviceId: DummyDeviceId, |
| request_addr: UnicastAddr<Ipv6Addr>, |
| ) -> Option<&mut AddressState> { |
| let MockDadContext { addr, state, retrans_timer: _ } = self.get_mut(); |
| (*addr == request_addr).then(|| state) |
| } |
| |
| fn retrans_timer( |
| &self, |
| _ctx: &mut MockNonSyncCtx, |
| DummyDeviceId: DummyDeviceId, |
| ) -> Duration { |
| let MockDadContext { addr: _, state: _, retrans_timer } = self.get_ref(); |
| *retrans_timer |
| } |
| } |
| |
| impl Ipv6LayerDadContext<MockNonSyncCtx> for MockCtx { |
| fn send_dad_packet( |
| &mut self, |
| ctx: &mut MockNonSyncCtx, |
| DummyDeviceId: DummyDeviceId, |
| dst_ip: MulticastAddr<Ipv6Addr>, |
| message: NeighborSolicitation, |
| ) -> Result<(), ()> { |
| self.send_frame(ctx, DadMessageMeta { dst_ip, message }, EmptyBuf) |
| .map_err(|EmptyBuf| ()) |
| } |
| } |
| |
| const DAD_ADDRESS: UnicastAddr<Ipv6Addr> = |
| unsafe { UnicastAddr::new_unchecked(Ipv6Addr::new([0xa, 0, 0, 0, 0, 0, 0, 1])) }; |
| const OTHER_ADDRESS: UnicastAddr<Ipv6Addr> = |
| unsafe { UnicastAddr::new_unchecked(Ipv6Addr::new([0xa, 0, 0, 0, 0, 0, 0, 2])) }; |
| |
| #[test] |
| #[should_panic(expected = "expected address to exist")] |
| fn panic_unknown_address() { |
| let DummyCtx { mut sync_ctx, mut non_sync_ctx } = |
| DummyCtx::with_sync_ctx(MockCtx::with_state(MockDadContext { |
| addr: DAD_ADDRESS, |
| state: AddressState::Tentative { dad_transmits_remaining: None }, |
| retrans_timer: Duration::default(), |
| })); |
| DadHandler::do_duplicate_address_detection( |
| &mut sync_ctx, |
| &mut non_sync_ctx, |
| DummyDeviceId, |
| OTHER_ADDRESS, |
| ); |
| } |
| |
| #[test] |
| #[should_panic(expected = "expected address to be tentative")] |
| fn panic_non_tentative_address() { |
| let DummyCtx { mut sync_ctx, mut non_sync_ctx } = |
| DummyCtx::with_sync_ctx(MockCtx::with_state(MockDadContext { |
| addr: DAD_ADDRESS, |
| state: AddressState::Assigned, |
| retrans_timer: Duration::default(), |
| })); |
| DadHandler::do_duplicate_address_detection( |
| &mut sync_ctx, |
| &mut non_sync_ctx, |
| DummyDeviceId, |
| DAD_ADDRESS, |
| ); |
| } |
| |
| #[test] |
| fn dad_disabled() { |
| let DummyCtx { mut sync_ctx, mut non_sync_ctx } = |
| DummyCtx::with_sync_ctx(MockCtx::with_state(MockDadContext { |
| addr: DAD_ADDRESS, |
| state: AddressState::Tentative { dad_transmits_remaining: None }, |
| retrans_timer: Duration::default(), |
| })); |
| DadHandler::do_duplicate_address_detection( |
| &mut sync_ctx, |
| &mut non_sync_ctx, |
| DummyDeviceId, |
| DAD_ADDRESS, |
| ); |
| let MockDadContext { addr: _, state, retrans_timer: _ } = sync_ctx.get_ref(); |
| assert_eq!(*state, AddressState::Assigned); |
| assert_eq!( |
| non_sync_ctx.take_events(), |
| &[DadEvent::AddressAssigned { device: DummyDeviceId, addr: DAD_ADDRESS }][..] |
| ); |
| } |
| |
| const DAD_TIMER_ID: DadTimerId<DummyDeviceId> = |
| DadTimerId { addr: DAD_ADDRESS, device_id: DummyDeviceId }; |
| |
| fn check_dad( |
| sync_ctx: &MockCtx, |
| non_sync_ctx: &MockNonSyncCtx, |
| frames_len: usize, |
| dad_transmits_remaining: Option<NonZeroU8>, |
| retrans_timer: Duration, |
| ) { |
| let MockDadContext { addr: _, state, retrans_timer: _ } = sync_ctx.get_ref(); |
| assert_eq!(*state, AddressState::Tentative { dad_transmits_remaining }); |
| let frames = sync_ctx.frames(); |
| assert_eq!(frames.len(), frames_len, "frames = {:?}", frames); |
| let (DadMessageMeta { dst_ip, message }, frame) = |
| frames.last().expect("should have transmitted a frame"); |
| |
| assert_eq!(*dst_ip, DAD_ADDRESS.to_solicited_node_address()); |
| assert_eq!(*message, NeighborSolicitation::new(DAD_ADDRESS.get())); |
| |
| let options = Options::parse(&frame[..]).expect("parse NDP options"); |
| assert_eq!(options.iter().count(), 0); |
| non_sync_ctx |
| .timer_ctx() |
| .assert_timers_installed([(DAD_TIMER_ID, non_sync_ctx.now() + retrans_timer)]); |
| } |
| |
| #[test] |
| fn perform_dad() { |
| const DAD_TRANSMITS_REQUIRED: u8 = 2; |
| const RETRANS_TIMER: Duration = Duration::from_secs(1); |
| |
| let DummyCtx { mut sync_ctx, mut non_sync_ctx } = |
| DummyCtx::with_sync_ctx(MockCtx::with_state(MockDadContext { |
| addr: DAD_ADDRESS, |
| state: AddressState::Tentative { |
| dad_transmits_remaining: NonZeroU8::new(DAD_TRANSMITS_REQUIRED), |
| }, |
| retrans_timer: RETRANS_TIMER, |
| })); |
| DadHandler::do_duplicate_address_detection( |
| &mut sync_ctx, |
| &mut non_sync_ctx, |
| DummyDeviceId, |
| DAD_ADDRESS, |
| ); |
| |
| for count in 0..=1u8 { |
| check_dad( |
| &sync_ctx, |
| &non_sync_ctx, |
| usize::from(count + 1), |
| NonZeroU8::new(DAD_TRANSMITS_REQUIRED - count - 1), |
| RETRANS_TIMER, |
| ); |
| assert_eq!( |
| non_sync_ctx.trigger_next_timer(&mut sync_ctx, DadHandler::handle_timer), |
| Some(DAD_TIMER_ID) |
| ); |
| } |
| let MockDadContext { addr: _, state, retrans_timer: _ } = sync_ctx.get_ref(); |
| assert_eq!(*state, AddressState::Assigned); |
| assert_eq!( |
| non_sync_ctx.take_events(), |
| &[DadEvent::AddressAssigned { device: DummyDeviceId, addr: DAD_ADDRESS }][..] |
| ); |
| } |
| |
| #[test] |
| fn stop_dad() { |
| const DAD_TRANSMITS_REQUIRED: u8 = 2; |
| const RETRANS_TIMER: Duration = Duration::from_secs(2); |
| |
| let DummyCtx { mut sync_ctx, mut non_sync_ctx } = |
| DummyCtx::with_sync_ctx(MockCtx::with_state(MockDadContext { |
| addr: DAD_ADDRESS, |
| state: AddressState::Tentative { |
| dad_transmits_remaining: NonZeroU8::new(DAD_TRANSMITS_REQUIRED), |
| }, |
| retrans_timer: RETRANS_TIMER, |
| })); |
| DadHandler::do_duplicate_address_detection( |
| &mut sync_ctx, |
| &mut non_sync_ctx, |
| DummyDeviceId, |
| DAD_ADDRESS, |
| ); |
| check_dad( |
| &sync_ctx, |
| &non_sync_ctx, |
| 1, |
| NonZeroU8::new(DAD_TRANSMITS_REQUIRED - 1), |
| RETRANS_TIMER, |
| ); |
| |
| DadHandler::stop_duplicate_address_detection( |
| &mut sync_ctx, |
| &mut non_sync_ctx, |
| DummyDeviceId, |
| DAD_ADDRESS, |
| ); |
| non_sync_ctx.timer_ctx().assert_no_timers_installed(); |
| } |
| } |