blob: dbe002ce53a9fef2e7e1e4abb349820831c44c77 [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! IPv6 Router Solicitation as defined by [RFC 4861 section 6.3.7].
//!
//! [RFC 4861 section 6.3.7]: https://datatracker.ietf.org/doc/html/rfc4861#section-6.3.7
use core::{num::NonZeroU8, time::Duration};
use net_types::{
ip::{Ipv6, Ipv6Addr},
UnicastAddr,
};
use packet::{EitherSerializer, EmptyBuf, InnerPacketBuilder as _, Serializer};
use packet_formats::icmp::ndp::{
options::NdpOptionBuilder, OptionSequenceBuilder, RouterSolicitation,
};
use rand::Rng as _;
use crate::{
context::{RngContext, TimerContext},
ip::IpDeviceIdContext,
};
/// Amount of time to wait after sending `MAX_RTR_SOLICITATIONS` Router
/// Solicitation messages before determining that there are no routers on the
/// link for the purpose of IPv6 Stateless Address Autoconfiguration if no
/// Router Advertisement messages have been received as defined in [RFC 4861
/// section 10].
///
/// This parameter is also used when a host sends its initial Router
/// Solicitation message, as per [RFC 4861 section 6.3.7]. Before a node sends
/// an initial solicitation, it SHOULD delay the transmission for a random
/// amount of time between 0 and `MAX_RTR_SOLICITATION_DELAY`. This serves to
/// alleviate congestion when many hosts start up on a link at the same time.
///
/// [RFC 4861 section 10]: https://tools.ietf.org/html/rfc4861#section-10
/// [RFC 4861 section 6.3.7]: https://tools.ietf.org/html/rfc4861#section-6.3.7
pub(crate) const MAX_RTR_SOLICITATION_DELAY: Duration = Duration::from_secs(1);
/// Minimum duration between router solicitation messages as defined in [RFC
/// 4861 section 10].
///
/// [RFC 4861 section 10]: https://tools.ietf.org/html/rfc4861#section-10
pub(crate) const RTR_SOLICITATION_INTERVAL: Duration = Duration::from_secs(4);
#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
pub(crate) struct RsTimerId<DeviceId> {
pub(crate) device_id: DeviceId,
}
/// The IP device context provided to RS.
pub(super) trait Ipv6DeviceRsContext<C>: IpDeviceIdContext<Ipv6> {
/// Gets the maximum number of router solicitations to send when
/// performing router solicitation.
fn get_max_router_solicitations(&self, device_id: Self::DeviceId) -> Option<NonZeroU8>;
/// Gets a mutable reference to the remaining number of router
/// solicitations.
fn get_router_soliciations_remaining_mut(
&mut self,
device_id: Self::DeviceId,
) -> &mut Option<NonZeroU8>;
/// Gets the device's link-layer address bytes, if the device supports
/// link-layer addressing.
fn get_link_layer_addr_bytes(&self, device_id: Self::DeviceId) -> Option<&[u8]>;
}
/// The IP layer context provided to RS.
pub(super) trait Ipv6LayerRsContext<C>: IpDeviceIdContext<Ipv6> {
/// Sends an NDP Router Solicitation to the local-link.
///
/// The callback is called with a source address suitable for an outgoing
/// router solicitation message and returns the message body.
fn send_rs_packet<
S: Serializer<Buffer = EmptyBuf>,
F: FnOnce(Option<UnicastAddr<Ipv6Addr>>) -> S,
>(
&mut self,
ctx: &mut C,
device_id: Self::DeviceId,
message: RouterSolicitation,
body: F,
) -> Result<(), S>;
}
/// The non-synchronized execution context for router solicitation.
pub(super) trait RsNonSyncContext<DeviceId>:
RngContext + TimerContext<RsTimerId<DeviceId>>
{
}
impl<DeviceId, C: RngContext + TimerContext<RsTimerId<DeviceId>>> RsNonSyncContext<DeviceId> for C {}
/// The execution context for router solicitation.
pub(super) trait RsContext<C: RsNonSyncContext<Self::DeviceId>>:
Ipv6DeviceRsContext<C> + Ipv6LayerRsContext<C>
{
}
impl<C: RsNonSyncContext<SC::DeviceId>, SC: Ipv6DeviceRsContext<C> + Ipv6LayerRsContext<C>>
RsContext<C> for SC
{
}
/// An implementation of Router Solicitation.
pub(crate) trait RsHandler<C>: IpDeviceIdContext<Ipv6> {
/// Starts router solicitation.
fn start_router_solicitation(&mut self, ctx: &mut C, device_id: Self::DeviceId);
/// Stops router solicitation.
///
/// Does nothing if router solicitaiton is not being performed
fn stop_router_solicitation(&mut self, ctx: &mut C, device_id: Self::DeviceId);
/// Handles a timer.
// TODO: Replace this with a `TimerHandler` bound.
fn handle_timer(&mut self, ctx: &mut C, id: RsTimerId<Self::DeviceId>);
}
impl<C: RsNonSyncContext<SC::DeviceId>, SC: RsContext<C>> RsHandler<C> for SC {
fn start_router_solicitation(&mut self, ctx: &mut C, device_id: Self::DeviceId) {
let max_router_solicitations = self.get_max_router_solicitations(device_id);
*self.get_router_soliciations_remaining_mut(device_id) = max_router_solicitations;
match max_router_solicitations {
None => {}
Some(_) => {
// As per RFC 4861 section 6.3.7, delay the first transmission for a
// random amount of time between 0 and `MAX_RTR_SOLICITATION_DELAY` to
// alleviate congestion when many hosts start up on a link at the same
// time.
let delay =
ctx.rng_mut().gen_range(Duration::new(0, 0)..MAX_RTR_SOLICITATION_DELAY);
assert_eq!(ctx.schedule_timer(delay, RsTimerId { device_id },), None);
}
}
}
fn stop_router_solicitation(&mut self, ctx: &mut C, device_id: Self::DeviceId) {
let _: Option<C::Instant> = ctx.cancel_timer(RsTimerId { device_id });
}
fn handle_timer(&mut self, ctx: &mut C, RsTimerId { device_id }: RsTimerId<SC::DeviceId>) {
do_router_solicitation(self, ctx, device_id)
}
}
/// Solicit routers once and schedule next message.
fn do_router_solicitation<C: RsNonSyncContext<SC::DeviceId>, SC: RsContext<C>>(
sync_ctx: &mut SC,
ctx: &mut C,
device_id: SC::DeviceId,
) {
let src_ll = sync_ctx.get_link_layer_addr_bytes(device_id).map(|a| a.to_vec());
// TODO(https://fxbug.dev/85055): Either panic or guarantee that this error
// can't happen statically.
let _: Result<(), _> =
sync_ctx.send_rs_packet(ctx, device_id, RouterSolicitation::default(), |src_ip| {
// As per RFC 4861 section 4.1,
//
// Valid Options:
//
// Source link-layer address The link-layer address of the
// sender, if known. MUST NOT be included if the
// Source Address is the unspecified address.
// Otherwise, it SHOULD be included on link
// layers that have addresses.
src_ip.map_or(EitherSerializer::A(EmptyBuf), |UnicastAddr { .. }| {
EitherSerializer::B(
OptionSequenceBuilder::new(
src_ll
.as_ref()
.map(AsRef::as_ref)
.into_iter()
.map(NdpOptionBuilder::SourceLinkLayerAddress),
)
.into_serializer(),
)
})
});
let remaining = sync_ctx.get_router_soliciations_remaining_mut(device_id);
*remaining = NonZeroU8::new(
remaining
.expect("should only send a router solicitations when at least one is remaining")
.get()
- 1,
);
match *remaining {
None => {}
Some(NonZeroU8 { .. }) => {
assert_eq!(
ctx.schedule_timer(RTR_SOLICITATION_INTERVAL, RsTimerId { device_id },),
None
);
}
}
}
#[cfg(test)]
mod tests {
use net_declare::net_ip_v6;
use packet_formats::icmp::ndp::{options::NdpOption, Options};
use test_case::test_case;
use super::*;
use crate::{
context::{
testutil::{DummyCtx, DummyNonSyncCtx, DummySyncCtx, DummyTimerCtxExt as _},
FrameContext as _, InstantContext as _,
},
ip::DummyDeviceId,
};
struct MockRsContext<'a> {
max_router_solicitations: Option<NonZeroU8>,
router_soliciations_remaining: Option<NonZeroU8>,
source_address: Option<UnicastAddr<Ipv6Addr>>,
link_layer_bytes: Option<&'a [u8]>,
}
#[derive(Debug, PartialEq)]
struct RsMessageMeta {
message: RouterSolicitation,
}
type MockCtx<'a> = DummySyncCtx<MockRsContext<'a>, RsMessageMeta, DummyDeviceId>;
type MockNonSyncCtx = DummyNonSyncCtx<RsTimerId<DummyDeviceId>, (), ()>;
impl<'a> Ipv6DeviceRsContext<MockNonSyncCtx> for MockCtx<'a> {
fn get_max_router_solicitations(&self, DummyDeviceId: DummyDeviceId) -> Option<NonZeroU8> {
let MockRsContext {
max_router_solicitations,
router_soliciations_remaining: _,
source_address: _,
link_layer_bytes: _,
} = self.get_ref();
*max_router_solicitations
}
fn get_router_soliciations_remaining_mut(
&mut self,
DummyDeviceId: DummyDeviceId,
) -> &mut Option<NonZeroU8> {
let MockRsContext {
max_router_solicitations: _,
router_soliciations_remaining,
source_address: _,
link_layer_bytes: _,
} = self.get_mut();
router_soliciations_remaining
}
fn get_link_layer_addr_bytes(&self, DummyDeviceId: DummyDeviceId) -> Option<&[u8]> {
let MockRsContext {
max_router_solicitations: _,
router_soliciations_remaining: _,
source_address: _,
link_layer_bytes,
} = self.get_ref();
*link_layer_bytes
}
}
impl<'a> Ipv6LayerRsContext<MockNonSyncCtx> for MockCtx<'a> {
fn send_rs_packet<
S: Serializer<Buffer = EmptyBuf>,
F: FnOnce(Option<UnicastAddr<Ipv6Addr>>) -> S,
>(
&mut self,
ctx: &mut MockNonSyncCtx,
DummyDeviceId: DummyDeviceId,
message: RouterSolicitation,
body: F,
) -> Result<(), S> {
let MockRsContext {
max_router_solicitations: _,
router_soliciations_remaining: _,
source_address,
link_layer_bytes: _,
} = self.get_ref();
self.send_frame(ctx, RsMessageMeta { message }, body(*source_address))
}
}
const RS_TIMER_ID: RsTimerId<DummyDeviceId> = RsTimerId { device_id: DummyDeviceId };
#[test]
fn stop_router_solicitation() {
let DummyCtx { mut sync_ctx, mut non_sync_ctx } =
DummyCtx::with_sync_ctx(MockCtx::with_state(MockRsContext {
max_router_solicitations: NonZeroU8::new(1),
router_soliciations_remaining: None,
source_address: None,
link_layer_bytes: None,
}));
RsHandler::start_router_solicitation(&mut sync_ctx, &mut non_sync_ctx, DummyDeviceId);
let now = non_sync_ctx.now();
non_sync_ctx
.timer_ctx()
.assert_timers_installed([(RS_TIMER_ID, now..=now + MAX_RTR_SOLICITATION_DELAY)]);
RsHandler::stop_router_solicitation(&mut sync_ctx, &mut non_sync_ctx, DummyDeviceId);
non_sync_ctx.timer_ctx().assert_no_timers_installed();
assert_eq!(sync_ctx.frames(), &[][..]);
}
const SOURCE_ADDRESS: UnicastAddr<Ipv6Addr> =
unsafe { UnicastAddr::new_unchecked(net_ip_v6!("fe80::1")) };
#[test_case(0, None, None, None; "disabled")]
#[test_case(1, None, None, None; "once_without_source_address_or_link_layer_option")]
#[test_case(
1,
Some(SOURCE_ADDRESS),
None,
None; "once_with_source_address_and_without_link_layer_option")]
#[test_case(
1,
None,
Some(&[1, 2, 3, 4, 5, 6]),
None; "once_without_source_address_and_with_mac_address_source_link_layer_option")]
#[test_case(
1,
Some(SOURCE_ADDRESS),
Some(&[1, 2, 3, 4, 5, 6]),
Some(&[1, 2, 3, 4, 5, 6]); "once_with_source_address_and_mac_address_source_link_layer_option")]
#[test_case(
1,
Some(SOURCE_ADDRESS),
Some(&[1, 2, 3, 4, 5]),
Some(&[1, 2, 3, 4, 5, 0]); "once_with_source_address_and_short_address_source_link_layer_option")]
#[test_case(
1,
Some(SOURCE_ADDRESS),
Some(&[1, 2, 3, 4, 5, 6, 7]),
Some(&[
1, 2, 3, 4, 5, 6, 7,
0, 0, 0, 0, 0, 0, 0,
]); "once_with_source_address_and_long_address_source_link_layer_option")]
fn perform_router_solicitation(
max_router_solicitations: u8,
source_address: Option<UnicastAddr<Ipv6Addr>>,
link_layer_bytes: Option<&[u8]>,
expected_sll_bytes: Option<&[u8]>,
) {
let DummyCtx { mut sync_ctx, mut non_sync_ctx } =
DummyCtx::with_sync_ctx(MockCtx::with_state(MockRsContext {
max_router_solicitations: NonZeroU8::new(max_router_solicitations),
router_soliciations_remaining: None,
source_address,
link_layer_bytes,
}));
RsHandler::start_router_solicitation(&mut sync_ctx, &mut non_sync_ctx, DummyDeviceId);
assert_eq!(sync_ctx.frames(), &[][..]);
let mut duration = MAX_RTR_SOLICITATION_DELAY;
for i in 0..max_router_solicitations {
assert_eq!(
*sync_ctx.get_router_soliciations_remaining_mut(DummyDeviceId),
NonZeroU8::new(max_router_solicitations - i)
);
let now = non_sync_ctx.now();
non_sync_ctx.timer_ctx().assert_timers_installed([(RS_TIMER_ID, now..=now + duration)]);
assert_eq!(
non_sync_ctx.trigger_next_timer(&mut sync_ctx, RsHandler::handle_timer),
Some(RS_TIMER_ID)
);
let frames = sync_ctx.frames();
assert_eq!(frames.len(), usize::from(i + 1), "frames = {:?}", frames);
let (RsMessageMeta { message }, frame) =
frames.last().expect("should have transmitted a frame");
assert_eq!(*message, RouterSolicitation::default());
let options = Options::parse(&frame[..]).expect("parse NDP options");
let sll_bytes = options.iter().find_map(|o| match o {
NdpOption::SourceLinkLayerAddress(a) => Some(a),
o => panic!("unexpected NDP option = {:?}", o),
});
assert_eq!(sll_bytes, expected_sll_bytes);
duration = RTR_SOLICITATION_INTERVAL;
}
non_sync_ctx.timer_ctx().assert_no_timers_installed();
assert_eq!(*sync_ctx.get_router_soliciations_remaining_mut(DummyDeviceId), None);
let frames = sync_ctx.frames();
assert_eq!(frames.len(), usize::from(max_router_solicitations), "frames = {:?}", frames);
}
}