From c069d40d7c7625fe9dbdbf7c49d1c0a032bf3e10 Mon Sep 17 00:00:00 2001 From: Koen Zandberg Date: Thu, 16 Jan 2025 09:14:18 +0100 Subject: [PATCH 1/8] interface: Split state maintenance from poll function This commit splits the state maintenance from the poll function to a separate poll_maintenance function. --- src/iface/interface/mod.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/iface/interface/mod.rs b/src/iface/interface/mod.rs index b00446f94..88bcb1e8a 100644 --- a/src/iface/interface/mod.rs +++ b/src/iface/interface/mod.rs @@ -430,7 +430,8 @@ impl Interface { /// If this is a concern for your application (i.e. your environment doesn't /// have preemptive scheduling, or `poll()` is called from a main loop where /// other important things are processed), you may use the lower-level methods - /// [`poll_egress()`](Self::poll_egress) and [`poll_ingress_single()`](Self::poll_ingress_single). + /// [`poll_egress()`](Self::poll_egress), [`poll_maintenance()`](Self::poll_maintenance) + /// and [`poll_ingress_single()`](Self::poll_ingress_single). /// This allows you to insert yields or process other events between processing /// individual ingress packets. pub fn poll( @@ -443,8 +444,7 @@ impl Interface { let mut res = PollResult::None; - #[cfg(feature = "_proto-fragmentation")] - self.fragments.assembler.remove_expired(timestamp); + self.poll_maintenance(timestamp); // Process ingress while there's packets available. loop { @@ -518,6 +518,16 @@ impl Interface { self.socket_ingress(device, sockets) } + /// Maintain stateful processing on the device. + /// + /// This is guaranteed to always perform a bounded amount of work. + pub fn poll_maintenance(&mut self, timestamp: Instant) { + self.inner.now = timestamp; + + #[cfg(feature = "_proto-fragmentation")] + self.fragments.assembler.remove_expired(timestamp); + } + /// Return a _soft deadline_ for calling [poll] the next time. /// The [Instant] returned is the time at which you should call [poll] next. /// It is harmless (but wastes energy) to call it before the [Instant], and From 29f5db25f162b3651443980cbac8bc460b1ef59c Mon Sep 17 00:00:00 2001 From: Koen Zandberg Date: Sat, 18 Jan 2025 17:46:27 +0100 Subject: [PATCH 2/8] IPv6: Add IPv6 autogenerated address functions --- src/wire/ethernet.rs | 11 +++++++ src/wire/ipv6.rs | 72 ++++++++++++++++++++++++++++++++++++++++++++ src/wire/mod.rs | 11 +++++++ 3 files changed, 94 insertions(+) diff --git a/src/wire/ethernet.rs b/src/wire/ethernet.rs index 89ff830ca..42fcfb06c 100644 --- a/src/wire/ethernet.rs +++ b/src/wire/ethernet.rs @@ -65,6 +65,17 @@ impl Address { pub const fn is_local(&self) -> bool { self.0[0] & 0x02 != 0 } + + /// Convert the address to an Extended Unique Identifier (EUI-64) + pub fn as_eui_64(&self) -> Option<[u8; 8]> { + let mut bytes = [0; 8]; + bytes[0..3].copy_from_slice(&self.0[0..3]); + bytes[3] = 0xFF; + bytes[4] = 0xFE; + bytes[5..8].copy_from_slice(&self.0[3..6]); + bytes[0] ^= 1 << 1; + Some(bytes) + } } impl fmt::Display for Address { diff --git a/src/wire/ipv6.rs b/src/wire/ipv6.rs index 24904b60a..badac4823 100644 --- a/src/wire/ipv6.rs +++ b/src/wire/ipv6.rs @@ -5,6 +5,7 @@ use core::fmt; use super::{Error, Result}; use crate::wire::ip::pretty_print_ip_payload; +use crate::wire::HardwareAddress; pub use super::IpProtocol as Protocol; @@ -83,6 +84,12 @@ pub(crate) trait AddressExt { /// The function panics if `data` is not sixteen octets long. fn from_bytes(data: &[u8]) -> Address; + /// Create an IPv6 address based on the provided prefix and hardware identifier. + fn from_link_prefix( + link_prefix: &Cidr, + interface_identifier: HardwareAddress, + ) -> Option
; + /// Query whether the IPv6 address is an [unicast address]. /// /// [unicast address]: https://tools.ietf.org/html/rfc4291#section-2.5 @@ -142,6 +149,23 @@ impl AddressExt for Address { Address::from(bytes) } + fn from_link_prefix( + link_prefix: &Cidr, + interface_identifier: HardwareAddress, + ) -> Option
{ + if let Some(eui64) = interface_identifier.as_eui_64() { + if link_prefix.prefix_len() != 64 { + return None; + } + let mut bytes = [0; 16]; + bytes[0..8].copy_from_slice(&link_prefix.address().octets()[0..8]); + bytes[8..16].copy_from_slice(&eui64); + Some(Address::from_bytes(&bytes)) + } else { + None + } + } + fn x_is_unicast(&self) -> bool { !(self.is_multicast() || self.is_unspecified()) } @@ -259,6 +283,15 @@ impl Cidr { } } + /// Create an IPv6 CIDR based on the provided prefix and hardware identifier. + pub fn from_link_prefix( + link_prefix: &Cidr, + interface_identifier: HardwareAddress, + ) -> Option { + Address::from_link_prefix(link_prefix, interface_identifier) + .map(|address| Self::new(address, link_prefix.prefix_len())) + } + /// Return the address of this IPv6 CIDR block. pub const fn address(&self) -> Address { self.address @@ -930,6 +963,45 @@ pub(crate) mod test { assert!(cidr_without_prefix.contains_addr(&Address::LOCALHOST)); } + #[test] + fn test_from_eui_64() { + #[cfg(feature = "medium-ethernet")] + use crate::wire::EthernetAddress; + #[cfg(feature = "medium-ieee802154")] + use crate::wire::Ieee802154Address; + let tests: std::vec::Vec<(HardwareAddress, Address)> = vec![ + #[cfg(feature = "medium-ethernet")] + ( + HardwareAddress::Ethernet(EthernetAddress::from_bytes(&[ + 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, + ])), + Address::new(0x2001, 0xdb8, 0x3, 0x0, 0xa8bb, 0xccff, 0xfedd, 0xeeff), + ), + #[cfg(feature = "medium-ieee802154")] + ( + HardwareAddress::Ieee802154(Ieee802154Address::from_bytes(&[ + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, + ])), + Address::new(0x2001, 0xdb8, 0x3, 0x0, 0x0211, 0x2233, 0x4455, 0x6677), + ), + ]; + + let prefix = Cidr::new(GLOBAL_UNICAST_ADDR.mask(64).into(), 64); + let wrong_prefix = Cidr::new(GLOBAL_UNICAST_ADDR.mask(72).into(), 72); + + for (hardware, result) in tests { + let generated = Address::from_link_prefix(&prefix, hardware).unwrap(); + assert!(prefix.contains_addr(&generated)); + assert_eq!(generated, result); + assert!(Address::from_link_prefix(&wrong_prefix, hardware).is_none()); + + let generated = Cidr::from_link_prefix(&prefix, hardware).unwrap(); + assert!(prefix.contains_subnet(&generated)); + assert_eq!(generated.address(), result); + assert!(Cidr::from_link_prefix(&wrong_prefix, hardware).is_none()); + } + } + #[test] #[should_panic(expected = "length")] fn test_from_bytes_too_long() { diff --git a/src/wire/mod.rs b/src/wire/mod.rs index 478f0cffc..e8a836e9b 100644 --- a/src/wire/mod.rs +++ b/src/wire/mod.rs @@ -426,6 +426,17 @@ impl HardwareAddress { HardwareAddress::Ieee802154(_) => Medium::Ieee802154, } } + + pub fn as_eui_64(&self) -> Option<[u8; 8]> { + match self { + #[cfg(feature = "medium-ip")] + HardwareAddress::Ip => None, + #[cfg(feature = "medium-ethernet")] + HardwareAddress::Ethernet(ethernet) => ethernet.as_eui_64(), + #[cfg(feature = "medium-ieee802154")] + HardwareAddress::Ieee802154(ieee802154) => ieee802154.as_eui_64(), + } + } } #[cfg(any( From 4627a5c350723972640a3832857891a3076838ce Mon Sep 17 00:00:00 2001 From: Koen Zandberg Date: Wed, 5 Feb 2025 17:40:48 +0100 Subject: [PATCH 3/8] ndisc: add router advertisement validity check --- src/wire/ndiscoption.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/wire/ndiscoption.rs b/src/wire/ndiscoption.rs index 583d9b569..35e33ef05 100644 --- a/src/wire/ndiscoption.rs +++ b/src/wire/ndiscoption.rs @@ -402,6 +402,16 @@ pub struct PrefixInformation { pub prefix: Ipv6Address, } +impl PrefixInformation { + /// Validates the prefix information option against check a, b, c in + /// https://www.rfc-editor.org/rfc/rfc4862#section-5.5.3 + pub fn valid_prefix_info(&self) -> bool { + self.flags.contains(PrefixInfoFlags::ADDRCONF) + && !self.prefix.is_link_local() + && self.preferred_lifetime <= self.valid_lifetime + } +} + #[derive(Debug, PartialEq, Eq, Clone, Copy)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct RedirectedHeader<'a> { From 74bd6d201c3345e9cdb3cbbbb5dedcb7dcb98e39 Mon Sep 17 00:00:00 2001 From: Koen Zandberg Date: Wed, 5 Feb 2025 17:40:12 +0100 Subject: [PATCH 4/8] slaac: add initial SLAAC implementation --- Cargo.toml | 9 + README.md | 5 + build.rs | 1 + gen_config.py | 1 + src/iface/mod.rs | 10 + src/iface/slaac.rs | 492 +++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 7 files changed, 519 insertions(+) create mode 100644 src/iface/slaac.rs diff --git a/Cargo.toml b/Cargo.toml index c3ae7d556..36a696129 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -184,6 +184,15 @@ iface-max-route-count-256 = [] iface-max-route-count-512 = [] iface-max-route-count-1024 = [] +iface-max-prefix-count-1 = [] # Default +iface-max-prefix-count-2 = [] +iface-max-prefix-count-3 = [] +iface-max-prefix-count-4 = [] +iface-max-prefix-count-5 = [] +iface-max-prefix-count-6 = [] +iface-max-prefix-count-7 = [] +iface-max-prefix-count-8 = [] + fragmentation-buffer-size-256 = [] fragmentation-buffer-size-512 = [] fragmentation-buffer-size-1024 = [] diff --git a/README.md b/README.md index 45c4c4970..949702281 100644 --- a/README.md +++ b/README.md @@ -256,6 +256,11 @@ Amount of "IP address -> hardware address" entries the neighbor cache (also know Max amount of routes that can be added to one interface. Includes the default route. Includes both IPv4 and IPv6. Default: 2. +### `IFACE_MAX_PREFIX_COUNT` + +Max amount of IPv6 prefixes that can be added to one interface via SLAAC. +Should be lower or equal to `IFACE_MAX_ADDR_COUNT`. + ### `FRAGMENTATION_BUFFER_SIZE` Size of the buffer used for fragmenting outgoing packets larger than the MTU. Packets larger than this setting will be dropped instead of fragmented. Default: 1500. diff --git a/build.rs b/build.rs index e1746d23f..0eab8edbe 100644 --- a/build.rs +++ b/build.rs @@ -11,6 +11,7 @@ static CONFIGS: &[(&str, usize)] = &[ ("IFACE_MAX_SIXLOWPAN_ADDRESS_CONTEXT_COUNT", 4), ("IFACE_NEIGHBOR_CACHE_COUNT", 8), ("IFACE_MAX_ROUTE_COUNT", 2), + ("IFACE_MAX_PREFIX_COUNT", 1), ("FRAGMENTATION_BUFFER_SIZE", 1500), ("ASSEMBLER_MAX_SEGMENT_COUNT", 4), ("REASSEMBLY_BUFFER_SIZE", 1500), diff --git a/gen_config.py b/gen_config.py index 1407ca2d6..58a2d7737 100644 --- a/gen_config.py +++ b/gen_config.py @@ -32,6 +32,7 @@ def feature(name, default, min, max, pow2=None): feature("iface_max_sixlowpan_address_context_count", default=4, min=1, max=1024, pow2=8) feature("iface_neighbor_cache_count", default=8, min=1, max=1024, pow2=8) feature("iface_max_route_count", default=2, min=1, max=1024, pow2=8) +feature("iface_max_prefix_count", default=1, min=1, max=8) feature("fragmentation_buffer_size", default=1500, min=256, max=65536, pow2=True) feature("assembler_max_segment_count", default=4, min=1, max=32, pow2=4) feature("reassembly_buffer_size", default=1500, min=256, max=65536, pow2=True) diff --git a/src/iface/mod.rs b/src/iface/mod.rs index 5b028d055..4b0653130 100644 --- a/src/iface/mod.rs +++ b/src/iface/mod.rs @@ -11,6 +11,11 @@ mod neighbor; mod route; #[cfg(feature = "proto-rpl")] mod rpl; +#[cfg(all( + feature = "proto-ipv6", + any(feature = "medium-ethernet", feature = "medium-ieee802154") +))] +mod slaac; mod socket_meta; mod socket_set; @@ -23,4 +28,9 @@ pub use self::interface::{ }; pub use self::route::{Route, RouteTableFull, Routes}; +#[cfg(all( + feature = "proto-ipv6", + any(feature = "medium-ethernet", feature = "medium-ieee802154") +))] +pub use self::slaac::Slaac; pub use self::socket_set::{SocketHandle, SocketSet, SocketStorage}; diff --git a/src/iface/slaac.rs b/src/iface/slaac.rs new file mode 100644 index 000000000..9d33b30f2 --- /dev/null +++ b/src/iface/slaac.rs @@ -0,0 +1,492 @@ +#![deny(missing_docs)] +use heapless::{LinearMap, Vec}; + +use crate::config::{IFACE_MAX_PREFIX_COUNT, IFACE_MAX_ROUTE_COUNT}; +use crate::time::{Duration, Instant}; +use crate::wire::NdiscPrefixInfoFlags; +use crate::wire::{ipv6::AddressExt, Ipv6Address, Ipv6Cidr, NdiscPrefixInformation}; + +const MAX_RTR_SOLICITATIONS: u8 = 3; +const RTR_SOLICITATION_INTERVAL: Duration = Duration::from_secs(4); +const IPV6_DEFAULT: Ipv6Cidr = Ipv6Cidr::new(Ipv6Address::UNSPECIFIED, 0); + +/// Router solicitation state machine +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Phase { + Start, + Discovering, + Maintaining, + None, +} + +/// A prefix of addresses received via router advertisements +#[derive(Debug, Clone, Copy)] +pub(crate) struct Route { + /// IPv6 cidr to route + pub cidr: Ipv6Cidr, + /// Router, origin of the advertisement + pub via_router: Ipv6Address, + /// Valid lifetime of the route + pub valid_until: Instant, +} + +/// Info associated with a prefix +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct PrefixInfo { + preferred_until: Instant, + valid_until: Instant, +} + +impl PrefixInfo { + fn new(preferred_until: Instant, valid_until: Instant) -> Self { + Self { + preferred_until, + valid_until, + } + } + + /// Derive the prefix information from the neighbor discovery option. + pub(crate) fn from_prefix(prefix: &NdiscPrefixInformation, now: Instant) -> Self { + let preferred_until = now + prefix.preferred_lifetime; + let valid_until = now + prefix.valid_lifetime; + + Self::new(preferred_until, valid_until) + } + + /// Get whether the prefix is still valid. + pub(crate) fn is_valid(&self, now: Instant) -> bool { + self.valid_until > now + } +} + +impl Route { + /// Compare this route based on the prefix and the next hop router. + pub fn same_route(&self, cidr: &Ipv6Cidr, via_router: &Ipv6Address) -> bool { + self.cidr == *cidr && self.via_router == *via_router + } + + /// Get whether the route is still valid. + pub fn is_valid(&self, now: Instant) -> bool { + self.valid_until > now + } +} + +/// SLAAC runtime state +/// +/// Tracks router solicitations and collects information from all received +/// router advertisements. +/// +/// State must be synchronized with the IP addresses and routes in the `Interface`. +#[derive(Debug)] +pub struct Slaac { + /// Set of prefixes received. + prefix: LinearMap, + /// Set of routes received. + routes: Vec, + /// Router discovery phase. + phase: Phase, + /// Signal for address and route updates. + sync_required: bool, + /// Time to next router solicitation. + retry_rs_at: Instant, + /// Number of solicitations emitted. + num_solicitations: u8, +} + +impl Slaac { + pub(super) fn new() -> Self { + Self { + prefix: LinearMap::new(), + routes: Vec::new(), + phase: Phase::Start, + sync_required: false, + retry_rs_at: Instant::from_millis(0), + num_solicitations: MAX_RTR_SOLICITATIONS, + } + } + + /// Get whether router advertisement information is updated. + /// + /// This flags whether new prefixes or routes have been received, or current prefixes and + /// routes have expired. + pub(crate) fn has_ra_update(&self) -> bool { + self.sync_required + } + + /// Get a reference to the map of prefixes stored. + pub(crate) fn prefix(&self) -> &LinearMap { + &self.prefix + } + + /// Get a reference to the set of routes stored. + pub(crate) fn routes(&self) -> &Vec { + &self.routes + } + + fn add_prefix(&mut self, cidr: &Ipv6Cidr, prefix: &NdiscPrefixInformation, now: Instant) { + if cidr.address().is_link_local() { + return; + } + let prefix_info = PrefixInfo::from_prefix(prefix, now); + if let Ok(old_info) = self.prefix.insert(*cidr, prefix_info) { + if old_info.is_none() { + self.sync_required = true; + } + } + } + + fn expire_prefix(&mut self, cidr: &Ipv6Cidr) { + if let Some(info) = self.prefix.get_mut(cidr) { + info.valid_until = Instant::from_millis(0); + info.preferred_until = Instant::from_millis(0); + self.sync_required = true; + } + } + + fn add_route(&mut self, cidr: &Ipv6Cidr, router: &Ipv6Address, valid_until: Instant) { + if let Some(route) = self.routes.iter_mut().find(|r| r.same_route(cidr, router)) { + route.valid_until = valid_until; + } else { + let _ = self.routes.push(Route { + cidr: *cidr, + via_router: *router, + valid_until, + }); + self.sync_required = true; + } + } + + fn expire_route(&mut self, cidr: &Ipv6Cidr, via_router: &Ipv6Address) { + for route in self.routes.iter_mut() { + if route.same_route(cidr, via_router) { + route.valid_until = Instant::from_millis(0); + self.sync_required = true; + } + } + } + + fn process_prefix(&mut self, prefix: NdiscPrefixInformation, now: Instant) { + if !prefix.flags.contains(NdiscPrefixInfoFlags::ADDRCONF) { + return; + } + + let cidr = Ipv6Cidr::new(prefix.prefix, prefix.prefix_len); + + if prefix.valid_lifetime > Duration::ZERO { + self.add_prefix(&cidr, &prefix, now); + } else { + self.expire_prefix(&cidr); + } + } + + /// Process a router advertisement's information. + pub(super) fn process_advertisement( + &mut self, + source: &Ipv6Address, + router_lifetime: Duration, // default route lifetime + prefix: Option, // prefix info + now: Instant, + ) { + if let Some(prefix) = prefix { + if prefix.valid_prefix_info() { + self.process_prefix(prefix, now) + } + } + + if router_lifetime > Duration::ZERO { + self.add_route(&IPV6_DEFAULT, source, now + router_lifetime); + } else { + self.expire_route(&IPV6_DEFAULT, source); + } + + // Advertisement might be unsolicited + if self.phase == Phase::Discovering { + self.phase = Phase::Maintaining; + } + } + + fn prefix_expire_sync_required(&self, now: Instant) -> bool { + self.prefix.values().any(|info| !info.is_valid(now)) + } + + fn route_expire_sync_required(&self, now: Instant) -> bool { + self.routes.iter().any(|r| !r.is_valid(now)) + } + + /// Get whether a route and prefix information must be synchronized with the interface. + pub(crate) fn sync_required(&self, now: Instant) -> bool { + self.has_ra_update() + || self.prefix_expire_sync_required(now) + || self.route_expire_sync_required(now) + } + + /// Remove expired routes and prefixes. + pub(crate) fn update_slaac_state(&mut self, now: Instant) { + let removals: Vec = self + .prefix + .iter() + .filter_map(|(cidr, info)| { + if info.is_valid(now) { + None + } else { + Some(*cidr) + } + }) + .collect(); + for cidr in removals.iter() { + self.prefix.remove(cidr); + } + self.routes.retain(|r| r.is_valid(now)); + self.sync_required = false; + } + + /// Get whether a router solicitation must be emitted. + pub(crate) fn rs_required(&self, now: Instant) -> bool { + match self.phase { + Phase::Start | Phase::Discovering + if self.retry_rs_at <= now && self.num_solicitations > 0 => + { + true + } + _ => false, + } + } + + /// Update router solicitation tracking state + /// + /// Must be called after sending a router solicitation on the interface. + pub(crate) fn rs_sent(&mut self, now: Instant) { + match self.phase { + Phase::Start | Phase::Discovering if self.retry_rs_at <= now => { + if self.num_solicitations == 0 { + self.phase = Phase::None; + } else { + self.num_solicitations -= 1; + self.phase = Phase::Discovering; + self.retry_rs_at = now + RTR_SOLICITATION_INTERVAL; + } + } + _ => (), + } + } + + /// Get the next time the SLAAC state must be polled for updates. + pub(crate) fn poll_at(&self, now: Instant) -> Option { + match self.phase { + Phase::Discovering | Phase::Start => Some(self.retry_rs_at), + Phase::Maintaining => { + let prefix_at = self.prefix.values().filter_map(|prefix_info| { + if prefix_info.is_valid(now) { + Some(prefix_info.valid_until) + } else { + None + } + }); + let routes_at = self.routes.iter().filter_map(|r| { + if r.is_valid(now) { + Some(r.valid_until) + } else { + None + } + }); + prefix_at.chain(routes_at).min() + } + _ => None, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + mod mock { + use super::super::*; + pub const SOURCE: Ipv6Address = Ipv6Address::new(0xfe80, 0xdb8, 0, 0, 0, 0, 0, 0); + pub const PREFIX: NdiscPrefixInformation = NdiscPrefixInformation { + prefix_len: 64, + flags: NdiscPrefixInfoFlags::ADDRCONF, + valid_lifetime: Duration::from_secs(700), + preferred_lifetime: Duration::from_secs(300), + prefix: Ipv6Address::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0), + }; + pub const VALID: Duration = Duration::from_secs(600); + + pub const ROUTE: Route = Route { + cidr: Ipv6Cidr::new(Ipv6Address::UNSPECIFIED, 0), + via_router: SOURCE, + valid_until: Instant::from_millis_const(100000), + }; + } + use mock::*; + + #[test] + fn test_route() { + assert!(ROUTE.same_route(&Ipv6Cidr::new(Ipv6Address::UNSPECIFIED, 0), &SOURCE)); + assert!(!ROUTE.same_route(&Ipv6Cidr::new(Ipv6Address::UNSPECIFIED, 64), &SOURCE)); + assert!(!ROUTE.same_route( + &Ipv6Cidr::new(Ipv6Address::UNSPECIFIED, 0), + &Ipv6Address::UNSPECIFIED + )); + assert!(!ROUTE.same_route(&Ipv6Cidr::new(SOURCE, 0), &Ipv6Address::UNSPECIFIED)); + assert!(!ROUTE.same_route(&Ipv6Cidr::new(SOURCE, 64), &Ipv6Address::UNSPECIFIED)); + } + + #[test] + fn test_route_valid() { + assert!(ROUTE.is_valid(Instant::ZERO)); + assert!(!ROUTE.is_valid(Instant::from_secs(200))); + } + + #[test] + fn test_solicitation() { + let mut slaac = Slaac::new(); + let now = Instant::from_millis(1); + assert!(slaac.rs_required(now)); + + slaac.rs_sent(now); + assert_eq!(slaac.num_solicitations, 2); + assert!(!slaac.rs_required(now)); + + let next_poll = slaac.poll_at(now).unwrap(); + assert_eq!(next_poll, now + RTR_SOLICITATION_INTERVAL); + + let now = next_poll; + assert!(slaac.rs_required(now)); + + slaac.num_solicitations = 0; + assert!(!slaac.rs_required(now)); + slaac.rs_sent(now); + assert_eq!(slaac.phase, Phase::None); + assert!(slaac.poll_at(now).is_none()); + } + + #[test] + fn test_ra_state() { + let mut slaac = Slaac::new(); + assert_eq!(slaac.phase, Phase::Start); + let now = Instant::from_millis(1); + assert!(!slaac.has_ra_update()); + + // Unsolicited advertisement + slaac.process_advertisement(&SOURCE, VALID, Some(PREFIX), now); + assert_eq!(slaac.phase, Phase::Start); + assert!(slaac.has_ra_update()); + + let now = Instant::from_secs(300); + slaac.rs_sent(now); + assert_eq!(slaac.phase, Phase::Discovering); + + // Solicited advertisement + slaac.process_advertisement(&SOURCE, VALID, Some(PREFIX), now); + slaac.process_advertisement(&SOURCE, VALID, Some(PREFIX), now); + assert_eq!(slaac.phase, Phase::Maintaining); + let poll_at = slaac.poll_at(now).unwrap(); + assert_eq!(poll_at, now + VALID); + + for (prefix, info) in slaac.prefix() { + assert_eq!(prefix.address(), PREFIX.prefix); + assert_eq!(prefix.prefix_len(), PREFIX.prefix_len); + assert_eq!(info.valid_until, now + PREFIX.valid_lifetime); + assert_eq!(info.preferred_until, now + PREFIX.preferred_lifetime); + assert!(info.is_valid(now)); + } + + for route in slaac.routes() { + assert_eq!(route.cidr, Ipv6Cidr::new(Ipv6Address::UNSPECIFIED, 0)); + assert_eq!(route.via_router, SOURCE); + assert_eq!(route.valid_until, now + VALID); + assert!(route.is_valid(now)); + } + assert_eq!(slaac.prefix().len(), 1); + assert_eq!(slaac.routes().len(), 1); + assert!(slaac.sync_required(now)); + + slaac.update_slaac_state(now); + assert!(!slaac.sync_required(now)); + + // Skip time until the route expires + let now = poll_at; + assert!(slaac.sync_required(now)); + for (_prefix, info) in slaac.prefix() { + assert!(info.is_valid(now)); + } + for route in slaac.routes() { + assert!(!route.is_valid(now)); + } + + slaac.update_slaac_state(now); + assert!(!slaac.sync_required(now)); + assert_eq!(slaac.routes().len(), 0); + + // Skip time until the prefix expires + let poll_at = slaac.poll_at(now).unwrap(); + let now = poll_at; + assert!(slaac.sync_required(now)); + for (_prefix, info) in slaac.prefix() { + assert!(!info.is_valid(now)); + } + // Should already return None + assert!(slaac.poll_at(now).is_none()); + slaac.update_slaac_state(now); + assert!(!slaac.sync_required(now)); + assert_eq!(slaac.routes().len(), 0); + assert_eq!(slaac.prefix().len(), 0); + + // No state remaining, nothing to wait on + assert!(slaac.poll_at(now).is_none()); + } + + #[test] + fn test_ra_expire() { + let mut slaac = Slaac::new(); + let now = Instant::from_millis(1); + slaac.rs_sent(now); + slaac.process_advertisement(&SOURCE, VALID, Some(PREFIX), now); + + let now = Instant::from_secs(300); + + assert!(slaac.sync_required(now)); + for (_prefix, info) in slaac.prefix() { + assert!(info.is_valid(now)); + } + for route in slaac.routes() { + assert!(route.is_valid(now)); + } + slaac.update_slaac_state(now); + + let mut expire_prefix = PREFIX; + expire_prefix.preferred_lifetime = Duration::ZERO; + expire_prefix.valid_lifetime = Duration::ZERO; + + // Invalidate the prefix, but not the route + slaac.process_advertisement(&SOURCE, VALID, Some(expire_prefix), now); + + assert!(slaac.sync_required(now)); + for (_prefix, info) in slaac.prefix() { + assert!(!info.is_valid(now)); + } + for route in slaac.routes() { + assert!(route.is_valid(now)); + } + slaac.update_slaac_state(now); + assert_eq!(slaac.prefix().len(), 0); + assert_eq!(slaac.routes().len(), 1); + + assert!(!slaac.sync_required(now)); + // Invalidate also the route + slaac.process_advertisement(&SOURCE, Duration::ZERO, Some(expire_prefix), now); + assert!(slaac.sync_required(now)); + for route in slaac.routes() { + assert!(!route.is_valid(now)); + } + assert!(slaac.poll_at(now).is_none()); + + slaac.update_slaac_state(now); + assert_eq!(slaac.prefix().len(), 0); + assert_eq!(slaac.routes().len(), 0); + assert!(!slaac.sync_required(now)); + // No state remaining, nothing to wait on + assert!(slaac.poll_at(now).is_none()); + } +} diff --git a/src/lib.rs b/src/lib.rs index 4b1073501..083c2fbc9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -140,6 +140,7 @@ pub mod config { pub const IFACE_MAX_ADDR_COUNT: usize = 8; pub const IFACE_MAX_MULTICAST_GROUP_COUNT: usize = 4; pub const IFACE_MAX_ROUTE_COUNT: usize = 4; + pub const IFACE_MAX_PREFIX_COUNT: usize = 1; pub const IFACE_MAX_SIXLOWPAN_ADDRESS_CONTEXT_COUNT: usize = 4; pub const IFACE_NEIGHBOR_CACHE_COUNT: usize = 3; pub const REASSEMBLY_BUFFER_COUNT: usize = 4; From a4deb316b3504a4d8119fb352e30e2f4d0fdb849 Mon Sep 17 00:00:00 2001 From: Koen Zandberg Date: Wed, 5 Feb 2025 17:41:26 +0100 Subject: [PATCH 5/8] interface: Hook up SLAAC to interface --- README.md | 4 +- src/iface/interface/ipv6.rs | 155 +++++++++++++++++++ src/iface/interface/mod.rs | 61 +++++++- src/iface/interface/tests/ipv6.rs | 249 ++++++++++++++++++++++++++++++ src/wire/ipv6.rs | 6 + 5 files changed, 472 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 949702281..18a00073b 100644 --- a/README.md +++ b/README.md @@ -103,8 +103,8 @@ The ICMPv6 protocol is supported, and ICMP sockets are available. #### NDISC * Neighbor Advertisement messages are generated in response to Neighbor Solicitations. - * Router Advertisement messages are **not** generated or read. - * Router Solicitation messages are **not** generated or read. + * Router Advertisement messages are read, but **not** generated. + * Router Solicitation messages are generated, but **not** read. * Redirected Header messages are **not** generated or read. ### UDP layer diff --git a/src/iface/interface/ipv6.rs b/src/iface/interface/ipv6.rs index ca8c2ef35..372024f48 100644 --- a/src/iface/interface/ipv6.rs +++ b/src/iface/interface/ipv6.rs @@ -1,5 +1,7 @@ use super::*; +use crate::iface::Route; + /// Enum used for the process_hopbyhop function. In some cases, when discarding a packet, an ICMP /// parameter problem message needs to be transmitted to the source of the address. In other cases, /// the processing of the IP packet can continue. @@ -502,6 +504,30 @@ impl InterfaceInner { None } } + NdiscRepr::RouterAdvert { + hop_limit: _, + flags: _, + router_lifetime, + reachable_time: _, + retrans_time: _, + lladdr: _, + mtu: _, + prefix_info, + } if self.slaac_enabled => { + if ip_repr.src_addr.is_link_local() + && (ip_repr.dst_addr == IPV6_LINK_LOCAL_ALL_NODES + || ip_repr.dst_addr.is_link_local()) + && ip_repr.hop_limit == 255 + { + self.slaac.process_advertisement( + &ip_repr.src_addr, + router_lifetime, + prefix_info, + self.now, + ) + } + None + } _ => None, } } @@ -581,3 +607,132 @@ impl InterfaceInner { )) } } + +impl Interface { + /// Synchronize the slaac address and router state with the interface state. + #[cfg(all( + feature = "proto-ipv6", + any(feature = "medium-ethernet", feature = "medium-ieee802154") + ))] + pub(super) fn sync_slaac_state(&mut self, timestamp: Instant) { + let required_addresses: Vec<_, IFACE_MAX_PREFIX_COUNT> = self + .inner + .slaac + .prefix() + .iter() + .filter_map(|(prefix, prefixinfo)| { + if prefixinfo.is_valid(timestamp) { + Ipv6Cidr::from_link_prefix(prefix, self.inner.hardware_addr()) + } else { + None + } + }) + .collect(); + let removed_addresses: Vec<_, IFACE_MAX_PREFIX_COUNT> = self + .inner + .slaac + .prefix() + .iter() + .filter_map(|(prefix, prefixinfo)| { + if !prefixinfo.is_valid(timestamp) { + Ipv6Cidr::from_link_prefix(prefix, self.inner.hardware_addr()) + } else { + None + } + }) + .collect(); + + self.update_ip_addrs(|addresses| { + for address in required_addresses { + if !addresses.contains(&IpCidr::Ipv6(address)) { + let _ = addresses.push(IpCidr::Ipv6(address)); + } + } + addresses.retain(|address| { + if let IpCidr::Ipv6(address) = address { + !removed_addresses.contains(address) + } else { + true + } + }); + }); + + { + let required_routes = self + .inner + .slaac + .routes() + .into_iter() + .filter(|required| required.is_valid(timestamp)); + + let removed_routes = self + .inner + .slaac + .routes() + .into_iter() + .filter(|r| !r.is_valid(timestamp)); + + self.inner.routes.update(|routes| { + routes.retain(|r| match (&r.cidr, &r.via_router) { + (IpCidr::Ipv6(cidr), IpAddress::Ipv6(via_router)) => !removed_routes + .clone() + .any(|f| f.same_route(cidr, via_router)), + _ => true, + }); + + for route in required_routes { + if routes.iter().all(|r| match (&r.cidr, &r.via_router) { + (IpCidr::Ipv6(cidr), IpAddress::Ipv6(via_router)) => { + !route.same_route(cidr, via_router) + } + _ => false, + }) { + let _ = routes.push(Route { + cidr: route.cidr.into(), + via_router: route.via_router.into(), + preferred_until: None, + expires_at: None, + }); + } + } + }); + } + + self.inner.slaac.update_slaac_state(timestamp); + } + + /// Emit a router solicitation when required by the interface's slaac state machine. + #[cfg(all( + feature = "proto-ipv6", + any(feature = "medium-ethernet", feature = "medium-ieee802154") + ))] + pub(super) fn ndisc_rs_egress(&mut self, device: &mut (impl Device + ?Sized)) { + if !self.inner.slaac.rs_required(self.inner.now) { + return; + } + let rs_repr = Icmpv6Repr::Ndisc(NdiscRepr::RouterSolicit { + lladdr: Some(self.hardware_addr().into()), + }); + let ipv6_repr = Ipv6Repr { + src_addr: self.inner.link_local_ipv6_address().unwrap(), + dst_addr: IPV6_LINK_LOCAL_ALL_ROUTERS, + next_header: IpProtocol::Icmpv6, + payload_len: rs_repr.buffer_len(), + hop_limit: 255, + }; + let packet = Packet::new_ipv6(ipv6_repr, IpPayload::Icmpv6(rs_repr)); + let Some(tx_token) = device.transmit(self.inner.now) else { + return; + }; + // NOTE(unwrap): packet destination is multicast, which is always routable and doesn't require neighbor discovery. + self.inner + .dispatch_ip( + tx_token, + PacketMeta::default(), + packet, + &mut self.fragmenter, + ) + .unwrap(); + self.inner.slaac.rs_sent(self.inner.now); + } +} diff --git a/src/iface/interface/mod.rs b/src/iface/interface/mod.rs index 88bcb1e8a..809c464d3 100644 --- a/src/iface/interface/mod.rs +++ b/src/iface/interface/mod.rs @@ -38,8 +38,15 @@ use super::fragmentation::{Fragmenter, FragmentsBuffer}; #[cfg(any(feature = "medium-ethernet", feature = "medium-ieee802154"))] use super::neighbor::{Answer as NeighborAnswer, Cache as NeighborCache}; use super::socket_set::SocketSet; -use crate::config::{IFACE_MAX_ADDR_COUNT, IFACE_MAX_SIXLOWPAN_ADDRESS_CONTEXT_COUNT}; +use crate::config::{ + IFACE_MAX_ADDR_COUNT, IFACE_MAX_PREFIX_COUNT, IFACE_MAX_SIXLOWPAN_ADDRESS_CONTEXT_COUNT, +}; use crate::iface::Routes; +#[cfg(all( + feature = "proto-ipv6", + any(feature = "medium-ethernet", feature = "medium-ieee802154") +))] +use crate::iface::Slaac; use crate::phy::PacketMeta; use crate::phy::{ChecksumCapabilities, Device, DeviceCapabilities, Medium, RxToken, TxToken}; use crate::rand::Rand; @@ -142,6 +149,16 @@ pub struct InterfaceInner { tag: u16, ip_addrs: Vec, any_ip: bool, + #[cfg(all( + feature = "proto-ipv6", + any(feature = "medium-ethernet", feature = "medium-ieee802154") + ))] + slaac_enabled: bool, + #[cfg(all( + feature = "proto-ipv6", + any(feature = "medium-ethernet", feature = "medium-ieee802154") + ))] + slaac: Slaac, routes: Routes, #[cfg(feature = "multicast")] multicast: multicast::State, @@ -169,6 +186,10 @@ pub struct Config { /// **NOTE**: we use the same PAN ID for destination and source. #[cfg(feature = "medium-ieee802154")] pub pan_id: Option, + + /// Enable stateless address autoconfiguration on the interface. + #[cfg(feature = "proto-ipv6")] + pub slaac: bool, } impl Config { @@ -178,6 +199,8 @@ impl Config { hardware_addr, #[cfg(feature = "medium-ieee802154")] pan_id: None, + #[cfg(feature = "proto-ipv6")] + slaac: false, } } } @@ -262,6 +285,16 @@ impl Interface { ipv4_id, #[cfg(feature = "proto-sixlowpan")] sixlowpan_address_context: Vec::new(), + #[cfg(all( + feature = "proto-ipv6", + any(feature = "medium-ethernet", feature = "medium-ieee802154") + ))] + slaac_enabled: config.slaac, + #[cfg(all( + feature = "proto-ipv6", + any(feature = "medium-ethernet", feature = "medium-ieee802154") + ))] + slaac: Slaac::new(), rand, }, } @@ -491,6 +524,14 @@ impl Interface { } } + #[cfg(all( + feature = "proto-ipv6", + any(feature = "medium-ethernet", feature = "medium-ieee802154") + ))] + if self.inner.slaac_enabled { + self.ndisc_rs_egress(device); + } + #[cfg(feature = "multicast")] self.multicast_egress(device); @@ -526,6 +567,14 @@ impl Interface { #[cfg(feature = "_proto-fragmentation")] self.fragments.assembler.remove_expired(timestamp); + + #[cfg(all( + feature = "proto-ipv6", + any(feature = "medium-ethernet", feature = "medium-ieee802154") + ))] + if self.inner.slaac.sync_required(timestamp) { + self.sync_slaac_state(timestamp) + } } /// Return a _soft deadline_ for calling [poll] the next time. @@ -546,6 +595,15 @@ impl Interface { let inner = &mut self.inner; + let other_polls = [ + #[cfg(all( + feature = "proto-ipv6", + any(feature = "medium-ethernet", feature = "medium-ieee802154") + ))] + inner.slaac.poll_at(timestamp), + None, + ]; + sockets .items() .filter_map(move |item| { @@ -559,6 +617,7 @@ impl Interface { PollAt::Now => Some(Instant::from_millis(0)), } }) + .chain(other_polls.into_iter().flatten()) .min() } diff --git a/src/iface/interface/tests/ipv6.rs b/src/iface/interface/tests/ipv6.rs index 5a75b5b8c..258f24f5e 100644 --- a/src/iface/interface/tests/ipv6.rs +++ b/src/iface/interface/tests/ipv6.rs @@ -839,6 +839,255 @@ fn test_handle_valid_ndisc_request(#[case] medium: Medium) { ); } +#[rstest] +#[case(Medium::Ethernet)] +#[cfg(feature = "medium-ethernet")] +fn test_router_advertisement(#[case] medium: Medium) { + fn recv_icmpv6( + device: &mut crate::tests::TestingDevice, + timestamp: Instant, + ) -> std::vec::Vec>> { + let caps = device.capabilities(); + recv_all(device, timestamp) + .iter() + .filter_map(|frame| { + let ipv6_packet = match caps.medium { + #[cfg(feature = "medium-ethernet")] + Medium::Ethernet => { + let eth_frame = EthernetFrame::new_checked(frame).ok()?; + Ipv6Packet::new_checked(eth_frame.payload()).ok()? + } + #[cfg(feature = "medium-ip")] + Medium::Ip => Ipv6Packet::new_checked(&frame[..]).ok()?, + #[cfg(feature = "medium-ieee802154")] + Medium::Ieee802154 => todo!(), + }; + let buf = ipv6_packet.into_inner().to_vec(); + Some(Ipv6Packet::new_unchecked(buf)) + }) + .collect::>() + } + let prefix_addr = Ipv6Address::new(0x2001, 0xdb8, 0x3, 0, 0, 0, 0, 0); + + let mut device = crate::tests::TestingDevice::new(medium); + let caps = device.capabilities(); + let checksum_caps = &caps.checksum; + + let mut eth_bytes = vec![0u8; 102]; + + // Create mac addresses with derived link local addresses + let local_hw_addr = EthernetAddress([0x02, 0x02, 0x02, 0x02, 0x02, 0x02]); + let remote_hw_addr = EthernetAddress([0x52, 0x54, 0x00, 0x00, 0x00, 0x00]); + let ll_prefix = Ipv6Cidr::new(Ipv6Cidr::LINK_LOCAL_PREFIX.address(), 64); + let local_ip_addr = + Ipv6Cidr::from_link_prefix(&ll_prefix, HardwareAddress::Ethernet(local_hw_addr)).unwrap(); + let remote_ip_addr = + Ipv6Cidr::from_link_prefix(&ll_prefix, HardwareAddress::Ethernet(remote_hw_addr)).unwrap(); + + // Create config with slaac enabled + let mut config = Config::new(match medium { + #[cfg(feature = "medium-ethernet")] + Medium::Ethernet => HardwareAddress::Ethernet(local_hw_addr), + _ => panic!("Not supported"), + }); + config.slaac = true; + + // Set up interface with link local address + let mut iface = Interface::new(config, &mut device, Instant::ZERO); + iface.update_ip_addrs(|ip_addrs| { + ip_addrs.push(IpCidr::Ipv6(local_ip_addr)).unwrap(); + }); + + let mut sockets = SocketSet::new(vec![]); + iface.poll(Instant::ZERO, &mut device, &mut sockets); + + let transmitted: std::vec::Vec>> = + recv_icmpv6(&mut device, Instant::ZERO) + .into_iter() + .filter(|packet| { + // Filter for router solicitations + packet.dst_addr() == IPV6_LINK_LOCAL_ALL_ROUTERS + }) + .collect(); + + assert_eq!(transmitted.len(), 1); + + for ipv6_packet in transmitted.into_iter() { + let buf = ipv6_packet.into_inner(); + let ipv6_packet = Ipv6Packet::new_unchecked(buf.as_slice()); + let ipv6_repr = Ipv6Repr::parse(&ipv6_packet).unwrap(); + if ipv6_repr.dst_addr == IPV6_LINK_LOCAL_ALL_MLDV2_ROUTERS { + continue; // Skip MLD reports + } + let icmpv6_packet = Icmpv6Packet::new_checked(ipv6_packet.payload()).unwrap(); + let icmp_repr = Icmpv6Repr::parse( + &ipv6_repr.src_addr, + &ipv6_repr.dst_addr, + &icmpv6_packet, + checksum_caps, + ) + .unwrap(); + + assert_eq!( + icmp_repr, + Icmpv6Repr::Ndisc(NdiscRepr::RouterSolicit { + lladdr: Some(local_hw_addr.into()), + }) + ); + + assert_eq!(ipv6_repr.dst_addr, IPV6_LINK_LOCAL_ALL_ROUTERS); + println!("repr {:?}", icmp_repr); + } + + // Craft the router advertisement + let mut prefix_information = NdiscPrefixInformation { + prefix: prefix_addr, + prefix_len: 64, + flags: NdiscPrefixInfoFlags::ADDRCONF, + valid_lifetime: Duration::from_secs(600), + preferred_lifetime: Duration::from_secs(300), + }; + let mut advertisement = NdiscRepr::RouterAdvert { + hop_limit: 255, + flags: NdiscRouterFlags::empty(), + router_lifetime: Duration::from_secs(600), + reachable_time: Duration::from_secs(0), + retrans_time: Duration::from_secs(0), + lladdr: None, + mtu: None, + prefix_info: Some(prefix_information), + }; + let ip_repr = IpRepr::Ipv6(Ipv6Repr { + src_addr: remote_ip_addr.address(), + dst_addr: local_ip_addr.address(), + next_header: IpProtocol::Icmpv6, + hop_limit: 255, + payload_len: advertisement.buffer_len(), + }); + let mut frame = EthernetFrame::new_unchecked(&mut eth_bytes); + frame.set_dst_addr(local_hw_addr); + frame.set_src_addr(remote_hw_addr); + frame.set_ethertype(EthernetProtocol::Ipv6); + ip_repr.emit(frame.payload_mut(), &ChecksumCapabilities::default()); + Icmpv6Repr::Ndisc(advertisement).emit( + &remote_ip_addr.address(), + &local_ip_addr.address(), + &mut Icmpv6Packet::new_unchecked(&mut frame.payload_mut()[ip_repr.header_len()..]), + &ChecksumCapabilities::default(), + ); + + iface.inner.process_ethernet( + &mut sockets, + PacketMeta::default(), + frame.into_inner(), + &mut iface.fragments, + ); + + iface.poll(Instant::ZERO, &mut device, &mut sockets); + + // Expect to have these two addresses after the router advertisement + let expected_addrs = [ + IpCidr::Ipv6(local_ip_addr), + IpCidr::Ipv6(Ipv6Cidr::new( + Ipv6Address::new(0x2001, 0xdb8, 0x3, 0x0, 0x2, 0x2ff, 0xfe02, 0x202), + 64, + )), + ]; + for (generated, expected) in iface.ip_addrs().iter().zip(expected_addrs.iter()) { + assert_eq!(generated, expected); + } + // Verify the pushed route matches expected + iface.routes_mut().update(|route| { + assert_eq!(route.len(), 1); + assert_eq!( + route[0].cidr, + IpCidr::new(IpAddress::v6(0, 0, 0, 0, 0, 0, 0, 0), 0) + ); + assert_eq!( + route[0].via_router, + IpAddress::Ipv6(remote_ip_addr.address()) + ); + assert_eq!(route[0].preferred_until, None); + assert_eq!(route[0].expires_at, None); + }); + + // Craft a router advertisement with zero lifetime for the prefix + // to remove the prefix, but retain the route + prefix_information.valid_lifetime = Duration::ZERO; + prefix_information.preferred_lifetime = Duration::ZERO; + if let NdiscRepr::RouterAdvert { + ref mut prefix_info, + .. + } = advertisement + { + *prefix_info = Some(prefix_information); + } + + let mut frame = EthernetFrame::new_unchecked(&mut eth_bytes); + frame.set_dst_addr(local_hw_addr); + frame.set_src_addr(remote_hw_addr); + frame.set_ethertype(EthernetProtocol::Ipv6); + ip_repr.emit(frame.payload_mut(), &ChecksumCapabilities::default()); + Icmpv6Repr::Ndisc(advertisement).emit( + &remote_ip_addr.address(), + &local_ip_addr.address(), + &mut Icmpv6Packet::new_unchecked(&mut frame.payload_mut()[ip_repr.header_len()..]), + &ChecksumCapabilities::default(), + ); + + iface.inner.process_ethernet( + &mut sockets, + PacketMeta::default(), + frame.into_inner(), + &mut iface.fragments, + ); + + let now = Instant::from_secs(10); + + iface.poll(now, &mut device, &mut sockets); + assert_eq!(iface.ip_addrs().len(), 1); + iface.routes_mut().update(|route| { + assert_eq!(route.len(), 1); + }); + + // Craft router advertisement with zero router lifetime + // to remove the route + if let NdiscRepr::RouterAdvert { + ref mut prefix_info, + ref mut router_lifetime, + .. + } = advertisement + { + *prefix_info = None; + *router_lifetime = Duration::ZERO; + } + + let mut frame = EthernetFrame::new_unchecked(&mut eth_bytes); + frame.set_dst_addr(local_hw_addr); + frame.set_src_addr(remote_hw_addr); + frame.set_ethertype(EthernetProtocol::Ipv6); + ip_repr.emit(frame.payload_mut(), &ChecksumCapabilities::default()); + Icmpv6Repr::Ndisc(advertisement).emit( + &remote_ip_addr.address(), + &local_ip_addr.address(), + &mut Icmpv6Packet::new_unchecked(&mut frame.payload_mut()[ip_repr.header_len()..]), + &ChecksumCapabilities::default(), + ); + + iface.inner.process_ethernet( + &mut sockets, + PacketMeta::default(), + frame.into_inner(), + &mut iface.fragments, + ); + + let now = Instant::from_secs(20); + iface.poll(now, &mut device, &mut sockets); + iface.routes_mut().update(|route| { + assert_eq!(route.len(), 0); + }); +} + #[rstest] #[case(Medium::Ip)] #[cfg(feature = "medium-ip")] diff --git a/src/wire/ipv6.rs b/src/wire/ipv6.rs index badac4823..27ef8f322 100644 --- a/src/wire/ipv6.rs +++ b/src/wire/ipv6.rs @@ -271,6 +271,12 @@ impl Cidr { prefix_len: 104, }; + /// The link-local address prefix. + pub const LINK_LOCAL_PREFIX: Cidr = Cidr { + address: Address::new(0xfe80, 0, 0, 0, 0, 0, 0, 0), + prefix_len: 10, + }; + /// Create an IPv6 CIDR block from the given address and prefix length. /// /// # Panics From 39738778a3e1e3e2d8210d6e3eda827666cae079 Mon Sep 17 00:00:00 2001 From: Koen Zandberg Date: Wed, 5 Feb 2025 17:41:48 +0100 Subject: [PATCH 6/8] examples/slaac: add SLAAC example --- Cargo.toml | 4 +++ examples/slaac.rs | 76 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 examples/slaac.rs diff --git a/Cargo.toml b/Cargo.toml index 36a696129..a266001f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -337,5 +337,9 @@ required-features = ["std", "medium-ieee802154", "phy-raw_socket", "proto-sixlow name = "dns" required-features = ["std", "medium-ethernet", "medium-ip", "phy-tuntap_interface", "proto-ipv4", "socket-dns"] +[[example]] +name = "slaac" +required-features = ["std", "medium-ethernet", "medium-ip", "phy-tuntap_interface", "proto-ipv6", "socket-udp"] + [profile.release] debug = 2 diff --git a/examples/slaac.rs b/examples/slaac.rs new file mode 100644 index 000000000..63174e0dc --- /dev/null +++ b/examples/slaac.rs @@ -0,0 +1,76 @@ +mod utils; + +use std::os::unix::io::AsRawFd; + +use smoltcp::iface::{Config, Interface, SocketSet}; +use smoltcp::phy::{wait as phy_wait, Device, Medium}; +use smoltcp::socket::udp; +use smoltcp::time::{Duration, Instant}; +use smoltcp::wire::{EthernetAddress, IpAddress, IpCidr, Ipv6Address}; + +const LOCAL_ADDR: Ipv6Address = Ipv6Address::new(0xfe80, 0, 0, 0, 0x0, 0, 0, 0x01); + +fn main() { + utils::setup_logging("warn"); + + let (mut opts, mut free) = utils::create_options(); + utils::add_tuntap_options(&mut opts, &mut free); + utils::add_middleware_options(&mut opts, &mut free); + + let mut matches = utils::parse_options(&opts, free); + let device = utils::parse_tuntap_options(&mut matches); + let fd = device.as_raw_fd(); + let mut device = + utils::parse_middleware_options(&mut matches, device, /*loopback=*/ false); + + // Create interface + let mut config = match device.capabilities().medium { + Medium::Ethernet => { + Config::new(EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]).into()) + } + Medium::Ip => Config::new(smoltcp::wire::HardwareAddress::Ip), + Medium::Ieee802154 => todo!(), + }; + config.slaac = true; + + let mut iface = Interface::new(config, &mut device, Instant::now()); + iface.update_ip_addrs(|ip_addrs| { + ip_addrs + .push(IpCidr::new(IpAddress::from(LOCAL_ADDR), 64)) + .unwrap(); + }); + + let mut sockets = SocketSet::new(vec![]); + let udp_rx_buffer = udp::PacketBuffer::new(vec![udp::PacketMetadata::EMPTY; 4], vec![0; 1024]); + let udp_tx_buffer = udp::PacketBuffer::new(vec![udp::PacketMetadata::EMPTY], vec![0; 0]); + let udp_socket = udp::Socket::new(udp_rx_buffer, udp_tx_buffer); + let _udp_handle = sockets.add(udp_socket); + + let mut last_print = Instant::now(); + loop { + let timestamp = Instant::now(); + iface.poll(timestamp, &mut device, &mut sockets); + let mut delay = iface.poll_delay(timestamp, &sockets); + if delay.is_none() || delay.is_some_and(|d| d > Duration::from_millis(1000)) { + delay = Some(Duration::from_millis(1000)); + } + + phy_wait(fd, delay).expect("wait error"); + + let timestamp = Instant::now(); + if timestamp > last_print + Duration::from_secs(1) { + last_print = timestamp; + println!(); + println!("Addresses:"); + for addr in iface.ip_addrs() { + println!(" - {addr}"); + } + println!("Routes:"); + iface.routes_mut().update(|routes| { + for route in routes { + println!(" - {} via {}", route.cidr, route.via_router); + } + }); + } + } +} From 6eb2406770a106fb2a122ada2fe7d056b7fb92f9 Mon Sep 17 00:00:00 2001 From: Koen Zandberg Date: Mon, 3 Mar 2025 17:28:18 +0100 Subject: [PATCH 7/8] fixup! slaac: add initial SLAAC implementation --- src/iface/slaac.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/iface/slaac.rs b/src/iface/slaac.rs index 9d33b30f2..199df06c7 100644 --- a/src/iface/slaac.rs +++ b/src/iface/slaac.rs @@ -189,7 +189,7 @@ impl Slaac { now: Instant, ) { if let Some(prefix) = prefix { - if prefix.valid_prefix_info() { + if prefix.is_valid_prefix_info() { self.process_prefix(prefix, now) } } From 8786ec8c31052964153fee450b3615111e3e245a Mon Sep 17 00:00:00 2001 From: Koen Zandberg Date: Mon, 3 Mar 2025 17:28:31 +0100 Subject: [PATCH 8/8] fixup! ndisc: add router advertisement validity check --- src/wire/ndiscoption.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wire/ndiscoption.rs b/src/wire/ndiscoption.rs index 35e33ef05..1706ffc97 100644 --- a/src/wire/ndiscoption.rs +++ b/src/wire/ndiscoption.rs @@ -405,7 +405,7 @@ pub struct PrefixInformation { impl PrefixInformation { /// Validates the prefix information option against check a, b, c in /// https://www.rfc-editor.org/rfc/rfc4862#section-5.5.3 - pub fn valid_prefix_info(&self) -> bool { + pub fn is_valid_prefix_info(&self) -> bool { self.flags.contains(PrefixInfoFlags::ADDRCONF) && !self.prefix.is_link_local() && self.preferred_lifetime <= self.valid_lifetime