//! Stateless Address Autoconfiguration (SLAAC) for IPv6 — RFC 4862. //! //! Mirrors Linux 7.1's implementation in: //! - `net/ipv6/addrconf.c` — `addrconf_add_linklocal()` (link-local formation), //! `addrconf_prefix_rcv()` (RA prefix processing), `inet6_addr_add()` //! - `net/ipv6/ndisc.c` — `ndisc_send_rs()` (RS sending), //! `ndisc_router_discovery()` (RA processing) //! //! The autoconfiguration flow: //! 1. Interface gets a MAC → form link-local address `fe80::/10` + EUI-64 //! 2. Send Router Solicitation to `ff02::2` (all-routers multicast) //! — mirrors Linux's `ndisc_send_rs()` (ndisc.c:674) //! 3. Router responds with Router Advertisement containing Prefix //! Information options //! — mirrors Linux's `ndisc_router_discovery()` (ndisc.c:1233) //! 4. Extract prefix, validate lifetimes, form SLAAC address //! — mirrors Linux's `addrconf_prefix_rcv()` (addrconf.c:2792) //! 5. Apply address to the interface use smoltcp::time::{Duration, Instant}; use smoltcp::wire::{EthernetAddress, Ipv6Address, Ipv6Cidr}; pub const LINK_LOCAL_PREFIX: Ipv6Cidr = Ipv6Cidr::new( Ipv6Address::new(0xfe80, 0, 0, 0, 0, 0, 0, 0), 10, ); pub const ALL_ROUTERS_MULTICAST: Ipv6Address = Ipv6Address::new(0xff02, 0, 0, 0, 0, 0, 0, 2); pub const ALL_NODES_MULTICAST: Ipv6Address = Ipv6Address::new(0xff02, 0, 0, 0, 0, 0, 0, 1); const ICMPV6_RS: u8 = 133; const ICMPV6_RA: u8 = 134; const _ICMPV6_NS: u8 = 135; const _ICMPV6_NA: u8 = 136; const ND_OPT_SOURCE_LL_ADDR: u8 = 1; const _ND_OPT_TARGET_LL_ADDR: u8 = 2; const ND_OPT_PREFIX_INFO: u8 = 3; const RA_TIMEOUT: Duration = Duration::from_secs(5); const MAX_RS_RETRIES: u8 = 3; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SlacdState { Idle, Solicited(Instant), Configured, } impl Default for SlacdState { fn default() -> Self { Self::Idle } } pub fn eui64_from_mac(mac: EthernetAddress) -> [u8; 8] { let b = mac.as_bytes(); let mut eui = [0u8; 8]; eui[0] = b[0] ^ 0x02; eui[1] = b[1]; eui[2] = b[2]; eui[3] = 0xff; eui[4] = 0xfe; eui[5] = b[3]; eui[6] = b[4]; eui[7] = b[5]; eui } pub fn form_link_local(mac: EthernetAddress) -> Ipv6Cidr { let eui = eui64_from_mac(mac); let addr = Ipv6Address::new( 0xfe80, 0, 0, 0, u16::from_be_bytes([eui[0], eui[1]]), u16::from_be_bytes([eui[2], eui[3]]), u16::from_be_bytes([eui[4], eui[5]]), u16::from_be_bytes([eui[6], eui[7]]), ); Ipv6Cidr::new(addr, 64) } pub fn form_slaac_addr(prefix: Ipv6Cidr, mac: EthernetAddress) -> Ipv6Address { let eui = eui64_from_mac(mac); let prefix_bytes = prefix.address().octets(); Ipv6Address::new( u16::from_be_bytes([prefix_bytes[0], prefix_bytes[1]]), u16::from_be_bytes([prefix_bytes[2], prefix_bytes[3]]), u16::from_be_bytes([prefix_bytes[4], prefix_bytes[5]]), u16::from_be_bytes([prefix_bytes[6], prefix_bytes[7]]), u16::from_be_bytes([eui[0], eui[1]]), u16::from_be_bytes([eui[2], eui[3]]), u16::from_be_bytes([eui[4], eui[5]]), u16::from_be_bytes([eui[6], eui[7]]), ) } /// Builds an ICMPv6 Router Solicitation message with source link-layer /// address option. Mirrors Linux's `ndisc_send_rs()` (ndisc.c:674). pub fn build_router_solicitation(mac: EthernetAddress) -> Vec { let mac_bytes = mac.as_bytes(); let mut rs = vec![ ICMPV6_RS, 0x00, 0x00, 0x00, 0x00, 0x00, ND_OPT_SOURCE_LL_ADDR, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]; rs.extend_from_slice(mac_bytes); rs } /// Parsed Router Advertisement information. /// Mirrors Linux's `addrconf_prefix_rcv()` (addrconf.c:2792). #[derive(Debug, Clone)] pub struct ParsedRa { pub cur_hop_limit: u8, pub router_lifetime: u16, pub reachable_time: u32, pub retrans_timer: u32, pub prefixes: Vec, } #[derive(Debug, Clone)] pub struct RaPrefix { pub prefix: Ipv6Cidr, pub on_link: bool, pub autonomous: bool, pub valid_lifetime: u32, pub preferred_lifetime: u32, } /// Parses an ICMPv6 Router Advertisement (Type 134) and extracts Prefix /// Information options. Returns None if the packet is not a valid RA. pub fn parse_router_advertisement(data: &[u8]) -> Option { if data.len() < 16 || data[0] != ICMPV6_RA { return None; } let cur_hop_limit = data[1]; let router_lifetime = u16::from_be_bytes([data[6], data[7]]); let reachable_time = u32::from_be_bytes([data[8], data[9], data[10], data[11]]); let retrans_timer = u32::from_be_bytes([data[12], data[13], data[14], data[15]]); let mut prefixes = Vec::new(); let mut pos = 16; while pos + 2 <= data.len() { let opt_type = data[pos]; let opt_len = data[pos + 1] as usize; pos += 2; if opt_len == 0 || pos + opt_len * 8 > data.len() { break; } if opt_type == ND_OPT_PREFIX_INFO && opt_len == 4 { let opt_data = &data[pos..pos + 32]; let prefix_bytes: [u8; 16] = opt_data[0..16].try_into().ok()?; let prefix = Ipv6Cidr::new( Ipv6Address::new( u16::from_be_bytes([prefix_bytes[0], prefix_bytes[1]]), u16::from_be_bytes([prefix_bytes[2], prefix_bytes[3]]), u16::from_be_bytes([prefix_bytes[4], prefix_bytes[5]]), u16::from_be_bytes([prefix_bytes[6], prefix_bytes[7]]), u16::from_be_bytes([prefix_bytes[8], prefix_bytes[9]]), u16::from_be_bytes([prefix_bytes[10], prefix_bytes[11]]), u16::from_be_bytes([prefix_bytes[12], prefix_bytes[13]]), u16::from_be_bytes([prefix_bytes[14], prefix_bytes[15]]), ), opt_data[2], ); let flags = opt_data[3]; let valid_lifetime = u32::from_be_bytes([opt_data[4], opt_data[5], opt_data[6], opt_data[7]]); let preferred_lifetime = u32::from_be_bytes([opt_data[8], opt_data[9], opt_data[10], opt_data[11]]); prefixes.push(RaPrefix { prefix, on_link: flags & 0x80 != 0, autonomous: flags & 0x40 != 0, valid_lifetime, preferred_lifetime, }); } pos += opt_len * 8; } Some(ParsedRa { cur_hop_limit, router_lifetime, reachable_time, retrans_timer, prefixes, }) } /// SLAAC daemon state machine. Drives RS → RA exchange and address /// configuration. Mirrors Linux's `addrconf_dad_start()` + /// `ndisc_router_discovery()` flow. #[derive(Debug)] pub struct Slacd { state: SlacdState, mac: EthernetAddress, ll_addr: Ipv6Cidr, retry_count: u8, } impl Slacd { pub fn new(mac: EthernetAddress) -> Self { let ll_addr = form_link_local(mac); Self { state: SlacdState::Idle, mac, ll_addr, retry_count: 0, } } pub fn state(&self) -> SlacdState { self.state } pub fn link_local(&self) -> Ipv6Cidr { self.ll_addr } /// Called periodically to drive the SLAAC state machine. /// Returns `Some(rs)` when a Router Solicitation should be sent. pub fn tick(&mut self, now: Instant) -> Option> { match self.state { SlacdState::Idle => { self.state = SlacdState::Solicited(now); self.retry_count = 0; Some(build_router_solicitation(self.mac)) } SlacdState::Solicited(since) => { if now > since + RA_TIMEOUT { if self.retry_count < MAX_RS_RETRIES { self.retry_count += 1; self.state = SlacdState::Solicited(now); return Some(build_router_solicitation(self.mac)); } self.state = SlacdState::Idle; } None } SlacdState::Configured => None, } } /// Processes a received Router Advertisement. Returns configured /// SLAAC addresses for autonomous prefixes. pub fn process_ra(&mut self, ra: &ParsedRa) -> Vec { let mut addrs = Vec::new(); for pfx in &ra.prefixes { if pfx.autonomous && pfx.valid_lifetime > 0 && pfx.prefix.prefix_len() == 64 { let addr = form_slaac_addr(pfx.prefix, self.mac); addrs.push(Ipv6Cidr::new(addr, pfx.prefix.prefix_len())); } } if !addrs.is_empty() { self.state = SlacdState::Configured; } addrs } }