From 2b278390eeacf6d27e9588345af9c74f9cf9f395 Mon Sep 17 00:00:00 2001 From: Red Bear OS Date: Wed, 8 Jul 2026 13:47:04 +0300 Subject: [PATCH] tun: wire TUN scheme into event loop + SLAAC RS/RA protocol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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() --- netstack/src/main.rs | 13 ++- netstack/src/scheme/mod.rs | 9 ++ netstack/src/slaac.rs | 188 +++++++++++++++++++++++++++++++++++++ 3 files changed, 209 insertions(+), 1 deletion(-) diff --git a/netstack/src/main.rs b/netstack/src/main.rs index a1990cf87d..aac3a58629 100644 --- a/netstack/src/main.rs +++ b/netstack/src/main.rs @@ -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)); } diff --git a/netstack/src/scheme/mod.rs b/netstack/src/scheme/mod.rs index d4f5d1a27a..9dff56271e 100644 --- a/netstack/src/scheme/mod.rs +++ b/netstack/src/scheme/mod.rs @@ -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 { 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::() { diff --git a/netstack/src/slaac.rs b/netstack/src/slaac.rs index 37bb7a547d..2b9ae4d7a7 100644 --- a/netstack/src/slaac.rs +++ b/netstack/src/slaac.rs @@ -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 { + 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 + } } \ No newline at end of file