base: apply Red Bear patches on latest upstream/main
251 files: init, acpid, ipcd, netcfg, ihdgd, virtio-gpud, scheme-utils, inputd, block driver, ptyd, ramfs, randd, initfs bootstrap, path deps, version +rb0.3.1, author attribution
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
//! 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<u8> {
|
||||
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<RaPrefix>,
|
||||
}
|
||||
|
||||
#[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<ParsedRa> {
|
||||
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];
|
||||
// PIO field layout per RFC 4861 §4.6.2 (Type and Length already
|
||||
// consumed by pos += 2 above):
|
||||
// opt_data[0] = Prefix Length
|
||||
// opt_data[1] = Flags (L|A|Reserved1)
|
||||
// opt_data[2..6] = Valid Lifetime
|
||||
// opt_data[6..10] = Preferred Lifetime
|
||||
// opt_data[10..14] = Reserved2
|
||||
// opt_data[14..30] = Prefix (16 bytes)
|
||||
let prefix = Ipv6Cidr::new(
|
||||
Ipv6Address::new(
|
||||
u16::from_be_bytes([opt_data[14], opt_data[15]]),
|
||||
u16::from_be_bytes([opt_data[16], opt_data[17]]),
|
||||
u16::from_be_bytes([opt_data[18], opt_data[19]]),
|
||||
u16::from_be_bytes([opt_data[20], opt_data[21]]),
|
||||
u16::from_be_bytes([opt_data[22], opt_data[23]]),
|
||||
u16::from_be_bytes([opt_data[24], opt_data[25]]),
|
||||
u16::from_be_bytes([opt_data[26], opt_data[27]]),
|
||||
u16::from_be_bytes([opt_data[28], opt_data[29]]),
|
||||
),
|
||||
opt_data[0],
|
||||
);
|
||||
let flags = opt_data[1];
|
||||
let valid_lifetime = u32::from_be_bytes([opt_data[2], opt_data[3], opt_data[4], opt_data[5]]);
|
||||
let preferred_lifetime = u32::from_be_bytes([opt_data[6], opt_data[7], opt_data[8], opt_data[9]]);
|
||||
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<Vec<u8>> {
|
||||
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<Ipv6Cidr> {
|
||||
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
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use smoltcp::wire::Ipv6Address;
|
||||
|
||||
fn make_pio_64(prefix_len: u8) -> Vec<u8> {
|
||||
// RA: 16-byte header + 32-byte PIO option.
|
||||
// The parser advances pos by 2 (type+length) before bounds check,
|
||||
// so pos + opt_len*8 = 18 + 32 = 50 must be <= data.len() = 50.
|
||||
let mut p = vec![0u8; 50];
|
||||
// RA header: type=134, code=0, cksum=0, hop_limit=64,
|
||||
// flags=0, router_lifetime=9000, reachable=0, retransmit=0
|
||||
p[0] = 134; // RA type
|
||||
p[1] = 0; // code
|
||||
p[2] = 0; p[3] = 0; // checksum
|
||||
p[4] = 64; // hop limit
|
||||
p[5] = 0; // flags
|
||||
p[6] = 0; p[7] = 0; // router lifetime (will be ignored)
|
||||
p[8] = 0; p[9] = 0; p[10] = 0; p[11] = 0; // reachable
|
||||
p[12] = 0; p[13] = 0; p[14] = 0; p[15] = 0; // retransmit
|
||||
// PIO option: type=3, length=4, prefix_len, flags, valid, preferred, reserved2, prefix
|
||||
p[16] = 3; // type = PIO
|
||||
p[17] = 4; // length = 4 (32 bytes)
|
||||
p[18] = prefix_len; // prefix length
|
||||
p[19] = 0xc0; // flags: L+A set
|
||||
p[20] = 0; p[21] = 0; p[22] = 0; p[23] = 1; // valid = 1s
|
||||
p[24] = 0; p[25] = 0; p[26] = 0; p[27] = 1; // preferred = 1s
|
||||
p[28] = 0; p[29] = 0; p[30] = 0; p[31] = 0; // reserved
|
||||
// Prefix 2001:db8:: (at opt_data[14..30] within the PIO = p[32..48] in full RA)
|
||||
let prefix = [0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||
for (i, b) in prefix.iter().enumerate() {
|
||||
p[32 + i] = *b;
|
||||
}
|
||||
p
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ra_with_pio_64_parses_correctly() {
|
||||
// Regression test: previous off-by-2 bug read prefix from
|
||||
// the wrong field and used wrong prefix length.
|
||||
let p = make_pio_64(64);
|
||||
let ra = parse_router_advertisement(&p);
|
||||
assert!(ra.is_some(), "Valid RA should parse");
|
||||
let ra = ra.unwrap();
|
||||
assert_eq!(ra.prefixes.len(), 1);
|
||||
let pfx = &ra.prefixes[0];
|
||||
// Verify the prefix length is 64 (not garbage from field 2)
|
||||
assert_eq!(pfx.prefix.prefix_len(), 64,
|
||||
"prefix_len should be 64, was {}", pfx.prefix.prefix_len());
|
||||
// Verify the prefix address is 2001:db8::
|
||||
let expected = Ipv6Address::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0);
|
||||
assert_eq!(pfx.prefix.address().octets(), expected.octets());
|
||||
// Verify flags
|
||||
assert!(pfx.on_link, "L flag should be set");
|
||||
assert!(pfx.autonomous, "A flag should be set");
|
||||
// Verify lifetimes (1s = 1)
|
||||
assert_eq!(pfx.valid_lifetime, 1);
|
||||
assert_eq!(pfx.preferred_lifetime, 1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user