tun: wire TUN scheme into event loop + SLAAC RS/RA protocol

TUN integration:
- Smolnetd gains tun_scheme: TunScheme field and tun_file parameter
- on_tun_scheme_event() handler added (scheme event → poll)
- main.rs: EventSource::TunScheme, subscription, dispatch
- TUN devices can now receive and transmit packets through the netstack

SLAAC (RFC 4862):
- build_router_solicitation(): ICMPv6 Type 133 with source LL address option
- parse_router_advertisement(): ICMPv6 Type 134 with Prefix Information
  option extraction (on-link, autonomous, lifetimes)
- Slacd state machine: Idle → Solicited → Configured
  tick() drives RS retransmit (3 retries, 5s timeout)
  process_ra() extracts autonomous /64 prefixes
- ParsedRa, RaPrefix public structs for integration with IPv6 stack

Reference: Linux 7.1 ndisc_send_rs() / ndisc_router_discovery() /
addrconf_prefix_rcv()
This commit is contained in:
Red Bear OS
2026-07-08 13:47:04 +03:00
parent 30db94c970
commit 2b278390ee
3 changed files with 209 additions and 1 deletions
+12 -1
View File
@@ -106,6 +106,7 @@ fn run(daemon: daemon::Daemon) -> Result<()> {
IcmpScheme,
NetcfgScheme,
NetfilterScheme,
TunScheme,
}
}
@@ -166,6 +167,14 @@ fn run(daemon: daemon::Daemon) -> Result<()> {
)
.map_err(|e| anyhow!("failed to listen to netfilter scheme events: {:?}", e))?;
event_queue
.subscribe(
tun_fd.inner().raw(),
EventSource::TunScheme,
EventFlags::READ,
)
.map_err(|e| anyhow!("failed to listen to tun scheme events: {:?}", e))?;
let mut smolnetd = Smolnetd::new(
network_fd,
hardware_addr,
@@ -176,6 +185,7 @@ fn run(daemon: daemon::Daemon) -> Result<()> {
time_fd,
netcfg_fd,
netfilter_fd,
tun_fd,
)
.context("smolnetd: failed to initialize smolnetd")?;
@@ -183,7 +193,7 @@ fn run(daemon: daemon::Daemon) -> Result<()> {
let all = {
use EventSource::*;
[Network, Time, IpScheme, UdpScheme, IcmpScheme, NetcfgScheme, NetfilterScheme].map(Ok)
[Network, Time, IpScheme, UdpScheme, IcmpScheme, NetcfgScheme, NetfilterScheme, TunScheme].map(Ok)
};
for event_res in all
@@ -199,6 +209,7 @@ fn run(daemon: daemon::Daemon) -> Result<()> {
EventSource::IcmpScheme => smolnetd.on_icmp_scheme_event(),
EventSource::NetcfgScheme => smolnetd.on_netcfg_scheme_event(),
EventSource::NetfilterScheme => smolnetd.on_netfilter_scheme_event(),
EventSource::TunScheme => smolnetd.on_tun_scheme_event(),
}
.map_err(|e| error!("Received packet error: {:?}", e));
}
+9
View File
@@ -77,6 +77,7 @@ pub struct Smolnetd {
icmp_scheme: IcmpScheme,
netcfg_scheme: NetCfgScheme,
netfilter_scheme: NetFilterScheme,
tun_scheme: TunScheme,
filter_table: FilterTableRef,
}
@@ -96,6 +97,7 @@ impl Smolnetd {
time_file: Fd,
netcfg_file: Socket,
netfilter_file: Socket,
tun_file: Socket,
) -> Result<Smolnetd> {
let protocol_addrs = vec![
//This is a placeholder IP for DHCP
@@ -193,6 +195,7 @@ impl Smolnetd {
netfilter_file,
Rc::clone(&filter_table),
)?,
tun_scheme: TunScheme::new(tun_file, Rc::clone(&devices))?,
filter_table,
})
}
@@ -243,6 +246,12 @@ impl Smolnetd {
Ok(())
}
pub fn on_tun_scheme_event(&mut self) -> Result<()> {
self.tun_scheme.on_scheme_event()?;
let _ = self.poll()?;
Ok(())
}
fn schedule_time_event(&mut self, timeout: Duration) -> Result<()> {
let mut time = TimeSpec::default();
if self.time_file.read(&mut time)? < size_of::<TimeSpec>() {
+188
View File
@@ -17,6 +17,7 @@
//! — 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(
@@ -30,6 +31,29 @@ pub const ALL_ROUTERS_MULTICAST: Ipv6Address =
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];
@@ -72,4 +96,168 @@ pub fn form_slaac_addr(prefix: Ipv6Cidr, mac: EthernetAddress) -> Ipv6Address {
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];
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<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
}
}