From b9319f8c064f3d95466ff6007a339952bf8c7571 Mon Sep 17 00:00:00 2001 From: ZCShou <72115@163.com> Date: Fri, 12 Jun 2026 14:05:34 +0800 Subject: [PATCH 01/31] feat(ax-net): add multi-interface support with per-interface routing, DNS, and SO_BINDTODEVICE --- net/ax-net/src/config.rs | 127 ++++- net/ax-net/src/device/ethernet.rs | 11 +- net/ax-net/src/device/loopback.rs | 6 +- net/ax-net/src/device/mod.rs | 5 +- net/ax-net/src/general.rs | 36 +- net/ax-net/src/lib.rs | 446 +++++++++++++---- net/ax-net/src/options.rs | 3 + net/ax-net/src/raw.rs | 52 +- net/ax-net/src/router.rs | 525 +++++++++++++++++---- net/ax-net/src/service.rs | 489 +++++++++++++++---- net/ax-net/src/tcp.rs | 137 ++++-- net/ax-net/src/udp.rs | 148 ++++-- net/ax-net/src/wrapper.rs | 43 +- os/StarryOS/kernel/src/file/net.rs | 218 +++++---- os/StarryOS/kernel/src/file/netlink.rs | 153 +++--- os/StarryOS/kernel/src/file/packet.rs | 171 +++---- os/StarryOS/kernel/src/pseudofs/proc.rs | 18 +- os/StarryOS/kernel/src/syscall/net/opt.rs | 63 ++- os/arceos/modules/axruntime/src/devices.rs | 60 +-- 19 files changed, 1990 insertions(+), 721 deletions(-) diff --git a/net/ax-net/src/config.rs b/net/ax-net/src/config.rs index 149361f04f..23864090ff 100644 --- a/net/ax-net/src/config.rs +++ b/net/ax-net/src/config.rs @@ -1,17 +1,97 @@ -use alloc::vec::Vec; +use alloc::{string::String, vec::Vec}; use core::net::Ipv4Addr; -use smoltcp::wire::{Ipv4Address, Ipv4Cidr}; +use smoltcp::wire::{EthernetAddress, Ipv4Address, Ipv4Cidr}; + +/// Stable network interface identifier. +/// +/// The numeric value is also used as the Linux ifindex exposed by StarryOS. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct InterfaceId(u32); + +impl InterfaceId { + pub const LOOPBACK: Self = Self(1); + + pub const fn new(raw: u32) -> Self { + Self(raw) + } + + pub const fn get(self) -> u32 { + self.0 + } + + /// Convert to Linux ifindex (i32). + pub const fn to_linux_ifindex(self) -> i32 { + self.0 as i32 + } + + /// Create from Linux ifindex (i32), rejecting invalid values. + pub const fn from_linux_ifindex(ifindex: i32) -> Option { + if ifindex > 0 { + Some(Self(ifindex as u32)) + } else { + None + } + } +} + +/// Network interface kind. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum InterfaceKind { + Loopback, + Ethernet, +} + +bitflags::bitflags! { + /// Runtime interface flags. + #[derive(Debug, Clone, Copy, Eq, PartialEq)] + pub struct InterfaceFlags: u32 { + const UP = 1 << 0; + const RUNNING = 1 << 1; + const LOOPBACK = 1 << 2; + const BROADCAST = 1 << 3; + const MULTICAST = 1 << 4; + } +} + +/// Public snapshot of a network interface. +#[derive(Debug, Clone)] +pub struct InterfaceInfo { + pub id: InterfaceId, + pub name: String, + pub kind: InterfaceKind, + pub mac: Option, + pub ipv4: Option, + pub mtu: usize, + pub flags: InterfaceFlags, + pub metric: u32, +} + +/// Interface matching rule for explicit configuration. +#[derive(Debug, Clone)] +pub enum InterfaceMatcher { + ByOrder(usize), + ByMac(EthernetAddress), + ByDriverName(String), +} /// Network initialization configuration. #[derive(Debug, Clone, Default)] pub struct NetworkConfig { - /// Static IP configuration. If None, DHCP will be used. + /// Per-interface configuration. + pub interfaces: Vec, + /// DNS servers used when no interface-level DNS server is available. + pub default_dns_servers: Vec, +} + +/// Per-interface network configuration. +#[derive(Debug, Clone)] +pub struct InterfaceConfig { + pub name: String, + pub match_by: InterfaceMatcher, pub static_ip: Option, - /// DNS servers. - /// - /// - In **static IP mode**: these are the primary DNS servers. - /// - In **DHCP mode**: these are fallback servers, used only if DHCP doesn't provide DNS servers. + pub dhcp: bool, + pub metric: u32, pub dns_servers: Vec, } @@ -29,3 +109,36 @@ pub struct Ipv4InterfaceConfig { pub address: Ipv4Cidr, pub gateway: Option, } + +/// DNS server origin. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum DnsSource { + Dhcp, + Static, + Fallback, +} + +/// Internal DNS server entry with origin metadata. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub(crate) struct DnsServerEntry { + pub server: Ipv4Address, + pub interface_id: InterfaceId, + pub metric: u32, + pub source: DnsSource, +} + +/// Public route snapshot. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub struct RouteInfo { + pub filter: smoltcp::wire::IpCidr, + pub via: Option, + pub interface_id: InterfaceId, + pub source: smoltcp::wire::IpAddress, + pub metric: u32, +} + +/// Ordinary socket interface binding. +#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)] +pub struct DeviceBinding { + pub bound_if: Option, +} diff --git a/net/ax-net/src/device/ethernet.rs b/net/ax-net/src/device/ethernet.rs index 5c669f897a..186a19e179 100644 --- a/net/ax-net/src/device/ethernet.rs +++ b/net/ax-net/src/device/ethernet.rs @@ -14,6 +14,7 @@ use smoltcp::{ }; use crate::{ + config::InterfaceId, consts::{ETHERNET_MAX_PENDING_PACKETS, STANDARD_MTU}, device::{ArpEntry, Device, EthernetDriver, NetDeviceError, NetIrqEvents}, }; @@ -231,7 +232,8 @@ impl EthernetDevice { fn handle_frame( &mut self, frame: &[u8], - buffer: &mut PacketBuffer<()>, + interface_id: InterfaceId, + buffer: &mut PacketBuffer, timestamp: Instant, snoop: &mut dyn FnMut(&[u8]), ) -> bool { @@ -252,7 +254,7 @@ impl EthernetDevice { EthernetProtocol::Ipv4 => { snoop(frame.payload()); buffer - .enqueue(frame.payload().len(), ()) + .enqueue(frame.payload().len(), interface_id) .unwrap() .copy_from_slice(frame.payload()); return true; @@ -450,7 +452,8 @@ impl Device for EthernetDevice { fn recv( &mut self, - buffer: &mut PacketBuffer<()>, + interface_id: InterfaceId, + buffer: &mut PacketBuffer, timestamp: Instant, snoop: &mut dyn FnMut(&[u8]), ) -> bool { @@ -473,7 +476,7 @@ impl Device for EthernetDevice { rx_buf.packet() ); - let result = self.handle_frame(rx_buf.packet(), buffer, timestamp, snoop); + let result = self.handle_frame(rx_buf.packet(), interface_id, buffer, timestamp, snoop); if let Err(err) = self.inner.driver.lock().recycle_rx_buffer(&mut *rx_buf) { warn!("recycle_rx_buffer failed: {:?}", err); } diff --git a/net/ax-net/src/device/loopback.rs b/net/ax-net/src/device/loopback.rs index 9f43ee6f50..b3ca9069c0 100644 --- a/net/ax-net/src/device/loopback.rs +++ b/net/ax-net/src/device/loopback.rs @@ -9,6 +9,7 @@ use smoltcp::{ }; use crate::{ + config::InterfaceId, consts::{SOCKET_BUFFER_SIZE, STANDARD_MTU}, device::Device, }; @@ -37,14 +38,15 @@ impl Device for LoopbackDevice { fn recv( &mut self, - buffer: &mut PacketBuffer<()>, + interface_id: InterfaceId, + buffer: &mut PacketBuffer, _timestamp: Instant, snoop: &mut dyn FnMut(&[u8]), ) -> bool { self.buffer.dequeue().ok().is_some_and(|(_, rx_buf)| { snoop(rx_buf); buffer - .enqueue(rx_buf.len(), ()) + .enqueue(rx_buf.len(), interface_id) .unwrap() .copy_from_slice(rx_buf); true diff --git a/net/ax-net/src/device/mod.rs b/net/ax-net/src/device/mod.rs index 2a67c3a0e1..7a5dd04523 100644 --- a/net/ax-net/src/device/mod.rs +++ b/net/ax-net/src/device/mod.rs @@ -7,6 +7,8 @@ use smoltcp::{ wire::{IpAddress, Ipv4Cidr}, }; +use crate::config::InterfaceId; + mod driver; mod ethernet; mod loopback; @@ -33,7 +35,8 @@ pub trait Device: Send + Sync { fn recv( &mut self, - buffer: &mut PacketBuffer<()>, + interface_id: InterfaceId, + buffer: &mut PacketBuffer, timestamp: Instant, snoop: &mut dyn FnMut(&[u8]), ) -> bool; diff --git a/net/ax-net/src/general.rs b/net/ax-net/src/general.rs index b1c4d89500..d62c28808a 100644 --- a/net/ax-net/src/general.rs +++ b/net/ax-net/src/general.rs @@ -9,7 +9,8 @@ use ax_task::future::{block_on, poll_io, timeout}; use axpoll::{IoEvents, Pollable}; use crate::{ - get_service, + config::{DeviceBinding, InterfaceId}, + get_service, interface_by_id, options::{Configurable, GetSocketOption, SetSocketOption}, }; @@ -23,7 +24,7 @@ pub(crate) struct GeneralOptions { send_timeout_nanos: AtomicU64, recv_timeout_nanos: AtomicU64, - device_mask: AtomicU32, + bound_if: AtomicU32, /// Socket type: SOCK_STREAM (1), SOCK_DGRAM (2), SOCK_RAW (3). socket_type: AtomicI32, @@ -45,7 +46,7 @@ impl GeneralOptions { send_timeout_nanos: AtomicU64::new(0), recv_timeout_nanos: AtomicU64::new(0), - device_mask: AtomicU32::new(0), + bound_if: AtomicU32::new(0), socket_type: AtomicI32::new(socket_type), domain, @@ -71,16 +72,22 @@ impl GeneralOptions { (nanos > 0).then(|| Duration::from_nanos(nanos)) } - pub fn set_device_mask(&self, mask: u32) { - self.device_mask.store(mask, Ordering::Release); + pub fn set_device_binding(&self, binding: DeviceBinding) { + self.bound_if.store( + binding.bound_if.map_or(0, InterfaceId::get), + Ordering::Release, + ); } - pub fn device_mask(&self) -> u32 { - self.device_mask.load(Ordering::Acquire) + pub fn device_binding(&self) -> DeviceBinding { + let raw = self.bound_if.load(Ordering::Acquire); + DeviceBinding { + bound_if: (raw != 0).then_some(InterfaceId::new(raw)), + } } pub fn register_waker(&self, waker: &Waker) { - get_service().register_waker(self.device_mask(), waker); + get_service().register_waker(self.device_binding(), waker); } pub fn send_poller AxResult, T>( @@ -171,6 +178,9 @@ impl Configurable for GeneralOptions { O::SocketDomain(domain) => { **domain = self.domain; } + O::BindToDevice(binding) => { + **binding = self.device_binding().bound_if; + } _ => return Ok(false), } Ok(true) @@ -197,6 +207,16 @@ impl Configurable for GeneralOptions { O::SendBuffer(_) | O::ReceiveBuffer(_) => { // TODO(mivik): implement buffer size options } + O::BindToDevice(interface_id) => { + if let Some(id) = *interface_id + && interface_by_id(id).is_none() + { + return Err(AxError::NoSuchDevice); + } + self.set_device_binding(DeviceBinding { + bound_if: *interface_id, + }); + } O::RecvErr(_) => { // TODO: Retrieve ICMP errors via errqueue } diff --git a/net/ax-net/src/lib.rs b/net/ax-net/src/lib.rs index 028b229805..e42fa8847d 100644 --- a/net/ax-net/src/lib.rs +++ b/net/ax-net/src/lib.rs @@ -45,15 +45,17 @@ pub mod unix; pub mod vsock; mod wrapper; -use alloc::{borrow::ToOwned, boxed::Box, vec, vec::Vec}; +use alloc::{borrow::ToOwned, boxed::Box, format, sync::Arc, task::Wake, vec, vec::Vec}; use core::{ net::IpAddr, sync::atomic::{AtomicBool, Ordering}, + task::Waker, time::Duration, }; use ax_errno::{AxError, AxResult, ax_err_type}; use ax_sync::Mutex; +use ax_task::WaitQueue; use smoltcp::{ socket::dns::{self, GetQueryResultError, StartQueryError}, wire::{DnsQueryType, EthernetAddress, IpAddress, Ipv4Address, Ipv4Cidr}, @@ -63,7 +65,10 @@ use spin::{LazyLock, Once}; #[cfg(feature = "vsock")] pub use self::device::{VsockDevice, VsockDeviceList}; pub use self::{ - config::{Ipv4InterfaceConfig, NetworkConfig, StaticIpConfig}, + config::{ + DeviceBinding, InterfaceConfig, InterfaceFlags, InterfaceId, InterfaceInfo, InterfaceKind, + InterfaceMatcher, Ipv4InterfaceConfig, NetworkConfig, RouteInfo, StaticIpConfig, + }, device::{ ArpEntry, EthernetDeviceList, EthernetDriver, EthernetIrqAction, EthernetIrqOutcome, EthernetIrqRegistrar, EthernetIrqRegistration, EthernetIrqRegistrationError, @@ -78,8 +83,8 @@ pub use self::{ use self::{ device::{EthernetDevice, LoopbackDevice}, listen_table::ListenTable, - router::{Router, Rule}, - service::Service, + router::{RouteTable, Router, Rule, SharedRouteTable}, + service::{NetControl, NetInterface, Service}, wrapper::SocketSetWrapper, }; @@ -87,8 +92,13 @@ static LISTEN_TABLE: LazyLock = LazyLock::new(ListenTable::new); static SOCKET_SET: LazyLock = LazyLock::new(SocketSetWrapper::new); static SERVICE: Once> = Once::new(); +static NET_CONTROL: Once> = Once::new(); static POLLING_INTERFACES: AtomicBool = AtomicBool::new(false); static POLL_AGAIN: AtomicBool = AtomicBool::new(false); +static NET_POLL_REQUESTED: AtomicBool = AtomicBool::new(false); +static NET_POLL_WAKE: WaitQueue = WaitQueue::new(); +static NET_POLL_DEVICE_WAKER: LazyLock = + LazyLock::new(|| Waker::from(Arc::new(NetPollWake))); const DHCP_BOOTSTRAP_ATTEMPTS: usize = 200; const DHCP_BOOTSTRAP_POLL_INTERVAL: Duration = Duration::from_millis(10); @@ -100,6 +110,13 @@ fn get_service() -> ax_sync::MutexGuard<'static, Service> { .lock() } +pub(crate) fn get_control() -> &'static NetControl { + NET_CONTROL + .get() + .expect("Network service not initialized") + .as_ref() +} + /// Initializes the network subsystem by NIC devices. /// /// # Panics @@ -112,106 +129,232 @@ pub fn init_network(mut net_devs: EthernetDeviceList, config: NetworkConfig) { info!("Initialize network subsystem..."); - // Validate configuration - if let Some(ref static_cfg) = config.static_ip { - if static_cfg.ip.is_unspecified() { - panic!("Invalid static IP: unspecified address"); + for cfg in &config.interfaces { + if cfg.name == "lo" { + panic!("interface name 'lo' is reserved"); } - if static_cfg.prefix_len > 32 { - panic!("Invalid static IP: prefix length > 32"); + if cfg.dhcp && cfg.static_ip.is_some() { + panic!( + "interface {} has both DHCP and static IP configured", + cfg.name + ); } - if static_cfg.gateway.is_unspecified() { - panic!("Invalid gateway: unspecified address"); + if let Some(static_cfg) = &cfg.static_ip { + if static_cfg.ip.is_unspecified() { + panic!("Invalid static IP for {}: unspecified address", cfg.name); + } + if static_cfg.prefix_len > 32 { + panic!("Invalid static IP for {}: prefix length > 32", cfg.name); + } + if static_cfg.gateway.is_unspecified() { + panic!("Invalid gateway for {}: unspecified address", cfg.name); + } + } + for (i, dns) in cfg.dns_servers.iter().enumerate() { + if dns.is_unspecified() { + panic!( + "Invalid DNS server for {} at index {}: unspecified address", + cfg.name, i + ); + } } } - for (i, dns) in config.dns_servers.iter().enumerate() { + for (i, dns) in config.default_dns_servers.iter().enumerate() { if dns.is_unspecified() { panic!("Invalid DNS server at index {}: unspecified address", i); } } - // Convert DNS servers to smoltcp types - let static_dns: Vec = config - .dns_servers - .iter() - .map(|addr| Ipv4Address::from(addr.octets())) - .collect(); + let routes: SharedRouteTable = Arc::new(spin::RwLock::new(RouteTable::new())); + let mut router = Router::new(routes.clone()); + let mut interfaces = Vec::new(); + let mut dns = Vec::new(); - let mut router = Router::new(); - let lo_dev = router.add_device(Box::new(LoopbackDevice::new())); + let lo_id = InterfaceId::LOOPBACK; + let lo_dev = router.add_device(lo_id, Box::new(LoopbackDevice::new())); let lo_ip = Ipv4Cidr::new(Ipv4Address::new(127, 0, 0, 1), 8); router.add_rule(Rule::new( lo_ip.into(), None, lo_dev, + lo_id, lo_ip.address().into(), + 0, )); + interfaces.push(NetInterface { + id: lo_id, + name: "lo".to_owned(), + kind: InterfaceKind::Loopback, + mac: None, + ipv4: Some(lo_ip), + gateway: None, + mtu: consts::STANDARD_MTU, + metric: 0, + flags: InterfaceFlags::UP | InterfaceFlags::RUNNING | InterfaceFlags::LOOPBACK, + }); - let mut dhcp_dev = None; - let mut dhcp_mac = None; - - let eth0_ip = if !net_devs.is_empty() { - let dev = net_devs.remove(0); - info!(" use NIC 0: {:?}", dev.device_name()); - - let eth0_address = EthernetAddress(dev.mac_address()); - let eth0_ip = config - .static_ip - .as_ref() - .map(|cfg| Ipv4Cidr::new(Ipv4Address::from(cfg.ip.octets()), cfg.prefix_len)); - - let eth0_dev = router.add_device(Box::new(EthernetDevice::new( - "eth0".to_owned(), - dev, - eth0_ip, - ))); + if net_devs.is_empty() { + warn!(" No network device found!"); + } - info!("eth0:"); - info!(" mac: {}", eth0_address); - if let Some(static_cfg) = &config.static_ip { - router.add_rule(Rule::new( - Ipv4Cidr::new(Ipv4Address::UNSPECIFIED, 0).into(), - Some(Ipv4Address::from(static_cfg.gateway.octets()).into()), - eth0_dev, - Ipv4Address::from(static_cfg.ip.octets()).into(), - )); + let mut used_configs = vec![false; config.interfaces.len()]; + let mut dhcp_ifaces = Vec::new(); + let mut eth_ips = Vec::new(); + + for (order, dev) in net_devs.drain(..).enumerate() { + info!(" use NIC {}: {:?}", order, dev.device_name()); + let default_name = format!("eth{}", order); + let mac = EthernetAddress(dev.mac_address()); + let cfg_idx = find_interface_config( + &config.interfaces, + &mut used_configs, + order, + mac, + dev.device_name(), + ); + let cfg = cfg_idx.map(|idx| &config.interfaces[idx]); + let name = cfg.map_or(default_name, |cfg| cfg.name.clone()); + if interfaces.iter().any(|interface| interface.name == name) { + panic!("interface name conflict: {}", name); + } + let id = InterfaceId::new((order as u32) + 2); + let metric = cfg.map_or(100, |cfg| cfg.metric); + let static_ip = cfg.and_then(|cfg| cfg.static_ip.as_ref()); + let ipv4 = + static_ip.map(|cfg| Ipv4Cidr::new(Ipv4Address::from(cfg.ip.octets()), cfg.prefix_len)); + let gateway = static_ip.map(|cfg| Ipv4Address::from(cfg.gateway.octets())); + let eth_dev = router.add_device(id, Box::new(EthernetDevice::new(name.clone(), dev, ipv4))); + + info!("{name}:"); + info!(" id: {}", id.get()); + info!(" mac: {}", mac); + if let Some(ipv4) = ipv4 { + router.set_ipv4_config( + eth_dev, + id, + metric, + Some(ipv4), + gateway.map(IpAddress::Ipv4), + ); + eth_ips.push(ipv4); info!(" mode: static"); - info!(" ip: {}/{}", static_cfg.ip, static_cfg.prefix_len); - info!(" gw: {}", static_cfg.gateway); + info!(" ip: {}/{}", ipv4.address(), ipv4.prefix_len()); + if let Some(gateway) = gateway { + info!(" gw: {}", gateway); + } } else { - dhcp_dev = Some(eth0_dev); - dhcp_mac = Some(eth0_address); + dhcp_ifaces.push((id, eth_dev, name.clone(), mac, metric)); info!(" mode: dhcp"); } + if let Some(cfg) = cfg { + dns.extend( + cfg.dns_servers + .iter() + .copied() + .map(|server| config::DnsServerEntry { + server: Ipv4Address::from(server.octets()), + interface_id: id, + metric, + source: config::DnsSource::Static, + }), + ); + } + interfaces.push(NetInterface { + id, + name, + kind: InterfaceKind::Ethernet, + mac: Some(mac), + ipv4, + gateway, + mtu: consts::STANDARD_MTU, + metric, + flags: InterfaceFlags::UP + | InterfaceFlags::RUNNING + | InterfaceFlags::BROADCAST + | InterfaceFlags::MULTICAST, + }); + } - eth0_ip - } else { - warn!(" No network device found!"); - None - }; + for (i, used) in used_configs.iter().enumerate() { + if !used { + panic!( + "interface config {} did not match any device", + config.interfaces[i].name + ); + } + } - for dev in &router.devices { - info!("Device: {}", dev.name()); + dns.extend( + config + .default_dns_servers + .iter() + .copied() + .map(|server| config::DnsServerEntry { + server: Ipv4Address::from(server.octets()), + interface_id: lo_id, + metric: u32::MAX, + source: config::DnsSource::Fallback, + }), + ); + + for name in router.device_names() { + info!("Device: {}", name); } + router.start_rx_workers(); + router.start_tx_workers(); - let mut service = Service::new(router, static_dns); + let control = Arc::new(NetControl::new(interfaces, routes, dns)); + let mut service = Service::new(router, control.clone()); service.iface.update_ip_addrs(|ip_addrs| { ip_addrs.push(lo_ip.into()).unwrap(); - if let Some(eth0_ip) = eth0_ip { - ip_addrs.push(eth0_ip.into()).unwrap(); + for ip in eth_ips { + ip_addrs.push(ip.into()).unwrap(); } }); - if let (Some(dhcp_dev), Some(dhcp_mac)) = (dhcp_dev, dhcp_mac) { - service.enable_dhcp(dhcp_dev, dhcp_mac); + for (id, dev, name, mac, metric) in dhcp_ifaces { + service.enable_dhcp(id, dev, name, mac, metric); } let dhcp_enabled = service.dhcp_enabled(); + NET_CONTROL.call_once(|| control); SERVICE.call_once(|| Mutex::new(service)); + get_service().register_device_waker(&NET_POLL_DEVICE_WAKER); + ax_task::spawn_with_name(net_poll_worker, "net-poll".to_owned()); if dhcp_enabled { ax_task::spawn_with_name(dhcp_bootstrap, "dhcp-bootstrap".to_owned()); } } +fn find_interface_config( + configs: &[InterfaceConfig], + used: &mut [bool], + order: usize, + mac: EthernetAddress, + driver_name: &str, +) -> Option { + let mut matched = None; + for (idx, cfg) in configs.iter().enumerate() { + if used[idx] { + continue; + } + let is_match = match &cfg.match_by { + InterfaceMatcher::ByOrder(expected) => *expected == order, + InterfaceMatcher::ByMac(expected) => *expected == mac, + InterfaceMatcher::ByDriverName(expected) => expected == driver_name, + }; + if is_match { + if matched.is_some() { + panic!("multiple interface configs match device {}", driver_name); + } + matched = Some(idx); + } + } + if let Some(idx) = matched { + used[idx] = true; + } + matched +} + /// Init vsock subsystem by vsock devices. #[cfg(feature = "vsock")] pub fn init_vsock(mut vsock_devs: device::VsockDeviceList) { @@ -227,8 +370,11 @@ pub fn init_vsock(mut vsock_devs: device::VsockDeviceList) { } } -/// Poll all network interfaces for new events. -pub fn poll_interfaces() { +fn poll_once() -> bool { + get_service().poll(&mut SOCKET_SET.inner.lock()) +} + +fn poll_until_idle() { POLL_AGAIN.store(true, Ordering::Release); loop { if POLLING_INTERFACES @@ -239,7 +385,7 @@ pub fn poll_interfaces() { } while POLL_AGAIN.swap(false, Ordering::AcqRel) { - while get_service().poll(&mut SOCKET_SET.inner.lock()) {} + while poll_once() {} } POLLING_INTERFACES.store(false, Ordering::Release); if !POLL_AGAIN.load(Ordering::Acquire) { @@ -248,12 +394,87 @@ pub fn poll_interfaces() { } } +/// Request network polling from the dedicated net-poll worker. +/// +/// This function is retained as a public trigger/debug entry. It no longer +/// synchronously drives the whole protocol stack from the caller's context. +pub fn poll_interfaces() { + request_poll(); +} + +/// Request network polling. +/// +/// This is the lightweight entry used by socket and device paths. +pub fn request_poll() { + NET_POLL_REQUESTED.store(true, Ordering::Release); + NET_POLL_WAKE.notify_one(true); +} + pub fn arp_entries() -> Vec { get_service().arp_entries() } -pub fn eth0_ipv4_config() -> Option { - get_service().eth0_ipv4_config() +pub fn interfaces() -> Vec { + get_control().interfaces() +} + +pub fn interface_by_name(name: &str) -> Option { + get_control().interface_by_name(name) +} + +pub fn interface_by_id(id: InterfaceId) -> Option { + get_control().interface_by_id(id) +} + +pub fn ipv4_config(name: &str) -> Option { + get_control().ipv4_config(name) +} + +pub fn default_routes() -> Vec { + get_control().default_routes() +} + +fn next_poll_delay() -> Duration { + const IDLE_POLL_INTERVAL: Duration = Duration::from_millis(100); + let next = { + let mut service = get_service(); + let sockets = SOCKET_SET.inner.lock(); + service.next_poll_at(&sockets) + }; + let Some(next) = next else { + return IDLE_POLL_INTERVAL; + }; + let now_micros = ax_hal::time::wall_time_nanos() / 1_000; + let next_micros = next.total_micros().max(0) as u64; + if next_micros <= now_micros { + Duration::ZERO + } else { + Duration::from_micros(next_micros - now_micros) + } +} + +struct NetPollWake; + +impl Wake for NetPollWake { + fn wake(self: Arc) { + request_poll(); + } + + fn wake_by_ref(self: &Arc) { + request_poll(); + } +} + +fn net_poll_worker() { + loop { + let delay = next_poll_delay(); + let timed_out = + NET_POLL_WAKE.wait_timeout_until(delay, || NET_POLL_REQUESTED.load(Ordering::Acquire)); + if !timed_out { + NET_POLL_REQUESTED.store(false, Ordering::Release); + } + poll_until_idle(); + } } /// Returns the list of configured DNS servers. @@ -261,7 +482,7 @@ pub fn eth0_ipv4_config() -> Option { /// Priority: DHCP-provided servers take precedence over statically configured servers. /// If DHCP hasn't provided servers, falls back to the servers from `NetworkConfig`. pub fn dns_servers() -> Vec { - get_service().dns_servers() + get_control().dns_servers() } const DNS_DEFAULT_TIMEOUT: Duration = Duration::from_secs(5); @@ -276,7 +497,21 @@ pub fn dns_query_timeout(name: &str, timeout: Duration) -> AxResult> return Err(ax_err_type!(NotFound, "no DNS server configured")); } - let servers = servers.into_iter().map(IpAddress::Ipv4).collect::>(); + let servers = servers + .into_iter() + .filter(|server| { + get_control() + .select_route(&IpAddress::Ipv4(*server)) + .is_ok() + }) + .map(IpAddress::Ipv4) + .collect::>(); + if servers.is_empty() { + return Err(ax_err_type!( + NoSuchDeviceOrAddress, + "no routable DNS server configured" + )); + } let handle = SOCKET_SET.add(dns::Socket::new(&servers, vec![])); DnsSocketGuard(handle).query_timeout(name, DnsQueryType::A, timeout) } @@ -316,7 +551,7 @@ impl DnsSocketGuard { let deadline = start_time.saturating_add(timeout_ns); loop { - poll_interfaces(); + request_poll(); match SOCKET_SET.with_socket_mut::(self.0, |socket| { socket .get_query_result(query_handle) @@ -350,32 +585,43 @@ impl Drop for DnsSocketGuard { fn dhcp_bootstrap() { for _ in 0..DHCP_BOOTSTRAP_ATTEMPTS { - poll_interfaces(); + request_poll(); if get_service().dhcp_configured() { return; } ax_task::sleep(DHCP_BOOTSTRAP_POLL_INTERVAL); } - warn!("eth0: DHCP bootstrap timed out"); + warn!("DHCP bootstrap timed out"); +} + +pub(crate) fn endpoint_from_ip_endpoint( + endpoint: smoltcp::wire::IpEndpoint, +) -> smoltcp::wire::IpListenEndpoint { + smoltcp::wire::IpListenEndpoint { + addr: Some(endpoint.addr), + port: endpoint.port, + } } #[cfg(test)] pub(crate) mod test_support { - use alloc::{boxed::Box, vec::Vec}; + use alloc::{boxed::Box, sync::Arc, vec, vec::Vec}; use std::sync::{Mutex as StdMutex, MutexGuard, Once}; use ax_sync::Mutex; use smoltcp::wire::{IpAddress, Ipv4Address, Ipv4Cidr}; use crate::{ - SERVICE, + NET_CONTROL, SERVICE, + config::{InterfaceFlags, InterfaceId, InterfaceKind}, + consts::STANDARD_MTU, device::LoopbackDevice, - router::{Router, Rule}, - service::Service, + router::{RouteTable, Router, Rule, SharedRouteTable}, + service::{NetControl, NetInterface, Service}, }; - pub(crate) const LOCAL_MASK: u32 = 1 << 0; - pub(crate) const PEER_MASK: u32 = 1 << 1; + pub(crate) const LOCAL_IF: InterfaceId = InterfaceId::new(2); + pub(crate) const PEER_IF: InterfaceId = InterfaceId::new(3); pub(crate) const LOCAL_ADDR: Ipv4Address = Ipv4Address::new(192, 0, 2, 10); pub(crate) const PEER_ADDR: Ipv4Address = Ipv4Address::new(198, 51, 100, 20); @@ -389,9 +635,10 @@ pub(crate) mod test_support { static INIT: Once = Once::new(); INIT.call_once(|| { - let mut router = Router::new(); - let local_dev = router.add_device(Box::new(LoopbackDevice::new())); - let peer_dev = router.add_device(Box::new(LoopbackDevice::new())); + let routes: SharedRouteTable = Arc::new(spin::RwLock::new(RouteTable::new())); + let mut router = Router::new(routes.clone()); + let local_dev = router.add_device(LOCAL_IF, Box::new(LoopbackDevice::new())); + let peer_dev = router.add_device(PEER_IF, Box::new(LoopbackDevice::new())); let local_cidr = Ipv4Cidr::new(LOCAL_ADDR, 24); let peer_cidr = Ipv4Cidr::new(PEER_ADDR, 24); @@ -399,21 +646,52 @@ pub(crate) mod test_support { local_cidr.into(), None, local_dev, + LOCAL_IF, IpAddress::Ipv4(LOCAL_ADDR), + 100, )); router.add_rule(Rule::new( peer_cidr.into(), None, peer_dev, + PEER_IF, IpAddress::Ipv4(PEER_ADDR), + 100, )); - let mut service = Service::new(router, Vec::new()); + let interfaces = vec![ + NetInterface { + id: LOCAL_IF, + name: "eth0".into(), + kind: InterfaceKind::Ethernet, + mac: None, + ipv4: Some(local_cidr), + gateway: None, + mtu: STANDARD_MTU, + metric: 100, + flags: InterfaceFlags::UP | InterfaceFlags::RUNNING, + }, + NetInterface { + id: PEER_IF, + name: "eth1".into(), + kind: InterfaceKind::Ethernet, + mac: None, + ipv4: Some(peer_cidr), + gateway: None, + mtu: STANDARD_MTU, + metric: 100, + flags: InterfaceFlags::UP | InterfaceFlags::RUNNING, + }, + ]; + + let control = Arc::new(NetControl::new(interfaces, routes, Vec::new())); + let mut service = Service::new(router, control.clone()); service.iface.update_ip_addrs(|ip_addrs| { ip_addrs.push(local_cidr.into()).unwrap(); ip_addrs.push(peer_cidr.into()).unwrap(); }); + NET_CONTROL.call_once(|| control); SERVICE.call_once(|| Mutex::new(service)); }); } diff --git a/net/ax-net/src/options.rs b/net/ax-net/src/options.rs index 9b3e798dac..43ae421823 100644 --- a/net/ax-net/src/options.rs +++ b/net/ax-net/src/options.rs @@ -4,6 +4,8 @@ use core::time::Duration; use ax_errno::{AxError, AxResult, LinuxError}; use enum_dispatch::enum_dispatch; +use crate::InterfaceId; + /// Linux-like TCP connection state reported by TCP_INFO. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum TcpState { @@ -159,6 +161,7 @@ define_options! { SocketType(i32), SocketProtocol(i32), SocketDomain(i32), + BindToDevice(Option), // --- TCP level options (TCP_*) ---- NoDelay(bool), diff --git a/net/ax-net/src/raw.rs b/net/ax-net/src/raw.rs index 1c42c3ffc1..4c34ed2f17 100644 --- a/net/ax-net/src/raw.rs +++ b/net/ax-net/src/raw.rs @@ -26,11 +26,12 @@ use spin::RwLock; use crate::{ RecvFlags, RecvOptions, SOCKET_SET, SendFlags, SendOptions, Shutdown, SocketAddrEx, SocketOps, + config::{DeviceBinding, InterfaceId}, consts::{RAW_RX_BUF_LEN, RAW_TX_BUF_LEN}, general::GeneralOptions, - get_service, + get_control, interface_by_id, options::{Configurable, GetSocketOption, SetSocketOption}, - poll_interfaces, + request_poll, }; pub(crate) fn new_raw_socket( @@ -63,7 +64,7 @@ impl RawSocket { pub fn new(ip_version: IpVersion, ip_protocol: IpProtocol) -> Self { let handle = SOCKET_SET.add(new_raw_socket(ip_version, ip_protocol)); let general = GeneralOptions::new(3, 2, u8::from(ip_protocol) as i32); // SOCK_RAW - general.set_device_mask(u32::MAX); + general.set_device_binding(DeviceBinding::default()); Self { handle, ip_version, @@ -77,6 +78,16 @@ impl RawSocket { } } + pub fn bind_device(&self, interface_id: InterfaceId) -> AxResult { + if interface_by_id(interface_id).is_none() { + return Err(AxError::NoSuchDevice); + } + self.general.set_device_binding(DeviceBinding { + bound_if: Some(interface_id), + }); + Ok(()) + } + fn with_smol_socket(&self, f: impl FnOnce(&mut smol::Socket) -> R) -> R { SOCKET_SET.with_socket_mut::(self.handle, f) } @@ -100,14 +111,14 @@ impl RawSocket { } } - fn local_address_for(&self, remote: IpAddress) -> IpAddress { + fn local_address_for(&self, remote: IpAddress) -> AxResult { if let Some(local) = *self.local_addr.read() { - return local; + return Ok(local); } if is_loopback_address(remote) { - return remote; + return Ok(remote); } - get_service().get_source_address(&remote) + Ok(get_control().select_route(&remote)?.source) } fn parse_ip_packet<'a>(&self, packet: &'a [u8]) -> AxResult<(IpAddress, &'a [u8])> { @@ -210,15 +221,15 @@ impl SocketOps for RawSocket { let local_addr = local_addr.into_ip()?; let local = self.check_ip_version(local_addr.ip().into())?; *self.local_addr.write() = Some(local); - let device_mask = if local.is_unspecified() { - u32::MAX + let binding = if local.is_unspecified() { + DeviceBinding::default() } else { - get_service().device_mask_for(&IpListenEndpoint { + get_control().local_binding_for(&IpListenEndpoint { addr: Some(local), port: 0, - }) + })? }; - self.general.set_device_mask(device_mask); + self.general.set_device_binding(binding); Ok(()) } @@ -226,14 +237,15 @@ impl SocketOps for RawSocket { let remote_addr = remote_addr.into_ip()?; let remote = self.check_ip_version(remote_addr.ip().into())?; if self.local_addr.read().is_none() { - *self.local_addr.write() = Some(get_service().get_source_address(&remote)); + *self.local_addr.write() = Some(get_control().select_route(&remote)?.source); } *self.peer_addr.write() = Some(remote); + let local = (*self.local_addr.read()).expect("raw socket local address"); self.general - .set_device_mask(get_service().device_mask_for(&IpListenEndpoint { - addr: Some(remote), + .set_device_binding(get_control().local_binding_for(&IpListenEndpoint { + addr: Some(local), port: 0, - })); + })?); Ok(()) } @@ -247,13 +259,13 @@ impl SocketOps for RawSocket { } let remote = self.remote_address(&options)?; - let local = self.local_address_for(remote); + let local = self.local_address_for(remote)?; let payload_len = src.remaining(); let extra_nb = options.flags.contains(crate::SendFlags::DONTWAIT); let loopback_ipv4 = self.ip_version == IpVersion::Ipv4 && is_loopback_address(remote); self.general.send_poller_with(self, extra_nb, || { - poll_interfaces(); + request_poll(); self.with_smol_socket(|socket| { if !socket.can_send() { return Err(AxError::WouldBlock); @@ -361,7 +373,7 @@ impl SocketOps for RawSocket { let mut options = options; self.general.recv_poller_with(self, extra_nb, || { - poll_interfaces(); + request_poll(); self.with_smol_socket(|socket| { loop { if let Some((source, packet)) = if options.flags.contains(RecvFlags::PEEK) { @@ -447,7 +459,7 @@ impl SocketOps for RawSocket { impl Pollable for RawSocket { fn poll(&self) -> IoEvents { - poll_interfaces(); + request_poll(); let mut events = IoEvents::empty(); self.with_smol_socket(|socket| { events.set( diff --git a/net/ax-net/src/router.rs b/net/ax-net/src/router.rs index aab831dd10..2cdfbb466d 100644 --- a/net/ax-net/src/router.rs +++ b/net/ax-net/src/router.rs @@ -1,5 +1,18 @@ -use alloc::{boxed::Box, vec, vec::Vec}; +use alloc::{ + boxed::Box, + collections::VecDeque, + format, + string::{String, ToString}, + sync::{Arc, Weak}, + task::Wake, + vec, + vec::Vec, +}; +use core::task::Waker; +use ax_hal::time::{NANOS_PER_MICROS, wall_time_nanos}; +use ax_sync::Mutex; +use ax_task::WaitQueue; use smoltcp::{ iface::SocketSet, phy::{DeviceCapabilities, Medium}, @@ -10,10 +23,11 @@ use smoltcp::{ TcpPacket, }, }; +use spin::RwLock; use crate::{ LISTEN_TABLE, - config::Ipv4InterfaceConfig, + config::{DeviceBinding, InterfaceId, RouteInfo}, consts::{SOCKET_BUFFER_SIZE, STANDARD_MTU}, device::{ArpEntry, Device}, }; @@ -23,64 +37,279 @@ pub struct Rule { pub filter: IpCidr, pub via: Option, pub dev: usize, + pub interface_id: InterfaceId, pub src: IpAddress, + pub metric: u32, + pub order: u64, } impl Rule { - pub fn new(filter: IpCidr, via: Option, dev: usize, src: IpAddress) -> Self { + pub fn new( + filter: IpCidr, + via: Option, + dev: usize, + interface_id: InterfaceId, + src: IpAddress, + metric: u32, + ) -> Self { Self { filter, via, dev, + interface_id, src, + metric, + order: 0, + } + } + + fn to_info(&self) -> RouteInfo { + RouteInfo { + filter: self.filter, + via: self.via, + interface_id: self.interface_id, + source: self.src, + metric: self.metric, } } } -type PacketBuffer = smoltcp::storage::PacketBuffer<'static, ()>; +type PacketBuffer = smoltcp::storage::PacketBuffer<'static, InterfaceId>; + +struct BoundedPacketQueue { + inner: Mutex>, + capacity: usize, +} + +impl BoundedPacketQueue { + fn new(capacity: usize) -> Self { + Self { + inner: Mutex::new(VecDeque::with_capacity(capacity)), + capacity, + } + } + + fn push(&self, packet: T) -> Result<(), T> { + let mut inner = self.inner.lock(); + if inner.len() >= self.capacity { + return Err(packet); + } + inner.push_back(packet); + Ok(()) + } + + fn pop(&self) -> Option { + self.inner.lock().pop_front() + } +} + +struct TxPacket { + next_hop: IpAddress, + bytes: Box<[u8]>, +} + +struct RxPacket { + interface_id: InterfaceId, + bytes: Box<[u8]>, +} + +struct RouterQueues { + rx: Arc>, +} + +struct DeviceHandle { + interface_id: InterfaceId, + name: String, + inner: Arc>>, + rx_queue: Arc>, + tx_queue: Arc>, + rx_wake: Arc, + tx_wake: Arc, + rx_waker: Waker, +} + +impl DeviceHandle { + fn new( + interface_id: InterfaceId, + device: Box, + queues: &Arc, + ) -> Arc { + let name = device.name().to_string(); + Arc::new_cyclic(|weak| Self { + interface_id, + name, + inner: Arc::new(Mutex::new(device)), + rx_queue: queues.rx.clone(), + tx_queue: Arc::new(BoundedPacketQueue::new(SOCKET_BUFFER_SIZE)), + rx_wake: Arc::new(WaitQueue::new()), + tx_wake: Arc::new(WaitQueue::new()), + rx_waker: Waker::from(Arc::new(DeviceRxWake { + device: weak.clone(), + })), + }) + } + + fn enqueue_tx(&self, next_hop: IpAddress, packet: &[u8]) -> bool { + let tx = TxPacket { + next_hop, + bytes: packet.to_vec().into_boxed_slice(), + }; + if self.tx_queue.push(tx).is_err() { + warn!( + "{}: TX queue is full, dropping packet to {}", + self.name, next_hop + ); + return false; + } + self.tx_wake.notify_one(true); + true + } +} + +struct DeviceRxWake { + device: Weak, +} + +impl Wake for DeviceRxWake { + fn wake(self: Arc) { + self.wake_by_ref(); + } + + fn wake_by_ref(self: &Arc) { + if let Some(device) = self.device.upgrade() { + device.rx_wake.notify_one(true); + } + } +} + +fn now() -> Instant { + Instant::from_micros_const((wall_time_nanos() / NANOS_PER_MICROS) as i64) +} + +#[derive(Debug, Clone, Copy)] +pub struct RouteDecision { + pub dev: usize, + pub interface_id: InterfaceId, + pub source: IpAddress, + pub next_hop: IpAddress, + pub metric: u32, +} -// TODO(mivik): optimize pub struct RouteTable { rules: Vec, + next_order: u64, } impl RouteTable { pub fn new() -> Self { - Self { rules: Vec::new() } + Self { + rules: Vec::new(), + next_order: 0, + } } - pub fn add_rule(&mut self, rule: Rule) { - let idx = self - .rules + pub fn add_rule(&mut self, mut rule: Rule) { + rule.order = self.next_order; + self.next_order = self.next_order.saturating_add(1); + self.rules.push(rule); + self.sort_rules(); + } + + fn sort_rules(&mut self) { + self.rules.sort_by(|a, b| { + b.filter + .prefix_len() + .cmp(&a.filter.prefix_len()) + .then_with(|| a.metric.cmp(&b.metric)) + .then_with(|| a.order.cmp(&b.order)) + }); + } + + pub fn select_route(&self, dst: &IpAddress) -> Option { + self.select_route_if(dst, |_| true) + } + + pub fn select_route_if( + &self, + dst: &IpAddress, + mut is_usable: impl FnMut(InterfaceId) -> bool, + ) -> Option { + self.rules .iter() - .position(|it| it.filter.prefix_len() < rule.filter.prefix_len()) - .unwrap_or(self.rules.len()); - self.rules.insert(idx, rule); + .find(|rule| rule.filter.contains_addr(dst) && is_usable(rule.interface_id)) + .map(|rule| RouteDecision { + dev: rule.dev, + interface_id: rule.interface_id, + source: rule.src, + next_hop: rule.via.unwrap_or(*dst), + metric: rule.metric, + }) } - pub fn lookup(&self, dst: &IpAddress) -> Option<&Rule> { + pub fn select_route_for_source( + &self, + dst: &IpAddress, + source: &IpAddress, + ) -> Option { self.rules .iter() - .find(|rule| rule.filter.contains_addr(dst)) + .find(|rule| rule.filter.contains_addr(dst) && &rule.src == source) + .map(|rule| RouteDecision { + dev: rule.dev, + interface_id: rule.interface_id, + source: rule.src, + next_hop: rule.via.unwrap_or(*dst), + metric: rule.metric, + }) + } + + pub fn default_routes(&self) -> Vec { + self.rules + .iter() + .filter(|rule| match rule.filter { + IpCidr::Ipv4(cidr) => { + cidr.address() == Ipv4Address::UNSPECIFIED && cidr.prefix_len() == 0 + } + _ => false, + }) + .map(Rule::to_info) + .collect() } - pub fn remove_ipv4_rules_for_dev(&mut self, dev: usize) { + pub fn remove_ipv4_rules_for_interface(&mut self, interface_id: InterfaceId) { self.rules.retain(|rule| { !matches!( rule.filter, - IpCidr::Ipv4(_) if rule.dev == dev + IpCidr::Ipv4(_) if rule.interface_id == interface_id ) }); } + + pub fn replace_ipv4_rules_for_interface( + &mut self, + interface_id: InterfaceId, + mut new_rules: Vec, + ) { + self.remove_ipv4_rules_for_interface(interface_id); + for rule in &mut new_rules { + rule.order = self.next_order; + self.next_order = self.next_order.saturating_add(1); + } + self.rules.extend(new_rules); + self.sort_rules(); + } } +pub(crate) type SharedRouteTable = Arc>; + pub struct Router { rx_buffer: PacketBuffer, tx_buffer: PacketBuffer, - pub(crate) devices: Vec>, - pub(crate) table: RouteTable, + queues: Arc, + devices: Vec>, + table: SharedRouteTable, } impl Router { - pub fn new() -> Self { + pub fn new(table: SharedRouteTable) -> Self { let rx_buffer = PacketBuffer::new( vec![PacketMetadata::EMPTY; SOCKET_BUFFER_SIZE], vec![0u8; STANDARD_MTU * SOCKET_BUFFER_SIZE], @@ -89,62 +318,125 @@ impl Router { vec![PacketMetadata::EMPTY; SOCKET_BUFFER_SIZE], vec![0u8; STANDARD_MTU * SOCKET_BUFFER_SIZE], ); + let queues = Arc::new(RouterQueues { + rx: Arc::new(BoundedPacketQueue::new(SOCKET_BUFFER_SIZE)), + }); Self { rx_buffer, tx_buffer, + queues, devices: Vec::new(), - table: RouteTable::new(), + table, } } pub fn add_rule(&mut self, rule: Rule) { - self.table.add_rule(rule); + self.table.write().add_rule(rule); } - pub fn add_device(&mut self, device: Box) -> usize { - self.devices.push(device); + pub fn add_device(&mut self, interface_id: InterfaceId, device: Box) -> usize { + self.devices + .push(DeviceHandle::new(interface_id, device, &self.queues)); self.devices.len() - 1 } + pub fn device_names(&self) -> Vec { + self.devices + .iter() + .map(|device| device.name.clone()) + .collect() + } + + pub fn start_tx_workers(&self) { + for device in &self.devices { + if device.interface_id == InterfaceId::LOOPBACK { + continue; + } + let device = device.clone(); + let name = format!("{}-tx", device.name); + ax_task::spawn_with_name(move || device_tx_worker(device), name); + } + } + + pub fn start_rx_workers(&self) { + for device in &self.devices { + if device.interface_id == InterfaceId::LOOPBACK { + continue; + } + let device = device.clone(); + let name = format!("{}-rx", device.name); + ax_task::spawn_with_name(move || device_rx_worker(device), name); + } + } + pub fn set_ipv4_config( &mut self, dev: usize, + interface_id: InterfaceId, + metric: u32, address: Option, gateway: Option, ) { - self.table.remove_ipv4_rules_for_dev(dev); - self.devices[dev].set_ipv4_addr(address); + let new_rules = self.ipv4_rules(dev, interface_id, metric, address, gateway); + self.table + .write() + .replace_ipv4_rules_for_interface(interface_id, new_rules); + } + pub(crate) fn ipv4_rules( + &mut self, + dev: usize, + interface_id: InterfaceId, + metric: u32, + address: Option, + gateway: Option, + ) -> Vec { + self.devices[dev].inner.lock().set_ipv4_addr(address); + + let mut rules = Vec::new(); if let Some(address) = address { - self.add_rule(Rule::new( + rules.push(Rule::new( address.into(), None, dev, + interface_id, address.address().into(), + metric, )); - self.add_rule(Rule::new( - Ipv4Cidr::new(Ipv4Address::UNSPECIFIED, 0).into(), - gateway, - dev, - address.address().into(), - )); + if let Some(gateway) = gateway { + rules.push(Rule::new( + Ipv4Cidr::new(Ipv4Address::UNSPECIFIED, 0).into(), + Some(gateway), + dev, + interface_id, + address.address().into(), + metric, + )); + } } + rules } pub fn poll( &mut self, - timestamp: Instant, + _timestamp: Instant, sockets: &mut SocketSet<'_>, - mut snoop: impl FnMut(usize, &[u8]), + mut snoop: impl FnMut(InterfaceId, &[u8]), ) { - for (dev_idx, dev) in self.devices.iter_mut().enumerate() { - let mut packet_snoop = |packet: &[u8]| { - snoop_tcp_packet(packet, sockets); - snoop(dev_idx, packet); + while !self.rx_buffer.is_full() { + let Some(packet) = self.queues.rx.pop() else { + break; + }; + snoop_tcp_packet(&packet.bytes, sockets); + snoop(packet.interface_id, &packet.bytes); + let Ok(dst) = self + .rx_buffer + .enqueue(packet.bytes.len(), packet.interface_id) + else { + warn!("Router RX buffer is full, dropping packet"); + break; }; - while !self.rx_buffer.is_full() - && dev.recv(&mut self.rx_buffer, timestamp, &mut packet_snoop) - {} + dst.copy_from_slice(&packet.bytes); } } @@ -155,65 +447,64 @@ impl Router { packet: &[u8], timestamp: Instant, ) -> bool { - self.devices[dev].send(next_hop, packet, timestamp) + let _ = timestamp; + self.devices[dev].enqueue_tx(next_hop, packet) } pub fn arp_entries(&self, timestamp: Instant) -> Vec { let mut entries = Vec::new(); for device in &self.devices { - entries.extend(device.arp_entries(timestamp)); + entries.extend(device.inner.lock().arp_entries(timestamp)); } entries } - pub fn ipv4_config_for_dev(&self, dev: usize) -> Option { - self.table.rules.iter().find_map(|rule| match rule.filter { - IpCidr::Ipv4(address) if rule.dev == dev && address.prefix_len() != 0 => { - Some(Ipv4InterfaceConfig { - address, - gateway: self.table.rules.iter().find_map(|default_rule| { - matches!( - default_rule.filter, - IpCidr::Ipv4(filter) - if default_rule.dev == dev - && filter.address() == Ipv4Address::UNSPECIFIED - && filter.prefix_len() == 0 - ) - .then(|| match default_rule.via { - Some(IpAddress::Ipv4(gateway)) => Some(gateway), - _ => None, - }) - .flatten() - }), - }) + pub fn register_device_waker(&self, waker: &core::task::Waker) { + for device in &self.devices { + device.inner.lock().register_waker(&device.rx_waker); + device.inner.lock().register_waker(waker); + } + } + + pub fn register_waker(&self, binding: DeviceBinding, waker: &core::task::Waker) { + for device in &self.devices { + if binding.bound_if.is_none_or(|id| id == device.interface_id) { + device.inner.lock().register_waker(&device.rx_waker); + device.inner.lock().register_waker(waker); } - _ => None, - }) + } } - pub fn dispatch(&mut self, timestamp: Instant) -> bool { + pub fn dispatch(&mut self, _timestamp: Instant) -> bool { let mut poll_next = false; - while let Ok(((), packet)) = self.tx_buffer.dequeue() { + while let Ok((_, packet)) = self.tx_buffer.dequeue() { match IpVersion::of_packet(packet).expect("got invalid IP packet") { IpVersion::Ipv4 => { let packet = smoltcp::wire::Ipv4Packet::new_checked(packet) .expect("got invalid IPv4 packet"); + let src_addr = IpAddress::Ipv4(packet.src_addr()); let dst_addr = IpAddress::Ipv4(packet.dst_addr()); if packet.dst_addr().is_broadcast() { let buf = packet.into_inner(); - for dev in &mut self.devices { - poll_next |= dev.send(dst_addr, buf, timestamp); + // Broadcast only to Ethernet devices (not loopback) + for dev in &self.devices { + if dev.interface_id != InterfaceId::LOOPBACK { + poll_next |= dev.enqueue_tx(dst_addr, buf); + } } } else { - let Some(rule) = self.table.lookup(&dst_addr) else { - warn!("No route found for destination: {}", dst_addr); + let routes = self.table.read(); + let Some(route) = routes.select_route_for_source(&dst_addr, &src_addr) + else { + warn!( + "No route found for source {} destination {}", + src_addr, dst_addr + ); continue; }; - assert_eq!(rule.src, IpAddress::Ipv4(packet.src_addr())); - let next_hop = rule.via.unwrap_or(dst_addr); - let dev = &mut self.devices[rule.dev]; - poll_next |= dev.send(next_hop, packet.into_inner(), timestamp); + let dev = &self.devices[route.dev]; + poll_next |= dev.enqueue_tx(route.next_hop, packet.into_inner()); } } IpVersion::Ipv6 => { @@ -222,19 +513,21 @@ impl Router { let dst_addr = IpAddress::Ipv6(packet.dst_addr()); if packet.dst_addr().is_multicast() { let buf = packet.into_inner(); - for dev in &mut self.devices { - poll_next |= dev.send(dst_addr, buf, timestamp); + // Multicast only to Ethernet devices (not loopback) + for dev in &self.devices { + if dev.interface_id != InterfaceId::LOOPBACK { + poll_next |= dev.enqueue_tx(dst_addr, buf); + } } } else { - let Some(rule) = self.table.lookup(&dst_addr) else { + let routes = self.table.read(); + let Some(route) = routes.select_route(&dst_addr) else { warn!("No route found for destination: {}", dst_addr); continue; }; - assert_eq!(rule.src, IpAddress::Ipv6(packet.src_addr())); - let next_hop = rule.via.unwrap_or(dst_addr); - let dev = &mut self.devices[rule.dev]; - poll_next |= dev.send(next_hop, packet.into_inner(), timestamp); + let dev = &self.devices[route.dev]; + poll_next |= dev.enqueue_tx(route.next_hop, packet.into_inner()); } } } @@ -243,6 +536,56 @@ impl Router { } } +fn device_tx_worker(device: Arc) { + loop { + if let Some(packet) = device.tx_queue.pop() { + let poll_next = device + .inner + .lock() + .send(packet.next_hop, &packet.bytes, now()); + if poll_next { + crate::request_poll(); + } + } else { + device.tx_wake.wait(); + } + } +} + +fn device_rx_worker(device: Arc) { + let mut rx_buffer = PacketBuffer::new(vec![PacketMetadata::EMPTY; 1], vec![0u8; STANDARD_MTU]); + + loop { + let mut received = false; + { + let mut device_inner = device.inner.lock(); + let mut snoop = |_packet: &[u8]| {}; + while rx_buffer.is_empty() + && device_inner.recv(device.interface_id, &mut rx_buffer, now(), &mut snoop) + { + received = true; + } + } + + while let Ok((interface_id, packet)) = rx_buffer.dequeue() { + let rx = RxPacket { + interface_id, + bytes: packet.to_vec().into_boxed_slice(), + }; + if device.rx_queue.push(rx).is_err() { + warn!("{}: RX queue is full, dropping packet", device.name); + break; + } + crate::request_poll(); + received = true; + } + + if !received { + device.rx_wake.wait(); + } + } +} + pub struct TxToken<'a>(&'a mut PacketBuffer); impl smoltcp::phy::TxToken for TxToken<'_> { @@ -252,7 +595,7 @@ impl smoltcp::phy::TxToken for TxToken<'_> { { f(self .0 - .enqueue(len, ()) + .enqueue(len, InterfaceId::new(0)) .expect("This was checked before creating the TxToken")) } } @@ -289,14 +632,18 @@ fn snoop_tcp_packet(buf: &[u8], sockets: &mut SocketSet<'_>) { } } -pub struct RxToken<'a>(&'a [u8]); +pub struct RxToken<'a> { + interface_id: InterfaceId, + packet: &'a [u8], +} impl<'a> smoltcp::phy::RxToken for RxToken<'a> { fn consume(self, f: F) -> R where F: FnOnce(&[u8]) -> R, { - f(self.0) + let _ingress_if = self.interface_id; + f(self.packet) } } @@ -309,7 +656,13 @@ impl smoltcp::phy::Device for Router { None } else { Some(( - RxToken(self.rx_buffer.dequeue().unwrap().1), + { + let (interface_id, packet) = self.rx_buffer.dequeue().unwrap(); + RxToken { + interface_id, + packet, + } + }, TxToken(&mut self.tx_buffer), )) } diff --git a/net/ax-net/src/service.rs b/net/ax-net/src/service.rs index 5653d98810..26736819be 100644 --- a/net/ax-net/src/service.rs +++ b/net/ax-net/src/service.rs @@ -1,9 +1,10 @@ -use alloc::{boxed::Box, vec, vec::Vec}; +use alloc::{boxed::Box, string::String, vec, vec::Vec}; use core::{ pin::Pin, task::{Context, Waker}, }; +use ax_errno::{AxResult, ax_err_type}; use ax_hal::time::{NANOS_PER_MICROS, TimeValue, monotonic_time_nanos, wall_time_nanos}; use ax_task::future::sleep_until; use smoltcp::{ @@ -18,24 +19,246 @@ use smoltcp::{ }; use crate::{ - SOCKET_SET, config::Ipv4InterfaceConfig, consts::STANDARD_MTU, device::ArpEntry, router::Router, + SOCKET_SET, + config::{ + DeviceBinding, DnsServerEntry, DnsSource, InterfaceFlags, InterfaceId, InterfaceInfo, + InterfaceKind, Ipv4InterfaceConfig, RouteInfo, + }, + consts::STANDARD_MTU, + device::ArpEntry, + router::{RouteDecision, Router, SharedRouteTable}, }; fn now() -> Instant { Instant::from_micros_const((monotonic_time_nanos() / NANOS_PER_MICROS) as i64) } +use alloc::sync::Arc; + +use spin::RwLock; + +struct ControlState { + interfaces: Vec, + dns: Vec, +} + +pub struct NetControl { + state: RwLock, + pub(crate) routes: SharedRouteTable, +} + +impl NetControl { + pub(crate) fn new( + interfaces: Vec, + routes: SharedRouteTable, + dns: Vec, + ) -> Self { + Self { + state: RwLock::new(ControlState { interfaces, dns }), + routes, + } + } + + pub fn dns_servers(&self) -> Vec { + let state = self.state.read(); + let mut entries = state.dns.clone(); + entries.sort_by_key(|entry| { + ( + entry.metric, + entry.interface_id.get(), + entry.server.octets(), + ) + }); + let mut servers = Vec::new(); + for entry in entries { + if !servers.contains(&entry.server) { + servers.push(entry.server); + } + } + servers + } + + pub fn interfaces(&self) -> Vec { + let state = self.state.read(); + state.interfaces.iter().map(NetInterface::to_info).collect() + } + + pub fn interface_by_name(&self, name: &str) -> Option { + let state = self.state.read(); + state + .interfaces + .iter() + .find(|interface| interface.name == name) + .map(NetInterface::to_info) + } + + pub fn interface_by_id(&self, id: InterfaceId) -> Option { + let state = self.state.read(); + state + .interfaces + .iter() + .find(|interface| interface.id == id) + .map(NetInterface::to_info) + } + + pub fn ipv4_config(&self, name: &str) -> Option { + let state = self.state.read(); + state + .interfaces + .iter() + .find(|interface| interface.name == name) + .and_then(|interface| interface.ipv4.map(|address| (interface, address))) + .map(|(interface, address)| Ipv4InterfaceConfig { + address, + gateway: interface.gateway, + }) + } + + pub fn default_routes(&self) -> Vec { + let _state = self.state.read(); + self.routes.read().default_routes() + } + + pub fn local_binding_for(&self, endpoint: &IpListenEndpoint) -> AxResult { + match endpoint.addr { + Some(addr) => { + let state = self.state.read(); + let bound_if = state.interfaces.iter().find_map(|interface| { + (interface + .ipv4 + .is_some_and(|ipv4| IpAddress::Ipv4(ipv4.address()) == addr)) + .then_some(interface.id) + }); + bound_if + .map(|interface_id| DeviceBinding { + bound_if: Some(interface_id), + }) + .ok_or(ax_err_type!( + NoSuchDeviceOrAddress, + "local address is not assigned to any interface" + )) + } + None => Ok(DeviceBinding::default()), + } + } + + pub fn select_route(&self, dst_addr: &IpAddress) -> AxResult { + let state = self.state.read(); + let routes = self.routes.read(); + let route = routes + .select_route_if(dst_addr, |interface_id| { + state + .interfaces + .iter() + .find(|interface| interface.id == interface_id) + .is_some_and(|interface| interface.flags.contains(InterfaceFlags::UP)) + }) + .ok_or(ax_err_type!( + NoSuchDeviceOrAddress, + "no route to destination" + ))?; + if let Some(interface) = state + .interfaces + .iter() + .find(|interface| interface.id == route.interface_id) + { + debug_assert!(interface.flags.contains(InterfaceFlags::UP)); + debug_assert_eq!(interface.metric, route.metric); + } + Ok(route) + } + + fn commit_interface_update( + &self, + update: &NetworkStateUpdate, + routes: Vec, + ) { + let mut state = self.state.write(); + if let Some(interface) = state + .interfaces + .iter_mut() + .find(|interface| interface.id == update.interface_id) + { + interface.ipv4 = update.ipv4; + interface.gateway = update.gateway; + } + state.dns.retain(|entry| { + entry.interface_id != update.interface_id || entry.source != update.dns_source + }); + state.dns.extend( + update + .dns_servers + .iter() + .copied() + .map(|server| DnsServerEntry { + server, + interface_id: update.interface_id, + metric: update.metric, + source: update.dns_source, + }), + ); + self.routes + .write() + .replace_ipv4_rules_for_interface(update.interface_id, routes); + } +} + pub struct Service { pub iface: Interface, router: Router, + control: Arc, timeout: Option + Send>>>, - dhcp: Option, - static_dns: Vec, + dhcp: Vec, +} + +#[derive(Clone)] +pub(crate) struct NetInterface { + pub id: InterfaceId, + pub name: String, + pub kind: InterfaceKind, + pub mac: Option, + pub ipv4: Option, + pub gateway: Option, + pub mtu: usize, + pub metric: u32, + pub flags: InterfaceFlags, +} + +impl NetInterface { + fn to_info(&self) -> InterfaceInfo { + InterfaceInfo { + id: self.id, + name: self.name.clone(), + kind: self.kind, + mac: self.mac, + ipv4: self.ipv4.map(|address| Ipv4InterfaceConfig { + address, + gateway: self.gateway, + }), + mtu: self.mtu, + flags: self.flags, + metric: self.metric, + } + } +} + +struct NetworkStateUpdate { + interface_id: InterfaceId, + dev: usize, + metric: u32, + old_ipv4: Option, + ipv4: Option, + gateway: Option, + dns_source: DnsSource, + dns_servers: Vec, } struct DhcpState { + interface_id: InterfaceId, dev: usize, + ifname: String, mac: EthernetAddress, + metric: u32, transaction_id: u32, phase: DhcpPhase, retry_at: Instant, @@ -57,10 +280,19 @@ const DHCP_PARAMETER_REQUEST_LIST: &[u8] = &[1, 3, 6, 42]; const DHCP_MAX_RETRY_SHIFT: usize = 4; impl DhcpState { - fn new(dev: usize, mac: EthernetAddress) -> Self { + fn new( + interface_id: InterfaceId, + dev: usize, + ifname: String, + mac: EthernetAddress, + metric: u32, + ) -> Self { Self { + interface_id, dev, + ifname, mac, + metric, transaction_id: dhcp_transaction_id(mac), phase: DhcpPhase::Discovering, retry_at: Instant::from_micros_const(0), @@ -74,11 +306,11 @@ impl DhcpState { fn process_packet( &mut self, - dev: usize, + interface_id: InterfaceId, packet: &[u8], timestamp: Instant, ) -> Option { - if dev != self.dev { + if interface_id != self.interface_id { return None; } @@ -119,7 +351,8 @@ impl DhcpState { self.retry = 0; self.retry_at = timestamp; info!( - "eth0: DHCP offered address {} from {}", + "{}: DHCP offered address {} from {}", + self.ifname, dhcp_repr.your_ip, self.server_identifier.unwrap_or(ipv4_repr.src_addr) ); @@ -136,6 +369,10 @@ impl DhcpState { self.retry = 0; let address = Ipv4Cidr::new(dhcp_repr.your_ip, prefix_len); Some(DhcpEvent::Configured { + interface_id: self.interface_id, + dev: self.dev, + ifname: self.ifname.clone(), + metric: self.metric, address, router: dhcp_repr.router, dns_servers: dhcp_repr @@ -148,7 +385,12 @@ impl DhcpState { (_, DhcpMessageType::Nak) => { let was_configured = self.address.is_some(); self.reset(timestamp); - was_configured.then_some(DhcpEvent::Deconfigured) + was_configured.then_some(DhcpEvent::Deconfigured { + interface_id: self.interface_id, + dev: self.dev, + ifname: self.ifname.clone(), + metric: self.metric, + }) } _ => None, } @@ -198,46 +440,43 @@ impl DhcpState { } } impl Service { - pub fn new(mut router: Router, static_dns: Vec) -> Self { + pub fn new(mut router: Router, control: Arc) -> Self { let config = smoltcp::iface::Config::new(HardwareAddress::Ip); let iface = Interface::new(config, &mut router, now()); Self { iface, router, + control, timeout: None, - dhcp: None, - static_dns, + dhcp: Vec::new(), } } - pub fn enable_dhcp(&mut self, dev: usize, mac: EthernetAddress) { - self.dhcp = Some(DhcpState::new(dev, mac)); - info!("eth0: DHCP enabled"); + pub fn enable_dhcp( + &mut self, + interface_id: InterfaceId, + dev: usize, + ifname: String, + mac: EthernetAddress, + metric: u32, + ) { + self.dhcp.push(DhcpState::new( + interface_id, + dev, + ifname.clone(), + mac, + metric, + )); + info!("{ifname}: DHCP enabled"); } pub fn dhcp_enabled(&self) -> bool { - self.dhcp.is_some() + !self.dhcp.is_empty() } pub fn dhcp_configured(&self) -> bool { - self.dhcp - .as_ref() - .is_some_and(|state| state.address.is_some()) - } - - pub fn dns_servers(&self) -> Vec { - let dhcp_dns = self - .dhcp - .as_ref() - .map(|state| state.dns_servers.clone()) - .unwrap_or_default(); - - if !dhcp_dns.is_empty() { - dhcp_dns - } else { - self.static_dns.clone() - } + self.dhcp.iter().all(|state| state.address.is_some()) } pub fn poll(&mut self, sockets: &mut SocketSet) -> bool { @@ -246,14 +485,14 @@ impl Service { { let dhcp = &mut self.dhcp; - self.router.poll(timestamp, sockets, |dev, packet| { - if let Some(event) = dhcp - .as_mut() - .and_then(|state| state.process_packet(dev, packet, timestamp)) - { - dhcp_events.push(event); - } - }); + self.router + .poll(timestamp, sockets, |interface_id, packet| { + for state in dhcp.iter_mut() { + if let Some(event) = state.process_packet(interface_id, packet, timestamp) { + dhcp_events.push(event); + } + } + }); } for event in dhcp_events { self.handle_dhcp_event(event); @@ -263,57 +502,112 @@ impl Service { self.router.dispatch(timestamp) || dhcp_poll_next } - fn poll_dhcp(&mut self, timestamp: Instant) -> bool { - let Some((dev, next_hop, packet)) = self - .dhcp - .as_mut() - .and_then(|state| state.poll_packet(timestamp)) - else { - return false; - }; + pub fn next_poll_at(&mut self, sockets: &SocketSet) -> Option { + self.iface.poll_at(now(), sockets) + } - self.router - .send_on_device(dev, next_hop, &packet, timestamp) + fn poll_dhcp(&mut self, timestamp: Instant) -> bool { + let mut poll_next = false; + for state in &mut self.dhcp { + if let Some((dev, next_hop, packet)) = state.poll_packet(timestamp) { + poll_next |= self + .router + .send_on_device(dev, next_hop, &packet, timestamp); + } + } + poll_next } fn handle_dhcp_event(&mut self, event: DhcpEvent) { - match event { + let update = match event { DhcpEvent::Configured { + interface_id, + dev, + ifname, + metric, address, router, dns_servers, } => { - let Some(state) = &mut self.dhcp else { - return; - }; - warn!("eth0: DHCP acquired address {address}"); + warn!("{ifname}: DHCP acquired address {address}"); match router { - Some(router) => warn!("eth0: DHCP router {router}"), - None => warn!("eth0: DHCP router not provided"), + Some(router) => warn!("{ifname}: DHCP router {router}"), + None => warn!("{ifname}: DHCP router not provided"), } for dns in &dns_servers { - info!("eth0: DHCP DNS {dns}"); + info!("{ifname}: DHCP DNS {dns}"); + } + let old_ipv4 = { + let Some(state) = self + .dhcp + .iter_mut() + .find(|state| state.interface_id == interface_id) + else { + return; + }; + let old_ipv4 = state.address; + state.address = Some(address); + state.dns_servers = dns_servers.clone(); + old_ipv4 + }; + NetworkStateUpdate { + interface_id, + dev, + metric, + old_ipv4, + ipv4: Some(address), + gateway: router, + dns_source: DnsSource::Dhcp, + dns_servers, } - - Self::set_interface_ipv4(&mut self.iface, state.address, Some(address)); - state.address = Some(address); - state.dns_servers = dns_servers; - self.router - .set_ipv4_config(state.dev, Some(address), router.map(IpAddress::Ipv4)); } - DhcpEvent::Deconfigured => { - let Some(state) = &mut self.dhcp else { - return; + DhcpEvent::Deconfigured { + interface_id, + dev, + ifname, + metric, + } => { + let old_ipv4 = { + let Some(state) = self + .dhcp + .iter_mut() + .find(|state| state.interface_id == interface_id) + else { + return; + }; + if state.address.is_some() { + info!("{ifname}: DHCP deconfigured"); + } + let old_ipv4 = state.address; + state.address = None; + state.dns_servers.clear(); + old_ipv4 }; - if state.address.is_some() { - info!("eth0: DHCP deconfigured"); + NetworkStateUpdate { + interface_id, + dev, + metric, + old_ipv4, + ipv4: None, + gateway: None, + dns_source: DnsSource::Dhcp, + dns_servers: Vec::new(), } - Self::set_interface_ipv4(&mut self.iface, state.address, None); - state.address = None; - state.dns_servers.clear(); - self.router.set_ipv4_config(state.dev, None, None); } - } + }; + self.commit_network_state(update); + } + + fn commit_network_state(&mut self, update: NetworkStateUpdate) { + Self::set_interface_ipv4(&mut self.iface, update.old_ipv4, update.ipv4); + let routes = self.router.ipv4_rules( + update.dev, + update.interface_id, + update.metric, + update.ipv4, + update.gateway.map(IpAddress::Ipv4), + ); + self.control.commit_interface_update(&update, routes); } fn set_interface_ipv4( @@ -334,33 +628,11 @@ impl Service { }); } - pub fn get_source_address(&self, dst_addr: &IpAddress) -> IpAddress { - let Some(rule) = self.router.table.lookup(dst_addr) else { - panic!("no route to destination: {dst_addr}"); - }; - rule.src - } - pub fn arp_entries(&self) -> Vec { self.router.arp_entries(now()) } - pub fn eth0_ipv4_config(&self) -> Option { - self.router.ipv4_config_for_dev(1) - } - - pub fn device_mask_for(&self, endpoint: &IpListenEndpoint) -> u32 { - match endpoint.addr { - Some(addr) => self - .router - .table - .lookup(&addr) - .map_or(0, |it| 1u32 << it.dev), - None => u32::MAX, - } - } - - pub fn register_waker(&mut self, mask: u32, waker: &Waker) { + pub fn register_waker(&mut self, binding: DeviceBinding, waker: &Waker) { let next = self.iface.poll_at(now(), &SOCKET_SET.inner.lock()); if let Some(t) = next { @@ -380,21 +652,30 @@ impl Service { } } - for (i, device) in self.router.devices.iter().enumerate() { - if mask & (1 << i) != 0 { - device.register_waker(waker); - } - } + self.router.register_waker(binding, waker); + } + + pub fn register_device_waker(&mut self, waker: &Waker) { + self.router.register_device_waker(waker); } } enum DhcpEvent { Configured { + interface_id: InterfaceId, + dev: usize, + ifname: String, + metric: u32, address: Ipv4Cidr, router: Option, dns_servers: Vec, }, - Deconfigured, + Deconfigured { + interface_id: InterfaceId, + dev: usize, + ifname: String, + metric: u32, + }, } fn dhcp_transaction_id(mac: EthernetAddress) -> u32 { diff --git a/net/ax-net/src/tcp.rs b/net/ax-net/src/tcp.rs index 66ce5e4c4e..d67c9e04ab 100644 --- a/net/ax-net/src/tcp.rs +++ b/net/ax-net/src/tcp.rs @@ -21,11 +21,13 @@ use spin::LazyLock; use crate::{ LISTEN_TABLE, RecvFlags, RecvOptions, SOCKET_SET, SendOptions, Shutdown, Socket, SocketAddrEx, SocketOps, + config::{DeviceBinding, InterfaceId}, consts::{TCP_RX_BUF_LEN, TCP_TX_BUF_LEN}, + endpoint_from_ip_endpoint, general::GeneralOptions, - get_service, + get_control, get_service, interface_by_id, options::{Configurable, GetSocketOption, SetSocketOption, TcpInfo, TcpInfoOptions, TcpState}, - poll_interfaces, + request_poll, state::*, }; @@ -89,6 +91,16 @@ impl TcpSocket { } } + pub fn bind_device(&self, interface_id: InterfaceId) -> AxResult { + if interface_by_id(interface_id).is_none() { + return Err(AxError::NoSuchDevice); + } + self.general.set_device_binding(DeviceBinding { + bound_if: Some(interface_id), + }); + Ok(()) + } + /// Creates a new TCP socket that is already connected. fn new_connected( handle: SocketHandle, @@ -113,9 +125,11 @@ impl TcpSocket { }; let endpoint = endpoint_from_ip_endpoint(local_endpoint); *result.bound_endpoint.lock() = endpoint; - result - .general - .set_device_mask(get_service().device_mask_for(&endpoint)); + result.general.set_device_binding( + get_control() + .local_binding_for(&endpoint) + .unwrap_or_default(), + ); result } } @@ -387,10 +401,10 @@ impl SocketOps for TcpSocket { if self.bound_endpoint.lock().port != 0 { return Err(AxError::InvalidInput); } + let binding = get_control().local_binding_for(&endpoint)?; self.register_bound_endpoint(endpoint)?; *self.bound_endpoint.lock() = endpoint; - self.general - .set_device_mask(get_service().device_mask_for(&endpoint)); + self.general.set_device_binding(binding); debug!("TCP socket {}: binding to {}", self.handle, local_addr); Ok(()) }) @@ -405,7 +419,7 @@ impl SocketOps for TcpSocket { // Here our state must be `CONNECTING`, and only one thread can run here. self.general.send_poller(self, || { - poll_interfaces(); + request_poll(); let events = self.poll_connect(); if !events.contains(IoEvents::OUT) { Err(AxError::WouldBlock) @@ -424,6 +438,7 @@ impl SocketOps for TcpSocket { if bound_endpoint.port == 0 { bound_endpoint.port = get_ephemeral_port()?; } + let binding = get_control().local_binding_for(&bound_endpoint)?; let register_bound = !self.bound_registered.load(Ordering::Acquire); if register_bound { register_tcp_bound(bound_endpoint)?; @@ -438,8 +453,7 @@ impl SocketOps for TcpSocket { if register_bound { self.bound_registered.store(true, Ordering::Release); } - self.general - .set_device_mask(get_service().device_mask_for(&bound_endpoint)); + self.general.set_device_binding(binding); debug!("listening on {}", bound_endpoint); Ok(()) })?; @@ -456,7 +470,7 @@ impl SocketOps for TcpSocket { let bound_port = self.bound_endpoint()?.port; self.general.recv_poller(self, || { - poll_interfaces(); + request_poll(); let accepted = { let mut sockets = SOCKET_SET.inner.lock(); LISTEN_TABLE.accept(bound_port, &mut sockets)? @@ -480,7 +494,7 @@ impl SocketOps for TcpSocket { // SAFETY: `self.handle` should be initialized in a connected socket. let extra_nb = options.flags.contains(crate::SendFlags::DONTWAIT); let result = self.general.send_poller_with(self, extra_nb, || { - poll_interfaces(); + request_poll(); self.with_smol_socket(|socket| { if !socket.is_active() { Err(AxError::NotConnected) @@ -503,7 +517,7 @@ impl SocketOps for TcpSocket { // network stack immediately. For loopback, this causes loopback.send() // to run, which wakes any epoll wakers registered on the peer socket. if result.is_ok() { - poll_interfaces(); + request_poll(); } result } @@ -517,7 +531,7 @@ impl SocketOps for TcpSocket { } let extra_nb = options.flags.contains(RecvFlags::DONTWAIT); self.general.recv_poller_with(self, extra_nb, || { - poll_interfaces(); + request_poll(); self.with_smol_socket(|socket| { if socket.recv_queue() > 0 { if options.flags.contains(RecvFlags::PEEK) { @@ -564,7 +578,7 @@ impl SocketOps for TcpSocket { if available > 0 { return Ok(available); } - poll_interfaces(); + request_poll(); Ok(self.with_smol_socket(|socket| socket.recv_queue())) } @@ -612,7 +626,7 @@ impl SocketOps for TcpSocket { }); self.unregister_bound_endpoint(); *self.bound_endpoint.lock() = empty_endpoint(); - poll_interfaces(); + request_poll(); Ok(()) })?; } else if how.has_write() { @@ -620,7 +634,7 @@ impl SocketOps for TcpSocket { debug!("TCP socket {}: shutting down write side", self.handle); socket.close(); }); - poll_interfaces(); + request_poll(); } } @@ -630,7 +644,7 @@ impl SocketOps for TcpSocket { LISTEN_TABLE.unlisten(self.bound_endpoint()?.port); self.unregister_bound_endpoint(); *self.bound_endpoint.lock() = empty_endpoint(); - poll_interfaces(); + request_poll(); Ok(()) })?; } @@ -642,7 +656,7 @@ impl SocketOps for TcpSocket { impl Pollable for TcpSocket { fn poll(&self) -> IoEvents { - poll_interfaces(); + request_poll(); let mut events = match self.state() { State::Connecting => self.poll_connect(), State::Connected | State::Idle | State::Closed => self.poll_stream(), @@ -671,7 +685,7 @@ impl Drop for TcpSocket { self.unregister_bound_endpoint(); SOCKET_SET.remove(self.handle); // This is crucial for the close messages to be sent. - poll_interfaces(); + request_poll(); } } @@ -706,13 +720,6 @@ const fn empty_endpoint() -> IpListenEndpoint { } } -fn endpoint_from_ip_endpoint(endpoint: IpEndpoint) -> IpListenEndpoint { - IpListenEndpoint { - addr: Some(endpoint.addr), - port: endpoint.port, - } -} - impl TcpSocket { fn start_connect(&self, remote_addr: SocketAddr) -> AxResult { self.state @@ -731,9 +738,15 @@ impl TcpSocket { // let (bound_endpoint, remote_endpoint) = self.get_endpoint_pair(remote_addr)?; let remote_endpoint = IpEndpoint::from(remote_addr); let mut bound_endpoint = *self.bound_endpoint.lock(); - if bound_endpoint.addr.is_none() { + + // Record original bind state before modifying + let was_unbound_or_unspecified = + bound_endpoint.addr.is_none_or(|addr| addr.is_unspecified()); + + // Fill source address if unbound or bound to 0.0.0.0 + if bound_endpoint.addr.is_none_or(|addr| addr.is_unspecified()) { bound_endpoint.addr = - Some(get_service().get_source_address(&remote_endpoint.addr)); + Some(get_control().select_route(&remote_endpoint.addr)?.source); } if bound_endpoint.port == 0 { bound_endpoint.port = get_ephemeral_port()?; @@ -774,9 +787,15 @@ impl TcpSocket { if register_bound { self.bound_registered.store(true, Ordering::Release); } - self.general.set_device_mask( - get_service().device_mask_for(&endpoint_from_ip_endpoint(remote_endpoint)), - ); + + // Only set device binding if was originally unbound or bound to 0.0.0.0 + // Binding to a specific IP should lock the interface + if was_unbound_or_unspecified { + self.general + .set_device_binding(get_control().local_binding_for(&bound_endpoint)?); + } + // else: bound to specific IP, keep existing interface binding + Ok(()) }) } @@ -872,8 +891,7 @@ mod tests { use crate::{ options::{Configurable, GetSocketOption, SetSocketOption, TcpState}, test_support::{ - LOCAL_ADDR, LOCAL_MASK, PEER_ADDR, PEER_MASK, init_split_route_network, - network_test_guard, + LOCAL_ADDR, LOCAL_IF, PEER_ADDR, PEER_IF, init_split_route_network, network_test_guard, }, }; @@ -901,7 +919,7 @@ mod tests { } #[test] - fn connect_uses_peer_route_for_device_mask() { + fn connect_preserves_bound_interface() { let _guard = network_test_guard(); init_split_route_network(); @@ -913,12 +931,57 @@ mod tests { socket .bind(SocketAddrEx::Ip(SocketAddr::new(IpAddr::V4(LOCAL_ADDR), 0))) .unwrap(); - assert_eq!(socket.general.device_mask(), LOCAL_MASK); + assert_eq!( + socket.general.device_binding(), + DeviceBinding { + bound_if: Some(LOCAL_IF) + } + ); + + // Connect to different network - should NOT change interface binding + // because we're bound to a specific local address + socket + .start_connect(SocketAddr::new(IpAddr::V4(PEER_ADDR), 80)) + .unwrap(); + + // Interface binding should remain LOCAL_IF (not changed to PEER_IF) + assert_eq!( + socket.general.device_binding(), + DeviceBinding { + bound_if: Some(LOCAL_IF) + } + ); + } + + #[test] + fn connect_uses_peer_route_when_unbound() { + let _guard = network_test_guard(); + init_split_route_network(); + + let socket = TcpSocket::new(); + let nonblocking = true; + socket + .set_option(SetSocketOption::NonBlocking(&nonblocking)) + .unwrap(); + + // Bind to 0.0.0.0 (unspecified) - interface should be determined by route + socket + .bind(SocketAddrEx::Ip(SocketAddr::new( + IpAddr::V4(Ipv4Addr::UNSPECIFIED), + 0, + ))) + .unwrap(); socket .start_connect(SocketAddr::new(IpAddr::V4(PEER_ADDR), 80)) .unwrap(); - assert_eq!(socket.general.device_mask(), PEER_MASK); + // Interface binding should use route decision (PEER_IF) + assert_eq!( + socket.general.device_binding(), + DeviceBinding { + bound_if: Some(PEER_IF) + } + ); } } diff --git a/net/ax-net/src/udp.rs b/net/ax-net/src/udp.rs index f17326f631..d646e16340 100644 --- a/net/ax-net/src/udp.rs +++ b/net/ax-net/src/udp.rs @@ -19,11 +19,12 @@ use spin::RwLock; use crate::{ RecvFlags, RecvOptions, SOCKET_SET, SendFlags, SendOptions, Shutdown, SocketAddrEx, SocketOps, + config::{DeviceBinding, InterfaceId}, consts::{UDP_RX_BUF_LEN, UDP_TX_BUF_LEN}, general::GeneralOptions, - get_service, + get_control, interface_by_id, options::{Configurable, GetSocketOption, SetSocketOption}, - poll_interfaces, + request_poll, }; /// Buffered state for MSG_MORE corking: captures the target endpoint @@ -73,6 +74,16 @@ impl UdpSocket { } } + pub fn bind_device(&self, interface_id: InterfaceId) -> AxResult { + if interface_by_id(interface_id).is_none() { + return Err(AxError::NoSuchDevice); + } + self.general.set_device_binding(DeviceBinding { + bound_if: Some(interface_id), + }); + Ok(()) + } + fn with_smol_socket(&self, f: impl FnOnce(&mut smol::Socket) -> R) -> R { SOCKET_SET.with_socket_mut::(self.handle, f) } @@ -143,11 +154,7 @@ impl SocketOps for UdpSocket { addr: (!local_endpoint.addr.is_unspecified()).then_some(local_endpoint.addr), port: local_endpoint.port, }; - - if !self.general.reuse_address() { - // Check if the address is already in use - SOCKET_SET.udp_bind_check(local_endpoint.addr, local_endpoint.port)?; - } + let binding = get_control().local_binding_for(&endpoint)?; self.with_smol_socket(|socket| { socket.bind(endpoint).map_err(|e| match e { @@ -155,8 +162,14 @@ impl SocketOps for UdpSocket { smol::BindError::Unaddressable => ax_err_type!(ConnectionRefused, "unaddressable"), }) })?; - self.general - .set_device_mask(get_service().device_mask_for(&endpoint)); + if !self.general.reuse_address() + && let Err(err) = + SOCKET_SET.udp_bind(self.handle, local_endpoint.addr, local_endpoint.port) + { + self.with_smol_socket(|socket| socket.close()); + return Err(err); + } + self.general.set_device_binding(binding); *guard = Some(local_endpoint); info!("UDP socket {}: bound on {}", self.handle, endpoint); @@ -166,6 +179,7 @@ impl SocketOps for UdpSocket { fn connect(&self, remote_addr: SocketAddrEx) -> AxResult { let remote_addr = remote_addr.into_ip()?; let mut guard = self.peer_addr.write(); + if self.local_addr.read().is_none() { self.bind(SocketAddrEx::Ip(SocketAddr::new( IpAddr::V4(Ipv4Addr::UNSPECIFIED), @@ -174,11 +188,31 @@ impl SocketOps for UdpSocket { } let remote_addr = IpEndpoint::from(remote_addr); - let src = get_service().get_source_address(&remote_addr.addr); + let local = self.local_addr.read(); + + // Determine source address and device binding based on bind state + let (src, should_update_binding) = if let Some(local_ep) = *local { + if local_ep.addr.is_unspecified() { + // Bound to 0.0.0.0, use route decision + (get_control().select_route(&remote_addr.addr)?.source, true) + } else { + // Bound to specific IP, use that address and keep interface + (local_ep.addr, false) + } + } else { + (get_control().select_route(&remote_addr.addr)?.source, true) + }; + *guard = Some((remote_addr, src)); - self.general.set_device_mask( - get_service().device_mask_for(&endpoint_from_ip_endpoint(remote_addr)), - ); + + if should_update_binding { + self.general + .set_device_binding(get_control().local_binding_for(&IpListenEndpoint { + addr: Some(src), + port: (*local).map_or(0, |endpoint| endpoint.port), + })?); + } + debug!("UDP socket {}: connected to {}", self.handle, remote_addr); Ok(()) } @@ -205,7 +239,16 @@ impl SocketOps for UdpSocket { let (remote_addr, source_addr) = match options.to { Some(addr) => { let addr = IpEndpoint::from(addr.into_ip()?); - let src = get_service().get_source_address(&addr.addr); + // Use bound address if bound to specific IP + let src = if let Some(local_ep) = *self.local_addr.read() { + if local_ep.addr.is_unspecified() { + get_control().select_route(&addr.addr)?.source + } else { + local_ep.addr + } + } else { + get_control().select_route(&addr.addr)?.source + }; (addr, src) } None => match self.remote_endpoint() { @@ -246,7 +289,16 @@ impl SocketOps for UdpSocket { let resolved = match options.to { Some(addr) => { let addr = IpEndpoint::from(addr.into_ip()?); - let src = get_service().get_source_address(&addr.addr); + // Use bound address if bound to specific IP, otherwise route decision + let src = if let Some(local_ep) = *self.local_addr.read() { + if local_ep.addr.is_unspecified() { + get_control().select_route(&addr.addr)?.source + } else { + local_ep.addr + } + } else { + get_control().select_route(&addr.addr)?.source + }; Some((addr, src)) } None => self.remote_endpoint().ok(), @@ -254,7 +306,7 @@ impl SocketOps for UdpSocket { let extra_nb = options.flags.contains(SendFlags::DONTWAIT); self.general.send_poller_with(self, extra_nb, || { - poll_interfaces(); + request_poll(); let mut cork_guard = self.cork.lock(); // When flushing corked data, always use the endpoint captured // at the first MSG_MORE call (matching Linux semantics). @@ -340,7 +392,7 @@ impl SocketOps for UdpSocket { } })?; // Flush TX so loopback packets reach the receiver immediately. - poll_interfaces(); + request_poll(); Ok(result) }) } @@ -361,7 +413,7 @@ impl SocketOps for UdpSocket { let extra_nb = options.flags.contains(RecvFlags::DONTWAIT); self.general.recv_poller_with(self, extra_nb, || { - poll_interfaces(); + request_poll(); self.with_smol_socket(|socket| { if !socket.can_recv() { Err(AxError::WouldBlock) @@ -433,7 +485,7 @@ impl SocketOps for UdpSocket { fn shutdown(&self, _how: Shutdown) -> AxResult { // TODO(mivik): shutdown - poll_interfaces(); + request_poll(); self.with_smol_socket(|socket| { debug!("UDP socket {}: shutting down", self.handle); @@ -445,7 +497,7 @@ impl SocketOps for UdpSocket { impl Pollable for UdpSocket { fn poll(&self) -> IoEvents { - poll_interfaces(); + request_poll(); if self.local_addr.read().is_none() { return IoEvents::empty(); } @@ -472,13 +524,6 @@ impl Drop for UdpSocket { } } -fn endpoint_from_ip_endpoint(endpoint: IpEndpoint) -> IpListenEndpoint { - IpListenEndpoint { - addr: Some(endpoint.addr), - port: endpoint.port, - } -} - fn get_ephemeral_port() -> AxResult { const PORT_START: u16 = 0xc000; const PORT_END: u16 = 0xffff; @@ -500,11 +545,11 @@ mod tests { use super::*; use crate::test_support::{ - LOCAL_ADDR, LOCAL_MASK, PEER_ADDR, PEER_MASK, init_split_route_network, network_test_guard, + LOCAL_ADDR, LOCAL_IF, PEER_ADDR, PEER_IF, init_split_route_network, network_test_guard, }; #[test] - fn connect_uses_peer_route_for_device_mask() { + fn connect_preserves_bound_interface() { let _guard = network_test_guard(); init_split_route_network(); @@ -512,12 +557,53 @@ mod tests { socket .bind(SocketAddrEx::Ip(SocketAddr::new(IpAddr::V4(LOCAL_ADDR), 0))) .unwrap(); - assert_eq!(socket.general.device_mask(), LOCAL_MASK); + assert_eq!( + socket.general.device_binding(), + DeviceBinding { + bound_if: Some(LOCAL_IF) + } + ); + // Connect to different network - should NOT change interface binding + // because we're bound to a specific local address socket .connect(SocketAddrEx::Ip(SocketAddr::new(IpAddr::V4(PEER_ADDR), 53))) .unwrap(); - assert_eq!(socket.general.device_mask(), PEER_MASK); + // Interface binding should remain LOCAL_IF (not changed to PEER_IF) + assert_eq!( + socket.general.device_binding(), + DeviceBinding { + bound_if: Some(LOCAL_IF) + } + ); + } + + #[test] + fn connect_uses_peer_route_when_unbound() { + let _guard = network_test_guard(); + init_split_route_network(); + + let socket = UdpSocket::new(); + + // Bind to 0.0.0.0 (unspecified) - interface should be determined by route + socket + .bind(SocketAddrEx::Ip(SocketAddr::new( + IpAddr::V4(Ipv4Addr::UNSPECIFIED), + 0, + ))) + .unwrap(); + + socket + .connect(SocketAddrEx::Ip(SocketAddr::new(IpAddr::V4(PEER_ADDR), 53))) + .unwrap(); + + // Interface binding should use route decision (PEER_IF) + assert_eq!( + socket.general.device_binding(), + DeviceBinding { + bound_if: Some(PEER_IF) + } + ); } } diff --git a/net/ax-net/src/wrapper.rs b/net/ax-net/src/wrapper.rs index b9bd1bc7a1..584b54f870 100644 --- a/net/ax-net/src/wrapper.rs +++ b/net/ax-net/src/wrapper.rs @@ -3,15 +3,23 @@ use alloc::vec; use ax_errno::{AxError, AxResult}; use ax_sync::Mutex; use event_listener::Event; +use hashbrown::HashMap; use smoltcp::{ iface::{SocketHandle, SocketSet}, - socket::{AnySocket, Socket}, + socket::AnySocket, wire::IpAddress, }; +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +struct UdpBindKey { + addr: Option, + port: u16, +} + pub(crate) struct SocketSetWrapper<'a> { pub inner: Mutex>, pub new_socket: Event, + udp_binds: Mutex>, } impl<'a> SocketSetWrapper<'a> { @@ -19,6 +27,7 @@ impl<'a> SocketSetWrapper<'a> { Self { inner: Mutex::new(SocketSet::new(vec![])), new_socket: Event::new(), + udp_binds: Mutex::new(HashMap::new()), } } @@ -38,27 +47,35 @@ impl<'a> SocketSetWrapper<'a> { f(socket) } - pub fn udp_bind_check(&self, addr: IpAddress, port: u16) -> AxResult { + pub fn udp_bind(&self, handle: SocketHandle, addr: IpAddress, port: u16) -> AxResult { if port == 0 { return Ok(()); } - // TODO(mivik): optimize - let mut sockets = self.inner.lock(); - for (_, socket) in sockets.iter_mut() { - match socket { - Socket::Udp(s) => { - if s.endpoint().addr == Some(addr) && s.endpoint().port == port { - return Err(AxError::AddrInUse); - } - } - _ => continue, - }; + let key = UdpBindKey { + addr: (!addr.is_unspecified()).then_some(addr), + port, + }; + let wildcard = UdpBindKey { addr: None, port }; + let mut binds = self.udp_binds.lock(); + if binds.contains_key(&key) || (key.addr.is_some() && binds.contains_key(&wildcard)) { + return Err(AxError::AddrInUse); + } + if key.addr.is_none() && binds.keys().any(|bind| bind.port == port) { + return Err(AxError::AddrInUse); } + binds.insert(key, handle); Ok(()) } + pub fn udp_unbind(&self, handle: SocketHandle) { + self.udp_binds + .lock() + .retain(|_, bound_handle| *bound_handle != handle); + } + pub fn remove(&self, handle: SocketHandle) { + self.udp_unbind(handle); self.inner.lock().remove(handle); debug!("socket {}: destroyed", handle); } diff --git a/os/StarryOS/kernel/src/file/net.rs b/os/StarryOS/kernel/src/file/net.rs index 770b611f9b..a825ddd422 100644 --- a/os/StarryOS/kernel/src/file/net.rs +++ b/os/StarryOS/kernel/src/file/net.rs @@ -1,4 +1,8 @@ -use alloc::{borrow::Cow, format, sync::Arc}; +use alloc::{ + borrow::{Cow, ToOwned}, + format, + sync::Arc, +}; use core::{ ffi::c_int, mem::offset_of, @@ -9,7 +13,8 @@ use core::{ use ax_errno::{AxError, AxResult}; use ax_net::{ - RecvOptions, SendOptions, Socket as SocketInner, SocketOps, + InterfaceFlags, InterfaceId, InterfaceInfo, InterfaceKind, RecvOptions, SendOptions, + Socket as SocketInner, SocketOps, options::{Configurable, GetSocketOption, SetSocketOption}, }; use axpoll::{IoEvents, Pollable}; @@ -30,17 +35,8 @@ use crate::{ syscall::in_root_net_ns, }; -/// Real eth0 MAC address. Uses the QEMU default; TODO: query -/// `EthernetDriver::mac_address()` from ax_net at init time once the API is -/// exposed, then replace this with a `static` or `LazyLock`. -pub const ETH0_REAL_MAC: [u8; 6] = [0x02, 0x00, 0x00, 0x00, 0x00, 0x01]; - -pub(super) const ETH0_IFINDEX: i32 = 2; -pub(super) const LO_IFINDEX: i32 = 1; -const ETH0_NAME: &[u8] = b"eth0"; -const LO_NAME: &[u8] = b"lo"; -const ARPHRD_ETHER: u16 = 1; -const ARPHRD_LOOPBACK: u16 = 772; +pub(super) const ARPHRD_ETHER: u16 = 1; +pub(super) const ARPHRD_LOOPBACK: u16 = 772; const IFF_UP: i16 = 0x0001; const IFF_BROADCAST: i16 = 0x0002; const IFF_LOOPBACK: i16 = 0x0008; @@ -51,8 +47,6 @@ const IFREQ_DATA_OFFSET: usize = 16; const IFREQ_COMPAT_LEN: usize = 40; const IFCONF_LEN_OFFSET: usize = 0; const IFCONF_BUF_OFFSET: usize = 8; -const ETH0_MTU: i32 = 1500; -const LO_MTU: i32 = 65536; pub struct Socket { inner: SocketInner, @@ -76,31 +70,22 @@ impl Socket { } } -#[derive(Clone, Copy)] -enum NetInterface { - Eth0, - Loopback, -} - -fn eth0_ipv4_config() -> AxResult { - ax_net::eth0_ipv4_config().ok_or(AxError::NoSuchDevice) +pub(super) fn visible_interfaces() -> impl Iterator { + ax_net::interfaces() + .into_iter() + .filter(|info| in_root_net_ns() || info.kind == InterfaceKind::Loopback) } -fn eth0_ipv4_addr() -> AxResult<[u8; 4]> { - Ok(eth0_ipv4_config()?.address.address().octets()) -} - -fn ipv4_netmask(prefix_len: u8) -> [u8; 4] { - if prefix_len == 0 { - return [0; 4]; - } - (!0u32 << (32 - prefix_len)).to_be_bytes() +pub(super) fn visible_interface_by_id(id: InterfaceId) -> AxResult { + ax_net::interface_by_id(id) + .filter(|info| in_root_net_ns() || info.kind == InterfaceKind::Loopback) + .ok_or(AxError::NoSuchDevice) } -fn ipv4_broadcast(config: ax_net::Ipv4InterfaceConfig) -> [u8; 4] { - let ip = u32::from_be_bytes(config.address.address().octets()); - let mask = u32::from_be_bytes(ipv4_netmask(config.address.prefix_len())); - (ip | !mask).to_be_bytes() +pub(super) fn first_visible_ethernet() -> AxResult { + visible_interfaces() + .find(|info| info.kind == InterfaceKind::Ethernet) + .ok_or(AxError::NoSuchDevice) } fn read_user_bytes(ptr: *const u8) -> AxResult<[u8; N]> { @@ -109,19 +94,19 @@ fn read_user_bytes(ptr: *const u8) -> AxResult<[u8; N]> { Ok(buf.map(|v| unsafe { v.assume_init() })) } -fn read_ifreq_interface(arg: usize) -> AxResult { +fn read_ifreq_name(arg: usize) -> AxResult { let name = read_user_bytes::(arg as *const u8)?; let end = name.iter().position(|&b| b == 0).unwrap_or(name.len()); - match &name[..end] { - ETH0_NAME => { - if !in_root_net_ns() { - return Err(AxError::NoSuchDevice); - } - Ok(NetInterface::Eth0) - } - LO_NAME => Ok(NetInterface::Loopback), - _ => Err(AxError::NoSuchDevice), - } + core::str::from_utf8(&name[..end]) + .map(str::to_owned) + .map_err(|_| AxError::InvalidInput) +} + +fn read_ifreq_interface(arg: usize) -> AxResult { + let name = read_ifreq_name(arg)?; + ax_net::interface_by_name(&name) + .filter(|info| in_root_net_ns() || info.kind == InterfaceKind::Loopback) + .ok_or(AxError::NoSuchDevice) } fn write_ifreq_data(arg: usize, data: &[u8]) -> AxResult<()> { @@ -146,38 +131,77 @@ fn write_ifreq_hwaddr(arg: usize, hw_type: u16, hwaddr: &[u8]) -> AxResult<()> { write_ifreq_data(arg, &addr) } -fn write_ifconf_entry(buf: usize, offset: usize, name: &[u8], ip: [u8; 4]) -> AxResult<()> { +fn write_ifconf_entry(buf: usize, offset: usize, name: &str, ip: [u8; 4]) -> AxResult<()> { let mut ifreq = [0; IFREQ_COMPAT_LEN]; - ifreq[..name.len()].copy_from_slice(name); + let name = name.as_bytes(); + let name_len = name.len().min(IFREQ_NAME_LEN - 1); + ifreq[..name_len].copy_from_slice(&name[..name_len]); ifreq[IFREQ_DATA_OFFSET..IFREQ_DATA_OFFSET + 16].copy_from_slice(&sockaddr_in_bytes(ip)); Ok(vm_write_slice((buf + offset) as *mut u8, &ifreq)?) } -fn write_eth0_ifconf(arg: usize) -> AxResult<()> { +fn interface_ipv4(info: &InterfaceInfo) -> AxResult { + info.ipv4.ok_or(AxError::NoSuchDeviceOrAddress) +} + +fn ipv4_netmask(prefix_len: u8) -> [u8; 4] { + if prefix_len == 0 { + return [0; 4]; + } + (!0u32 << (32 - prefix_len)).to_be_bytes() +} + +fn ipv4_broadcast(config: ax_net::Ipv4InterfaceConfig) -> [u8; 4] { + let ip = u32::from_be_bytes(config.address.address().octets()); + let mask = u32::from_be_bytes(ipv4_netmask(config.address.prefix_len())); + (ip | !mask).to_be_bytes() +} + +fn linux_flags(info: &InterfaceInfo) -> i16 { + let mut flags = 0; + if info.flags.contains(InterfaceFlags::UP) { + flags |= IFF_UP; + } + if info.flags.contains(InterfaceFlags::RUNNING) { + flags |= IFF_RUNNING; + } + if info.flags.contains(InterfaceFlags::LOOPBACK) { + flags |= IFF_LOOPBACK; + } + if info.flags.contains(InterfaceFlags::BROADCAST) { + flags |= IFF_BROADCAST; + } + if info.flags.contains(InterfaceFlags::MULTICAST) { + flags |= IFF_MULTICAST; + } + flags +} + +fn write_ifconf(arg: usize) -> AxResult<()> { let mut len = read_user_bytes::<4>((arg + IFCONF_LEN_OFFSET) as *const u8)?; let ifc_len = i32::from_ne_bytes(len); let buf = usize::from_ne_bytes(read_user_bytes::<{ core::mem::size_of::() }>( (arg + IFCONF_BUF_OFFSET) as *const u8, )?); + let interfaces: alloc::vec::Vec<_> = visible_interfaces() + .filter_map(|info| { + info.ipv4 + .map(|ipv4| (info.name, ipv4.address.address().octets())) + }) + .collect(); if buf != 0 { let mut written = 0; - if in_root_net_ns() && ifc_len >= IFREQ_COMPAT_LEN as i32 { - write_ifconf_entry(buf, written, ETH0_NAME, eth0_ipv4_addr()?)?; - written += IFREQ_COMPAT_LEN; - } - if ifc_len >= (written + IFREQ_COMPAT_LEN) as i32 { - write_ifconf_entry(buf, written, LO_NAME, [127, 0, 0, 1])?; + for (name, ip) in interfaces { + if ifc_len < (written + IFREQ_COMPAT_LEN) as i32 { + break; + } + write_ifconf_entry(buf, written, &name, ip)?; written += IFREQ_COMPAT_LEN; } len = (written as i32).to_ne_bytes(); } else { - // SIOCGIFCONF sizing call (ifc_buf == NULL): Linux's dev_ifconf returns - // the number of bytes needed to hold all interfaces so the caller can - // size its buffer. Returning 0 made OpenJDK's - // NetworkInterface.enumIPv4Interfaces malloc a 0-byte buffer and find no - // interfaces. Report space for eth0 + lo. - len = (2 * IFREQ_COMPAT_LEN as i32).to_ne_bytes(); + len = ((interfaces.len() * IFREQ_COMPAT_LEN) as i32).to_ne_bytes(); } vm_write_slice((arg + IFCONF_LEN_OFFSET) as *mut u8, &len)?; Ok(()) @@ -201,9 +225,8 @@ impl FileLike for Socket { } fn stat(&self) -> AxResult { - // TODO(mivik): implement stat for sockets Ok(Kstat { - mode: S_IFSOCK | 0o777u32, // rwxrwxrwx + mode: S_IFSOCK | 0o777u32, blksize: 4096, ..Default::default() }) @@ -257,51 +280,52 @@ impl FileLike for Socket { let available = self.inner.recv_available()?.min(c_int::MAX as usize) as c_int; (arg as *mut c_int).vm_write(available)?; } - SIOCGIFCONF => write_eth0_ifconf(arg)?, + SIOCGIFCONF => write_ifconf(arg)?, SIOCGIFFLAGS => { - let flags = match read_ifreq_interface(arg)? { - NetInterface::Eth0 => IFF_UP | IFF_BROADCAST | IFF_RUNNING | IFF_MULTICAST, - NetInterface::Loopback => IFF_UP | IFF_LOOPBACK | IFF_RUNNING, - }; - write_ifreq_data(arg, &flags.to_ne_bytes())?; + let info = read_ifreq_interface(arg)?; + write_ifreq_data(arg, &linux_flags(&info).to_ne_bytes())?; } SIOCGIFADDR => { - let addr = match read_ifreq_interface(arg)? { - NetInterface::Eth0 => eth0_ipv4_addr()?, - NetInterface::Loopback => [127, 0, 0, 1], - }; - write_ifreq_sockaddr(arg, addr)?; + let info = read_ifreq_interface(arg)?; + write_ifreq_sockaddr(arg, interface_ipv4(&info)?.address.address().octets())?; } SIOCGIFDSTADDR => { - let addr = match read_ifreq_interface(arg)? { - NetInterface::Eth0 => [0, 0, 0, 0], - NetInterface::Loopback => [127, 0, 0, 1], + let info = read_ifreq_interface(arg)?; + let addr = if info.kind == InterfaceKind::Loopback { + interface_ipv4(&info)?.address.address().octets() + } else { + [0, 0, 0, 0] }; write_ifreq_sockaddr(arg, addr)?; } SIOCGIFBRDADDR => { - let addr = match read_ifreq_interface(arg)? { - NetInterface::Eth0 => ipv4_broadcast(eth0_ipv4_config()?), - NetInterface::Loopback => [127, 0, 0, 1], + let info = read_ifreq_interface(arg)?; + let addr = if info.kind == InterfaceKind::Loopback { + interface_ipv4(&info)?.address.address().octets() + } else { + ipv4_broadcast(interface_ipv4(&info)?) }; write_ifreq_sockaddr(arg, addr)?; } SIOCGIFNETMASK => { - let addr = match read_ifreq_interface(arg)? { - NetInterface::Eth0 => ipv4_netmask(eth0_ipv4_config()?.address.prefix_len()), - NetInterface::Loopback => [255, 0, 0, 0], - }; - write_ifreq_sockaddr(arg, addr)?; + let info = read_ifreq_interface(arg)?; + write_ifreq_sockaddr( + arg, + ipv4_netmask(interface_ipv4(&info)?.address.prefix_len()), + )?; + } + SIOCGIFHWADDR => { + let info = read_ifreq_interface(arg)?; + match info.kind { + InterfaceKind::Ethernet => { + let mac = info.mac.ok_or(AxError::NoSuchDevice)?; + write_ifreq_hwaddr(arg, ARPHRD_ETHER, &mac.0)? + } + InterfaceKind::Loopback => write_ifreq_hwaddr(arg, ARPHRD_LOOPBACK, &[])?, + } } - SIOCGIFHWADDR => match read_ifreq_interface(arg)? { - NetInterface::Eth0 => write_ifreq_hwaddr(arg, ARPHRD_ETHER, Ð0_REAL_MAC)?, - NetInterface::Loopback => write_ifreq_hwaddr(arg, ARPHRD_LOOPBACK, &[])?, - }, SIOCGIFMTU => { - let mtu = match read_ifreq_interface(arg)? { - NetInterface::Eth0 => ETH0_MTU, - NetInterface::Loopback => LO_MTU, - }; + let mtu = read_ifreq_interface(arg)?.mtu as i32; write_ifreq_data(arg, &mtu.to_ne_bytes())?; } SIOCGIFMETRIC => { @@ -318,10 +342,7 @@ impl FileLike for Socket { qlen_ptr.vm_write(1000)?; } SIOCGIFINDEX => { - let idx = match read_ifreq_interface(arg)? { - NetInterface::Eth0 => ETH0_IFINDEX, - NetInterface::Loopback => LO_IFINDEX, - }; + let idx = read_ifreq_interface(arg)?.id.get() as i32; write_ifreq_data(arg, &idx.to_ne_bytes())?; } _ => return Err(AxError::NotATty), @@ -338,6 +359,7 @@ impl FileLike for Socket { .map_err(|_| AxError::NotASocket) } } + impl Pollable for Socket { fn poll(&self) -> IoEvents { self.inner.poll() diff --git a/os/StarryOS/kernel/src/file/netlink.rs b/os/StarryOS/kernel/src/file/netlink.rs index 5f8d019113..2db222fea6 100644 --- a/os/StarryOS/kernel/src/file/netlink.rs +++ b/os/StarryOS/kernel/src/file/netlink.rs @@ -33,6 +33,7 @@ use core::{ use ax_errno::{AxError, AxResult}; use ax_kspin::SpinNoIrq as Mutex; +use ax_net::{InterfaceFlags, InterfaceInfo, InterfaceKind}; use ax_task::future::{block_on, poll_io}; use axpoll::{IoEvents, PollSet, Pollable}; use linux_raw_sys::{ @@ -42,7 +43,6 @@ use linux_raw_sys::{ }; use spin::LazyLock; -use super::net::{ETH0_IFINDEX, ETH0_REAL_MAC}; use crate::{ file::{FileLike, IoDst, IoSrc}, syscall::in_root_net_ns, @@ -165,7 +165,7 @@ struct IfAddrMsg { struct LinkInfo { index: i32, - name: &'static str, + name: String, ty: u16, flags: u32, mtu: u32, @@ -178,59 +178,13 @@ struct LinkInfo { struct AddrInfo { index: u32, - label: &'static str, + label: String, prefix_len: u8, scope: u8, local: [u8; 4], broadcast: Option<[u8; 4]>, } -const LINKS: &[LinkInfo] = &[ - LinkInfo { - index: 1, - name: "lo", - ty: ARPHRD_LOOPBACK, - flags: IFF_UP | IFF_LOOPBACK | IFF_RUNNING | IFF_LOWER_UP, - mtu: 65536, - qlen: 1000, - qdisc: "noqueue", - operstate: IF_OPER_UNKNOWN, - address: [0; 6], - broadcast: [0; 6], - }, - LinkInfo { - index: ETH0_IFINDEX, - name: "eth0", - ty: ARPHRD_ETHER, - flags: IFF_UP | IFF_BROADCAST | IFF_RUNNING | IFF_MULTICAST | IFF_LOWER_UP, - mtu: 1500, - qlen: 1000, - qdisc: "mq", - operstate: IF_OPER_UP, - address: ETH0_REAL_MAC, - broadcast: [0xff; 6], - }, -]; - -const ADDRS: &[AddrInfo] = &[ - AddrInfo { - index: 1, - label: "lo", - prefix_len: 8, - scope: RT_SCOPE_HOST, - local: [127, 0, 0, 1], - broadcast: None, - }, - AddrInfo { - index: ETH0_IFINDEX as u32, - label: "eth0", - prefix_len: 24, - scope: RT_SCOPE_UNIVERSE, - local: [10, 0, 2, 15], - broadcast: Some([10, 0, 2, 255]), - }, -]; - #[derive(Clone, Copy, Default)] struct NetlinkState { addr: Option, @@ -399,19 +353,19 @@ impl NetlinkSocket { let mut response = Vec::new(); match header.ty { RTM_GETLINK => { - for link in LINKS { + for link in link_infos() { if !in_root && link.index != 1 { continue; } - push_link_message(&mut response, header.seq, pid, link); + push_link_message(&mut response, header.seq, pid, &link); } } RTM_GETADDR => { - for addr in ADDRS { + for addr in addr_infos() { if !in_root && addr.index != 1 { continue; } - push_addr_message(&mut response, header.seq, pid, addr); + push_addr_message(&mut response, header.seq, pid, &addr); } } _ => {} @@ -591,6 +545,95 @@ impl Pollable for NetlinkSocket { } } +fn link_infos() -> Vec { + ax_net::interfaces() + .into_iter() + .map(|info| { + let flags = linux_link_flags(&info); + let mut address = [0; 6]; + if let Some(mac) = info.mac { + address = mac.0; + } + LinkInfo { + index: info.id.get() as i32, + name: info.name, + ty: match info.kind { + InterfaceKind::Loopback => ARPHRD_LOOPBACK, + InterfaceKind::Ethernet => ARPHRD_ETHER, + }, + flags, + mtu: info.mtu as u32, + qlen: 1000, + qdisc: match info.kind { + InterfaceKind::Loopback => "noqueue", + InterfaceKind::Ethernet => "mq", + }, + operstate: if info.flags.contains(InterfaceFlags::RUNNING) { + IF_OPER_UP + } else { + IF_OPER_UNKNOWN + }, + address, + broadcast: if info.kind == InterfaceKind::Ethernet { + [0xff; 6] + } else { + [0; 6] + }, + } + }) + .collect() +} + +fn addr_infos() -> Vec { + ax_net::interfaces() + .into_iter() + .filter_map(|info| { + let ipv4 = info.ipv4?; + let local = ipv4.address.address().octets(); + let broadcast = (info.kind == InterfaceKind::Ethernet).then(|| { + let ip = u32::from_be_bytes(local); + let mask = if ipv4.address.prefix_len() == 0 { + 0 + } else { + !0u32 << (32 - ipv4.address.prefix_len()) + }; + (ip | !mask).to_be_bytes() + }); + Some(AddrInfo { + index: info.id.get(), + label: info.name, + prefix_len: ipv4.address.prefix_len(), + scope: match info.kind { + InterfaceKind::Loopback => RT_SCOPE_HOST, + InterfaceKind::Ethernet => RT_SCOPE_UNIVERSE, + }, + local, + broadcast, + }) + }) + .collect() +} + +fn linux_link_flags(info: &InterfaceInfo) -> u32 { + let mut flags = 0; + if info.flags.contains(InterfaceFlags::UP) { + flags |= IFF_UP; + } + if info.flags.contains(InterfaceFlags::BROADCAST) { + flags |= IFF_BROADCAST; + } + if info.flags.contains(InterfaceFlags::LOOPBACK) { + flags |= IFF_LOOPBACK; + } + if info.flags.contains(InterfaceFlags::RUNNING) { + flags |= IFF_RUNNING | IFF_LOWER_UP; + } + if info.flags.contains(InterfaceFlags::MULTICAST) { + flags |= IFF_MULTICAST; + } + flags +} + fn push_link_message(out: &mut Vec, seq: u32, pid: u32, link: &LinkInfo) { let mut body = Vec::new(); push_struct( @@ -604,7 +647,7 @@ fn push_link_message(out: &mut Vec, seq: u32, pid: u32, link: &LinkInfo) { change: 0, }, ); - push_attr_string(&mut body, IFLA_IFNAME, link.name); + push_attr_string(&mut body, IFLA_IFNAME, &link.name); push_attr(&mut body, IFLA_ADDRESS, &link.address); push_attr(&mut body, IFLA_BROADCAST, &link.broadcast); push_attr(&mut body, IFLA_MTU, &link.mtu.to_ne_bytes()); @@ -630,7 +673,7 @@ fn push_addr_message(out: &mut Vec, seq: u32, pid: u32, addr: &AddrInfo) { ); push_attr(&mut body, IFA_ADDRESS, &addr.local); push_attr(&mut body, IFA_LOCAL, &addr.local); - push_attr_string(&mut body, IFA_LABEL, addr.label); + push_attr_string(&mut body, IFA_LABEL, &addr.label); if let Some(broadcast) = addr.broadcast { push_attr(&mut body, IFA_BROADCAST, &broadcast); } diff --git a/os/StarryOS/kernel/src/file/packet.rs b/os/StarryOS/kernel/src/file/packet.rs index fbf8de9c34..3d382e4d9e 100644 --- a/os/StarryOS/kernel/src/file/packet.rs +++ b/os/StarryOS/kernel/src/file/packet.rs @@ -1,13 +1,13 @@ -use alloc::{borrow::Cow, format, sync::Arc, vec, vec::Vec}; +use alloc::{borrow::Cow, format, sync::Arc, vec::Vec}; use core::{ ffi::c_int, - mem::{MaybeUninit, size_of}, + mem::size_of, sync::atomic::{AtomicBool, Ordering}, task::Context, }; use ax_errno::{AxError, AxResult, LinuxError}; -use ax_io::prelude::*; +use ax_net::{InterfaceFlags, InterfaceId, InterfaceInfo, InterfaceKind}; use ax_sync::Mutex; use ax_task::future::{block_on, poll_io}; use axpoll::{IoEvents, PollSet, Pollable}; @@ -20,24 +20,19 @@ use starry_vm::{vm_read_slice, vm_write_slice}; use super::{ FileLike, Kstat, - net::{ETH0_IFINDEX, ETH0_REAL_MAC}, + net::{ARPHRD_ETHER, first_visible_ethernet, visible_interface_by_id}, }; use crate::{ file::{IoDst, IoSrc, get_file_like}, syscall::in_root_net_ns, }; -const ETH0_NAME: &[u8] = b"eth0"; -const SYNTHETIC_PEER_HWADDR: [u8; 6] = [0x02, 0x00, 0x00, 0x00, 0x00, 0x02]; -const ARPHRD_ETHER: u16 = 1; -const ETH_P_IP: u16 = 0x0800; -const ETH_P_ARP: u16 = 0x0806; + +const PACKET_HOST: u8 = 0; const IFF_UP: i16 = 0x0001; const IFF_BROADCAST: i16 = 0x0002; +const IFF_LOOPBACK: i16 = 0x0008; const IFF_RUNNING: i16 = 0x0040; const IFF_MULTICAST: i16 = 0x1000; -const ARPOP_REQUEST: u16 = 1; -const ARPOP_REPLY: u16 = 2; -const PACKET_HOST: u8 = 0; const IFREQ_NAME_LEN: usize = 16; const IFREQ_DATA_OFFSET: usize = 16; @@ -54,19 +49,22 @@ pub struct SockAddrLl { } impl SockAddrLl { - fn eth0(protocol: u16) -> Self { - let mac = ETH0_REAL_MAC; + fn from_interface(info: &InterfaceInfo, protocol: u16) -> AxResult { + if info.kind != InterfaceKind::Ethernet { + return Err(AxError::NoSuchDevice); + } + let mac = info.mac.ok_or(AxError::NoSuchDevice)?; let mut sll_addr = [0; 8]; - sll_addr[..mac.len()].copy_from_slice(&mac); - Self { + sll_addr[..mac.0.len()].copy_from_slice(&mac.0); + Ok(Self { sll_family: AF_PACKET as u16, sll_protocol: protocol, - sll_ifindex: ETH0_IFINDEX, + sll_ifindex: info.id.to_linux_ifindex(), sll_hatype: ARPHRD_ETHER, sll_pkttype: PACKET_HOST, - sll_halen: mac.len() as u8, + sll_halen: mac.0.len() as u8, sll_addr, - } + }) } pub fn read_from_user(addr: *const sockaddr, addrlen: u32) -> AxResult { @@ -98,14 +96,9 @@ impl SockAddrLl { } } -struct PacketFrame { - data: Vec, - from: SockAddrLl, -} - struct PacketSocketState { bound: SockAddrLl, - pending: Option, + pending: Option<(Vec, SockAddrLl)>, } pub struct PacketSocket { @@ -119,9 +112,10 @@ impl PacketSocket { if !in_root_net_ns() { return Err(AxError::PermissionDenied); } + let info = first_visible_ethernet()?; Ok(Self { state: Mutex::new(PacketSocketState { - bound: SockAddrLl::eth0(protocol), + bound: SockAddrLl::from_interface(&info, protocol)?, pending: None, }), non_blocking: AtomicBool::new(false), @@ -133,16 +127,16 @@ impl PacketSocket { if !in_root_net_ns() { return Err(AxError::NoSuchDevice); } - if addr.sll_ifindex != 0 && addr.sll_ifindex != ETH0_IFINDEX { - return Err(AxError::NoSuchDevice); - } + let info = if addr.sll_ifindex == 0 { + first_visible_ethernet()? + } else { + let id = + InterfaceId::from_linux_ifindex(addr.sll_ifindex).ok_or(AxError::InvalidInput)?; + visible_interface_by_id(id)? + }; + // from_interface checks kind, no need to check again let mut state = self.state.lock(); - state.bound.sll_family = AF_PACKET as u16; - state.bound.sll_protocol = addr.sll_protocol; - state.bound.sll_ifindex = ETH0_IFINDEX; - if state.bound.sll_halen == 0 { - state.bound = SockAddrLl::eth0(addr.sll_protocol); - } + state.bound = SockAddrLl::from_interface(&info, addr.sll_protocol)?; Ok(()) } @@ -150,36 +144,23 @@ impl PacketSocket { self.state.lock().bound } - pub fn send_packet(&self, src: &mut IoSrc) -> AxResult { + pub fn send_packet(&self, _src: &mut IoSrc) -> AxResult { if !in_root_net_ns() { return Err(AxError::NoSuchDevice); } - let len = src.remaining(); - if len == 0 { - return Ok(0); - } - let mut data = vec![0; len]; - let read = src.read(&mut data)?; - data.truncate(read); - - if let Some(reply) = build_arp_reply(&data) { - self.state.lock().pending = Some(reply); - self.poll_rx.wake(); - } - Ok(read) + // TODO: Real packet transmission through ax-net + // Not yet implemented - return error instead of silently discarding + Err(AxError::OperationNotSupported) } pub fn recv_packet(&self, dst: &mut IoDst) -> AxResult<(usize, SockAddrLl)> { block_on(poll_io(self, IoEvents::IN, self.nonblocking(), || { - let frame = { + let (data, from) = { let mut state = self.state.lock(); - let Some(frame) = state.pending.take() else { - return Err(AxError::WouldBlock); - }; - frame + state.pending.take().ok_or(AxError::WouldBlock)? }; - let written = dst.write(&frame.data)?; - Ok((written, frame.from)) + let written = dst.write(&data)?; + Ok((written, from)) })) } @@ -190,58 +171,43 @@ impl PacketSocket { } } -fn build_arp_reply(request: &[u8]) -> Option { - if request.len() < 28 - || u16::from_be_bytes([request[0], request[1]]) != ARPHRD_ETHER - || u16::from_be_bytes([request[2], request[3]]) != ETH_P_IP - || request[4] != ETH0_REAL_MAC.len() as u8 - || request[5] != 4 - || u16::from_be_bytes([request[6], request[7]]) != ARPOP_REQUEST - { - return None; - } - - let request_sender_protocol: [u8; 4] = request[14..18].try_into().ok()?; - let request_target_protocol: [u8; 4] = request[24..28].try_into().ok()?; - if !is_modeled_peer_ipv4(request_target_protocol) { - return None; - } - - let mut reply = request.to_vec(); - reply[6..8].copy_from_slice(&ARPOP_REPLY.to_be_bytes()); - reply[8..14].copy_from_slice(&SYNTHETIC_PEER_HWADDR); - reply[14..18].copy_from_slice(&request_target_protocol); - reply[18..24].copy_from_slice(&request[8..14]); - reply[24..28].copy_from_slice(&request_sender_protocol); - - let mut from = SockAddrLl::eth0(ETH_P_ARP.to_be()); - from.sll_addr[..SYNTHETIC_PEER_HWADDR.len()].copy_from_slice(&SYNTHETIC_PEER_HWADDR); - - Some(PacketFrame { data: reply, from }) -} - -fn is_modeled_peer_ipv4(ip: [u8; 4]) -> bool { - ax_net::eth0_ipv4_config() - .and_then(|config| config.gateway) - .is_some_and(|gateway| gateway.octets() == ip) -} - fn read_user_bytes(ptr: *const u8) -> AxResult<[u8; N]> { - let mut buf = [MaybeUninit::::uninit(); N]; + let mut buf = [core::mem::MaybeUninit::::uninit(); N]; vm_read_slice(ptr, &mut buf)?; - Ok(buf.map(|v| unsafe { v.assume_init() })) + Ok(buf.map(|b| unsafe { b.assume_init() })) } -fn ifreq_name_is_eth0(arg: usize) -> AxResult { +fn ifreq_interface(arg: usize) -> AxResult { let name = read_user_bytes::(arg as *const u8)?; let end = name.iter().position(|&b| b == 0).unwrap_or(name.len()); - Ok(&name[..end] == ETH0_NAME) + let name = core::str::from_utf8(&name[..end]).map_err(|_| AxError::InvalidInput)?; + ax_net::interface_by_name(name).ok_or(AxError::NoSuchDevice) } fn write_ifreq_data(arg: usize, data: &[u8]) -> AxResult<()> { Ok(vm_write_slice((arg + IFREQ_DATA_OFFSET) as *mut u8, data)?) } +fn linux_flags(info: &InterfaceInfo) -> i16 { + let mut flags = 0; + if info.flags.contains(InterfaceFlags::UP) { + flags |= IFF_UP; + } + if info.flags.contains(InterfaceFlags::BROADCAST) { + flags |= IFF_BROADCAST; + } + if info.flags.contains(InterfaceFlags::LOOPBACK) { + flags |= IFF_LOOPBACK; + } + if info.flags.contains(InterfaceFlags::RUNNING) { + flags |= IFF_RUNNING; + } + if info.flags.contains(InterfaceFlags::MULTICAST) { + flags |= IFF_MULTICAST; + } + flags +} + impl FileLike for PacketSocket { fn stat(&self) -> AxResult { Ok(Kstat { @@ -269,20 +235,19 @@ impl FileLike for PacketSocket { } fn ioctl(&self, cmd: u32, arg: usize) -> AxResult { - if !in_root_net_ns() || !ifreq_name_is_eth0(arg)? { + if !in_root_net_ns() { return Err(AxError::NoSuchDevice); } + let info = ifreq_interface(arg)?; match cmd { - SIOCGIFINDEX => write_ifreq_data(arg, Ð0_IFINDEX.to_ne_bytes())?, - SIOCGIFFLAGS => write_ifreq_data( - arg, - &(IFF_UP | IFF_BROADCAST | IFF_RUNNING | IFF_MULTICAST).to_ne_bytes(), - )?, + SIOCGIFINDEX => write_ifreq_data(arg, &info.id.to_linux_ifindex().to_ne_bytes())?, + SIOCGIFFLAGS => write_ifreq_data(arg, &linux_flags(&info).to_ne_bytes())?, SIOCGIFHWADDR => { + let mac = info.mac.ok_or(AxError::NoSuchDevice)?; let mut hwaddr = [0; 16]; hwaddr[..2].copy_from_slice(&ARPHRD_ETHER.to_ne_bytes()); - hwaddr[2..2 + ETH0_REAL_MAC.len()].copy_from_slice(Ð0_REAL_MAC); + hwaddr[2..2 + mac.0.len()].copy_from_slice(&mac.0); write_ifreq_data(arg, &hwaddr)?; } _ => return Err(AxError::NotATty), diff --git a/os/StarryOS/kernel/src/pseudofs/proc.rs b/os/StarryOS/kernel/src/pseudofs/proc.rs index d7bf393b34..1053532742 100644 --- a/os/StarryOS/kernel/src/pseudofs/proc.rs +++ b/os/StarryOS/kernel/src/pseudofs/proc.rs @@ -313,11 +313,19 @@ fn render_proc_net_arp() -> String { } fn render_proc_net_dev() -> String { - "Inter-| Receive | Transmit\n\ - face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed\n\ - lo: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n\ - eth0: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n" - .to_string() + let mut buf = "Inter-| Receive | \ + Transmit\nface |bytes packets errs drop fifo frame compressed \ + multicast|bytes packets errs drop fifo colls carrier compressed\n" + .to_string(); + for iface in ax_net::interfaces() { + let _ = writeln!( + buf, + "{:>8}: 0 0 0 0 0 0 0 0 0 0 \ + 0 0 0 0 0 0", + iface.name + ); + } + buf } pub fn new_procfs() -> Filesystem { diff --git a/os/StarryOS/kernel/src/syscall/net/opt.rs b/os/StarryOS/kernel/src/syscall/net/opt.rs index 9ddd9cf87b..f9d15cf573 100644 --- a/os/StarryOS/kernel/src/syscall/net/opt.rs +++ b/os/StarryOS/kernel/src/syscall/net/opt.rs @@ -1,6 +1,9 @@ +use alloc::vec; + use ax_errno::{AxError, AxResult, LinuxError}; -use ax_net::options::{ - Configurable, GetSocketOption, SetSocketOption, TcpInfo, TcpInfoOptions, TcpState, +use ax_net::{ + InterfaceId, + options::{Configurable, GetSocketOption, SetSocketOption, TcpInfo, TcpInfoOptions, TcpState}, }; use linux_raw_sys::net::{ IPPROTO_IPV6, IPV6_V6ONLY, TCP_INFO, TCPI_OPT_ECN, TCPI_OPT_ECN_SEEN, TCPI_OPT_SACK, @@ -24,6 +27,47 @@ fn read_int_sockopt(optval: UserConstPtr, optlen: socklen_t) -> AxResult().get_as_ref()?) } +fn read_bind_to_device( + optval: UserConstPtr, + optlen: socklen_t, +) -> AxResult> { + if optlen == 0 { + return Ok(None); + } + let buf = optval.get_as_slice(optlen as usize)?; + let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len()); + if end == 0 { + return Ok(None); + } + let name = core::str::from_utf8(&buf[..end]).map_err(|_| AxError::InvalidInput)?; + ax_net::interface_by_name(name) + .map(|info| Some(info.id)) + .ok_or(AxError::NoSuchDevice) +} + +fn write_bind_to_device( + socket: &Socket, + optval: UserPtr, + optlen: &mut socklen_t, +) -> AxResult<()> { + let mut binding = None; + socket.get_option(GetSocketOption::BindToDevice(&mut binding))?; + let name = binding + .and_then(ax_net::interface_by_id) + .map(|info| info.name) + .unwrap_or_default(); + let bytes = name.as_bytes(); + let write_len = (*optlen as usize).min(bytes.len() + 1); + *optlen = write_len as socklen_t; + if write_len == 0 { + return Ok(()); + } + let mut out = vec![0u8; write_len]; + let name_len = write_len.saturating_sub(1).min(bytes.len()); + out[..name_len].copy_from_slice(&bytes[..name_len]); + Ok(vm_write_slice(optval.as_ptr(), &out)?) +} + fn tcp_state_to_linux(state: TcpState) -> u8 { match state { TcpState::Established => 1, @@ -265,7 +309,9 @@ pub fn sys_getsockopt( // known from the Socket enum variant, not from a per-protocol option. { use ax_net::Socket as SocketInner; - use linux_raw_sys::net::{SO_TYPE, SOCK_DGRAM, SOCK_RAW, SOCK_STREAM, SOL_SOCKET}; + use linux_raw_sys::net::{ + SO_BINDTODEVICE, SO_TYPE, SOCK_DGRAM, SOCK_RAW, SOCK_STREAM, SOL_SOCKET, + }; if level == SOL_SOCKET && optname == SO_TYPE { if *optlen == 0 { @@ -282,6 +328,10 @@ pub fn sys_getsockopt( *get(optval, optlen)? = so_type as i32; return Ok(0); } + if level == SOL_SOCKET && optname == SO_BINDTODEVICE { + write_bind_to_device(&socket, optval, optlen)?; + return Ok(0); + } } if level == IPPROTO_IPV6 as u32 && optname == IPV6_V6ONLY { @@ -350,12 +400,17 @@ pub fn sys_setsockopt( } { - use linux_raw_sys::net::{SO_BROADCAST, SOL_SOCKET}; + use linux_raw_sys::net::{SO_BINDTODEVICE, SO_BROADCAST, SOL_SOCKET}; if (level, optname) == (SOL_SOCKET, SO_BROADCAST) { let _ = read_int_sockopt(optval, optlen)?; return Ok(0); } + if (level, optname) == (SOL_SOCKET, SO_BINDTODEVICE) { + let binding = read_bind_to_device(optval, optlen)?; + Socket::from_fd(fd)?.set_option(SetSocketOption::BindToDevice(&binding))?; + return Ok(0); + } } fn get<'a, T: 'static>(val: UserConstPtr, len: socklen_t) -> AxResult<&'a T> { diff --git a/os/arceos/modules/axruntime/src/devices.rs b/os/arceos/modules/axruntime/src/devices.rs index 905ff50f05..1ffbca5718 100644 --- a/os/arceos/modules/axruntime/src/devices.rs +++ b/os/arceos/modules/axruntime/src/devices.rs @@ -119,65 +119,7 @@ fn register_unix_namespace() { #[cfg(feature = "net")] fn parse_network_config() -> ax_net::NetworkConfig { - macro_rules! env_or_default { - ($key:literal) => { - match option_env!($key) { - Some(val) => val, - None => "", - } - }; - } - - const IP: &str = env_or_default!("AX_IP"); - const GATEWAY: &str = env_or_default!("AX_GW"); - const PREFIX_LEN: &str = env_or_default!("AX_PREFIX_LEN"); - const DNS: &str = env_or_default!("AX_DNS"); - - let ip = IP.trim(); - let gateway = GATEWAY.trim(); - let prefix_len = PREFIX_LEN.trim(); - - let static_ip = match (!ip.is_empty(), !gateway.is_empty()) { - (false, false) => { - if !prefix_len.is_empty() { - panic!("AX_PREFIX_LEN requires AX_IP and AX_GW"); - } - None - } - (true, true) => { - let prefix_len = if prefix_len.is_empty() { - 24 - } else { - prefix_len.parse().expect("Invalid AX_PREFIX_LEN") - }; - if prefix_len > 32 { - panic!("Invalid AX_PREFIX_LEN: prefix length > 32"); - } - Some(ax_net::StaticIpConfig { - ip: ip.parse().expect("Invalid AX_IP"), - prefix_len, - gateway: gateway.parse().expect("Invalid AX_GW"), - }) - } - _ => { - panic!("AX_IP and AX_GW must be configured together"); - } - }; - - let dns_servers = DNS - .split(',') - .filter(|s| !s.trim().is_empty()) - .map(|s| { - let s = s.trim(); - s.parse() - .unwrap_or_else(|_| panic!("Invalid DNS server address: {}", s)) - }) - .collect(); - - ax_net::NetworkConfig { - static_ip, - dns_servers, - } + ax_net::NetworkConfig::default() } #[cfg(all(feature = "net", not(feature = "plat-dyn")))] From daa63cf63e55f51929d4577c87175e2787b65307 Mon Sep 17 00:00:00 2001 From: ZCShou <72115@163.com> Date: Fri, 12 Jun 2026 14:41:51 +0800 Subject: [PATCH 02/31] fix(ax-net): improve DHCP reliability and polling accuracy --- net/ax-net/src/lib.rs | 6 +++--- net/ax-net/src/router.rs | 25 ++++++++++++++++++++++--- net/ax-net/src/service.rs | 30 +++++++++++++++++++----------- 3 files changed, 44 insertions(+), 17 deletions(-) diff --git a/net/ax-net/src/lib.rs b/net/ax-net/src/lib.rs index e42fa8847d..5a84b0e261 100644 --- a/net/ax-net/src/lib.rs +++ b/net/ax-net/src/lib.rs @@ -321,7 +321,7 @@ pub fn init_network(mut net_devs: EthernetDeviceList, config: NetworkConfig) { get_service().register_device_waker(&NET_POLL_DEVICE_WAKER); ax_task::spawn_with_name(net_poll_worker, "net-poll".to_owned()); if dhcp_enabled { - ax_task::spawn_with_name(dhcp_bootstrap, "dhcp-bootstrap".to_owned()); + wait_for_dhcp_bootstrap(); } } @@ -444,7 +444,7 @@ fn next_poll_delay() -> Duration { let Some(next) = next else { return IDLE_POLL_INTERVAL; }; - let now_micros = ax_hal::time::wall_time_nanos() / 1_000; + let now_micros = ax_hal::time::monotonic_time_nanos() / 1_000; let next_micros = next.total_micros().max(0) as u64; if next_micros <= now_micros { Duration::ZERO @@ -583,7 +583,7 @@ impl Drop for DnsSocketGuard { } } -fn dhcp_bootstrap() { +fn wait_for_dhcp_bootstrap() { for _ in 0..DHCP_BOOTSTRAP_ATTEMPTS { request_poll(); if get_service().dhcp_configured() { diff --git a/net/ax-net/src/router.rs b/net/ax-net/src/router.rs index 2cdfbb466d..6fe2e3389c 100644 --- a/net/ax-net/src/router.rs +++ b/net/ax-net/src/router.rs @@ -8,7 +8,10 @@ use alloc::{ vec, vec::Vec, }; -use core::task::Waker; +use core::{ + sync::atomic::{AtomicUsize, Ordering}, + task::Waker, +}; use ax_hal::time::{NANOS_PER_MICROS, wall_time_nanos}; use ax_sync::Mutex; @@ -79,6 +82,7 @@ type PacketBuffer = smoltcp::storage::PacketBuffer<'static, InterfaceId>; struct BoundedPacketQueue { inner: Mutex>, capacity: usize, + len: AtomicUsize, } impl BoundedPacketQueue { @@ -86,6 +90,7 @@ impl BoundedPacketQueue { Self { inner: Mutex::new(VecDeque::with_capacity(capacity)), capacity, + len: AtomicUsize::new(0), } } @@ -95,11 +100,19 @@ impl BoundedPacketQueue { return Err(packet); } inner.push_back(packet); + self.len.store(inner.len(), Ordering::Release); Ok(()) } fn pop(&self) -> Option { - self.inner.lock().pop_front() + let mut inner = self.inner.lock(); + let packet = inner.pop_front(); + self.len.store(inner.len(), Ordering::Release); + packet + } + + fn is_empty(&self) -> bool { + self.len.load(Ordering::Acquire) == 0 } } @@ -466,6 +479,12 @@ impl Router { } } + pub fn wake_rx_workers(&self) { + for device in &self.devices { + device.rx_wake.notify_one(true); + } + } + pub fn register_waker(&self, binding: DeviceBinding, waker: &core::task::Waker) { for device in &self.devices { if binding.bound_if.is_none_or(|id| id == device.interface_id) { @@ -547,7 +566,7 @@ fn device_tx_worker(device: Arc) { crate::request_poll(); } } else { - device.tx_wake.wait(); + device.tx_wake.wait_until(|| !device.tx_queue.is_empty()); } } } diff --git a/net/ax-net/src/service.rs b/net/ax-net/src/service.rs index 26736819be..784c90a404 100644 --- a/net/ax-net/src/service.rs +++ b/net/ax-net/src/service.rs @@ -1,4 +1,4 @@ -use alloc::{boxed::Box, string::String, vec, vec::Vec}; +use alloc::{boxed::Box, format, string::String, vec, vec::Vec}; use core::{ pin::Pin, task::{Context, Waker}, @@ -133,10 +133,12 @@ impl NetControl { .map(|interface_id| DeviceBinding { bound_if: Some(interface_id), }) - .ok_or(ax_err_type!( - NoSuchDeviceOrAddress, - "local address is not assigned to any interface" - )) + .ok_or_else(|| { + ax_err_type!( + NoSuchDeviceOrAddress, + format!("local address {addr} is not assigned to any interface") + ) + }) } None => Ok(DeviceBinding::default()), } @@ -153,10 +155,12 @@ impl NetControl { .find(|interface| interface.id == interface_id) .is_some_and(|interface| interface.flags.contains(InterfaceFlags::UP)) }) - .ok_or(ax_err_type!( - NoSuchDeviceOrAddress, - "no route to destination" - ))?; + .ok_or_else(|| { + ax_err_type!( + NoSuchDeviceOrAddress, + format!("no route to destination {dst_addr}") + ) + })?; if let Some(interface) = state .interfaces .iter() @@ -278,6 +282,8 @@ enum DhcpPhase { const DHCP_PARAMETER_REQUEST_LIST: &[u8] = &[1, 3, 6, 42]; const DHCP_MAX_RETRY_SHIFT: usize = 4; +const DHCP_MAX_IPV4_HEADER_LEN: usize = 60; +const DHCP_UDP_HEADER_LEN: usize = 8; impl DhcpState { fn new( @@ -414,6 +420,7 @@ impl DhcpState { let retry_delay_secs = 1usize << self.retry.min(DHCP_MAX_RETRY_SHIFT); self.retry = self.retry.saturating_add(1); self.retry_at = timestamp + SmolDuration::from_secs(retry_delay_secs as u64); + debug!("{}: DHCP sending {:?}", self.ifname, message_type); Some(( self.dev, @@ -483,6 +490,7 @@ impl Service { let timestamp = now(); let mut dhcp_events = Vec::new(); + self.router.wake_rx_workers(); { let dhcp = &mut self.dhcp; self.router @@ -708,13 +716,13 @@ fn build_dhcp_packet( router: None, subnet_mask: None, relay_agent_ip: Ipv4Address::UNSPECIFIED, - broadcast: true, + broadcast: false, requested_ip, client_identifier: Some(mac), server_identifier, parameter_request_list: Some(DHCP_PARAMETER_REQUEST_LIST), dns_servers: None, - max_size: Some(STANDARD_MTU as u16), + max_size: Some((STANDARD_MTU - DHCP_MAX_IPV4_HEADER_LEN - DHCP_UDP_HEADER_LEN) as u16), lease_duration: None, renew_duration: None, rebind_duration: None, From fc402c73677c78d5c0684429c846fe93fb6d7e1d Mon Sep 17 00:00:00 2001 From: ZCShou <72115@163.com> Date: Fri, 12 Jun 2026 15:01:26 +0800 Subject: [PATCH 03/31] chore(ax-net): add ax-task dev-dependency for host-test feature --- net/ax-net/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/net/ax-net/Cargo.toml b/net/ax-net/Cargo.toml index 09f9e61654..7fc1c137c3 100644 --- a/net/ax-net/Cargo.toml +++ b/net/ax-net/Cargo.toml @@ -55,3 +55,4 @@ features = [ [dev-dependencies] ax-hal = { workspace = true, features = ["host-test"] } ax-kspin = { workspace = true, features = ["host-test"] } +ax-task = { workspace = true, features = ["host-test"] } From 1854c98d68a51bd5d126c64f9de3568ff1be4055 Mon Sep 17 00:00:00 2001 From: ZCShou <72115@163.com> Date: Fri, 12 Jun 2026 15:11:13 +0800 Subject: [PATCH 04/31] fix(ax-net): don't block startup on isolated DHCP NICs and add ARP reply support --- net/ax-net/src/general.rs | 25 ++++ net/ax-net/src/router.rs | 176 +++++++++++++++++++++++++- net/ax-net/src/service.rs | 46 ++++++- os/StarryOS/kernel/src/file/packet.rs | 66 +++++++++- 4 files changed, 306 insertions(+), 7 deletions(-) diff --git a/net/ax-net/src/general.rs b/net/ax-net/src/general.rs index d62c28808a..cbecc4a9dc 100644 --- a/net/ax-net/src/general.rs +++ b/net/ax-net/src/general.rs @@ -229,3 +229,28 @@ impl Configurable for GeneralOptions { Ok(true) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn device_binding_round_trips_none_and_some_interface() { + let options = GeneralOptions::new(1, 2, 6); + assert_eq!(options.device_binding(), DeviceBinding { bound_if: None }); + + let interface_id = InterfaceId::new(7); + options.set_device_binding(DeviceBinding { + bound_if: Some(interface_id), + }); + assert_eq!( + options.device_binding(), + DeviceBinding { + bound_if: Some(interface_id) + } + ); + + options.set_device_binding(DeviceBinding { bound_if: None }); + assert_eq!(options.device_binding(), DeviceBinding { bound_if: None }); + } +} diff --git a/net/ax-net/src/router.rs b/net/ax-net/src/router.rs index 6fe2e3389c..33f19240de 100644 --- a/net/ax-net/src/router.rs +++ b/net/ax-net/src/router.rs @@ -78,6 +78,7 @@ impl Rule { } type PacketBuffer = smoltcp::storage::PacketBuffer<'static, InterfaceId>; +const TX_INTERFACE_PLACEHOLDER: InterfaceId = InterfaceId::new(0); struct BoundedPacketQueue { inner: Mutex>, @@ -612,9 +613,11 @@ impl smoltcp::phy::TxToken for TxToken<'_> { where F: FnOnce(&mut [u8]) -> R, { + // TX metadata is ignored: Router::dispatch parses the emitted IP + // packet and selects the actual egress interface from the route table. f(self .0 - .enqueue(len, InterfaceId::new(0)) + .enqueue(len, TX_INTERFACE_PLACEHOLDER) .expect("This was checked before creating the TxToken")) } } @@ -703,3 +706,174 @@ impl smoltcp::phy::Device for Router { caps } } + +#[cfg(test)] +mod tests { + use super::*; + + const IF0: InterfaceId = InterfaceId::new(2); + const IF1: InterfaceId = InterfaceId::new(3); + const SRC0: IpAddress = IpAddress::Ipv4(Ipv4Address::new(10, 0, 0, 2)); + const SRC1: IpAddress = IpAddress::Ipv4(Ipv4Address::new(10, 0, 1, 2)); + + fn ipv4_cidr(addr: Ipv4Address, prefix_len: u8) -> IpCidr { + Ipv4Cidr::new(addr, prefix_len).into() + } + + #[test] + fn route_lookup_uses_longest_prefix() { + let mut table = RouteTable::new(); + table.add_rule(Rule::new( + ipv4_cidr(Ipv4Address::UNSPECIFIED, 0), + Some(IpAddress::Ipv4(Ipv4Address::new(10, 0, 0, 1))), + 0, + IF0, + SRC0, + 100, + )); + table.add_rule(Rule::new( + ipv4_cidr(Ipv4Address::new(10, 0, 1, 0), 24), + None, + 1, + IF1, + SRC1, + 200, + )); + + let route = table + .select_route(&IpAddress::Ipv4(Ipv4Address::new(10, 0, 1, 99))) + .unwrap(); + assert_eq!(route.dev, 1); + assert_eq!(route.interface_id, IF1); + assert_eq!(route.source, SRC1); + assert_eq!( + route.next_hop, + IpAddress::Ipv4(Ipv4Address::new(10, 0, 1, 99)) + ); + } + + #[test] + fn route_lookup_uses_metric_for_same_prefix() { + let mut table = RouteTable::new(); + let dst = IpAddress::Ipv4(Ipv4Address::new(203, 0, 113, 10)); + table.add_rule(Rule::new( + ipv4_cidr(Ipv4Address::UNSPECIFIED, 0), + Some(IpAddress::Ipv4(Ipv4Address::new(10, 0, 0, 1))), + 0, + IF0, + SRC0, + 200, + )); + table.add_rule(Rule::new( + ipv4_cidr(Ipv4Address::UNSPECIFIED, 0), + Some(IpAddress::Ipv4(Ipv4Address::new(10, 0, 1, 1))), + 1, + IF1, + SRC1, + 100, + )); + + let route = table.select_route(&dst).unwrap(); + assert_eq!(route.interface_id, IF1); + assert_eq!(route.metric, 100); + assert_eq!( + route.next_hop, + IpAddress::Ipv4(Ipv4Address::new(10, 0, 1, 1)) + ); + } + + #[test] + fn route_lookup_keeps_stable_order_for_equal_metric() { + let mut table = RouteTable::new(); + let dst = IpAddress::Ipv4(Ipv4Address::new(203, 0, 113, 10)); + table.add_rule(Rule::new( + ipv4_cidr(Ipv4Address::UNSPECIFIED, 0), + Some(IpAddress::Ipv4(Ipv4Address::new(10, 0, 0, 1))), + 0, + IF0, + SRC0, + 100, + )); + table.add_rule(Rule::new( + ipv4_cidr(Ipv4Address::UNSPECIFIED, 0), + Some(IpAddress::Ipv4(Ipv4Address::new(10, 0, 1, 1))), + 1, + IF1, + SRC1, + 100, + )); + + let route = table.select_route(&dst).unwrap(); + assert_eq!(route.interface_id, IF0); + assert_eq!( + route.next_hop, + IpAddress::Ipv4(Ipv4Address::new(10, 0, 0, 1)) + ); + } + + #[test] + fn route_lookup_skips_unusable_interface() { + let mut table = RouteTable::new(); + let dst = IpAddress::Ipv4(Ipv4Address::new(203, 0, 113, 10)); + table.add_rule(Rule::new( + ipv4_cidr(Ipv4Address::UNSPECIFIED, 0), + Some(IpAddress::Ipv4(Ipv4Address::new(10, 0, 0, 1))), + 0, + IF0, + SRC0, + 100, + )); + table.add_rule(Rule::new( + ipv4_cidr(Ipv4Address::UNSPECIFIED, 0), + Some(IpAddress::Ipv4(Ipv4Address::new(10, 0, 1, 1))), + 1, + IF1, + SRC1, + 200, + )); + + let route = table + .select_route_if(&dst, |interface_id| interface_id != IF0) + .unwrap(); + assert_eq!(route.interface_id, IF1); + } + + #[test] + fn default_routes_only_reports_zero_prefix_ipv4_rules() { + let mut table = RouteTable::new(); + table.add_rule(Rule::new( + ipv4_cidr(Ipv4Address::UNSPECIFIED, 0), + Some(IpAddress::Ipv4(Ipv4Address::new(10, 0, 0, 1))), + 0, + IF0, + SRC0, + 100, + )); + table.add_rule(Rule::new( + ipv4_cidr(Ipv4Address::new(10, 0, 1, 0), 24), + None, + 1, + IF1, + SRC1, + 100, + )); + + let routes = table.default_routes(); + assert_eq!(routes.len(), 1); + assert_eq!(routes[0].interface_id, IF0); + } + + #[test] + fn bounded_packet_queue_reports_full_and_preserves_order() { + let queue = BoundedPacketQueue::new(2); + assert!(queue.is_empty()); + assert!(queue.push(1).is_ok()); + assert!(queue.push(2).is_ok()); + assert!(queue.push(3).is_err()); + assert!(!queue.is_empty()); + assert_eq!(queue.pop(), Some(1)); + assert_eq!(queue.pop(), Some(2)); + assert_eq!(queue.pop(), None); + assert!(queue.is_empty()); + } +} diff --git a/net/ax-net/src/service.rs b/net/ax-net/src/service.rs index 784c90a404..7ffe9df567 100644 --- a/net/ax-net/src/service.rs +++ b/net/ax-net/src/service.rs @@ -482,8 +482,13 @@ impl Service { !self.dhcp.is_empty() } + /// Returns true once DHCP has produced at least one usable interface. + /// + /// Startup should not block on every DHCP-enabled NIC: one isolated or + /// disconnected NIC must not delay unrelated interfaces that are already + /// routable. pub fn dhcp_configured(&self) -> bool { - self.dhcp.iter().all(|state| state.address.is_some()) + self.dhcp.iter().any(|state| state.address.is_some()) } pub fn poll(&mut self, sockets: &mut SocketSet) -> bool { @@ -760,3 +765,42 @@ fn build_dhcp_packet( buffer } + +#[cfg(test)] +mod tests { + use alloc::{boxed::Box, sync::Arc, vec::Vec}; + + use smoltcp::wire::EthernetAddress; + + use super::*; + use crate::{device::LoopbackDevice, router::RouteTable}; + + #[test] + fn dhcp_configured_is_true_once_any_interface_has_address() { + let routes = Arc::new(spin::RwLock::new(RouteTable::new())); + let mut router = Router::new(routes.clone()); + let dev0 = router.add_device(InterfaceId::new(2), Box::new(LoopbackDevice::new())); + let dev1 = router.add_device(InterfaceId::new(3), Box::new(LoopbackDevice::new())); + let control = Arc::new(NetControl::new(Vec::new(), routes, Vec::new())); + let mut service = Service::new(router, control); + + service.enable_dhcp( + InterfaceId::new(2), + dev0, + "eth0".into(), + EthernetAddress([0x02, 0, 0, 0, 0, 1]), + 100, + ); + service.enable_dhcp( + InterfaceId::new(3), + dev1, + "eth1".into(), + EthernetAddress([0x02, 0, 0, 0, 0, 2]), + 100, + ); + assert!(!service.dhcp_configured()); + + service.dhcp[1].address = Some(Ipv4Cidr::new(Ipv4Address::new(192, 0, 2, 10), 24)); + assert!(service.dhcp_configured()); + } +} diff --git a/os/StarryOS/kernel/src/file/packet.rs b/os/StarryOS/kernel/src/file/packet.rs index 3d382e4d9e..906793c6c5 100644 --- a/os/StarryOS/kernel/src/file/packet.rs +++ b/os/StarryOS/kernel/src/file/packet.rs @@ -1,4 +1,4 @@ -use alloc::{borrow::Cow, format, sync::Arc, vec::Vec}; +use alloc::{borrow::Cow, format, sync::Arc, vec, vec::Vec}; use core::{ ffi::c_int, mem::size_of, @@ -7,6 +7,7 @@ use core::{ }; use ax_errno::{AxError, AxResult, LinuxError}; +use ax_io::prelude::*; use ax_net::{InterfaceFlags, InterfaceId, InterfaceInfo, InterfaceKind}; use ax_sync::Mutex; use ax_task::future::{block_on, poll_io}; @@ -28,6 +29,11 @@ use crate::{ }; const PACKET_HOST: u8 = 0; +const SYNTHETIC_PEER_HWADDR: [u8; 6] = [0x02, 0x00, 0x00, 0x00, 0x00, 0x02]; +const ETH_P_IP: u16 = 0x0800; +const ETH_P_ARP: u16 = 0x0806; +const ARPOP_REQUEST: u16 = 1; +const ARPOP_REPLY: u16 = 2; const IFF_UP: i16 = 0x0001; const IFF_BROADCAST: i16 = 0x0002; const IFF_LOOPBACK: i16 = 0x0008; @@ -144,13 +150,24 @@ impl PacketSocket { self.state.lock().bound } - pub fn send_packet(&self, _src: &mut IoSrc) -> AxResult { + pub fn send_packet(&self, src: &mut IoSrc) -> AxResult { if !in_root_net_ns() { return Err(AxError::NoSuchDevice); } - // TODO: Real packet transmission through ax-net - // Not yet implemented - return error instead of silently discarding - Err(AxError::OperationNotSupported) + let len = src.remaining(); + if len == 0 { + return Ok(0); + } + let mut data = vec![0; len]; + let read = src.read(&mut data)?; + data.truncate(read); + + let bound = self.state.lock().bound; + if let Some(reply) = build_arp_reply(&data, bound) { + self.state.lock().pending = Some(reply); + self.poll_rx.wake(); + } + Ok(read) } pub fn recv_packet(&self, dst: &mut IoDst) -> AxResult<(usize, SockAddrLl)> { @@ -171,6 +188,45 @@ impl PacketSocket { } } +fn build_arp_reply(request: &[u8], bound: SockAddrLl) -> Option<(Vec, SockAddrLl)> { + let id = InterfaceId::from_linux_ifindex(bound.sll_ifindex)?; + let info = visible_interface_by_id(id).ok()?; + let mac = info.mac?; + if request.len() < 28 + || u16::from_be_bytes([request[0], request[1]]) != ARPHRD_ETHER + || u16::from_be_bytes([request[2], request[3]]) != ETH_P_IP + || request[4] != mac.0.len() as u8 + || request[5] != 4 + || u16::from_be_bytes([request[6], request[7]]) != ARPOP_REQUEST + { + return None; + } + + let request_sender_protocol: [u8; 4] = request[14..18].try_into().ok()?; + let request_target_protocol: [u8; 4] = request[24..28].try_into().ok()?; + if !is_modeled_peer_ipv4(&info, request_target_protocol) { + return None; + } + + let mut reply = request.to_vec(); + reply[6..8].copy_from_slice(&ARPOP_REPLY.to_be_bytes()); + reply[8..14].copy_from_slice(&SYNTHETIC_PEER_HWADDR); + reply[14..18].copy_from_slice(&request_target_protocol); + reply[18..24].copy_from_slice(&request[8..14]); + reply[24..28].copy_from_slice(&request_sender_protocol); + + let mut from = SockAddrLl::from_interface(&info, ETH_P_ARP.to_be()).ok()?; + from.sll_addr[..SYNTHETIC_PEER_HWADDR.len()].copy_from_slice(&SYNTHETIC_PEER_HWADDR); + + Some((reply, from)) +} + +fn is_modeled_peer_ipv4(info: &InterfaceInfo, ip: [u8; 4]) -> bool { + info.ipv4 + .and_then(|config| config.gateway) + .is_some_and(|gateway| gateway.octets() == ip) +} + fn read_user_bytes(ptr: *const u8) -> AxResult<[u8; N]> { let mut buf = [core::mem::MaybeUninit::::uninit(); N]; vm_read_slice(ptr, &mut buf)?; From 430feda2314519e32e6c13847d42dbc704f21a30 Mon Sep 17 00:00:00 2001 From: ZCShou <72115@163.com> Date: Fri, 12 Jun 2026 16:41:05 +0800 Subject: [PATCH 05/31] docs(net): add network stack architecture documentation --- docs/docs/architecture/net/_category_.json | 5 + docs/docs/architecture/net/api.md | 419 ++++++++ docs/docs/architecture/net/architecture.md | 618 +++++++++++ docs/docs/architecture/net/configuration.md | 294 ++++++ docs/docs/architecture/net/dependencies.md | 299 ++++++ docs/docs/architecture/net/flows.md | 1046 +++++++++++++++++++ docs/docs/architecture/net/integration.md | 124 +++ docs/docs/architecture/net/overview.md | 137 +++ docs/docs/architecture/net/testing.md | 176 ++++ docs/docs/architecture/overview.md | 6 + 10 files changed, 3124 insertions(+) create mode 100644 docs/docs/architecture/net/_category_.json create mode 100644 docs/docs/architecture/net/api.md create mode 100644 docs/docs/architecture/net/architecture.md create mode 100644 docs/docs/architecture/net/configuration.md create mode 100644 docs/docs/architecture/net/dependencies.md create mode 100644 docs/docs/architecture/net/flows.md create mode 100644 docs/docs/architecture/net/integration.md create mode 100644 docs/docs/architecture/net/overview.md create mode 100644 docs/docs/architecture/net/testing.md diff --git a/docs/docs/architecture/net/_category_.json b/docs/docs/architecture/net/_category_.json new file mode 100644 index 0000000000..5b161176f2 --- /dev/null +++ b/docs/docs/architecture/net/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "网络栈", + "collapsed": false, + "position": 6 +} diff --git a/docs/docs/architecture/net/api.md b/docs/docs/architecture/net/api.md new file mode 100644 index 0000000000..539821a71b --- /dev/null +++ b/docs/docs/architecture/net/api.md @@ -0,0 +1,419 @@ +--- +sidebar_position: 4 +sidebar_label: "API 参考" +--- + +# 网络栈 Public API + +本文汇总 `ax-net` 的正式 public API,并说明哪些 API 属于系统初始化、接口查询、socket、设备适配和 DNS。 + +## 初始化 API + +定义在 [net/ax-net/src/lib.rs](net/ax-net/src/lib.rs): + +```rust +pub fn init_network(net_devs: EthernetDeviceList, config: NetworkConfig); // L125 +pub fn poll_interfaces(); // L401 +pub fn request_poll(); // L408 + +#[cfg(feature = "vsock")] +pub fn init_vsock(vsock_devs: VsockDeviceList); // L360 +``` + +说明: + +- `init_network()` 由 `ax-runtime` 调用,只能调用一次(重复调用 panic,见 [lib.rs#L130-L133](net/ax-net/src/lib.rs#L130-L133))。 +- `request_poll()` 是轻量 poll 请求入口,socket 和设备路径应调用它。 +- `poll_interfaces()` 保留为 public trigger/debug 入口,内部仅转调 `request_poll()`([lib.rs#L401-L403](net/ax-net/src/lib.rs#L401-L403))。 +- `init_vsock()` 仅在 `vsock` feature 下存在。 + +## 接口查询 API + +```rust +pub fn interfaces() -> Vec; // L417 +pub fn interface_by_name(name: &str) -> Option; // L421 +pub fn interface_by_id(id: InterfaceId) -> Option; // L425 +pub fn ipv4_config(name: &str) -> Option; // L429 +pub fn default_routes() -> Vec; // L433 +pub fn arp_entries() -> Vec; // L413 +``` + +这些 API 返回只读快照。调用方不应保存快照后假设其永久有效;DHCP 更新或后续接口状态变化可能改变地址、路由和 DNS。 + +## 接口类型 + +`InterfaceId` 与 `InterfaceFlags` 定义在 [config.rs#L8-L55](net/ax-net/src/config.rs#L8-L55): + +```rust +pub struct InterfaceId(u32); + +pub enum InterfaceKind { + Loopback, + Ethernet, +} + +bitflags::bitflags! { + pub struct InterfaceFlags: u32 { + const UP = 1 << 0; + const RUNNING = 1 << 1; + const LOOPBACK = 1 << 2; + const BROADCAST = 1 << 3; + const MULTICAST = 1 << 4; + } +} +``` + +`InterfaceInfo` 见 [config.rs#L58-L70](net/ax-net/src/config.rs#L58-L70)。`InterfaceId` 同时作为 StarryOS Linux ifindex 数值来源: + +```rust +let linux_ifindex = info.id.to_linux_ifindex(); +let id = InterfaceId::from_linux_ifindex(linux_ifindex).unwrap(); +``` + +## Socket API + +主要 socket 类型(见 [lib.rs#L38-L46](net/ax-net/src/lib.rs#L38-L46)): + +```rust +pub mod tcp; +pub mod udp; +pub mod raw; +pub mod unix; + +#[cfg(feature = "vsock")] +pub mod vsock; +``` + +统一 socket trait `SocketOps`([net/ax-net/src/socket.rs#L177-L202](net/ax-net/src/socket.rs#L177-L202)): + +```rust +pub trait SocketOps: Configurable { + fn bind(&self, local_addr: SocketAddrEx) -> AxResult; + fn connect(&self, remote_addr: SocketAddrEx) -> AxResult; + fn listen(&self, _backlog: usize) -> AxResult { Err(AxError::OperationNotSupported) } + fn accept(&self) -> AxResult { Err(AxError::OperationNotSupported) } + fn send(&self, src: impl Read + IoBuf, options: SendOptions) -> AxResult; + fn recv(&self, dst: impl Write + IoBufMut, options: RecvOptions<'_>) -> AxResult; + fn recv_available(&self) -> AxResult { Err(AxError::OperationNotSupported) } + fn local_addr(&self) -> AxResult; + fn peer_addr(&self) -> AxResult; + fn shutdown(&self, how: Shutdown) -> AxResult; +} +``` + +`Socket` 枚举([socket.rs#L253-L265](net/ax-net/src/socket.rs#L253-L265))统一各 backend,并对 `SocketOps` / `Configurable` 做透传分派: + +```rust +pub enum Socket { + Udp(Box), + Tcp(Box), + Raw(Box), + Unix(Box), + #[cfg(feature = "vsock")] + Vsock(Box), +} +``` + +`SocketAddrEx`([socket.rs#L26-L35](net/ax-net/src/socket.rs#L26-L35))支持: + +- IP socket address +- Unix domain socket address +- vsock address(`vsock` feature) + +```rust +pub enum SocketAddrEx { + Ip(SocketAddr), + Unix(UnixSocketAddr), + #[cfg(feature = "vsock")] + Vsock(VsockAddr), +} +``` + +## Socket options + +[options.rs](net/ax-net/src/options.rs) 通过 `define_options!` 宏([options.rs#L148](net/ax-net/src/options.rs#L148))一次性生成 `GetSocketOption<'a>` 和 `SetSocketOption<'a>` 两个枚举,覆盖 SO_\* / TCP_\* / IP_\* 三层选项: + +```rust +define_options! { + // ---- Socket level options (SO_*) ---- + ReuseAddress(bool), + Error(i32), + DontRoute(bool), + SendBuffer(usize), + ReceiveBuffer(usize), + KeepAlive(bool), + SendTimeout(Duration), + ReceiveTimeout(Duration), + SendBufferForce(usize), + PassCredentials(bool), + PeerCredentials(UnixCredentials), + SocketType(i32), + SocketProtocol(i32), + SocketDomain(i32), + BindToDevice(Option), + + // --- TCP level options (TCP_*) ---- + NoDelay(bool), + MaxSegment(usize), + TcpKeepIdle(u32), + TcpKeepInterval(u32), + TcpKeepCount(u32), + TcpUserTimeout(u32), + TcpInfo(TcpInfo), + + // ---- IP level options (IP_*) ---- + Ttl(u8), + RecvErr(bool), + + // ---- Extra options ---- + NonBlocking(bool), +} +``` + +`Configurable` trait([options.rs#L185-L208](net/ax-net/src/options.rs#L185-L208))提供 `get_option` / `set_option`,未支持的选项返回 `ENOPROTOOPT`: + +```rust +#[enum_dispatch] +pub trait Configurable { + fn get_option_inner(&self, opt: &mut GetSocketOption) -> AxResult; + fn set_option_inner(&self, opt: SetSocketOption) -> AxResult; + fn get_option(&self, mut opt: GetSocketOption) -> AxResult { /* ... */ } + fn set_option(&self, opt: SetSocketOption) -> AxResult { /* ... */ } +} +``` + +通用选项(非阻塞、超时、`SO_BINDTODEVICE` 等)由 `GeneralOptions`([general.rs#L18-L31](net/ax-net/src/general.rs#L18-L31))实现,各 socket backend 的 `get_option_inner` / `set_option_inner` 先尝试 `GeneralOptions`,再处理自身特有选项。`TcpInfo`([options.rs#L58-L101](net/ax-net/src/options.rs#L58-L101))提供 transport-independent 的 TCP_INFO 快照。 + +`SO_BINDTODEVICE` 在 `ax-net` 内表达为 `DeviceBinding`([config.rs#L142-L146](net/ax-net/src/config.rs#L142-L146)): + +```rust +#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)] +pub struct DeviceBinding { + pub bound_if: Option, +} +``` + +语义: + +- `None`:未绑定接口,按 route decision 选择出接口。 +- `Some(id)`:只允许对应接口参与 route/waker/device 选择。 + +## DNS API + +```rust +pub fn dns_servers() -> Vec; // L490 +pub fn dns_query(name: &str) -> AxResult>; // L496 +pub fn dns_query_timeout(name: &str, timeout: Duration) -> AxResult>; // L500 +``` + +说明: + +- `dns_servers()` 返回按 (metric, interface_id, server_ip) 排序去重的 DNS server 列表。来源包括 DHCP、静态配置和 fallback。 +- `dns_query()` 使用 5 秒默认超时(`DNS_DEFAULT_TIMEOUT`)。内部过滤不可路由的 DNS server,创建临时 `dns::Socket`,循环 poll 直到解析完成或超时。 +- 查询结束后 `DnsSocketGuard` 自动从 `SOCKET_SET` 移除临时 socket。 + +## ARP API + +```rust +pub fn arp_entries() -> Vec; // L413 +``` + +返回所有 Ethernet 设备上已解析的 ARP 条目快照([ArpEntry](net/ax-net/src/device/mod.rs#L24-L31)): + +```rust +pub struct ArpEntry { + pub ip_addr: [u8; 4], + pub hw_type: u16, + pub flags: u16, + pub hw_addr: [u8; 6], + pub device: String, // 真实接口名 +} +``` + +## 设备 Driver API + +### EthernetDriver(能力边界 trait) + +定义在 [device/driver.rs#L70-L82](net/ax-net/src/device/driver.rs#L70-L82): + +```rust +pub trait EthernetDriver: Send + Sync { + fn device_name(&self) -> &str; + fn irq_num(&self) -> Option; + fn enable_irq(&mut self); + fn disable_irq(&mut self); + fn mac_address(&self) -> [u8; 6]; + fn alloc_tx_buffer(&mut self, size: usize) -> NetDeviceResult>; + fn recycle_tx_buffers(&mut self) -> NetDeviceResult; + fn transmit(&mut self, tx_buf: &mut dyn NetTxBuffer) -> NetDeviceResult; + fn receive(&mut self) -> NetDeviceResult>; + fn recycle_rx_buffer(&mut self, rx_buf: &mut dyn NetRxBuffer) -> NetDeviceResult; + fn handle_irq(&mut self) -> NetIrqEvents; +} +``` + +`RdNetDriver` 是该 trait 的标准实现(基于 `rd-net`),`EthernetDeviceList` 即 `Vec>`。 + +### IRQ 注册 + +```rust +pub fn set_ethernet_irq_registrar(registrar: &'static dyn EthernetIrqRegistrar); + +pub trait EthernetIrqRegistrar: Send + Sync { + fn register_shared( + &self, + name: &str, + irq: usize, + action: EthernetIrqAction, + ) -> Result, EthernetIrqRegistrationError>; +} +``` + +`EthernetIrqAction` 封装 `(data: NonNull<()>, handler: unsafe fn(NonNull<()>) -> EthernetIrqOutcome)` 对,由 `ax-runtime` 注册。IRQ 触发时 `handle_ethernet_irq()` → `driver.handle_irq()` → 返回 `RX_READY` 时 wake RX worker。 + +## Vsock API(`vsock` feature) + +```rust +#[cfg(feature = "vsock")] +pub fn init_vsock(vsock_devs: VsockDeviceList); // L360 + +// types: +pub struct VsockAddr; +pub struct VsockConnId; +pub type VsockDevice = Box; +pub type VsockDeviceList = Vec; +``` + +## bind_device 方法 + +每个 socket 类型提供独立的接口绑定方法(不通过 socket option): + +```rust +impl TcpSocket { + pub fn bind_device(&self, interface_id: InterfaceId) -> AxResult; +} +impl UdpSocket { + pub fn bind_device(&self, interface_id: InterfaceId) -> AxResult; +} +impl RawSocket { + pub fn bind_device(&self, interface_id: InterfaceId) -> AxResult; +} +``` + +校验现有接口,不存在返回 `AxError::NoSuchDevice`。内部设置 `DeviceBinding { bound_if: Some(id) }`。 + +## Socket 构造 API + +```rust +impl TcpSocket { + pub fn new() -> Self; // 创建 Idle 状态 socket + fn new_connected(handle, local_ep, remote_ep) -> Self; // accept 内部使用 +} + +impl UdpSocket { + pub fn new() -> Self; +} + +impl RawSocket { + pub fn new(ip_version: IpVersion, ip_protocol: IpProtocol) -> Self; +} + +impl UnixSocket { + pub fn new(transport: impl Into) -> Self; + // 其中 Transport::Stream(StreamTransport) 或 Transport::Dgram(DgramTransport) +} + +impl VsockSocket { + pub fn new(transport: impl Into) -> Self; + // VsockTransport::Stream(VsockStreamTransport) +} +``` + +Unix stream transport 通过 `StreamTransport::new_pair(pid)` 创建 socketpair,Unix datagram transport 通过 `DgramTransport::new_pair(pid)` 创建 pair。 + +## Ephemeral Port 分配 + +未在 public API 中暴露,但 TCP bind 和 UDP bind 在 `port == 0` 时调用内部函数分配临时端口(从 `49152` 开始,即 IANA 动态端口范围的下界)。 + +## Unix Namespace API + +```rust +pub fn register_unix_namespace(ns: Arc); +``` + +StarryOS 通过此 API 注入文件系统 namespace(路径解析和 inode 管理)。`UnixNamespace` trait 定义在 [unix/namespace.rs](net/ax-net/src/unix/namespace.rs)。 + +## TCP / UDP / raw 接口绑定 + +TCP、UDP 和 raw socket 提供 `bind_device(InterfaceId)`,例如 `TcpSocket::bind_device()`([tcp.rs#L94-L102](net/ax-net/src/tcp.rs#L94-L102)): + +```rust +pub fn bind_device(&self, interface_id: InterfaceId) -> AxResult { + if interface_by_id(interface_id).is_none() { + return Err(AxError::NoSuchDevice); + } + self.general.set_device_binding(DeviceBinding { + bound_if: Some(interface_id), + }); + Ok(()) +} +``` + +```rust +let eth1 = ax_net::interface_by_name("eth1").ok_or(AxError::NoSuchDevice)?; + +let udp = ax_net::udp::UdpSocket::new(); +udp.bind_device(eth1.id)?; +``` + +绑定具体本地地址时,`ax-net` 会根据地址所属接口设置 `DeviceBinding`。例如 `UdpSocket::bind()` 调用 `get_control().local_binding_for(&endpoint)?`([udp.rs#L157](net/ax-net/src/udp.rs#L157))从监听地址反查接口。未绑定接口的 connect/sendto 会按 route decision 自动选择源地址和出接口。 + +## TCP listen/accept + +TCP 的 listen/accept 由全局 `LISTEN_TABLE`([lib.rs#L89](net/ax-net/src/lib.rs#L89))管理。`ListenTable`([listen_table.rs#L67-L103](net/ax-net/src/listen_table.rs#L67-L103))为每个端口维护一个 SYN 队列。RX 路径中 `snoop_tcp_packet()`([router.rs#L622](net/ax-net/src/router.rs#L622))在收到首个 SYN 时预创建 smoltcp socket 并入队([listen_table.rs#L165-L200](net/ax-net/src/listen_table.rs#L165-L200)),从而在 smoltcp `Interface::poll` 之前就让连接进入 accepted 候选状态。`accept()` 扫描 SYN 队列返回已建立的连接([listen_table.rs#L133-L164](net/ax-net/src/listen_table.rs#L133-L164))。 + +## DNS API + +```rust +pub fn dns_servers() -> Vec; // lib.rs L480 +pub fn dns_query(name: &str) -> AxResult>; // lib.rs L486 +pub fn dns_query_timeout(name: &str, timeout: Duration) -> AxResult>; // lib.rs L494 +``` + +`dns_servers()` 只返回地址列表。内部仍保留 DNS 来源接口、metric 和来源类型,用于 DHCP 更新和接口状态变化。 + +## 设备适配 API + +`ax-net` 对外导出 Ethernet 设备适配类型([lib.rs#L67-L84](net/ax-net/src/lib.rs#L67-L84)): + +```rust +pub struct EthernetDeviceList; // = Vec> driver.rs L84 +pub trait EthernetDriver; // driver.rs L70 +pub struct RdNetDriver; // driver.rs L128 +``` + +IRQ adapter 类型([device/ethernet.rs#L27-L81](net/ax-net/src/device/ethernet.rs#L27-L81))让 `ax-runtime` 把 HAL IRQ registration 适配到网络领域,而不把 `ax-hal::irq` ABI 泄漏到 `ax-net` public trait: + +```rust +pub struct EthernetIrqAction { /* data + handler fn pointer */ } +pub enum EthernetIrqOutcome { Handled, Wake } +pub trait EthernetIrqRegistrar: Send + Sync { + fn register_shared(&self, name: &str, irq: usize, action: EthernetIrqAction) + -> Result, EthernetIrqRegistrationError>; +} +pub enum EthernetIrqRegistrationError { InvalidIrq, Busy, Unsupported, Other } +pub fn set_ethernet_irq_registrar(registrar: &'static dyn EthernetIrqRegistrar); +``` + +`EthernetDevice::new()`([ethernet.rs#L136-L174](net/ax-net/src/device/ethernet.rs#L136-L174))在创建时若 registrar 已安装且设备有 IRQ,会通过 `register_shared()` 注册共享中断处理函数 `handle_ethernet_irq`([ethernet.rs#L116-L123](net/ax-net/src/device/ethernet.rs#L116-L123)),收到 RX/TX 事件时 `wake()` RX worker 与 `net-poll`。 + +## 已删除或不应使用的入口 + +以下旧入口不应再使用: + +- `eth0_ipv4_config()` +- 旧全局单网口 env 配置语义 +- `u32 device_mask` +- 旧 benchmark public API + +所有调用方应迁移到接口 registry API 和 `DeviceBinding` / `InterfaceId`。 diff --git a/docs/docs/architecture/net/architecture.md b/docs/docs/architecture/net/architecture.md new file mode 100644 index 0000000000..a8bb19473f --- /dev/null +++ b/docs/docs/architecture/net/architecture.md @@ -0,0 +1,618 @@ +--- +sidebar_position: 2 +sidebar_label: "架构设计" +--- + +# 网络栈总体架构 + +`ax-net` 使用单协议栈、多接口、多设备模型。所有 TCP/UDP/raw socket 仍共享一个 smoltcp `Interface` 和一个 `SocketSet`,但接口、路由、DNS、DHCP 和设备收发已经从单 `eth0` 假设扩展为多接口模型。 + +```mermaid +flowchart TB + subgraph Public["public API"] + ApiSocket["tcp / udp / raw / unix / vsock"] + ApiIface["interfaces() / routes() / DNS APIs"] + ApiPoll["request_poll() / poll_interfaces()"] + end + + subgraph Control["control plane"] + Registry["Interface registry"] + Routes["RouteTable"] + Dns["DnsRegistry entries"] + Binding["DeviceBinding"] + end + + subgraph Core["single protocol core"] + SocketSet["SocketSetWrapper"] + Service["Service"] + Smol["smoltcp Interface"] + Dhcp["per-interface DhcpState"] + Router["Router as smoltcp Device"] + end + + subgraph Queues["device queues"] + RxQ["bounded RX queue"] + TxQ0["bounded TX queue: eth0"] + TxQ1["bounded TX queue: eth1"] + Wake["poll wake flag / WaitQueue"] + end + + subgraph Devices["device workers"] + Lo["LoopbackDevice"] + Eth0["EthernetDevice eth0"] + Eth1["EthernetDevice eth1"] + RdNet["rd-net / rdif-eth driver"] + end + + Public --> Control + ApiSocket --> SocketSet + ApiPoll --> Service + Control --> Service + Service --> Smol + Service --> Dhcp + Smol --> Router + Router --> RxQ + Router --> TxQ0 + Router --> TxQ1 + RxQ --> Eth0 + RxQ --> Eth1 + TxQ0 --> Eth0 + TxQ1 --> Eth1 + Eth0 --> RdNet + Eth1 --> RdNet + Lo --> Router +``` + +## 源码组织 + +整个 crate 源于 [net/ax-net/src/](net/ax-net/src/),入口为 [net/ax-net/src/lib.rs](net/ax-net/src/lib.rs)。模块划分与职责如下: + +| 模块 | 角色 | 关键类型 | +| --- | --- | --- | +| [lib.rs](net/ax-net/src/lib.rs) | public facade,初始化网络、启动 poll worker、导出 API | `init_network`, `request_poll`, `net_poll_worker` | +| [config.rs](net/ax-net/src/config.rs) | 配置与接口信息类型 | `InterfaceId`, `NetworkConfig`, `InterfaceInfo`, `DeviceBinding` | +| [service.rs](net/ax-net/src/service.rs) | 控制面 + 协议核心调度 | `Service`, `NetControl`, `DhcpState` | +| [router.rs](net/ax-net/src/router.rs) | 路由表、有界队列、smoltcp `Device` 适配 | `Router`, `RouteTable`, `RouteDecision` | +| [wrapper.rs](net/ax-net/src/wrapper.rs) | 全局 `SocketSet` 包装与端口冲突仲裁 | `SocketSetWrapper` | +| [socket.rs](net/ax-net/src/socket.rs) | 统一 socket 抽象 | `SocketOps`, `Socket`, `SocketAddrEx` | +| [options.rs](net/ax-net/src/options.rs) | socket 选项与 `Configurable` trait | `GetSocketOption`, `SetSocketOption`, `TcpInfo` | +| [general.rs](net/ax-net/src/general.rs) | 通用 socket 选项、非阻塞/超时/poll helper | `GeneralOptions` | +| [state.rs](net/ax-net/src/state.rs) | socket 状态机锁 | `StateLock`, `StateGuard` | +| [listen_table.rs](net/ax-net/src/listen_table.rs) | TCP listen/accept 表与 SYN 预创建 | `ListenTable` | +| [tcp.rs](net/ax-net/src/tcp.rs) / [udp.rs](net/ax-net/src/udp.rs) / [raw.rs](net/ax-net/src/raw.rs) | IP socket 实现 | `TcpSocket`, `UdpSocket`, `RawSocket` | +| [unix/](net/ax-net/src/unix/) | Unix domain socket | `UnixSocket`, `Transport` | +| [vsock/](net/ax-net/src/vsock/) | 可选 vsock 支持(`vsock` feature) | `VsockSocket`, `VsockTransport` | +| [device/](net/ax-net/src/device/) | loopback、Ethernet、rd-net、vsock 设备适配 | `Device`, `EthernetDevice`, `RdNetDriver` | +| [consts.rs](net/ax-net/src/consts.rs) | 缓冲区大小等常量 | `STANDARD_MTU`, `SOCKET_BUFFER_SIZE` | + +## 全局单例 + +`ax-net` 通过若干 `Once` / `LazyLock` 全局单例持有协议核心与控制面,定义在 [net/ax-net/src/lib.rs#L89-L103](net/ax-net/src/lib.rs#L89-L103): + +```rust +static LISTEN_TABLE: LazyLock = LazyLock::new(ListenTable::new); +static SOCKET_SET: LazyLock = LazyLock::new(SocketSetWrapper::new); + +static SERVICE: Once> = Once::new(); +static NET_CONTROL: Once> = Once::new(); +static POLLING_INTERFACES: AtomicBool = AtomicBool::new(false); +static POLL_AGAIN: AtomicBool = AtomicBool::new(false); +static NET_POLL_REQUESTED: AtomicBool = AtomicBool::new(false); +static NET_POLL_WAKE: WaitQueue = WaitQueue::new(); +static NET_POLL_DEVICE_WAKER: LazyLock = + LazyLock::new(|| Waker::from(Arc::new(NetPollWake))); +``` + +- `SOCKET_SET` 是所有 IP socket(TCP/UDP/raw/DNS)共享的 smoltcp `SocketSet`。 +- `SERVICE` 持有 `Service`(smoltcp `Interface` + `Router` + DHCP 状态),被 mutex 保护。 +- `NET_CONTROL` 持有只读控制面(接口 registry、路由表、DNS),可在不持有 `Service` 锁的情况下被查询。 +- `NET_POLL_WAKE` 是 `net-poll` worker 的等待队列,`request_poll()` 通过它唤醒 worker。 + +## 接口标识 + +`InterfaceId(u32)` 是 `ax-net` 内部接口 ID,同时也是 StarryOS 暴露给 Linux ABI 的 ifindex 数值来源。定义在 [net/ax-net/src/config.rs#L8-L38](net/ax-net/src/config.rs#L8-L38): + +```rust +#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct InterfaceId(u32); + +impl InterfaceId { + pub const LOOPBACK: Self = Self(1); + pub const fn new(raw: u32) -> Self { Self(raw) } + pub const fn get(self) -> u32 { self.0 } + /// Convert to Linux ifindex (i32). + pub const fn to_linux_ifindex(self) -> i32 { self.0 as i32 } + pub const fn from_linux_ifindex(ifindex: i32) -> Option { /* ... */ } +} +``` + +- `InterfaceId::LOOPBACK == 1`,固定对应 `lo`。 +- Ethernet 接口从 `2` 开始,默认发现顺序为 `eth0`、`eth1`(见 [lib.rs](net/ax-net/src/lib.rs#L221) 中 `InterfaceId::new((order as u32) + 2)`)。 +- `InterfaceInfo`([config.rs#L58-L70](net/ax-net/src/config.rs#L58-L70))是对外只读快照,包含 ID、名称、类型、MAC、IPv4、MTU、flags 和 metric。 + +```rust +pub struct InterfaceInfo { + pub id: InterfaceId, + pub name: String, + pub kind: InterfaceKind, + pub mac: Option, + pub ipv4: Option, + pub mtu: usize, + pub flags: InterfaceFlags, + pub metric: u32, +} +``` + +## 控制面 + +控制面由 `NetControl`([service.rs#L45-L50](net/ax-net/src/service.rs#L45-L50))统一持有: + +```rust +pub struct NetControl { + state: RwLock, + pub(crate) routes: SharedRouteTable, +} +``` + +内部 `ControlState` 保存 `Vec`(接口 registry)与 `Vec`(DNS 来源信息)。控制面提供: + +- `interfaces()` / `interface_by_name()` / `interface_by_id()`:名称与 ID 查询([service.rs#L81-L107](net/ax-net/src/service.rs#L81-L107))。 +- `select_route()`:按目的地址查路由,并校验目标接口 `UP`([service.rs#L147-L174](net/ax-net/src/service.rs#L147-L174))。 +- `local_binding_for()`:把本地监听地址映射为 `DeviceBinding`([service.rs#L122-L135](net/ax-net/src/service.rs#L122-L135))。 +- `commit_interface_update()`:DHCP ACK 等运行期更新通过一次性事务提交接口地址、smoltcp IP 地址、路由和 DNS,避免外部看到半更新状态([service.rs#L175-L198](net/ax-net/src/service.rs#L175-L198))。 + +控制面查询不进入设备锁,也不直接推进 smoltcp poll。`select_route_if`([router.rs#L245](net/ax-net/src/router.rs#L245))会传入一个 `is_usable` 闭包,跳过未 `UP` 的接口: + +```rust +pub fn select_route_if(&self, dst, mut is_usable: impl FnMut(InterfaceId) -> bool) + -> Option +{ + self.rules.iter() + .find(|rule| rule.filter.contains_addr(dst) && is_usable(rule.interface_id)) + .map(|rule| RouteDecision { /* ... */ }) +} +``` + +## SocketSet 包装层 + +`SocketSetWrapper`([wrapper.rs](net/ax-net/src/wrapper.rs))在 smoltcp 原生 `SocketSet` 之上增加了 UDP 端口冲突仲裁。原生 smoltcp 允许多个 UDP socket bind 同一端口(由上层保证),`ax-net` 在此层实现 `SO_REUSEADDR` 语义: + +```rust +pub(crate) struct SocketSetWrapper<'a> { + pub inner: Mutex>, // smoltcp SocketSet + pub new_socket: Event, // 通知 accept()/poll() 有新 socket + udp_binds: Mutex>, // UDP 端口占用表 +} +``` + +UDP bind 仲裁规则([wrapper.rs#L59-L71](net/ax-net/src/wrapper.rs#L59-L71)): + +- **精确 bind**(如 `192.168.1.1:53`):如果已有同地址同端口则拒绝;如果有 wildcard `0.0.0.0:53` 则拒绝。 +- **Wildcard bind**(`0.0.0.0:53`):如果任何地址已占用同一端口则拒绝。 +- **`SO_REUSEADDR`**:设置后跳过仲裁。 + +`new_socket` Event 用于通知 `accept()` 阻塞等待和 poll 就绪检测:当 TCP SYN pre-create 或 DNS socket 创建时,`add()` 方法调用 `new_socket.notify(1)` 唤醒等待者。 + +## Socket 状态机 + +`StateLock`([state.rs](net/ax-net/src/state.rs))为 socket 提供基于 CAS 的轻量状态转换锁: + +``` +Idle ──bind──→ Idle (不变,但注册 endpoint) +Idle ──listen──→ Listening +Idle ──connect──→ Connecting ──SYN/SYN+ACK──→ Connected +Listening ──accept──→ Listening (不变,但生成新 socket) +Connected ──close──→ Closed +``` + +核心 API: + +```rust +pub fn lock(&self, expect: State) -> Result, State> { + // CAS: expect → Busy,失败返回当前状态 +} +pub fn transit(self, new: State, f: impl FnOnce() -> AxResult) -> AxResult { + // 执行 f(),成功则写 new 状态,失败则回退到原始状态 +} +``` + +`StateLock` 保证绑定/监听/连接等操作的原子性:并发 bind+connect 在同一 socket 上只能有一个成功。 + +## TCP Listen Table 与 SYN Pre-create + +`ListenTable`([listen_table.rs](net/ax-net/src/listen_table.rs))是 TCP 监听的核心数据结构。它包含 65536 个槽位(每端口一个 `Arc>>>`),每个活跃端口存储: + +```rust +struct ListenTableEntryInner { + listen_endpoint: IpListenEndpoint, // 监听地址和端口 + backlog: usize, // SYN 队列容量(clamp 到 LISTEN_QUEUE_SIZE=512) + syn_queue: VecDeque, // 预创建的连接 +} +``` + +### SYN 预创建机制 + +`snoop_tcp_packet()`([router.rs](net/ax-net/src/router.rs#L474-L500))在 RX 路径中拦截 TCP SYN 包: + +1. 解析 TCP 包头,检查 SYN 标志。 +2. 查 `LISTEN_TABLE` 是否存在匹配的 listening entry。 +3. 如果 SYN queue 未满,**预创建**一个 `TcpSocket` 并调用 `socket.listen()`,将其加入 `syn_queue`。 +4. smoltcp 随后 `Interface::poll()` 完成三次握手。 +5. `accept()` 时直接从前端取出已完成握手的 socket,无需阻塞等待。 + +SYN queue 满时丢弃 SYN 包(打 warning),由客户端重传机制保证可靠性。 + +### accept() 流程 + +`ListenTable::accept()`([listen_table.rs#L133-L157](net/ax-net/src/listen_table.rs#L133-L157)): + +1. 遍历 `syn_queue`。 +2. 跳过已关闭且无未读数据的 socket(`is_closed_without_data`),移除并销毁。 +3. 返回第一个已完成握手的 socket(`is_acceptable` 检查 TCP state 为 Established/CloseWait/FinWait1/FinWait2 等可 accept 状态)。 +4. 为减少慢速枚举,`idx > 0` 时打印 warning。 + +## 通用 Poll 与超时 + +`GeneralOptions`([general.rs](net/ax-net/src/general.rs))为所有 socket 类型提供通用设施: + +```rust +pub(crate) struct GeneralOptions { + nonblock: AtomicBool, // O_NONBLOCK + reuse_address: AtomicBool, // SO_REUSEADDR + send_timeout_nanos: AtomicU64, // SO_SNDTIMEO + recv_timeout_nanos: AtomicU64, // SO_RCVTIMEO + bound_if: AtomicU32, // SO_BINDTODEVICE (InterfaceId) + socket_type: AtomicI32, // SOCK_STREAM/DGRAM/RAW + domain: i32, // AF_INET/AF_UNIX/AF_VSOCK + protocol: i32, // IPPROTO_TCP/UDP/ICMP +} +``` + +提供的 poll helper: + +- `send_poller()` / `recv_poller()`:阻塞等待 I/O 就绪或超时。内部流程:检查 `nonblock` → 调用 `register_waker()` 注册到 `Service` → `block_on(poll_io())` 自旋等待 → 超时检查 → 执行操作 → 成功或返回错误。 +- `send_poller_with()`:支持指定是否检测 HUP 事件。 +- `register_waker()`:根据 `DeviceBinding` 将调用者 waker 注册到对应设备的 `PollSet`。 + +### socket.domain / socket.protocol / socket_type 常量 + +每个 socket 创建时固化: + +| socket 类型 | socket_type (SOCK_\*) | domain (AF_\*) | protocol (IPPROTO_\*) | +| --- | --- | --- | --- | +| `TcpSocket` | 1 (STREAM) | 2 (INET) | 6 (TCP) | +| `UdpSocket` | 2 (DGRAM) | 2 (INET) | 17 (UDP) | +| `RawSocket` | 3 (RAW) | 2 (INET) | 根据 `IpProtocol` | +| `UnixSocket` (stream) | 1 (STREAM) | 1 (UNIX) | 0 | +| `UnixSocket` (dgram) | 2 (DGRAM) | 1 (UNIX) | 0 | +| `VsockSocket` | 1 (STREAM) | 40 (VSOCK) | 0 | + +## Router 内部结构 + +`Router`([router.rs](net/ax-net/src/router.rs))是 smoltcp 与多设备之间的适配层: + +```rust +pub struct Router { + rx_buffer: PacketBuffer, // smoltcp Device RX buffer(64 个 MTU 槽位) + tx_buffer: PacketBuffer, // smoltcp Device TX buffer + queues: Arc, // 全局 RX 队列(所有设备共享) + devices: Vec>, // 设备列表(按 add_device() 顺序) + table: SharedRouteTable, // 路由表 +} +``` + +### BoundedPacketQueue + +`BoundedPacketQueue` 是有界 MPSC 队列,容量默认 `SOCKET_BUFFER_SIZE=64`: + +```rust +struct BoundedPacketQueue { + inner: Mutex>, + capacity: usize, + len: AtomicUsize, // 无锁长度读取,用于 is_empty() 检查 +} +``` + +- `push()`:加锁,满则返回 `Err(T)`(上层丢弃并打 warning)。 +- `pop()`:加锁,空返回 `None`。 +- `is_empty()`:原子读 `len`,无锁。 + +### DeviceHandle + +每设备一个 `DeviceHandle`,持有设备锁、收发队列和唤醒机制: + +```rust +struct DeviceHandle { + interface_id: InterfaceId, + name: String, + inner: Arc>>, // 设备 trait object + rx_queue: Arc>, // 指向 RouterQueues::rx(共享) + tx_queue: Arc>, // 独立 TX 队列 + rx_wake: Arc, // 唤醒 RX worker + tx_wake: Arc, // 唤醒 TX worker + rx_waker: Waker, // 驱动→rx_wake 的转换器 +} +``` + +RX 队列是所有设备共享的(`RouterQueues::rx`),因为 smoltcp 从同一个 `Router::rx_buffer` 消费;TX 队列每设备独立,由 `dispatch()` 按路由决策分发。 + +### TX Dispatch + +`Router::dispatch()`([router.rs#L523-L565](net/ax-net/src/router.rs#L523-L565))从 `tx_buffer` 取出 smoltcp 发出的 IP 包: + +- **IPv4 广播**(dst=255.255.255.255):复制到所有非 loopback Ethernet 设备。 +- **IPv4 单播**:按 `select_route_for_source()` 选路由(要求源地址匹配),将包推入对应设备 TX 队列。 +- **IPv6 多播**:复制到所有非 loopback Ethernet 设备。 +- **IPv6 单播**:按 `select_route()` 选路由。 + +### RX 数据流 + +`Router::poll()`([router.rs#L476-L490](net/ax-net/src/router.rs#L476-L490)): + +1. 从 `RouterQueues::rx` 循环出队,填充 `rx_buffer`。 +2. 每包先调 `snoop_tcp_packet()` 检查 TCP SYN,预创建 listen socket。 +3. 再调 snoop 回调(DHCP packet 分发)。 +4. `rx_buffer` 满则停止出队(剩余包留在队列供下次 poll)。 + +## Ethernet 设备实现 + +`EthernetDevice`([ethernet.rs](net/ax-net/src/device/ethernet.rs))是 `Device` trait 的主要实现: + +```rust +pub struct EthernetDevice { + name: String, + inner: Arc, // 共享 IRQ 状态 + neighbors: HashMap, // ARP 缓存 + pending_neighbors: HashMap, // 等待 ARP reply + ip: Option, // 本机 IP + pending_packets: PacketBuffer, // ARP-pending 发包队列(128 个槽位) +} +``` + +### ARP 处理 + +`EthernetDevice::recv()` 处理入站 Ethernet 帧: + +1. 调用 `EthernetDriver::receive()` 从硬件获取一帧。 +2. 解析 `EthernetFrame`,按 `EthernetProtocol` 分派: + - **ARP**:更新 neighbor 表(reply 和 gratuitous request),从 `pending_packets` 释放等待的包。 + - **IPv4/IPv6**:encapsulation 检查,将 payload 写入 smoltcp `PacketBuffer`。 + +`send()` 路径: + +1. 查 `neighbors` 表,有则直接发送。 +2. 无则检查 `pending_neighbors`:如果已发送 ARP request 未超时,将包写入 `pending_packets`。 +3. 否则发送 ARP request,记录到 `pending_neighbors`。 +4. Neighbor TTL = 300s(与 Linux 一致),ARP retry = 1s。 + +### IRQ 模型 + +`EthernetIrqState` 管理 IRQ 注册和驱动锁: + +```rust +struct EthernetIrqState { + irq: Option, + irq_registration: spin::Once>, + driver: SpinNoIrq>, + poll_ready: PollSet, +} +``` + +IRQ 处理链: +1. `ax-runtime` 通过 `set_ethernet_irq_registrar()` 注册 `EthernetIrqRegistrar`。 +2. `EthernetDevice::new()` 中通过 `registrar.register_shared(name, irq, action)` 注册 IRQ handler。 +3. IRQ 触发时调用 `handle_ethernet_irq()`:运行 `driver.handle_irq()`,如果返回 `RX_READY` 则 `poll_ready.wake()` 唤醒 RX worker。 +4. 如果没有注册 IRQ registrar,回退到纯 poll 模式(RX worker 的 `rx_wake.wait()` 可被 `request_poll()` 路径唤醒)。 + +### RdNetDriver + +`RdNetDriver`([driver.rs](net/ax-net/src/device/driver.rs))是 `EthernetDriver` trait 的 `rd-net` 适配实现。内部持有 `rd_net::TxQueue` 和 `rd_net::RxQueue`,通过预取 `RX_PREFETCH_TARGET=1` 个包到 `pending_rx: VecDeque` 减少锁竞争。`alloc_tx_buffer()` 返回 `VecTxBuffer`(栈友好),`transmit()` 执行硬件发送。 + +## Loopback 设备 + +`LoopbackDevice`([loopback.rs](net/ax-net/src/device/loopback.rs))实现本地回环: + +- `send()`:将 IP 包写入内部 `PacketBuffer`(64 槽 × 1500 字节),调用 `poll.wake()` 通知 poller。 +- `recv()`:从内部 buffer 出队,将 IP 包写入 smoltcp `rx_buffer`。不封装/解封装 Ethernet 帧。 +- `register_waker()`:将 waker 注册到内部 `PollSet`。 + +## 数据面 poll 流程 + +`Service::poll()`([service.rs#L494-L509](net/ax-net/src/service.rs#L494-L509))在每个 poll 周期执行: + +```rust +pub fn poll(&mut self, sockets: &mut SocketSet) -> bool { + let timestamp = now(); + let mut dhcp_events = Vec::new(); + // 1. 唤醒所有 RX worker,让它们有机会推新数据到队列 + self.router.wake_rx_workers(); + // 2. Router::poll():从 RX 队列消费包到 smoltcp buffer,同时分发 DHCP 包 + { + let dhcp = &mut self.dhcp; + self.router.poll(timestamp, sockets, |interface_id, packet| { + for state in dhcp.iter_mut() { + if let Some(event) = state.process_packet(interface_id, packet, timestamp) { + dhcp_events.push(event); + } + } + }); + } + // 3. 处理 DHCP 事件(更新地址、路由、DNS) + for event in dhcp_events { self.handle_dhcp_event(event); } + // 4. smoltcp Interface::poll():协议状态机推进 + self.iface.poll(timestamp, &mut self.router, sockets); + // 5. DHCP 发包定时器 + let dhcp_poll_next = self.poll_dhcp(timestamp); + // 6. TX dispatch:将 smoltcp 输出的包路由到设备 + self.router.dispatch(timestamp) || dhcp_poll_next +} +``` + +返回值 `bool` 指示是否需要立即再 poll 一次(用于 `poll_until_idle` 循环)。`next_poll_at()` 调用 `Interface::poll_at()` 获取 smoltcp 建议的下次 poll 时间(用于 `net-poll` worker 的 idle 休眠决策)。 + +## net-poll Worker + +`net_poll_worker`([lib.rs#L479-L489](net/ax-net/src/lib.rs#L479-L489))是协议核心的唯一驱动线程: + +```rust +fn net_poll_worker() { + loop { + let delay = next_poll_delay(); // smoltcp poll_at() 建议延迟 + let timed_out = NET_POLL_WAKE.wait_timeout_until(delay, + || NET_POLL_REQUESTED.load(Acquire)); // 等 request_poll() 或超时 + if !timed_out { NET_POLL_REQUESTED.store(false, Release); } + poll_until_idle(); // 循环 poll 直到无新事件 + } +} +``` + +`poll_until_idle()` 使用 `POLLING_INTERFACES` CAS 锁防止重入(同一时刻最多一个 poll 进行中),内部循环调用 `poll_once()` 直到 `POLL_AGAIN` 为 false。空闲时 `next_poll_delay()` 返回最多 100ms 的 idle poll interval。 + +## DHCP 状态机 + +`DhcpState`([service.rs#L269-L510](net/ax-net/src/service.rs#L269-L510))维护 per-interface DHCP 客户端: + +``` +Discovering ──Offer──→ Requesting ──ACK──→ Bound + │ │ │ + └──timeout→retry── └──timeout→retry── └──NAK→Discovering (reset) +``` + +- **Discovering**:广播 DHCPDISCOVER,指数退避重试(1,2,4,8... 最多 16 秒间隔)。 +- **Requesting**:收到 DHCPOFFER 后广播 DHCPREQUEST。 +- **Bound**:收到 DHCPACK,状态转为 Bound。NAK 触发 reset → Discovering。 + +`process_packet()` 按 ingress `InterfaceId` 匹配、解析 IPv4→UDP→DHCP,校验 `transaction_id` 和 `client_hardware_address` 防止误收。`poll_packet()` 在非 Bound 状态且重试时间到达时构造 DHCP 发包。 + +`dhcp_transaction_id(mac)` 将 MAC 地址低 32 位与 `wall_time_nanos()` 混合生成 transaction ID。 + +## DNS 解析流程 + +`dns_query()`([lib.rs#L506-L536](net/ax-net/src/lib.rs#L506-L536)): + +1. `dns_servers()` 获取按 (metric, interface_id, server_ip) 排序的去重 DNS server 列表。 +2. 过滤不可路由的 server(通过 `select_route()` 检查可达性)。 +3. 在 `SOCKET_SET` 中创建 `dns::Socket`,发起 A 记录查询。 +4. 循环 `request_poll()` + `yield_now()`,检查 `get_query_result()`。 +5. 默认 5 秒超时(`DNS_DEFAULT_TIMEOUT`),超时返回 `ETIMEDOUT`。 +6. `DnsSocketGuard` 在 drop 时从 `SOCKET_SET` 移除 DNS socket。 + +执行顺序: + +```text +Service::poll() + -> Router::poll() drain RX queue + -> DHCP packet snoop + -> smoltcp Interface::poll() + -> poll DHCP retry + -> Router::dispatch() route TX packets +``` + +`Router`([router.rs#L318-L326](net/ax-net/src/router.rs#L318-L326))是 smoltcp `phy::Device` 适配层。它对 smoltcp 实现 `phy::Device` trait([router.rs#L672](net/ax-net/src/router.rs#L672)),对外只暴露 IP 层(`Medium::Ip`): + +```rust +impl smoltcp::phy::Device for Router { + type RxToken<'a> = RxToken<'a>; + type TxToken<'a> = TxToken<'a>; + fn receive(&mut self, _timestamp: Instant) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> { /* ... */ } + fn transmit(&mut self, _timestamp: Instant) -> Option> { /* ... */ } + fn capabilities(&self) -> DeviceCapabilities { + let mut caps = DeviceCapabilities::default(); + caps.medium = Medium::Ip; + caps.max_transmission_unit = STANDARD_MTU; + caps.max_burst_size = Some(SOCKET_BUFFER_SIZE); + caps + } +} +``` + +设备 worker 不直接访问 `Router` 本体,只和 `RouterQueues` / per-device TX queue 交互: + +- RX worker([router.rs#L575](net/ax-net/src/router.rs#L575))从 rd-net 设备读取 packet,复制到有界 RX queue,并 `request_poll()`。 +- smoltcp `RxToken`([router.rs#L656](net/ax-net/src/router.rs#L656))从 RX buffer 读 packet,并保留 ingress `InterfaceId` 元数据。 +- smoltcp `TxToken`([router.rs#L642](net/ax-net/src/router.rs#L642))只写入 TX buffer;真实出接口由 `Router::dispatch()`([router.rs#L498](net/ax-net/src/router.rs#L498))根据 IP 包源/目的地址和路由表选择。 +- TX worker([router.rs#L559](net/ax-net/src/router.rs#L559))从对应设备 TX queue 发包。 + +## 设备能力边界(Device trait) + +`ax-net` 不直接依赖硬件驱动框架。所有设备实现统一的内部 `Device` trait([device/mod.rs#L33-L55](net/ax-net/src/device/mod.rs#L33-L55)): + +```rust +pub trait Device: Send + Sync { + fn name(&self) -> &str; + fn recv(&mut self, interface_id: InterfaceId, + buffer: &mut PacketBuffer, timestamp: Instant, + snoop: &mut dyn FnMut(&[u8])) -> bool; + /// Sends a packet to the next hop. Returns true if receive became ready. + fn send(&mut self, next_hop: IpAddress, packet: &[u8], timestamp: Instant) -> bool; + fn set_ipv4_addr(&mut self, _addr: Option) {} + fn arp_entries(&self, _timestamp: Instant) -> Vec { Vec::new() } + fn register_waker(&self, waker: &Waker); +} +``` + +两个实现: + +- `LoopbackDevice`([device/loopback.rs#L16](net/ax-net/src/device/loopback.rs#L16)):用 `PacketBuffer` 自环,`send()` 直接 enqueue 并 wake。 +- `EthernetDevice`([device/ethernet.rs#L106](net/ax-net/src/device/ethernet.rs#L106)):维护 ARP 邻居表(`NEIGHBOR_TTL = 300s`,见 [ethernet.rs#L133](net/ax-net/src/device/ethernet.rs#L133))、pending packet 队列、Ethernet 帧封装/解析,并对接 IRQ 适配。 + +`EthernetDevice` 之下是能力边界 trait `EthernetDriver`([device/driver.rs#L70-L82](net/ax-net/src/device/driver.rs#L70-L82)),由 `RdNetDriver`([device/driver.rs#L128](net/ax-net/src/device/driver.rs#L128))包装 `rd-net` 的 `TxQueue` / `RxQueue` 实现。这样 `ax-net` 避免直接依赖 FDT、PCI、MMIO、DMA、VirtIO 或平台 IRQ ABI。 + +## SocketSetWrapper + +全局 socket 容器 `SocketSetWrapper`([wrapper.rs#L16-L24](net/ax-net/src/wrapper.rs#L16-L24))除了持有 smoltcp `SocketSet`,还维护 UDP 端口冲突仲裁(`udp_binds`): + +```rust +pub(crate) struct SocketSetWrapper<'a> { + pub inner: Mutex>, + pub new_socket: Event, + udp_binds: Mutex>, +} +``` + +`udp_bind()`([wrapper.rs#L48-L68](net/ax-net/src/wrapper.rs#L48-L68))实现 Linux 语义的端口复用规则:具体地址与通配地址互斥,相同地址端口冲突返回 `AddrInUse`(除非 `SO_REUSEADDR`)。 + +## 为什么不是 multi-smoltcp domain + +多个 smoltcp 实例可以让不同 NIC 的协议处理并行,但会引入更复杂的 socket 语义: + +- TCP wildcard listen 需要在多个 `SocketSet` 创建 listener 并聚合 accept queue。 +- UDP wildcard bind 需要聚合多个 backend 的 recv readiness。 +- 端口冲突检查需要跨 domain 统一仲裁。 +- 动态路由切换会涉及 socket backend 迁移或重新绑定。 +- `SO_BINDTODEVICE`、AF_PACKET、raw socket 和 multicast 的接口集合语义会更复杂。 + +因此当前 `ax-net` 优先保留单协议栈语义,用多接口 registry、路由决策和设备队列解耦补齐功能和部分性能优化。 + +## Socket backend 分层 + +`Socket` 枚举([socket.rs#L253-L265](net/ax-net/src/socket.rs#L253-L265))聚合 5 类 backend,统一实现 `SocketOps` + `Configurable`: + +| Backend | 类型 | 协议核心 | 源码 | +| --- | --- | --- | --- | +| `TcpSocket` | SOCK_STREAM / AF_INET | smoltcp TCP socket | [tcp.rs#L50](net/ax-net/src/tcp.rs#L50) | +| `UdpSocket` | SOCK_DGRAM / AF_INET | smoltcp UDP socket | [udp.rs#L50](net/ax-net/src/udp.rs#L50) | +| `RawSocket` | SOCK_RAW / AF_INET | smoltcp raw socket | [raw.rs#L50](net/ax-net/src/raw.rs#L50) | +| `UnixSocket` | SOCK_STREAM/SOCK_DGRAM / AF_UNIX | 自包含 `Transport`(不经 smoltcp) | [unix/mod.rs#L70](net/ax-net/src/unix/mod.rs#L70) | +| `VsockSocket` | SOCK_STREAM / AF_VSOCK | `rdif-vsock` 驱动 + ring buffer | [vsock/mod.rs#L68](net/ax-net/src/vsock/mod.rs#L68) | + +IP 类 socket 通过 `SOCKET_SET.add()` 注册到全局 `SocketSet`,获得 `SocketHandle`;Unix / vsock 不依赖 smoltcp,各自维护连接状态。 + +## Socket 状态机 + +IP socket 使用 `StateLock`([state.rs#L32-L57](net/ax-net/src/state.rs#L32-L57))做无锁状态转换,避免在 socket 上长期持锁: + +```rust +pub(crate) enum State { Idle, Busy, Connecting, Connected, Listening, Closed } + +pub struct StateLock(AtomicU8); +impl StateLock { + pub fn lock(&self, expect: State) -> Result, State> { + // CAS: expect -> Busy + } +} +``` + +`StateGuard::transit()`([state.rs#L65-L76](net/ax-net/src/state.rs#L65-L76))在执行操作时把状态临时置为 `Busy`,操作成功才提交新状态,失败回滚。这样并发 `connect` / `accept` 不会互相破坏。 + +非阻塞与超时由 `GeneralOptions`([general.rs#L113-L145](net/ax-net/src/general.rs#L113-L145))统一处理,基于 `axpoll::poll_io` 与 `block_on`/`timeout` 实现 `send_poller_with` / `recv_poller_with`。 diff --git a/docs/docs/architecture/net/configuration.md b/docs/docs/architecture/net/configuration.md new file mode 100644 index 0000000000..bfd2804f55 --- /dev/null +++ b/docs/docs/architecture/net/configuration.md @@ -0,0 +1,294 @@ +--- +sidebar_position: 3 +sidebar_label: "配置参考" +--- + +# 配置与 Feature + +`ax-net` 的配置目标是用结构化数据表达接口意图,而不是依赖旧的全局单网口环境变量。每个 Ethernet 接口可以独立使用静态 IPv4 或 DHCP,并携带 metric 和 DNS 配置。 + +## Cargo Feature + +`ax-net` 的 feature 定义在 [net/ax-net/Cargo.toml#L10-L12](net/ax-net/Cargo.toml#L10-L12): + +```toml +[features] +vsock = ["dep:rdif-vsock"] +``` + +| feature | 作用 | +| --- | --- | +| `vsock` | 启用 `rdif-vsock` 依赖和 vsock socket / device 支持 | + +启用后,`lib.rs` 中通过 `#[cfg(feature = "vsock")]` 条件编译导出 `init_vsock()`([lib.rs#L360](net/ax-net/src/lib.rs#L360))、`vsock` 模块([lib.rs#L45](net/ax-net/src/lib.rs#L45))以及 `Socket::Vsock` 变体([socket.rs#L263-L264](net/ax-net/src/socket.rs#L263-L264))。基础 TCP、UDP、raw、Unix domain socket、DNS、DHCP 和 Ethernet 能力不需要额外 feature。 + +smoltcp 在 [Cargo.toml#L34-L52](net/ax-net/Cargo.toml#L34-L52) 中固定启用以下能力: + +- `alloc` +- `log` +- `async` +- `medium-ethernet` +- `medium-ip` +- `proto-ipv4` +- `proto-ipv6` +- `socket-raw` +- `socket-icmp` +- `socket-udp` +- `socket-tcp` +- `socket-dhcpv4` +- `socket-dns` + +## 缓冲区常量 + +socket 与设备缓冲区大小集中定义在 [net/ax-net/src/consts.rs](net/ax-net/src/consts.rs): + +```rust +pub const STANDARD_MTU: usize = 1500; +pub const TCP_RX_BUF_LEN: usize = 64 * 1024; +pub const TCP_TX_BUF_LEN: usize = 64 * 1024; +pub const UDP_RX_BUF_LEN: usize = 64 * 1024; +pub const UDP_TX_BUF_LEN: usize = 64 * 1024; +pub const RAW_RX_BUF_LEN: usize = 64 * 1024; +pub const RAW_TX_BUF_LEN: usize = 64 * 1024; +pub const LISTEN_QUEUE_SIZE: usize = 512; +pub const SOCKET_BUFFER_SIZE: usize = 64; +pub const ETHERNET_MAX_PENDING_PACKETS: usize = 128; +``` + +`ETHERNET_MAX_PENDING_PACKETS` 取 128 而非 32,是为了容纳应用启动时多个并发 TCP 连接的首个 SYN 突发,以及长连接在 `NEIGHBOR_TTL` 过期后重新进入 ARP-pending 队列的 burst(见源码注释 [consts.rs#L14-L27](net/ax-net/src/consts.rs#L14-L27))。 + +## NetworkConfig + +定义在 [net/ax-net/src/config.rs#L80-L97](net/ax-net/src/config.rs#L80-L97): + +```rust +#[derive(Debug, Clone, Default)] +pub struct NetworkConfig { + /// Per-interface configuration. + pub interfaces: Vec, + /// DNS servers used when no interface-level DNS server is available. + pub default_dns_servers: Vec, +} + +#[derive(Debug, Clone)] +pub struct InterfaceConfig { + pub name: String, + pub match_by: InterfaceMatcher, + pub static_ip: Option, + pub dhcp: bool, + pub metric: u32, + pub dns_servers: Vec, +} +``` + +`NetworkConfig` 由 `ax-runtime` 构造并传入 `ax_net::init_network()`。`ax-net` 不再读取旧的全局 `AX_IP`、`AX_GW`、`AX_PREFIX_LEN`、`AX_DNS` 单网口语义。 + +## InterfaceMatcher + +显式接口配置通过 `InterfaceMatcher` 匹配真实设备,定义在 [config.rs#L72-L76](net/ax-net/src/config.rs#L72-L76): + +```rust +#[derive(Debug, Clone)] +pub enum InterfaceMatcher { + ByOrder(usize), + ByMac(EthernetAddress), + ByDriverName(String), +} +``` + +匹配规则: + +- `ByOrder(0)` 匹配第一个发现的 Ethernet 设备,通常命名为 `eth0`。 +- `ByMac(mac)` 用 MAC 地址匹配设备。 +- `ByDriverName(name)` 用 driver 暴露的设备名匹配。 +- 显式配置必须匹配唯一设备。 +- 未显式配置的 Ethernet 设备默认启用 DHCP。 + +匹配逻辑在 `find_interface_config()`([lib.rs#L328-L355](net/ax-net/src/lib.rs#L328-L355)),多配置匹配同一设备会 panic。 + +## 静态 IPv4 + +`StaticIpConfig` 定义在 [config.rs#L100-L104](net/ax-net/src/config.rs#L100-L104): + +```rust +#[derive(Debug, Clone)] +pub struct StaticIpConfig { + pub ip: Ipv4Addr, + pub prefix_len: u8, + pub gateway: Ipv4Addr, +} +``` + +静态接口初始化时会: + +- 设置接口 IPv4 地址。 +- 安装直连路由。 +- 如果 gateway 有效,安装默认路由。 +- 将接口级 DNS server 记录为静态 DNS 来源。 + +配置错误会在初始化时 panic,包括: + +- 接口名为保留名 `lo`。 +- `dhcp = true` 且 `static_ip.is_some()`。 +- 静态 IP 为 unspecified。 +- prefix 大于 32。 +- gateway 为 unspecified。 +- DNS server 为 unspecified。 +- 显式配置没有匹配任何设备。 +- 接口名冲突。 + +这些校验集中在 `init_network()` 开头([lib.rs#L127-L170](net/ax-net/src/lib.rs#L127-L170))。 + +## DHCP + +未显式配置的 Ethernet 接口默认 DHCP。显式配置也可以设置 `dhcp = true`。 + +DHCP 行为: + +- 每个 DHCP 接口独立 `DhcpState`。 +- DHCP packet 按 ingress `InterfaceId` 分发。 +- DHCP ACK 更新对应接口 IPv4、gateway、路由和 DNS。 +- 启动等待只要求任一 DHCP 接口成功配置,避免一个断开的接口阻塞系统启动。 +- DHCP 租约续期、过期回收和地址冲突检测仍属于后续增强。 + +## DNS 配置 + +DNS server 来源分三类: + +| 来源 | 说明 | +| --- | --- | +| DHCP | DHCP ACK 下发,绑定来源接口和 metric | +| Static | `InterfaceConfig::dns_servers` | +| Fallback | `NetworkConfig::default_dns_servers` | + +内部记录为 `DnsServerEntry`,对外 `dns_servers()` 返回按 metric 和接口 ID 排序后的去重地址列表。 + +DNS 查询时会先检查 DNS server 是否可路由,不可路由则尝试下一个 server。 + +## 配置示例 + +```rust +use alloc::{string::ToString, vec}; +use core::net::Ipv4Addr; + +use ax_net::{InterfaceConfig, InterfaceMatcher, NetworkConfig, StaticIpConfig}; + +let config = NetworkConfig { + interfaces: vec![ + InterfaceConfig { + name: "eth0".to_string(), + match_by: InterfaceMatcher::ByOrder(0), + static_ip: Some(StaticIpConfig { + ip: Ipv4Addr::new(10, 0, 2, 15), + prefix_len: 24, + gateway: Ipv4Addr::new(10, 0, 2, 2), + }), + dhcp: false, + metric: 100, + dns_servers: vec![Ipv4Addr::new(10, 0, 2, 3)], + }, + InterfaceConfig { + name: "eth1".to_string(), + match_by: InterfaceMatcher::ByOrder(1), + static_ip: Some(StaticIpConfig { + ip: Ipv4Addr::new(192, 168, 100, 10), + prefix_len: 24, + gateway: Ipv4Addr::new(192, 168, 100, 1), + }), + dhcp: false, + metric: 200, + dns_servers: vec![], + }, + ], + default_dns_servers: vec![], +}; +``` + +## 接口命名与 ID 分配 + +`InterfaceId` 数值的分配规则: + +| InterfaceId | 接口 | 说明 | +| --- | --- | --- | +| 0 | `TX_INTERFACE_PLACEHOLDER` | 内部占位符([router.rs](net/ax-net/src/router.rs#L76)),不出现在任何 public API 中 | +| 1 | `lo` | Loopback,固定 ID | +| 2+ | `eth0`, `eth1`, ... | Ethernet 设备,按发现顺序(`net_devs.drain(..)`)递增 | + +默认命名 `"eth{order}"`,但配置中的 `name` 字段会覆盖默认名称。接口名不允许冲突,且 `"lo"` 为保留名。 + +## 路由 Metric 行为 + +- 直连路由的 metric 与接口配置相同。 +- 默认路由的 metric 与接口配置相同。 +- 路由表排序:最长前缀匹配 > 低 metric 优先 > 同 metric 稳定插入顺序。 +- 未配置 metric 的接口默认为 `u32::MAX`。 +- 同一目的地址存在多个匹配路由时,优先选择 metric 较低且接口 `UP` 的路由。 + +## TCP Keep-Alive 默认值 + +TCP keep-alive 相关常量定义在 [tcp.rs](net/ax-net/src/tcp.rs#L45-L52): + +```rust +const TCP_KEEPIDLE_DEFAULT_SECS: u32 = 7200; // 2小时空闲后开始探测 +const TCP_KEEPINTVL_DEFAULT_SECS: u32 = 75; // 探测间隔 75s +const TCP_KEEPCNT_DEFAULT: u32 = 9; // 最多 9 次探测 +const TCP_USER_TIMEOUT_DEFAULT_MS: u32 = 0; // 默认不限制 +``` + +上限约束: + +```rust +const TCP_KEEPIDLE_MAX_SECS: u32 = 32767; // ~9.1 小时 +const TCP_KEEPINTVL_MAX_SECS: u32 = 32767; +const TCP_KEEPCNT_MAX: u32 = 127; +``` + +`TCP_USER_TIMEOUT_DEFAULT_MS = 0` 表示使用 smoltcp 内置默认超时。 + +## TCP_INFO 默认值 + +`TcpInfo` 快照中的默认/估计值([tcp.rs](net/ax-net/src/tcp.rs#L48-L52)): + +```rust +const TCP_INFO_DEFAULT_MSS: u32 = 1460; // 1500 - 20(IP) - 20(TCP) +const TCP_INFO_DEFAULT_PMTU: u32 = 1500; +const TCP_INFO_INITIAL_RTO_MICROS: u32 = 1_000_000; // 1s 初始 RTO +const TCP_INFO_DEFAULT_REORDERING: u32 = 3; +``` + +## Ephemeral Port 范围 + +自动端口分配(TCP/UDP bind 时 `port == 0`)从 `49152`(IANA 动态端口范围下界,`0xC000`)开始,逐个尝试直到找到未占用端口。 + +## 超时与 Poll 间隔 + +| 常量 | 位置 | 值 | 说明 | +| --- | --- | --- | --- | +| `DNS_DEFAULT_TIMEOUT` | [lib.rs#L488](net/ax-net/src/lib.rs#L488) | 5s | DNS 查询超时 | +| `DHCP_BOOTSTRAP_ATTEMPTS` | [lib.rs#L107](net/ax-net/src/lib.rs#L107) | 200 | DHCP bootstrap 最大重试次数 | +| `DHCP_BOOTSTRAP_POLL_INTERVAL` | [lib.rs#L108](net/ax-net/src/lib.rs#L108) | 10ms | DHCP bootstrap poll 间隔 | +| `DHCP_MAX_RETRY_SHIFT` | [service.rs#L283](net/ax-net/src/service.rs#L283) | 4 | DHCP 指数退避最大位移(最大 16s) | +| `NEIGHBOR_TTL` | [ethernet.rs#L118](net/ax-net/src/device/ethernet.rs#L118) | 300s | ARP neighbor 缓存 TTL | +| `ARP_REQUEST_RETRY` | [ethernet.rs#L119](net/ax-net/src/device/ethernet.rs#L119) | 1s | ARP 请求重试间隔 | +| Idle poll interval | [lib.rs#L438](net/ax-net/src/lib.rs#L438) | 100ms | net-poll worker 空闲轮询间隔 | + +## Router 缓冲区配置 + +| 名称 | 位置 | 容量 | +| --- | --- | --- | +| `Router::rx_buffer` | [router.rs#L326](net/ax-net/src/router.rs#L326) | `SOCKET_BUFFER_SIZE` 个 MTU 槽位 × 1500 字节 | +| `Router::tx_buffer` | [router.rs#L330](net/ax-net/src/router.rs#L330) | 同上 | +| `RouterQueues::rx` | [router.rs#L335](net/ax-net/src/router.rs#L335) | `SOCKET_BUFFER_SIZE` 个 `RxPacket` | +| `DeviceHandle::tx_queue` | [router.rs#L140](net/ax-net/src/router.rs#L140) | `SOCKET_BUFFER_SIZE` 个 `TxPacket` | +| `EthernetDevice::pending_packets` | [ethernet.rs#L123](net/ax-net/src/device/ethernet.rs#L123) | `ETHERNET_MAX_PENDING_PACKETS`(128)个 IP 包 | +| `LoopbackDevice::buffer` | [loopback.rs#L17](net/ax-net/src/device/loopback.rs#L17) | `SOCKET_BUFFER_SIZE` 个 MTU 槽位 × 1500 字节 | + +## Unix Stream 缓冲区 + +Unix stream transport 使用 `ringbuf::HeapRb` 作为双向通道: + +```rust +const BUF_SIZE: usize = 64 * 1024; // 64 KiB 每方向 +``` + +一对 socketpair 的总内存开销为 `2 × 64 KiB = 128 KiB`。 diff --git a/docs/docs/architecture/net/dependencies.md b/docs/docs/architecture/net/dependencies.md new file mode 100644 index 0000000000..88d4654a96 --- /dev/null +++ b/docs/docs/architecture/net/dependencies.md @@ -0,0 +1,299 @@ +--- +sidebar_position: 2 +sidebar_label: "依赖关系" +--- + +# 网络组件依赖关系 + +`ax-net` 是一个共享网络栈 crate,不包含具体硬件驱动。它位于 `net/ax-net`,上接 ArceOS、StarryOS、Axvisor,下接 `rd-net` / `rdif-eth` 设备能力。 + +## 分层关系 + +```mermaid +flowchart TB + subgraph Upper["上层系统"] + ArceApi["ax-api / ax-posix-api"] + AxStd["ax-std / ax-libc"] + Starry["starry-kernel"] + Axvisor["Axvisor"] + end + + subgraph Runtime["运行时接入"] + AxRuntime["ax-runtime"] + IrqAdapter["runtime IRQ adapter"] + end + + subgraph Net["net/ax-net"] + Public["public socket + interface API"] + Service["Service + Router + SocketSet"] + DeviceAdapt["device adapter"] + end + + subgraph Protocol["协议与同步"] + Smol["smoltcp"] + Poll["axpoll"] + Task["ax-task"] + Sync["ax-sync / ax-kspin"] + end + + subgraph Driver["设备与驱动"] + RdNet["rd-net"] + RdifEth["rdif-eth"] + AxDriver["ax-driver / rdrive"] + Virtio["virtio-net / SoC net driver"] + end + + Upper --> Net + AxRuntime --> Net + IrqAdapter --> Net + Net --> Smol + Net --> Poll + Net --> Task + Net --> Sync + Net --> RdNet + RdNet --> RdifEth + AxDriver --> RdifEth + Virtio --> AxDriver +``` + +## 直接依赖 + +依赖声明在 [net/ax-net/Cargo.toml](net/ax-net/Cargo.toml): + +| 依赖 | 用途 | +| --- | --- | +| `smoltcp` | TCP/IP、UDP、raw、DHCPv4、DNS、ARP cache 和 `Device` token 模型 | +| `rd-net` | Ethernet 设备抽象和 RX/TX buffer trait | +| `rdif-vsock` | 可选 vsock 设备能力(`vsock` feature) | +| `ax-task` | worker、wait queue、future helper、sleep/yield | +| `axpoll` | readiness 和 `Pollable` | +| `ax-sync` / `ax-kspin` | mutex、spin lock 和同步原语 | +| `ax-hal` | monotonic/wall clock 等 HAL 能力 | +| `ax-io` | socket read/write buffer trait | +| `ax-errno` | `AxError` / `AxResult` | +| `spin` | `Once`、`LazyLock`、`RwLock` | +| `hashbrown` / `ringbuf` / `async-channel` | socket 和辅助数据结构 | +| `event-listener` / `bitflags` / `enum_dispatch` / `async-trait` | 事件通知、标志位、trait 分派 | + +`smoltcp` 在 `Cargo.toml` 中固定启用一组能力(`medium-ethernet`、`medium-ip`、`proto-ipv4`、`proto-ipv6`、`socket-{raw,icmp,udp,tcp,dhcpv4,dns}`、`async`、`alloc`、`log`)。`ax-net` 自身只用 `Medium::Ip` 层,Ethernet 帧处理由 `EthernetDevice` 自行完成。 + +## 能力边界(EthernetDriver) + +`ax-net` 不直接依赖硬件驱动框架,而是通过能力边界 trait `EthernetDriver`([net/ax-net/src/device/driver.rs#L70-L82](net/ax-net/src/device/driver.rs#L70-L82))对接网卡: + +```rust +pub trait EthernetDriver: Send + Sync { + fn device_name(&self) -> &str; + fn irq_num(&self) -> Option; + fn enable_irq(&mut self); + fn disable_irq(&mut self); + fn mac_address(&self) -> [u8; 6]; + fn alloc_tx_buffer(&mut self, size: usize) -> NetDeviceResult>; + fn recycle_tx_buffers(&mut self) -> NetDeviceResult; + fn transmit(&mut self, tx_buf: &mut dyn NetTxBuffer) -> NetDeviceResult; + fn receive(&mut self) -> NetDeviceResult>; + fn recycle_rx_buffer(&mut self, rx_buf: &mut dyn NetRxBuffer) -> NetDeviceResult; + fn handle_irq(&mut self) -> NetIrqEvents; +} +``` + +`RdNetDriver`([driver.rs#L128](net/ax-net/src/device/driver.rs#L128))实现该 trait,内部包装 `rd-net` 的 `TxQueue` / `RxQueue`。`EthernetDeviceList` 即 `Vec>`([driver.rs#L84](net/ax-net/src/device/driver.rs#L84)),由 `ax-runtime` 收集后传入 `init_network()`。 + +## 主要消费者 + +| 消费方 | 使用方式 | +| --- | --- | +| `ax-runtime` | 收集 Ethernet/vsock 设备,构造 `NetworkConfig`,调用 `init_network()` / `init_vsock()` | +| `ax-api` | 提供 ArceOS socket API,复用 `SocketOps` | +| `ax-posix-api` | 提供 POSIX socket、poll、epoll 等适配 | +| `ax-std` / `ax-libc` | 面向应用暴露标准库和 libc 网络接口 | +| `starry-kernel` | Linux socket syscall、ioctl、AF_PACKET、AF_NETLINK 等 ABI 层 | +| Axvisor | 通过结构化配置表达管理面/服务面接口需求 | + +## 与 `ax-runtime` 的边界 + +`ax-runtime` 负责平台设备与网络栈之间的装配: + +- 从 rdrive / rdif registry 获取 Ethernet 设备。 +- 包装为 `RdNetDriver` / `EthernetDeviceList`。 +- 注册网络 IRQ adapter。 +- 构造 `NetworkConfig`。 +- 调用 `ax_net::init_network(net_devs, config)`。 + +`ax-runtime` 不维护第二套接口状态,不直接修改路由表,也不解析 Linux socket 语义。 + +## 与 StarryOS 的边界 + +StarryOS 负责 Linux ABI: + +- `socket()`、`bind()`、`connect()`、`sendmsg()`、`recvmsg()` syscall 参数解析。 +- `ifreq`、`sockaddr_in`、`sockaddr_ll`、`sockaddr_nl` 编解码。 +- Linux errno 映射。 +- root net namespace 可见性过滤。 +- `SO_BINDTODEVICE` 到 `InterfaceId` 的转换。 +- AF_PACKET 的 ifindex 绑定和兼容行为。 + +StarryOS 不缓存独立 IPv4/gateway/MAC 状态。接口信息统一来自: + +- `ax_net::interfaces()` +- `ax_net::interface_by_name()` +- `ax_net::interface_by_id()` +- `ax_net::ipv4_config()` +- `ax_net::arp_entries()` + +## 与驱动框架的边界 + +具体网卡驱动属于 `drivers/`、`ax-driver`、`rdrive` 和 `rdif-*`。`ax-net` 只关心 Ethernet 能力边界: + +```text +driver core + -> rdif-eth / rd-net + -> ax-runtime collects devices + -> ax-net EthernetDevice +``` + +这种边界避免网络协议栈直接依赖 FDT、PCI、MMIO、DMA、VirtIO 或平台 IRQ ABI。 + +## 与 smoltcp 的边界 + +smoltcp 提供协议核心,但不直接知道 TGOSKits 的多接口 registry。`ax-net` 在 smoltcp 外层维护: + +- `InterfaceId` +- `InterfaceInfo` +- `NetworkConfig` +- `RouteTable` +- per-interface DHCP metadata +- DNS 来源信息 +- StarryOS ifindex 映射 + +`Router` 是两者之间的适配层:对 smoltcp 它是一个 `phy::Device`([router.rs#L672](net/ax-net/src/router.rs#L672));对 TGOSKits 它知道多个设备、路由和接口元数据。`Service`([service.rs#L210-L218](net/ax-net/src/service.rs#L210-L218))持有 smoltcp `Interface` 并驱动其 poll: + +```rust +pub struct Service { + pub iface: Interface, + router: Router, + control: Arc, + timeout: Option + Send>>>, + dhcp: Vec, +} +``` + +`Service::new()`([service.rs#L450-L462](net/ax-net/src/service.rs#L450-L462))使用 `HardwareAddress::Ip` 创建 smoltcp `Interface`,把 `Router` 作为 `phy::Device` 传入。 + +## 内部模块依赖关系 + +`ax-net` crate 内部模块间调用关系: + +```mermaid +flowchart TB + lib[lib.rs
Public API + 全局单例 + init_network] + + service[service.rs
Service + NetControl + DhcpState] + router[router.rs
Router + RouteTable + DeviceHandle] + wrapper[wrapper.rs
SocketSetWrapper] + listen[listen_table.rs
ListenTable] + + tcp[tcp.rs
TcpSocket] + udp[udp.rs
UdpSocket] + raw[raw.rs
RawSocket] + unix[unix/
UnixSocket + Transport] + vsock[vsock/
VsockSocket + VsockTransport] + + general[general.rs
GeneralOptions] + options[options.rs
Configurable + Get/SetSocketOption] + state[state.rs
StateLock] + socket[socket.rs
SocketOps + SocketAddrEx] + + config[config.rs
InterfaceId + NetworkConfig] + consts[consts.rs
缓冲区常量] + device[device/
EthernetDevice + LoopbackDevice + RdNetDriver] + + lib --> service + lib --> router + lib --> wrapper + lib --> listen + lib --> tcp + lib --> udp + lib --> raw + lib --> unix + lib --> vsock + lib --> config + lib --> device + + tcp --> general + tcp --> options + tcp --> state + tcp --> socket + tcp --> config + tcp --> consts + tcp --> wrapper + tcp --> listen + tcp --> service + + udp --> general + udp --> options + udp --> socket + udp --> config + udp --> consts + udp --> wrapper + udp --> service + + raw --> general + raw --> options + raw --> state + raw --> socket + raw --> config + raw --> consts + raw --> wrapper + raw --> service + + unix --> general + unix --> options + unix --> socket + + vsock --> general + vsock --> options + vsock --> state + vsock --> socket + vsock --> device + vsock --> service + + service --> router + service --> config + service --> consts + service --> device + service --> wrapper + + router --> config + router --> consts + router --> device + router --> listen + + device --> config + device --> consts +``` + +### 关键数据流方向 + +| 方向 | 数据 | 载体 | +| --- | --- | --- | +| 设备 → Router | RX 包(bytes + InterfaceId) | `BoundedPacketQueue` | +| Router → smoltcp | RX payload(IP 包裸内容) | `Router.rx_buffer`(`PacketBuffer`) | +| smoltcp → Router | TX IP 包(完整 IP packet) | `Router.tx_buffer`(`PacketBuffer`) | +| Router → 设备 | TX 包(next_hop + bytes) | `DeviceHandle.tx_queue`(`BoundedPacketQueue`) | +| 设备 → Service | DHCP 事件 | `DhcpEvent` 枚举(通过 snoop callback) | +| Service → NetControl | 接口/routing/DNS 更新 | `commit_interface_update()` + `RouteTable::replace_ipv4_rules_for_interface()` | +| Socket → Service | Poll 请求 | `request_poll()` + `NET_POLL_REQUESTED` atomic + `NET_POLL_WAKE` | +| Service → Socket | I/O 就绪通知 | `register_waker()` 注册 → smoltcp readiness → waker 唤醒 | + +### 对外提供的 trait 与类型 + +| trait/类型 | 定义位置 | 消费者 | +| --- | --- | --- | +| `SocketOps` | [socket.rs](net/ax-net/src/socket.rs) | `ax-api`, `ax-posix-api`, `starry-kernel` | +| `Configurable` | [options.rs](net/ax-net/src/options.rs) | 同上 + 各 socket backend | +| `Pollable` | 通过 `axpoll` | `axpoll` 消费者 | +| `EthernetDriver` | [device/driver.rs](net/ax-net/src/device/driver.rs) | `ax-runtime` | +| `EthernetIrqRegistrar` | [device/ethernet.rs](net/ax-net/src/device/ethernet.rs) | `ax-runtime` | +| `VsockDriver` | [device/vsock.rs](net/ax-net/src/device/vsock.rs) | `ax-runtime`(`vsock` feature) | +| `UnixNamespace` | [unix/namespace.rs](net/ax-net/src/unix/namespace.rs) | `starry-kernel` | diff --git a/docs/docs/architecture/net/flows.md b/docs/docs/architecture/net/flows.md new file mode 100644 index 0000000000..ce1301553d --- /dev/null +++ b/docs/docs/architecture/net/flows.md @@ -0,0 +1,1046 @@ +--- +sidebar_position: 5 +sidebar_label: "运行时流程" +--- + +# 运行时流程 + +本章详述 `ax-net` 的初始化、收包、发包、ARP、TCP/UDP socket 操作、DHCP、DNS 和 poll 循环的完整运行路径。每个流程给出精确的锁获取顺序、数据结构转换和错误处理分支。 + +--- + +## 1. 初始化流程 + +入口 `init_network()` 在 [net/ax-net/src/lib.rs#L125](net/ax-net/src/lib.rs#L125)。 + +```mermaid +sequenceDiagram + participant Runtime as ax-runtime + participant AxNet as init_network() + participant Router as Router + participant Control as NetControl + participant Service as Service + participant Worker as device / net-poll workers + + Runtime->>AxNet: init_network(net_devs, NetworkConfig) + Note over AxNet: 1. 校验配置(IP/前缀/网关/DNS) + AxNet->>Router: Router::new(routes) + AxNet->>Router: add_device(LOOPBACK, LoopbackDevice) + AxNet->>Router: add_rule(lo 直连路由 127.0.0.0/8) + Note over AxNet: 2. 遍历 net_devs + loop 每个 Ethernet 设备 + AxNet->>AxNet: find_interface_config(order, mac, driver_name) + AxNet->>Router: add_device(id, EthernetDevice) + alt 静态 IP + AxNet->>Router: set_ipv4_config(ip, gw) + else DHCP + AxNet->>AxNet: dhcp_ifaces.push(...) + end + end + AxNet->>Router: start_rx_workers() + AxNet->>Router: start_tx_workers() + AxNet->>Control: NetControl::new(interfaces, routes, dns) + AxNet->>Service: Service::new(router, control) + Note over Service: 安装 lo_ip + 静态 eth_ips 到 smoltcp ip_addrs + opt DHCP enabled + loop 每个 dhcp_ifaces + AxNet->>Service: enable_dhcp(id, dev, mac, metric) + end + end + AxNet->>AxNet: NET_CONTROL.call_once(control) + AxNet->>AxNet: SERVICE.call_once(Mutex(service)) + AxNet->>Service: register_device_waker(&NET_POLL_DEVICE_WAKER) + AxNet->>Worker: spawn net_poll_worker + opt DHCP enabled + AxNet->>AxNet: wait_for_dhcp_bootstrap() + end +``` + +### 步骤详解 + +**步骤 1 — 配置校验**([lib.rs#L132-L168](net/ax-net/src/lib.rs#L132-L168)) + +对每个 `InterfaceConfig`: + +- 名字不能是 `"lo"`(保留)。 +- `dhcp` 和 `static_ip` 不能同时配置。 +- 静态 IP 不能是 `0.0.0.0`(unspecified)。 +- 静态前缀长度不能超过 32。 +- 静态网关不能是 `0.0.0.0`。 +- 所有 DNS server 都不能是 unspecified。 + +不满足则 `panic!`(启动阶段配置错误应 fail-fast)。 + +**步骤 2 — 创建 loopback**([lib.rs#L184-L201](net/ax-net/src/lib.rs#L184-L201)) + +```rust +let lo_id = InterfaceId::LOOPBACK; // InterfaceId(1) +let lo_dev = router.add_device(lo_id, Box::new(LoopbackDevice::new())); +let lo_ip = Ipv4Cidr::new(Ipv4Address::new(127,0,0,1), 8); +router.add_rule(Rule::new(lo_ip.into(), None, lo_dev, lo_id, lo_ip.address().into(), 0)); +``` + +Loopback 设备注册后,立即安装直连路由 `127.0.0.0/8 → lo_dev`,源地址 `127.0.0.1`,metric 0。`LoopbackDevice` 的 `send()`/`recv()` 直接在内部 buffer 中回环,不经过 Ethernet 帧封装。 + +**步骤 3 — 注册 Ethernet 设备**([lib.rs#L205-L292](net/ax-net/src/lib.rs#L205-L292)) + +对每个 driver 提供的 `EthernetDevice`(顺序由 `net_devs.drain(..)` 决定): + +1. 计算 `default_name = "eth{order}"`。 +2. 用 `find_interface_config()` 匹配 `InterfaceConfig`。匹配方式由 `InterfaceMatcher` 决定:`ByOrder`、`ByMac` 或 `ByDriverName`([lib.rs#L328-L356](net/ax-net/src/lib.rs#L328-L356))。如果有多个 config 匹配同一设备则 panic。 +3. 检查接口名冲突。 +4. 分配 `InterfaceId`:`order + 2`(0 保留给 placeholder,1 是 loopback)。 +5. 调用 `router.add_device()` 将 `EthernetDevice` 包装为 `DeviceHandle`,注册到 `Router::devices` 列表。 +6. **静态接口**:调用 `router.set_ipv4_config()` 安装直连路由和默认路由(如果有 gateway)。 +7. **DHCP 接口**:推入 `dhcp_ifaces` 向量,稍后在 `Service` 中启用。 + +匹配完成后,检查 `used_configs`:如果某个配置没有匹配到任何设备,则 panic([lib.rs#L294-L301](net/ax-net/src/lib.rs#L294-L301))。 + +**步骤 4 — 启动 worker 线程**([lib.rs#L305-L306](net/ax-net/src/lib.rs#L305-L306)) + +```rust +router.start_rx_workers(); // 每个 Ethernet DeviceHandle 一个 RX worker +router.start_tx_workers(); // 每个 Ethernet DeviceHandle 一个 TX worker +``` + +RX worker 入口 `device_rx_worker`([router.rs#L596-L626](net/ax-net/src/router.rs#L596-L626)),TX worker 入口 `device_tx_worker`([router.rs#L584-L596](net/ax-net/src/router.rs#L584-L596))。Loopback 不需要 worker。 + +**步骤 5 — 构建 Service 和控制面**([lib.rs#L308-L323](net/ax-net/src/lib.rs#L308-L323)) + +```rust +let control = Arc::new(NetControl::new(interfaces, routes, dns)); +let mut service = Service::new(router, control.clone()); +service.iface.update_ip_addrs(|ip_addrs| { + ip_addrs.push(lo_ip.into()).unwrap(); + for ip in eth_ips { ip_addrs.push(ip.into()).unwrap(); } +}); +``` + +`NetControl` 持有接口 registry、路由表和 DNS 来源的可读快照。`Service` 持有 `smoltcp::Interface`、`Router` 和 `NetControl` 的引用。smoltcp 的 `ip_addrs` 在此阶段注入 loopback 和所有静态接口的 IP。 + +DHCP 接口的 IP 在 ACK 后由 `commit_network_state()` → `set_interface_ipv4()` 动态注入。 + +**步骤 6 — 注册全局单例并启动 poll worker**([lib.rs#L325-L332](net/ax-net/src/lib.rs#L325-L332)) + +```rust +NET_CONTROL.call_once(|| control); +SERVICE.call_once(|| Mutex::new(service)); +get_service().register_device_waker(&NET_POLL_DEVICE_WAKER); +ax_task::spawn_with_name(net_poll_worker, "net-poll".to_owned()); +``` + +`NET_POLL_DEVICE_WAKER` 是一个 `NetPollWake` 实例,其 `wake()` 直接调用 `request_poll()`([lib.rs#L455-L464](net/ax-net/src/lib.rs#L455-L464))。这样当设备 driver 有新数据可读时,能直接唤醒 net-poll worker。 + +**步骤 7 — DHCP bootstrap 等待**([lib.rs#L333-L335](net/ax-net/src/lib.rs#L333-L335)) + +```rust +if dhcp_enabled { wait_for_dhcp_bootstrap(); } +``` + +`wait_for_dhcp_bootstrap()`([lib.rs#L613-L624](net/ax-net/src/lib.rs#L613-L624))循环最多 `DHCP_BOOTSTRAP_ATTEMPTS` 次,每次 `request_poll()` + `sleep(DHCP_BOOTSTRAP_POLL_INTERVAL)`,检查 `dhcp_configured()` 是否为 true。 + +关键策略:**只要任一 DHCP 接口配置成功就返回**([service.rs#L490-L496](net/ax-net/src/service.rs#L490-L496))。这避免了一个断网的 DHCP 网卡阻塞整个系统启动。 + +超时后只打印 warning,不 panic。未配置的 DHCP 接口后续仍可在运行时异步完成配置。 + +--- + +## 2. 接收(RX)流程 + +```mermaid +sequenceDiagram + participant Dev as Ethernet driver + participant RxW as device_rx_worker + participant DevLock as DeviceHandle.inner + participant RxQ as DeviceHandle.rx_queue
(BoundedPacketQueue) + participant PollW as net-poll worker + participant Router as Router + participant RouterRx as Router.rx_buffer
(PacketBuffer) + participant Smol as smoltcp Interface + participant Sock as SocketSet + + Dev-->>RxW: IRQ → rx_wake.wait() returns + RxW->>DevLock: lock() + loop rx_buffer is empty + RxW->>Dev: device_inner.recv(interface_id, &mut rx_buffer) + Note over Dev: EthernetDevice::handle_frame:
parse → snoop → enqueue payload + end + DevLock-->>RxW: unlock + loop rx_buffer.dequeue() + RxW->>RxQ: push(RxPacket{interface_id, bytes}) + alt 队列满 + RxW->>RxW: warn + drop packet + end + RxW->>PollW: request_poll() + end + RxW->>Dev: rx_wake.wait() (if no data) + Note over PollW: === poll_until_idle() === + PollW->>Router: Service::poll() + Router->>Router: wake_rx_workers() + loop Router.rx_buffer not full + Router->>RxQ: pop() + Router->>Sock: snoop_tcp_packet() → LISTEN_TABLE + Router->>RouterRx: enqueue(payload, interface_id) + end + Router->>Smol: Interface::poll(timestamp, router, sockets) + Smol->>Router: RxToken::consume(payload) + Smol->>Smol: TCP/UDP/ICMP state machine + Smol-->>PollW: readiness changes + PollW->>PollW: wake socket waiters (outside locks) +``` + +### 步骤详解 + +**步骤 1 — RX worker 唤醒**([router.rs#L596-L626](net/ax-net/src/router.rs#L596-L626)) + +RX worker 在 `device.rx_wake.wait()` 上阻塞。设备 driver 收到 IRQ 或被其他路径唤醒后,调用 `rx_wake.notify_one()` 唤醒 worker。 + +Worker 使用一个本地 `PacketBuffer`(容量 1,大小 `STANDARD_MTU`)作为从 driver 读取的临时 buffer: + +```rust +let mut rx_buffer = PacketBuffer::new(vec![PacketMetadata::EMPTY; 1], vec![0u8; STANDARD_MTU]); +``` + +**步骤 2 — 从设备读取**(持设备锁) + +```rust +let mut device_inner = device.inner.lock(); // 获取设备写锁 +let mut snoop = |_packet: &[u8]| {}; // 空 snoop(已在 Router::poll 中做 TCP snoop) +while rx_buffer.is_empty() + && device_inner.recv(device.interface_id, &mut rx_buffer, now(), &mut snoop) +{ + received = true; +} +// 释放设备锁 +``` + +`EthernetDevice::recv()`([device/ethernet.rs](net/ax-net/src/device/ethernet.rs#L230))内部流程: + +1. 从 driver 的 RX ring 取一帧。 +2. 用 `EthernetFrame::new_unchecked()` 解析帧头。 +3. 检查目的 MAC:广播/空 MAC/自身 MAC 才接受,否则丢弃。 +4. `EthernetProtocol::Ipv4` → `snoop(payload)` + `buffer.enqueue(len, interface_id)` + `copy_from_slice`。 +5. `EthernetProtocol::Arp` → `process_arp()`(见 ARP 流程)。 +6. 其他 ethertype → 丢弃。 + +**步骤 3 — 推入有界 RX 队列**(释放设备锁后) + +```rust +while let Ok((interface_id, packet)) = rx_buffer.dequeue() { + let rx = RxPacket { interface_id, bytes: packet.to_vec().into_boxed_slice() }; + if device.rx_queue.push(rx).is_err() { + warn!("{}: RX queue is full, dropping packet", device.name); + break; + } + crate::request_poll(); // 每包唤醒 net-poll worker + received = true; +} +``` + +`rx_queue` 是 `BoundedPacketQueue`,容量 `SOCKET_BUFFER_SIZE = 64`([consts.rs#L12](net/ax-net/src/consts.rs#L12))。队列满时丢弃剩余包并 break,避免在队列已满时继续从 driver 拷贝。 + +**步骤 4 — Router drain 共享 RX 队列** + +`net-poll` worker 被唤醒后进入 `poll_until_idle()` → `Service::poll()`。`Service::poll()` 首先调用 `Router::poll()`([router.rs#L434-L455](net/ax-net/src/router.rs#L434-L455)): + +```rust +while !self.rx_buffer.is_full() { + let Some(packet) = self.queues.rx.pop() else { break; }; + snoop_tcp_packet(&packet.bytes, sockets); // 预创建 listen socket + snoop(packet.interface_id, &packet.bytes); // DHCP snoop 回调 + let Ok(dst) = self.rx_buffer.enqueue(packet.bytes.len(), packet.interface_id) else { + warn!("Router RX buffer is full, dropping packet"); + break; + }; + dst.copy_from_slice(&packet.bytes); +} +``` + +`Router.rx_buffer` 是 smoltcp 的 `phy::Device` RX token 来源。smoltcp 在 `Interface::poll()` 中通过 `Router::receive()` 从 `rx_buffer` 取出一个包构造 `RxToken`([router.rs#L680-L700](net/ax-net/src/router.rs#L680-L700))。 + +**步骤 5 — TCP SYN 预探测** + +`snoop_tcp_packet()`([router.rs#L640-L678](net/ax-net/src/router.rs#L640-L678))在交付 smoltcp 前解析 IP 头和 TCP 头: + +- 如果是 `SYN && !ACK`(连接首包),调用 `LISTEN_TABLE.incoming_tcp_packet(src, dst, sockets)`。 +- `LISTEN_TABLE` 检查是否有 socket 在 `(src_ip, dst_port)` 上 listen。 +- 如果是,在 `SocketSet` 中预创建一个 `smoltcp::socket::tcp::Socket`,使其处于 `SYN-RECEIVED` 状态。 +- 这样当 smoltcp 正式处理这个 SYN 时,已经有 socket 接收它,避免 SYN 被丢弃。 + +**步骤 6 — smoltcp 协议处理** + +`self.iface.poll(timestamp, &mut self.router, sockets)` 在持有 `Service` 锁和 `SocketSet` 锁时执行。smoltcp 从 `Router`(作为 `phy::Device`)获取 RxToken,调用 `RxToken::consume()` 将 payload 交给 TCP/UDP/ICMP 状态机处理。 + +处理结果反映在 `SocketSet` 中各 socket 的 readiness 上。poll 返回后,等待者在外部(不持锁)被唤醒。 + +### RX 数据结构转换链 + +``` +driver RX ring + → [copy] PacketBuffer (local, capacity=1, MTU=1500) + → [copy] BoundedPacketQueue (per-device, capacity=64) + → [copy] Router.rx_buffer (PacketBuffer, shared, capacity from smoltcp config) + → [zero-copy ref] RxToken → smoltcp Interface::poll +``` + +每次 hop 最多一次内存拷贝。最坏路径有 3 次拷贝(driver → local buffer → queue → router buffer),但通过有界队列控制了背压。 + +--- + +## 3. 发送(TX)流程 + +```mermaid +sequenceDiagram + participant App as socket send()/connect() + participant Smol as smoltcp Interface + participant Router as Router (phy::Device) + participant RouterTx as Router.tx_buffer
(PacketBuffer) + participant TxQ as DeviceHandle.tx_queue
(BoundedPacketQueue) + participant TxW as device_tx_worker + participant Dev as Ethernet driver + + App->>Smol: socket.send(data) → smoltcp TCP/UDP state machine + Smol->>Router: TxToken::consume(len) → write IP packet + Note over RouterTx: IP packet 进入 Router.tx_buffer + App->>App: request_poll() → 唤醒 net-poll + Note over net-poll: === poll_until_idle() → dispatch() === + Router->>Router: dispatch(timestamp) + loop tx_buffer.dequeue() + Router->>Router: parse IP version + alt IPv4 unicast + Router->>Router: select_route_for_source(dst, src) + Router->>TxQ: enqueue_tx(next_hop, packet) + else IPv4 broadcast + loop 每个 Ethernet 设备 + Router->>TxQ: enqueue_tx(dst, packet) + end + end + end + TxQ-->>TxW: tx_wake.notify() + TxW->>TxQ: pop() + TxW->>Dev: device.inner.lock().send(next_hop, bytes) + Note over Dev: EthernetDevice::send:
查 ARP → 封装帧 → driver.send() + Dev-->>TxW: poll_next (true if needs re-poll) + opt poll_next + TxW->>App: request_poll() + end +``` + +### 步骤详解 + +**步骤 1 — Socket 层写入** + +TCP socket 的 `send()`([tcp.rs#L489-L514](net/ax-net/src/tcp.rs#L489-L514)): + +```rust +fn send(&self, mut src: impl Read, options: SendOptions) -> AxResult { + let extra_nb = options.flags.contains(crate::SendFlags::DONTWAIT); + let result = self.general.send_poller_with(self, extra_nb, || { + request_poll(); // 请求 poll 驱动协议栈 + self.with_smol_socket(|socket| { + if !socket.is_active() { Err(AxError::NotConnected) } + else if !socket.can_send() { Err(AxError::WouldBlock) } + else { + let len = socket.send(|buffer| { + let result = src.read(buffer); + let len = result.unwrap_or(0); + (len, result) + }).map_err(|_| ax_err_type!(NotConnected))??; + Ok(len) + } + }) + }); + if result.is_ok() { request_poll(); } // 写入后再请求一次 poll 触发发送 + result +} +``` + +`send_poller_with` 是一个通用 poller:在闭包返回 `WouldBlock` 时注册 waker 并让出任务,直到有 `IoEvents::OUT` 事件再次唤醒。 + +数据写入 smoltcp socket 的 TX buffer 后,**socket 层不直接发送**。发送发生在 smoltcp `Interface::poll()` 时。 + +**步骤 2 — smoltcp 生成 IP 包** + +在 `Service::poll()` 中,`self.iface.poll()` 驱动 TCP/UDP 状态机。当 socket 有待发送数据且发送窗口允许时,smoltcp 构造 IP 包并通过 `phy::Device` 的 `TxToken::consume()` 输出。 + +`Router` 实现了 smoltcp 的 `phy::Device` trait,`transmit()` 返回 `TxToken`([router.rs#L660-L678](net/ax-net/src/router.rs#L660-L678)): + +```rust +fn transmit(&mut self, _timestamp: Instant) -> Option> { + if self.tx_buffer.is_full() { None } + else { Some(TxToken(&mut self.tx_buffer)) } +} +``` + +`TxToken::consume()` 将 smoltcp 构造的 IP 包写入 `Router.tx_buffer`,metadata 设为 `TX_INTERFACE_PLACEHOLDER`(占位符,出接口由 `dispatch()` 决定)。 + +**步骤 3 — Router::dispatch() 选择出接口**([router.rs#L498-L557](net/ax-net/src/router.rs#L498-L557)) + +`dispatch()` 在 smoltcp `Interface::poll()` 返回后执行,从 `tx_buffer` 取出 IP 包: + +```rust +while let Ok((_, packet)) = self.tx_buffer.dequeue() { + match IpVersion::of_packet(packet).expect("got invalid IP packet") { + IpVersion::Ipv4 => { + let packet = Ipv4Packet::new_checked(packet).expect("got invalid IPv4 packet"); + let src_addr = IpAddress::Ipv4(packet.src_addr()); + let dst_addr = IpAddress::Ipv4(packet.dst_addr()); + if packet.dst_addr().is_broadcast() { + // 广播:发往所有 Ethernet 设备(不含 loopback) + for dev in &self.devices { + if dev.interface_id != InterfaceId::LOOPBACK { + poll_next |= dev.enqueue_tx(dst_addr, packet.into_inner()); + } + } + } else { + // 单播:按 (源地址, 目的地址) 查路由 + let routes = self.table.read(); + let Some(route) = routes.select_route_for_source(&dst_addr, &src_addr) else { + warn!("No route found for source {} destination {}", src_addr, dst_addr); + continue; // 丢包但不 panic + }; + let dev = &self.devices[route.dev]; + poll_next |= dev.enqueue_tx(route.next_hop, packet.into_inner()); + } + } + IpVersion::Ipv6 => { /* 类似逻辑 */ } + } +} +``` + +关键点: + +- **出接口不由 socket binding 决定**,而是由 IP 包的 `(源地址, 目的地址)` 在路由表中查 `select_route_for_source()` 决定。 +- 源地址由 smoltcp 根据 socket binding 或 connect 时的路由查询结果注入。 +- 广播包发往所有 Ethernet 设备。 +- 查不到路由时 `warn!` + `continue`,不 panic。 + +**步骤 4 — 推入 per-device TX 队列** + +`DeviceHandle::enqueue_tx()` 将 `TxPacket { next_hop, bytes }` 推入 `tx_queue`(同样是 `BoundedPacketQueue`)。如果队列满,返回 `true` 表示需要重新 poll(`poll_next`)。 + +**步骤 5 — TX worker 发送**([router.rs#L584-L596](net/ax-net/src/router.rs#L584-L596)) + +```rust +fn device_tx_worker(device: Arc) { + loop { + if let Some(packet) = device.tx_queue.pop() { + let poll_next = device.inner.lock().send(packet.next_hop, &packet.bytes, now()); + if poll_next { crate::request_poll(); } + } else { + device.tx_wake.wait_until(|| !device.tx_queue.is_empty()); + } + } +} +``` + +`EthernetDevice::send()` 内部: + +1. 查 `neighbors` 表获取下一跳的 MAC 地址。 +2. 如果找到:封装 Ethernet 帧(`src=自身MAC, dst=邻居MAC, ethertype=IPv4`),调用 `driver.send()`。 +3. 如果没找到:请求 ARP(见 ARP 流程),包暂存在 `pending_packets` 队列。 + +**步骤 6 — ARP 完成后发送暂存包**([device/ethernet.rs#L330](net/ax-net/src/device/ethernet.rs#L330)) + +ARP reply 到达后,`process_arp()` 查到邻居 MAC,drain `pending_packets` 队列: + +```rust +let mut kept: Vec<(IpAddress, Vec)> = Vec::with_capacity(ETHERNET_MAX_PENDING_PACKETS); +for _ in 0..ETHERNET_MAX_PENDING_PACKETS { + let Ok((&next_hop, buf)) = self.pending_packets.peek() else { break; }; + let action = match self.neighbors.get(&next_hop) { + Some(neighbor) => Action::Send(neighbor.hardware_address, buf), + None => Action::Keep(buf), // 保留不匹配的包,避免头阻塞 + }; + // ... +} +``` + +重要策略:drain 遍历所有暂存包,**不从头阻塞**。如果一个不可达 IP 的包在队列头部,不会阻止后面可达 IP 的包发送。 + +### TX 数据结构转换链 + +``` +socket.send(data) + → [copy] smoltcp socket TX buffer + → [smoltcp 内部] IP packet 构造 + → [TxToken::consume → copy] Router.tx_buffer + → [dispatch → copy] DeviceHandle.tx_queue + → [EthernetDevice::send → copy] driver TX buffer +``` + +--- + +## 4. ARP / 邻居发现流程 + +```mermaid +stateDiagram-v2 + [*] --> NoEntry: 包待发, 无邻居 + NoEntry --> Pending: request_arp() + Pending --> Resolved: 收到 ARP Reply + Pending --> Pending: 超时重发 (retry < MAX) + Pending --> Expired: 超过最大重试 + Resolved --> Resolved: TTL 内正常收发 + Resolved --> NoEntry: TTL 过期 (300s) + Expired --> [*] +``` + +### 步骤详解 + +**ARP 请求**([device/ethernet.rs#L278-L310](net/ax-net/src/device/ethernet.rs#L278-L310)) + +当 `EthernetDevice::send()` 发现下一跳 IP 不在 `neighbors` 表中时,调用 `request_arp()`: + +1. 构造 `ArpRepr::EthernetIpv4 { Request, src_mac=self.mac, src_ip=self.ip, target_mac=EMPTY, target_ip=next_hop }`。 +2. 用 `send_to()` 通过 driver 发送广播 ARP 请求帧。 +3. 将 `(target_ip, PendingNeighbor { requested_at: now })` 插入 `pending_neighbors`。 +4. 将待发送的 IP 包存入 `pending_packets` 有界队列。 + +**ARP 响应处理**([device/ethernet.rs#L312-L405](net/ax-net/src/device/ethernet.rs#L312-L405)) + +收到 ARP 帧时 `process_arp()`: + +1. 解析 ARP 包。 +2. 检查目的 MAC(unicast 才检查是否是自身)。 +3. 验证源 MAC 和源 IP 合法性(unicast MAC, 非 broadcast/multicast/unspecified IP)。 +4. 如果是 Request 且目标是自己:构造 Reply 发回。 +5. **无论 Request 还是 Reply**:更新 `neighbors` 表(`src_ip → src_mac, expires_at=now+NEIGHBOR_TTL`)。 +6. 从 `pending_neighbors` 移除该 IP。 +7. Drain `pending_packets`,发送所有下一跳已解析的包。 + +**邻居表 TTL** + +- `NEIGHBOR_TTL = 300s`(5 分钟)。 +- 过期后条目从 `neighbors` 移除,后续发送触发新的 ARP 请求。 +- `pending_neighbors` 中的条目在 `poll_dhcp` 或下一次发送时检查 `requested_at`,超时后重发 ARP。 + +--- + +## 5. TCP Socket 操作流程 + +### 5.1 Connect + +```mermaid +sequenceDiagram + participant App as 应用 + participant Sock as TcpSocket + participant Control as NetControl + participant SockSet as SOCKET_SET + participant Poll as net-poll + + App->>Sock: connect(remote_addr) + Sock->>Control: select_route(remote_addr.addr) + Control-->>Sock: RouteDecision { source, dev, next_hop } + Sock->>Sock: state: Idle → Connecting + Sock->>SockSet: smoltcp socket.connect(remote, local) + Sock->>Sock: yield_now() (让 net-poll 处理 SYN) + Sock->>Poll: request_poll() + loop poll_connect() + Sock->>SockSet: socket.state() == Connected? + alt Connected + Sock-->>App: Ok + else still connecting + Sock->>Poll: request_poll() → yield + end + end +``` + +`TcpSocket::connect()`([tcp.rs#L415-L432](net/ax-net/src/tcp.rs#L415-L432)): + +1. `start_connect()` 查路由获取源地址,在 smoltcp socket 上调用 `connect()`。 +2. **`ax_task::yield_now()`** — 这是一个 hack:让出当前任务,让 net-poll worker 有机会处理 SYN 发送和 SYN-ACK 接收。 +3. 在 `send_poller` 闭包中循环 `poll_connect()`,每次 `request_poll()` 触发协议栈推进。 +4. 连接成功返回 `Ok(())`,连接失败返回对应错误(`ConnectionRefused` 等)。 + +### 5.2 Listen + Accept + +`listen()`([tcp.rs#L434-L462](net/ax-net/src/tcp.rs#L434-L462)): + +1. 状态转换 `Idle → Listening`。 +2. 分配端口(如果未绑定)。 +3. 在 `LISTEN_TABLE` 注册 `(port, backlog)`。 +4. 设置 `DeviceBinding`。 + +`accept()`([tcp.rs#L464-L487](net/ax-net/src/tcp.rs#L464-L487)): + +```rust +fn accept(&self) -> AxResult { + let bound_port = self.bound_endpoint()?.port; + self.general.recv_poller(self, || { + request_poll(); + let accepted = { + let mut sockets = SOCKET_SET.inner.lock(); + LISTEN_TABLE.accept(bound_port, &mut sockets)? + }; + Ok(TcpSocket::new_connected(accepted.handle, ...).into()) + }) +} +``` + +Accept 依赖 RX 流程中的 `snoop_tcp_packet()` 预创建 socket。当 SYN 到达时,listen table 已在 SocketSet 中创建了 SYN-RECEIVED socket。三次握手完成后,`LISTEN_TABLE.accept()` 将该 socket 从 listen pool 移出并返回。 + +### 5.3 Send / Recv + +见 TX 流程中的 TCP send 部分。TCP `recv()`([tcp.rs#L516-L561](net/ax-net/src/tcp.rs#L516-L561))在 `recv_poller_with` 中循环读取 smoltcp socket 的 RX buffer,直到用户 buffer 填满或 RX 队列空。 + +--- + +## 6. UDP Socket 操作流程 + +### 6.1 Send + +UDP `send()`([udp.rs#L236-L353](net/ax-net/src/udp.rs#L236-L353))比 TCP 多了几个分支: + +1. **MSG_MORE corking**:如果 `SendFlags::MORE` 被设置,数据缓存在 `self.cork` 中而非立即发送。Cork buffer 上限 `UDP_TX_BUF_LEN`。后续不带 MORE 的 send 会 flush cork buffer。 +2. **目标地址解析**:connected socket 用 `remote_endpoint()`,unconnected socket 用 `options.to`。 +3. **路由选择**:`select_route(dst_addr)` 获取源地址。 +4. **smoltcp socket.send()**:写入 UDP payload + metadata(endpoint, local_address)。 + +零长度 UDP payload(只有 IP+UDP 头)是合法的,smoltcp `send(0, ...)` 处理。 + +### 6.2 Recv + +UDP `recv()`([udp.rs#L364-L425](net/ax-net/src/udp.rs#L364-L425)): + +```rust +fn recv(&self, mut dst: impl Write, mut options: RecvOptions) -> AxResult { + // 确定 expected_remote:Any(addr_out) / AnyDiscard / Expecting(connected_peer) + self.general.recv_poller_with(self, extra_nb, || { + request_poll(); + self.with_smol_socket(|socket| { + if !socket.can_recv() { Err(AxError::WouldBlock) } + else { + let result = socket.recv(); // 或 peek() for MSG_PEEK + match result { + Ok((src, meta)) => { + // 检查 expected_remote 匹配 + // 写入 dst,处理 truncation + } + Err(Exhausted) => Err(WouldBlock), + } + } + }) + }) +} +``` + +Connected UDP socket 只接受来自 peer 的包,其他包返回 `WouldBlock`。 + +--- + +## 7. DHCP 流程 + +```mermaid +sequenceDiagram + participant Sock as SocketSet + participant Poll as net-poll worker + participant Service as Service + participant Dhcp as DhcpState + participant Router as Router + participant Control as NetControl + participant Dev as Ethernet device + + Note over Dhcp: phase = Discovering + Poll->>Service: poll() + Service->>Dhcp: poll_packet(now) + alt retry_at <= now && phase == Discovering + Dhcp->>Router: send_on_device(DHCP Discover broadcast) + end + Note over Dev: === RX 路径 === + Dev-->>Router: DHCP Offer packet + Router->>Service: poll() → snoop callback + Service->>Dhcp: process_packet(interface_id, offer) + Dhcp->>Dhcp: phase → Requesting, offered_address = offer.yiaddr + Dhcp->>Router: send_on_device(DHCP Request) + Note over Dev: === RX 路径 === + Dev-->>Router: DHCP ACK + Router->>Service: poll() → snoop callback + Service->>Dhcp: process_packet(interface_id, ack) + Dhcp-->>Service: DhcpEvent::Configured { address, router, dns } + Service->>Service: handle_dhcp_event() + Service->>Service: set_interface_ipv4(iface, new_addr) + Service->>Control: commit_interface_update(routes) + Note over Dhcp: phase = Bound +``` + +### 步骤详解 + +**DhcpState 结构**([service.rs#L242-L275](net/ax-net/src/service.rs#L242-L275)) + +```rust +struct DhcpState { + interface_id: InterfaceId, + dev: usize, + ifname: String, + mac: EthernetAddress, + metric: u32, + transaction_id: u32, // 由 MAC 派生,见 dhcp_transaction_id() + phase: DhcpPhase, // Discovering → Requesting → Bound + retry_at: Instant, + retry: usize, + offered_address: Option, + server_identifier: Option, + address: Option, + dns_servers: Vec, +} +``` + +**状态机转换**([service.rs#L313-L402](net/ax-net/src/service.rs#L313-L402)) + +`process_packet()` 按 `(phase, message_type)` 驱动: + +| 当前 phase | 收到消息 | 动作 | +| --- | --- | --- | +| Discovering | Offer | 验证 yiaddr 是 unicast;记录 offered_address + server_id;phase → Requesting | +| Requesting | ACK | 解析 subnet_mask → prefix_len;验证 yiaddr;phase → Bound;返回 `DhcpEvent::Configured` | +| Bound | ACK | 更新地址(续约) | +| 任意 | NAK | reset;如果之前已配置则返回 `DhcpEvent::Deconfigured` | + +**DHCP 包构造** + +`poll_packet()`([service.rs#L434-L460](net/ax-net/src/service.rs#L434-L460))在 retry 到期时构造 DHCP Discover 或 Request: + +- 使用 `dhcp_transaction_id()`(由 MAC 派生,[service.rs#L694](net/ax-net/src/service.rs#L694))。 +- `DHCP_PARAMETER_REQUEST_LIST = [1, 3, 6, 42]`(subnet mask, router, DNS, NTP)。 +- 指数退避:`retry` 最大位移 `DHCP_MAX_RETRY_SHIFT = 4`。 + +**ACK 后状态提交**([service.rs#L534-L612](net/ax-net/src/service.rs#L534-L612)) + +`handle_dhcp_event(DhcpEvent::Configured)` → `commit_network_state()`: + +1. `set_interface_ipv4(iface, old_addr, new_addr)` — 更新 smoltcp `ip_addrs`:移除旧地址,添加新地址。 +2. `router.ipv4_rules()` — 生成直连路由 + 默认路由(如果有 gateway)。 +3. `control.commit_interface_update()` — 在 `NetControl` 的 state 中更新接口 IPv4、gateway,替换该接口的 DNS 条目,替换路由表中该接口的 IPv4 规则。 + +**Bootstrap 等待策略** + +`dhcp_configured()`([service.rs#L490-L496](net/ax-net/src/service.rs#L490-L496))只需**任一** DHCP 接口配置成功即返回 true。这确保一个断网的网卡不会阻塞启动。 + +--- + +## 8. DNS 查询流程 + +```mermaid +sequenceDiagram + participant App as 应用 + participant Lib as dns_query_timeout() + participant Control as NetControl + participant SockSet as SOCKET_SET + participant DnsSock as dns::Socket + participant Poll as net-poll + + App->>Lib: dns_query_timeout(name, timeout=5s) + Lib->>Control: dns_servers() + Control-->>Lib: [server1, server2, ...] (按 metric 排序) + Lib->>Control: 过滤不可路由的 server + Lib->>SockSet: add(dns::Socket::new(servers, [])) + Lib->>DnsSock: start_query(name, Type::A) + loop deadline 未到 + Lib->>Poll: request_poll() + Lib->>DnsSock: get_query_result(handle) + alt Pending + Lib->>Lib: yield_now() + else Resolved + DnsSock-->>Lib: Vec + Lib-->>App: Ok(addrs) + else Failed + Lib-->>App: Err(ConnectionRefused) + end + end + Lib-->>App: Err(TimedOut) +``` + +### 步骤详解 + +`dns_query_timeout()`([lib.rs#L494-L510](net/ax-net/src/lib.rs#L494-L510)): + +1. 获取 DNS server 列表(DHCP + 静态 + fallback,去重排序)。 +2. 用 `select_route()` 过滤掉不可路由的 server。 +3. 创建临时 `dns::Socket`,加入 `SocketSet`。 +4. `start_query()` 发起 A 记录查询。 +5. 循环 `request_poll()` + `get_query_result()`,超时由 monotonic clock 判断。 +6. `DnsSocketGuard` 的 `Drop` 自动移除 socket,避免泄漏。 + +超时常量:`DNS_DEFAULT_TIMEOUT = 5s`([lib.rs#L488](net/ax-net/src/lib.rs#L488))。 + +--- + +## 9. Poll 循环与 Waker 机制 + +### request_poll() 轻量唤醒 + +所有热路径(socket send/recv/connect/drop、设备 RX/TX、timer)只调用 `request_poll()`([lib.rs#L408-L411](net/ax-net/src/lib.rs#L408-L411)): + +```rust +pub fn request_poll() { + NET_POLL_REQUESTED.store(true, Ordering::Release); + NET_POLL_WAKE.notify_one(true); +} +``` + +- `NET_POLL_REQUESTED` 是一个 `AtomicBool`,Release store 保证可见性。 +- `NET_POLL_WAKE` 是一个 `Waker`(基于 `ax_task`),`notify_one(true)` 表示如果已有 pending wake 则合并(不重复唤醒)。 + +### net-poll worker 主循环 + +```rust +fn net_poll_worker() { + loop { + let delay = next_poll_delay(); + let timed_out = NET_POLL_WAKE.wait_timeout_until(delay, + || NET_POLL_REQUESTED.load(Ordering::Acquire)); + if !timed_out { + NET_POLL_REQUESTED.store(false, Ordering::Release); + } + poll_until_idle(); + } +} +``` + +1. `next_poll_delay()`([lib.rs#L437-L460](net/ax-net/src/lib.rs#L437-L460))向 smoltcp 询问下一次 poll 截止时间(TCP retransmit、DHCP retry、keep-alive 等),结合 monotonic clock 计算睡眠时长。空闲时回退到 100ms。 +2. `wait_timeout_until()` 等待 wake 或超时。 +3. 被 wake 唤醒(非超时)时清除 `NET_POLL_REQUESTED` 标志。 +4. 进入 `poll_until_idle()`。 + +### poll_until_idle() 原子互斥 + +```rust +fn poll_until_idle() { + POLL_AGAIN.store(true, Ordering::Release); + loop { + // CAS 获取 POLLING_INTERFACES 锁 + if POLLING_INTERFACES.compare_exchange(false, true, Acquire, Acquire).is_err() { + return; // 已有其他 poller 在运行 + } + while POLL_AGAIN.swap(false, AcqRel) { + while poll_once() {} // 循环直到 smoltcp 返回 idle + } + POLLING_INTERFACES.store(false, Release); + if !POLL_AGAIN.load(Acquire) { return; } + } +} +``` + +- `POLLING_INTERFACES`:原子标志,保证同一时刻只有一个线程进入协议栈。 +- `POLL_AGAIN`:在 poll 期间如果有新的 `request_poll()` 到来,设置此标志使循环重新执行。 + +### poll_once() 锁顺序 + +```rust +fn poll_once() -> bool { + get_service().poll(&mut SOCKET_SET.inner.lock()) +} +``` + +`Service::poll()` 内部锁顺序固定为: + +``` +1. Service lock (get_service()) +2. SocketSet lock (SOCKET_SET.inner.lock()) + ├─ Router::poll() — drain RX queue → rx_buffer + ├─ smoltcp Interface::poll(timestamp, router, sockets) + │ ├─ Router::receive() / Router::transmit() (phy::Device trait) + │ └─ TCP/UDP/ICMP state machine + ├─ Router::dispatch() — tx_buffer → per-device tx_queue + └─ poll_dhcp() — DHCP retry packets +3. 释放 SocketSet lock +4. 释放 Service lock +5. wake socket waiters (在锁外部) +``` + +**严格禁止**: + +- 禁止在持有设备锁(`DeviceHandle.inner`)时进入 `Service` 或 `SocketSet` 锁。 +- 禁止在持有 control-plane 写锁(`NetControl.state.write()`)时进入 smoltcp poll。 +- 锁的获取和释放严格嵌套,不允许跨层级持有。 + +### Waker 注册路径 + +Socket 的 `register()` 将 task waker 注册到 `general.waker` 中。当 smoltcp poll 改变 socket readiness 后,`Service::poll()` 返回,poll worker 在释放锁后调用各 socket 的 waker: + +``` +socket.send/recv/connect returns WouldBlock + → general.recv_poller / send_poller registers waker + → task yields + → net-poll worker calls poll_until_idle() + → smoltcp updates socket readiness + → poll worker wakes registered wakers + → task resumes, retries send/recv +``` + +`NetPollWake` 实现了 `core::task::Wake` trait([lib.rs#L455-L464](net/ax-net/src/lib.rs#L455-L464)),使设备 driver 可以通过标准 waker 接口唤醒 poll worker。 + +--- + +## 流程总结 + +| 流程 | 热路径入口 | 锁层级 | 延迟来源 | +| --- | --- | --- | --- | +| RX | IRQ → rx_wake → rx_worker → rx_queue | 设备锁 → RX 队列(无 Service 锁) | driver recv + 1 次 copy | +| TX | socket.send → smoltcp buffer → dispatch → tx_queue | Service 锁 → SocketSet 锁 → 路由表读锁 | smoltcp TCP 状态机 + ARP | +| TCP connect | socket.connect → smoltcp connect → yield → poll | Service 锁 → SocketSet 锁 | RTT × 1.5(SYN + SYN-ACK) | +| DHCP | poll_dhcp → send_on_device → RX → process_packet | Service 锁 → 路由表写锁 | DHCP server 响应时间 | +| DNS | dns_query_timeout → add socket → poll loop | Service 锁 → SocketSet 锁 | DNS server RTT | +| Poll | request_poll → net-poll worker → poll_until_idle | POLLING_INTERFACES 原子 → Service 锁 → SocketSet 锁 | smoltcp poll duration | + +--- + +## 10. Unix Domain Socket 流程 + +### 10.1 Stream Transport(socketpair / connect-accept) + +Unix stream 基于 `ringbuf::HeapRb` 实现全双工双向通道。`new_channels(pid)` 创建一对交叉连接的 `Channel`: + +```rust +(client_tx → server_rx) + (server_tx → client_rx) +shared: poll_update (Arc), cmsg queues (s2c + c2s) +``` + +**Bind + Listen + Accept 流程**: + +1. `bind(addr)` — 在 `ABSTRACT_BINDS` 或 `UnixNamespace` 中注册 `BindSlot`(stream bind + dgram bind)。Stream bind 存储在 `slot.stream: Mutex>`。 +2. `listen()` — Unix 中为 no-op(`Bind` 已准备就绪)。 +3. `accept()` — `block_on(transport.accept())`,内部 `bind.inner.accept.recv()` 阻塞等对端的 `connect()` 调用。 + +**Connect 流程**: + +1. `connect(remote_addr)` — 查 `with_slot(remote_addr, |slot| transport.connect(slot, local_addr))`。 +2. `StreamTransport::connect()` 创建新的 `(client_channel, server_channel)` 对。 +3. 将 `server_channel` 通过 `bind.inner.accept.send(server_channel)` 推给监听端。 +4. 返回后,对端 accept 解除阻塞,两端各自持有 `client_channel` / `server_channel`。 + +**Cmsg 管道**: + +- 每个 `Channel` 维护 `tx_cmsg` 和 `rx_cmsg` 两个 `CmsgQueue`(`Arc>>`)。 +- `sendmsg` 时记录 `start_byte = tx_bytes_total + 1, end_byte = start_byte + len - 1`,推入 `tx_cmsg`。 +- `recvmsg` 时从 `rx_cmsg` 前端检查:如果 `rx_bytes_total >= entry.start_byte`,释放 cmsg 给调用者;同时用 `end_byte` 限制本次 recv 长度(cmsg 边界语义)。 + +### 10.2 Datagram Transport + +Unix datagram 基于 `async_channel::unbounded()` 无界通道: + +```rust +struct Packet { + data: Vec, + cmsg: Vec, + sender: UnixSocketAddr, +} +``` + +**Bind**:注册 slot 时创建 `(tx, rx)` 通道对,`tx` 存入 `Bind` 供 connect/send 方使用,`rx` 存入 `DgramTransport::data_rx`。 + +**Connect**:从 slot 获取 `Bind::connect()`(Clones `data_tx`),更新 `connected: RwLock>`。 + +**Send**:读取用户数据到 `Vec`,构造 `Packet`,通过 `connected.data_tx.try_send()` 或查 `with_slot(options.to, |slot| slot.dgram.lock().as_ref().unwrap().data_tx.try_send())` 发送。 + +**Recv**:从 `data_rx.recv()` 或 `try_recv()` 取 `Packet`,处理 cmsg、包截断、sender address。 + +**SO_PASSCRED**:设置后 `send()` 自动添加 `UnixCredentials` 到每个包的 cmsg 中。 + +--- + +## 11. Vsock Stream 流程 + +Vsock 基于 `rdif-vsock` 驱动 + 全局 `VSOCK_CONN_MANAGER`: + +```mermaid +sequenceDiagram + participant App as 应用 + participant Sock as VsockSocket + participant Trans as VsockStreamTransport + participant Mgr as VSOCK_CONN_MANAGER + participant Driver as rdif-vsock + + App->>Sock: bind(VsockAddr { cid=VMADDR_CID_ANY, port=0 }) + Sock->>Trans: bind() + Trans->>Mgr: create_connection(conn_id, local_addr, None, Idle) + Trans->>Trans: state: Idle → Idle (不改变) + + App->>Sock: listen() + Sock->>Trans: listen() + Trans->>Mgr: listen(local_addr) + Trans->>Driver: driver.listen(local_addr) + Trans->>Trans: state: Idle → Listening + + App->>Sock: accept() + Sock->>Trans: accept() + Trans->>Trans: recv_poller() 循环 + alt 有连接 + Trans->>Mgr: can_accept(local_port)? + Trans->>Mgr: accept(local_port) → (conn_id, peer_addr) + Trans-->>App: Ok(VsockSocket(新 VsockStreamTransport)) + else 无连接 + Trans->>Trans: WouldBlock → yield → retry + end + + App->>Sock: connect(peer_addr) + Sock->>Trans: connect() + Trans->>Mgr: allocate_port() + create_connection() + Trans->>Driver: driver.connect(conn_id) + Trans->>Trans: send_poller() 等 Connected +``` + +Connection Manager([vsock/connection_manager.rs](net/ax-net/src/vsock/connection_manager.rs))维护 per-connection 状态机:`Idle → Listening/Connecting → Connected → Closed`。 + +Vsock stream 的 send/recv 直接从 `Connection` 的 ring buffer 读写数据。 + +--- + +## 12. ICMP Loopback Echo 流程 + +`RawSocket::send()` 对 loopback 地址有特殊处理([raw.rs](net/ax-net/src/raw.rs#L188-L203)): + +1. 检查目的地址是否是 loopback(`127.0.0.1` 或 `::1`)。 +2. 如果是,**不经过 smoltcp 发送**。而是解析 ICMP echo request(type=8, code=0)。 +3. 调用 `build_loopback_icmp_reply()`:将 type 改为 0(echo reply),清空 code,重新计算 ICMP checksum。 +4. 将 reply 存入 `self.loopback_rx` 队列。 +5. 下次 `recv()` 时优先消费 `loopback_rx` 中的内容。 + +这允许 `ping 127.0.0.1` 在不需要完整 IP stack 回环的情况下正确工作。 + +--- + +## 13. TCP Accept 完整流程图 + +```mermaid +sequenceDiagram + participant Remote as 远端 SYN + participant Eth as EthernetDevice + participant Router as Router::poll() + participant Smol as smoltcp Interface + participant ListenTbl as LISTEN_TABLE + participant SockSet as SOCKET_SET + participant App as accept() + + Remote->>Eth: TCP SYN + Eth->>Router: packet 进入 rx_buffer + Router->>Router: snoop_tcp_packet(bytes) + Router->>ListenTbl: incoming_tcp_packet(src, dst, sockets) + ListenTbl->>ListenTbl: 查 port → ListenTableEntryInner + ListenTbl->>SockSet: add(new TcpSocket) + ListenTbl->>ListenTbl: socket.listen(dst.port) + ListenTbl->>ListenTbl: syn_queue.push_back(PendingTcp) + + Router->>Smol: Interface::poll() + Smol->>Smol: process SYN → SYN-RECEIVED state + Smol->>Smol: send SYN-ACK (via TX path) + + Remote->>Eth: TCP ACK (三次握手完成) + Smol->>Smol: socket.state → Established + Smol->>SockSet: socket can_accept() = true + + App->>ListenTbl: accept(port, sockets) + ListenTbl->>ListenTbl: 遍历 syn_queue + ListenTbl->>SockSet: is_acceptable(handle)? + alt Established + ListenTbl-->>App: AcceptedTcp { handle, local_ep, remote_ep } + else 仍在握手 + ListenTbl-->>App: WouldBlock → retry + end +``` + +关键状态检查: + +- `is_closed_without_data()`:TCP socket 处于 `Closed/TimeWait/Closing/LastAck` 且无未读数据 → 跳过并从 `SocketSet` 移除。 +- `is_acceptable()`:TCP state 为 `Established/CloseWait/FinWait1/FinWait2/Closing/LastAck/TimeWait` → 可以 accept,应用可通过 `recv()` 读取已到达数据或获知 EOF。 +- `SYN_RECEIVED` → 不可 accept,仍在握手中。 diff --git a/docs/docs/architecture/net/integration.md b/docs/docs/architecture/net/integration.md new file mode 100644 index 0000000000..8de9756c23 --- /dev/null +++ b/docs/docs/architecture/net/integration.md @@ -0,0 +1,124 @@ +--- +sidebar_position: 7 +sidebar_label: "系统集成" +--- + +# 系统接入 + +本文说明 ArceOS、StarryOS 和 Axvisor 如何接入 `ax-net`,以及各自负责的边界。 + +## ArceOS / ax-runtime + +`ax-runtime` 是网络栈初始化的主要入口。它负责: + +- 在平台设备 probe 完成后收集 Ethernet 设备。 +- 将 `rd-net` 设备包装为 `ax-net` 的 `EthernetDriver`(由 `RdNetDriver` 实现,[driver.rs#L128](net/ax-net/src/device/driver.rs#L128))。 +- 注册网络 IRQ adapter(`set_ethernet_irq_registrar()`,[ethernet.rs#L80-L82](net/ax-net/src/device/ethernet.rs#L80-L82))。 +- 构造结构化 `NetworkConfig`。 +- 调用 `ax_net::init_network(net_devs, config)`([lib.rs#L125](net/ax-net/src/lib.rs#L125))。 +- 在 `vsock` feature 下调用 `ax_net::init_vsock(vsock_devs)`([lib.rs#L367](net/ax-net/src/lib.rs#L367))。 + +`ax-runtime` 不负责: + +- 维护接口 registry。 +- 维护路由表。 +- 解析 StarryOS Linux socket ABI。 +- 直接读写 smoltcp `SocketSet`。 + +## ArceOS API 层 + +ArceOS API 层通过 `SocketOps` 访问 `ax-net`: + +```text +ax-api / ax-posix-api + -> ax-net Socket / SocketOps + -> tcp / udp / raw / unix / vsock +``` + +应用侧通常不直接接触 `InterfaceId`,除非需要类似 `SO_BINDTODEVICE` 的接口绑定语义。 + +## StarryOS + +StarryOS 在 Linux ABI 层接入 `ax-net`。它负责 syscall 参数和 Linux 数据结构,不维护第二套网络状态。 + +```mermaid +flowchart TB + User["Linux user program"] + Syscall["StarryOS syscall/net"] + FileNet["file/net.rs"] + Packet["file/packet.rs"] + Proc["/proc/net/*"] + AxNet["ax-net"] + + User --> Syscall + Syscall --> FileNet + Syscall --> Packet + FileNet --> AxNet + Packet --> AxNet + Proc --> AxNet +``` + +### ioctl + +StarryOS `SIOCGIF*` 系列 ioctl 从 `ax-net` 查询接口: + +- `SIOCGIFCONF` 遍历 `ax_net::interfaces()`。 +- `SIOCGIFADDR` / `SIOCGIFBRDADDR` / `SIOCGIFNETMASK` 按接口名查询 IPv4。 +- `SIOCGIFHWADDR` 返回真实 MAC 或 loopback 类型。 +- `SIOCGIFINDEX` 返回 `InterfaceInfo::id.to_linux_ifindex()`。 +- `SIOCGIFFLAGS` 从 `InterfaceFlags` 映射 Linux `IFF_*`。 + +### SO_BINDTODEVICE + +StarryOS 将 Linux 传入的接口名解析为 `InterfaceId`: + +```text +SO_BINDTODEVICE="eth1" + -> ax_net::interface_by_name("eth1") + -> SetSocketOption::BindToDevice(Some(id)) +``` + +`getsockopt(SO_BINDTODEVICE)` 则反向从 socket 的 `DeviceBinding` 查询接口名。 + +### AF_PACKET + +StarryOS 保留 Linux `sockaddr_ll` 编解码,接口信息来自 `ax-net`: + +- `sll_ifindex == 0`:绑定第一个可见 Ethernet 接口。 +- `sll_ifindex != 0`:通过 `InterfaceId::from_linux_ifindex()` 和 `ax_net::interface_by_id()` 查询接口。 +- `SockAddrLl::from_interface(info, protocol)` 填充 ifindex、硬件类型和 MAC。 +- `send_packet()` 保留旧的 ARP reply 模拟能力,并基于当前绑定接口的 gateway 和 MAC 工作。 + +### `/proc/net/arp` + +`/proc/net/arp` 使用 `ax_net::arp_entries()`,device 字段必须是真实接口名,不写死 `eth0`。 + +## Axvisor + +Axvisor 不维护自己的网络接口模型。它通过结构化 `NetworkConfig` 表达网络意图: + +- 管理面接口。 +- VM 服务面接口。 +- 静态地址或 DHCP。 +- 默认路由 metric。 +- 接口级 DNS。 + +普通 TCP/UDP 连接交给 `ax-net` route decision 自动选择接口。需要固定接口时使用 `bind_device(InterfaceId)`。 + +```rust +let mgmt = ax_net::interface_by_name("eth0").ok_or(AxError::NoSuchDevice)?; +let sock = ax_net::tcp::TcpSocket::new(); +sock.bind_device(mgmt.id)?; +``` + +## root net namespace + +当前 StarryOS 初期 root net namespace 可以看到所有接口。非 root namespace 侧的完整 Linux net namespace 隔离尚未实现,现有路径主要做可见性过滤,不复制 `ax-net` 的 domain 或 route table。 + +## 边界原则 + +- `ax-net` 统一维护接口、地址、路由、DNS、ARP 和 socket 状态。 +- ArceOS runtime 只做设备和 IRQ 装配。 +- StarryOS 只做 Linux ABI。 +- Axvisor 只表达网络意图。 +- 具体驱动和平台发现不进入 `ax-net`。 diff --git a/docs/docs/architecture/net/overview.md b/docs/docs/architecture/net/overview.md new file mode 100644 index 0000000000..3db998f593 --- /dev/null +++ b/docs/docs/architecture/net/overview.md @@ -0,0 +1,137 @@ +--- +sidebar_position: 1 +sidebar_label: "概览" +--- + +# 网络栈概览 + +TGOSKits 的网络能力收敛在 `net/ax-net`。它是 ArceOS、StarryOS 和 Axvisor 共享的统一网络栈,向上提供 TCP、UDP、raw socket、Unix domain socket、可选 vsock、DNS、DHCP、ARP、接口查询和 readiness/poll 能力,向下通过 `rd-net` / `rdif-eth` 消费 Ethernet 设备。 + +源码位于 [net/ax-net/src/](net/ax-net/src/),入口 [lib.rs](net/ax-net/src/lib.rs)。Socket backend 包括 IP 类([tcp.rs](net/ax-net/src/tcp.rs)、[udp.rs](net/ax-net/src/udp.rs)、[raw.rs](net/ax-net/src/raw.rs),基于 smoltcp)、[unix/](net/ax-net/src/unix/)(自包含 stream/dgram,不经 smoltcp)和可选的 [vsock/](net/ax-net/src/vsock/)(基于 `rdif-vsock` 驱动,含 connection manager 与 ring buffer)。 + +## 能力矩阵 + +| 能力 | 实现方式 | 状态 | +| --- | --- | --- | +| TCP | smoltcp `socket::tcp`,含 keep-alive、Nagle、TCP_INFO、SYN pre-create | 完整 | +| UDP | smoltcp `socket::udp`,含 MSG_MORE corking、端口冲突仲裁 | 完整 | +| Raw IP/ICMP | smoltcp `socket::raw`,含 loopback ICMP echo reply 模拟 | 完整 | +| Unix domain stream | 自包含,`ringbuf` 双向通道 + cmsg 管道 + peer credentials | 完整 | +| Unix domain datagram | 自包含,`async_channel` 无界队列 + cmsg + SO_PASSCRED | 完整 | +| Vsock stream | `rdif-vsock` 驱动 + connection manager + ring buffer stream | 需要 `vsock` feature | +| DHCPv4 client | 内核态 `DhcpState` 状态机,per-interface,启动阻塞等待 | 基础完成 | +| DNS resolver | smoltcp `socket::dns`,自动过滤不可路由 server,5s 超时 | 完整 | +| ARP | `EthernetDevice` 内部 `HashMap` + 300s TTL + ARP-pending 队列 | 完整 | +| 多 NIC 路由 | `RouteTable` 最长前缀匹配 + metric 排序 + per-interface 替换 | 完整 | +| IRQ 感知 | `EthernetIrqRegistrar` + `EthernetIrqAction` + IRQ→wake 转换 | 完整 | +| Loopback | 自包含 `LoopbackDevice`,直接 buffer 回环,不封装 Ethernet 帧 | 完整 | + +## 线程与锁模型 + +`ax-net` 使用多线程模型,但协议核心串行: + +| 线程 | 职责 | 阻塞点 | +| --- | --- | --- | +| `net-poll` worker | 驱动 smoltcp poll、DHCP 状态机、ARP、DNS、TX dispatch | `NET_POLL_WAKE.wait_timeout_until()` | +| `{ifname}-rx` worker | 每网卡一个,从 driver 收包压入 `RouterQueues::rx` 有界队列 | `device.rx_wake.wait()` | +| `{ifname}-tx` worker | 每网卡一个,从 `DeviceHandle::tx_queue` 取包调用 driver send | `device.tx_wake.wait_until()` | +| 调用者线程 | 应用/内核线程调用 socket API | `StateLock::lock()`、`block_on(poll_io())` | + +### 全局锁顺序 + +严格的锁嵌套顺序,防止死锁: + +``` +SERVICE (Mutex) + → SOCKET_SET.inner (Mutex) + → LISTEN_TABLE.tcp[port] (Mutex) + → DeviceHandle.inner (Mutex) + → NET_CONTROL.state (RwLock) +``` + +- `SOCKET_SET.inner` 全局保护 smoltcp `SocketSet`,socket 创建/销毁/访问均需持有。 +- `SERVICE` mutex 保护 smoltcp `Interface` 和 DHCP 状态机,poll 期间独占。 +- `NET_CONTROL.state` 是独立 RwLock,接口查询(只读)可以在不持有 `SERVICE` 的情况下进行。 +- `ListenTable` 条目锁在 `SOCKET_SET` 锁内获取,保证 accept/snoop 的一致性。 +- 设备锁(`DeviceHandle.inner`)在 poll 路径的最内层获取。 + +## 核心设计取舍 + +当前网络栈的核心取舍是:**保持单 `smoltcp::iface::Interface + SocketSet` 协议栈实例**,不拆成多 smoltcp domain。多网口能力通过接口 registry、路由表、每接口配置、设备队列和 `Router` 适配层实现。 + +这一选择的根本原因是 socket 语义的稳定性。多协议栈实例会让 TCP wildcard listen、UDP wildcard bind、端口冲突仲裁、动态路由切换在多个 `SocketSet` 之间重复实现,引入难以测试的竞态。单实例串行 poll 配合设备队列解耦,在保持语义简单的同时获得多网口支持。 + +## 与主流实现对比 + +### 与 Linux 网络栈对比 + +| 维度 | Linux | ax-net | +| --- | --- | --- | +| 协议栈实例 | 每 net namespace 独立协议栈 | 全局单实例,namespace 仅做可见性过滤 | +| poll 模型 | NAPI + 软中断,per-CPU backlog | 单 `net-poll` worker + `request_poll()` 唤醒 | +| 多 NIC | 独立 netdev + per-device NAPI queue | 单 `Router`(smoltcp `Device`)+ per-device 有界 RX/TX queue | +| ARP/邻居发现 | 内核 neighbour table + GC | `EthernetDevice` 内部 `HashMap` + `NEIGHBOR_TTL=300s` | +| DHCP | 用户态 dhclient / systemd-networkd | 内核态 `DhcpState` 状态机,bootstrap 阻塞启动 | +| Socket 缓冲区 | 动态可调 sk_buff 链 | 固定大小 `PacketBuffer`(TX/RX 各 64 KiB) | +| Zero-copy | `MSG_ZEROCOPY` / `io_uring` | 不支持,RX/TX 均有一次 copy | + +### 与 smoltcp 原生使用对比 + +smoltcp 原生使用需要一个 `phy::Device` + 一个 `Interface`。`ax-net` 在此基础上增加了: + +- **多设备路由**:`Router` 实现了 smoltcp 的 `phy::Device` trait,内部管理多个 `DeviceHandle` 和路由表,在 TX 路径解析 IP 包选择出接口。 +- **控制面分离**:`NetControl` 独立持有接口 registry、路由表和 DNS 来源信息,socket 查询不需要持有 `Service` 锁。 +- **设备队列解耦**:RX/TX worker 通过有界队列连接设备 driver 和 `Router` token 模型,避免 poll 直接阻塞在设备上。 +- **DHCP 集成**:内核态 DHCP 状态机在 bootstrap 阶段完成地址获取,而非依赖外部 DHCP client。 +- **TCP SYN 预创建**:RX 路径在交付 smoltcp 前用 `snoop_tcp_packet()` 预创建 listen socket,加速 accept。 + +### 与 lwIP 对比 + +| 维度 | lwIP | ax-net | +| --- | --- | --- | +| 主要场景 | 嵌入式 MCU(RAM < 64 KiB) | 服务器级 unikernel(128 MiB+) | +| 线程模型 | NO_SYS 单线程 / SYS 多线程 | 多线程,per-device worker + net-poll worker | +| socket API | 有限 POSIX 子集 | `SocketOps` trait + `Configurable`,覆盖 SO_\*/TCP_\*/IP_\* | +| 多接口 | 基本不支持 | 原生多 NIC + 路由表 + metric | + +## 设计原则 + +- **单协议栈语义优先**:所有 TCP/UDP/raw socket 共享一个 smoltcp `Interface` 和 `SocketSet`,端口冲突、listen 聚合、wildcard bind 等语义自然正确。 +- **控制面与数据面分离**:接口查询(`interfaces()`、`interface_by_name()`)走只读 `NetControl`,不进入设备锁或 smoltcp poll。 +- **异步 poll 解耦热路径**:socket 操作只调用轻量 `request_poll()` 唤醒 worker,不在调用者上下文同步驱动整个协议栈。 +- **能力边界隔离**:`ax-net` 通过 `EthernetDriver` trait 对接网卡驱动,不直接依赖 FDT、PCI、MMIO、DMA 或平台 IRQ ABI。 +- **Linux ABI 友好**:`InterfaceId` 直接映射 Linux ifindex,`DeviceBinding` 对应 `SO_BINDTODEVICE`,socket option 覆盖主流 `getsockopt`/`setsockopt` 语义。 + +## 当前限制 + +### 协议处理串行 + +单 smoltcp 实例意味着 TCP/UDP 协议状态机在同一 `net-poll` worker 上串行执行。设备队列解耦可以减少收发阻塞,但不能让多核并行处理协议状态机。Linux 的 per-CPU softirq 在这里没有对应物。 + +### 收发路径有拷贝 + +RX worker 从 driver buffer 复制到有界队列中的 packet bytes,TX worker 同样从队列复制到 driver buffer。真正 zero-copy 需要 `rd-net` buffer ownership、packet pool 和 smoltcp token 适配改造。当前每次收发各有一次内存拷贝。 + +### DHCP 租约管理不完整 + +当前重点覆盖 DHCP bootstrap(Discover → Offer → Request → ACK)和 per-interface 状态管理。完整 renew/rebind、租约过期回收和地址冲突检测仍需后续补齐。 + +### IPv6 支持最小 + +多接口能力主要保证 IPv4 正确性。完整 IPv6 地址配置(SLAAC/DHCPv6)、邻居发现、IPv6 route、AAAA DNS 查询和 multicast scope 不在当前范围。 + +### 无 IGMP/MLD + +IPv4 multicast 只保证基础发送选择策略。IGMP/MLD snooping、按接口 membership 和 multicast routing 不在当前范围。 + +### 无完整 net namespace 隔离 + +StarryOS 目前只做初步可见性过滤(root namespace 可见全部接口)。完整 Linux net namespace 需要独立 route table、接口集合和 resolver 策略。 + +### 无动态接口管理 + +动态 link down/up、接口热插拔、队列重建和已存在 socket 的错误传播仍属于后续工作。 + +### 无高性能 dataplane + +RSS、多队列 NIC、per-queue poll、NAPI 类 batch 调度和 zero-copy dataplane 是后续更底层优化,不由当前 `ax-net` 架构自然获得。 diff --git a/docs/docs/architecture/net/testing.md b/docs/docs/architecture/net/testing.md new file mode 100644 index 0000000000..fde5eb79c8 --- /dev/null +++ b/docs/docs/architecture/net/testing.md @@ -0,0 +1,176 @@ +--- +sidebar_position: 8 +sidebar_label: "测试与已知限制" +--- + +# 测试与限制 + +网络栈同时影响 ArceOS、StarryOS 和 Axvisor。修改 `net/ax-net` 时,应按改动范围选择验证集合。 + +## 单元测试 + +推荐至少覆盖: + +- 接口 ID 与 `DeviceBinding` 语义。 +- 多接口 route lookup。 +- longest prefix 优先级。 +- 同前缀 metric 选择。 +- 同 metric 稳定插入顺序。 +- 接口 down / 不可用时跳过 route。 +- default route 查询。 +- bounded RX/TX queue 满时行为。 +- DHCP bootstrap 和 per-interface 状态。 +- TCP/UDP bind 到具体本地地址后保留接口绑定。 +- 未绑定 TCP/UDP connect 按 route decision 选择接口。 + +当前 `ax-net` host 单测可用: + +```bash +cargo test -p ax-net +``` + +`ax-net` host test 需要相关底层 crate 启用 host-test feature,避免链接内核 percpu 路径。 + +### 测试覆盖建议 + +| 模块 | 关键测试点 | 优先级 | +| --- | --- | --- | +| `RouteTable` | 添加/删除/替换规则、排序验证、默认路由查询、`select_route_if` with mock filter | 高 | +| `ListenTable` | port 冲突、backlog 上限、`can_listen`/`listen`/`unlisten`、SYN 队列溢出、`can_accept`/`accept` | 高 | +| `SocketSetWrapper` | UDP bind 冲突仲裁(精确/wildcard/SO_REUSEADDR)、add/remove | 中 | +| `StateLock` | Idle→Busy CAS、transit 成功/失败回退、状态读取一致性 | 中 | +| `GeneralOptions` | nonblock/send_timeout/recv_timeout、device_binding 读写、Atomic 一致性 | 中 | +| `TcpSocket` | `connect()` 成功/失败、`bind_device()` 无效接口、`poll_connect()` 状态转换、`tcp_info_snapshot()` 字段 | 高 | +| `UdpSocket` | `send()` with MSG_MORE corking、connected/disconnected send、`recv()` truncation | 高 | +| `RawSocket` | `send()` ICMP echo→loopback reply、TTL 读写、IP version 校验 | 中 | +| `UnixSocket` | stream pair send/recv、cmsg 传递、datagram pair、abstract namespace bind/connect | 中 | +| `VsockSocket` | bind/listen/connect/accept 状态转换、send/recv | 低(需 `vsock` feature) | +| `DhcpState` | Discovering→Offer→Requesting→ACK→Bound 状态机、NAK→reset、`process_packet` 校验 | 中 | +| DNS | `dns_query_timeout` 超时、不可路由 server 过滤、`DnsSocketGuard` drop | 中 | + +## 接口路由测试 + +建议对 `RouteTable` 进行以下专项测试: + +``` +Test: longest prefix match + Add: 10.0.0.0/8 → dev0, metric=100 + Add: 10.0.2.0/24 → dev1, metric=200 + Query 10.0.0.1 → dev0 (只有 /8 匹配) + Query 10.0.2.15 → dev1 (/24 比 /8 更长) + +Test: metric tie-breaking + Add: 0.0.0.0/0 → dev0, metric=100 + Add: 0.0.0.0/0 → dev1, metric=50 + Query 8.8.8.8 → dev1 (metric 更低) + +Test: insert order stability (same prefix, same metric) + Add rule_a: metric=100, order=0 + Add rule_b: metric=100, order=1 + rule_a 排在 rule_b 前面 + +Test: unavailable interface skip + Add: 0.0.0.0/0 → dev0, metric=100 + Add: 0.0.0.0/0 → dev1, metric=200 + dev0 is not UP → select_route_if with is_usable → returns dev1 + +Test: replace_ipv4_rules_for_interface + Add rules for interface=2 + Call replace with new rules → old rules removed, new rules in place +``` + +## DHCP 测试 + +DHCP 逻辑建议覆盖: + +``` +Test: Discovering → Offer → Requesting transition + 创建 DhcpState(phase=Discovering) + 构造 DHCPOFFER packet (unicast yiaddr, correct xid, correct mac) + process_packet → phase=Requesting, offered_address=yiaddr + 验证未产生 DhcpEvent + +Test: Requesting → ACK → Bound transition + 创建 DhcpState(phase=Requesting, offered_address=addr) + 构造 DHCPACK (subnet_mask 255.255.255.0, yiaddr=addr, router=x, dns=[y]) + process_packet → phase=Bound, 返回 DhcpEvent::Configured + +Test: NAK → reset + 创建 DhcpState(phase=Bound, address=some_addr) + process_packet(DHCPNAK) → phase=Discovering, 返回 DhcpEvent::Deconfigured + +Test: packet filtering + DhcpState for eth0 (interface_id=2) 忽略 interface_id=3 的包 + 错误 xid 或 mac 的包被忽略 +``` + +## TCP 测试 + +``` +Test: SYN pre-create + ListenTable.listen(port=8080, backlog=10) + 调用 incoming_tcp_packet(SYN, dst_port=8080) + → SocketSet 中有新 socket,ListenTableEntryInner.syn_queue.len() == 1 + +Test: SYN queue overflow + backlog=2, 压入 2 个 SYN, 第 3 个 SYN → 丢弃 (warn) + +Test: accept existing connection + 压入 2 个 PendingTcp, 第一个 ESTABLISHED, 第二个 SYN_RECEIVED + accept() → 返回第一个 + +Test: accept skip closed + 第一个 CLOSED without data, 第二个 ESTABLISHED + accept() → 跳过第一个(移除), 返回第二个 + +Test: bind_device + TcpSocket::new().bind_device(nonexistent_id) → NoSuchDevice + TcpSocket::new().bind_device(valid_id) → Ok + +Test: SO_REUSEADDR + 两个 TcpSocket 都设 SO_REUSEADDR → 都可以 bind 同一端口 +``` + +## 已知 Debug 定位指南 + +### "no route to destination" + +可能原因: + +1. DHCP 未完成 — 检查 `dhcp_configured()` 返回值。 +2. 默认路由未提交 — 检查 `default_routes()` 是否非空。 +3. 接口未 UP — 检查 `interface_by_id(id).flags` 包含 `UP`。 +4. `next_poll_delay()` 使用 wall clock 而非 monotonic clock — 可能导致 poll 延迟。 +5. RX worker 沉睡 — `device.rx_wake` 未被通知。 + +检查方法: + +```rust +// 在出现错误前插入 +info!("routes: {:?}", ax_net::default_routes()); +info!("interfaces: {:?}", ax_net::interfaces()); +info!("arp entries: {:?}", ax_net::arp_entries()); +``` + +### "address already in use" (UDP) + +1. 检查是否有其他 socket 绑定了同一 `(addr, port)`。 +2. 检查 wildcard `(0.0.0.0, port)` 是否已存在。 +3. 检查 `SO_REUSEADDR` 是否正确设置。 + +### DHCP 超时 + +1. `DHCP_BOOTSTRAP_ATTEMPTS = 200`,每次 interval 10ms → 总超时 ≈ 2 秒。 +2. 检查 DHCP server 是否可达。 +3. 检查 `process_packet()` 中的 xid/mac 校验是否误过滤。 + +### ARP pending 队列溢出 + +`ETHERNET_MAX_PENDING_PACKETS = 128`,当大量并发连接的目标 MAC 都未解析时触发。增加 `ETHERNET_MAX_PENDING_PACKETS` 或确保 ARP reply 在预期时间内到达。 + +### net-poll worker 不推进 + +1. 检查 `NET_POLL_REQUESTED` atomic 标志。 +2. 检查 `NET_POLL_WAKE` 是否有竞争。 +3. 检查 `poll_until_idle()` 中的 CAS 锁是否一直失败。 +4. 检查 `next_poll_delay()` 返回值 — 如果 smoltcp `poll_at()` 返回 `None`,idle poll interval 为 100ms。 diff --git a/docs/docs/architecture/overview.md b/docs/docs/architecture/overview.md index 888b9aee8b..af6e86da7e 100644 --- a/docs/docs/architecture/overview.md +++ b/docs/docs/architecture/overview.md @@ -102,6 +102,12 @@ Axvisor 是基于 ArceOS 的统一组件化 Type-I Hypervisor,建立在 ArceOS → 详细设计见 [rdrive + rdif 驱动框架](./rdrive-rdif) +## 网络栈架构 + +网络栈能力收敛在 `net/ax-net`,对上提供 TCP、UDP、raw socket、DNS、DHCP、ARP、poll/waker 等统一 API,对下通过 `rd-net` 设备适配真实网卡。多网口方案保持单 `smoltcp::iface::Interface + SocketSet` 协议栈模型,通过接口 registry、路由表、设备队列和 net-poll worker 管理多个接口。 + +→ 详细设计见 [网络栈架构](./net/overview) + ## 核心层次 TGOSKits 按职责将 crate 组织为六个核心层次和一个辅助层,每一层都面向明确的职责边界。上层依赖下层,但下层不感知上层——这一原则使得同一套组件可以同时服务于多个系统。 From 07393920a19a3f956f60d435abbc573d1d62d04a Mon Sep 17 00:00:00 2001 From: ZCShou <72115@163.com> Date: Fri, 12 Jun 2026 17:10:29 +0800 Subject: [PATCH 06/31] feat(ax-net): add loopback interface send/receive support --- net/ax-net/src/router.rs | 54 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/net/ax-net/src/router.rs b/net/ax-net/src/router.rs index 33f19240de..9cba4449ac 100644 --- a/net/ax-net/src/router.rs +++ b/net/ax-net/src/router.rs @@ -433,10 +433,11 @@ impl Router { pub fn poll( &mut self, - _timestamp: Instant, + timestamp: Instant, sockets: &mut SocketSet<'_>, mut snoop: impl FnMut(InterfaceId, &[u8]), ) { + self.poll_loopback(timestamp, sockets, &mut snoop); while !self.rx_buffer.is_full() { let Some(packet) = self.queues.rx.pop() else { break; @@ -454,6 +455,35 @@ impl Router { } } + fn poll_loopback( + &mut self, + timestamp: Instant, + sockets: &mut SocketSet<'_>, + snoop: &mut dyn FnMut(InterfaceId, &[u8]), + ) { + while !self.rx_buffer.is_full() { + let mut received = false; + for device in &self.devices { + if device.interface_id != InterfaceId::LOOPBACK { + continue; + } + let mut loopback_snoop = |packet: &[u8]| { + snoop_tcp_packet(packet, sockets); + snoop(InterfaceId::LOOPBACK, packet); + }; + received |= device.inner.lock().recv( + device.interface_id, + &mut self.rx_buffer, + timestamp, + &mut loopback_snoop, + ); + } + if !received { + break; + } + } + } + pub fn send_on_device( &mut self, dev: usize, @@ -461,7 +491,12 @@ impl Router { packet: &[u8], timestamp: Instant, ) -> bool { - let _ = timestamp; + if self.devices[dev].interface_id == InterfaceId::LOOPBACK { + return self.devices[dev] + .inner + .lock() + .send(next_hop, packet, timestamp); + } self.devices[dev].enqueue_tx(next_hop, packet) } @@ -524,7 +559,8 @@ impl Router { }; let dev = &self.devices[route.dev]; - poll_next |= dev.enqueue_tx(route.next_hop, packet.into_inner()); + poll_next |= + Self::dispatch_packet(dev, route.next_hop, packet.into_inner()); } } IpVersion::Ipv6 => { @@ -547,13 +583,22 @@ impl Router { }; let dev = &self.devices[route.dev]; - poll_next |= dev.enqueue_tx(route.next_hop, packet.into_inner()); + poll_next |= + Self::dispatch_packet(dev, route.next_hop, packet.into_inner()); } } } } poll_next } + + fn dispatch_packet(device: &DeviceHandle, next_hop: IpAddress, packet: &[u8]) -> bool { + if device.interface_id == InterfaceId::LOOPBACK { + device.inner.lock().send(next_hop, packet, now()) + } else { + device.enqueue_tx(next_hop, packet) + } + } } fn device_tx_worker(device: Arc) { @@ -601,6 +646,7 @@ fn device_rx_worker(device: Arc) { } if !received { + device.inner.lock().register_waker(&device.rx_waker); device.rx_wake.wait(); } } From f1cd506436e3d2c3a8390424a73ce2312d789815 Mon Sep 17 00:00:00 2001 From: ZCShou <72115@163.com> Date: Fri, 12 Jun 2026 17:23:43 +0800 Subject: [PATCH 07/31] docs(net): add protocol stack mapping and expand architecture details --- docs/docs/architecture/net/architecture.md | 35 +++++- docs/docs/architecture/net/flows.md | 16 +++ docs/docs/architecture/net/overview.md | 3 +- docs/docs/architecture/net/protocol.md | 136 +++++++++++++++++++++ 4 files changed, 186 insertions(+), 4 deletions(-) create mode 100644 docs/docs/architecture/net/protocol.md diff --git a/docs/docs/architecture/net/architecture.md b/docs/docs/architecture/net/architecture.md index a8bb19473f..fa8844c932 100644 --- a/docs/docs/architecture/net/architecture.md +++ b/docs/docs/architecture/net/architecture.md @@ -1,5 +1,5 @@ --- -sidebar_position: 2 +sidebar_position: 3 sidebar_label: "架构设计" --- @@ -173,7 +173,17 @@ pub fn select_route_if(&self, dst, mut is_usable: impl FnMut(InterfaceId) -> boo } ``` -## SocketSet 包装层 +`RouteTable` 还提供 `select_route_for_source(dst, src)`([router.rs#L262](net/ax-net/src/router.rs#L262)),用于在 TX dispatch 时同时匹配目的地址和源地址。例如,多宿主(multi-homed)主机有两个接口分别有不同源 IP,发送到同一目的地的包需根据 smoltcp 注入的源地址选择正确的出接口。 + +### 路由排序策略 + +`sort_rules()` 的三级排序([router.rs#L235-L240](net/ax-net/src/router.rs#L235-L240)): + +1. **最长前缀优先**:`b.filter.prefix_len().cmp(&a.filter.prefix_len())` — 更长的前缀排在前面 +2. **低 metric 优先**:同前缀长度时,`a.metric.cmp(&b.metric)` — metric 更小的优先 +3. **插入顺序稳定**:同前缀同 metric 时,`a.order.cmp(&b.order)` — order 由 `next_order` 自增产生 + +这使得 `select_route_if` 使用 `find()` 返回第一个匹配项(即最高优先级的路由)。 `SocketSetWrapper`([wrapper.rs](net/ax-net/src/wrapper.rs))在 smoltcp 原生 `SocketSet` 之上增加了 UDP 端口冲突仲裁。原生 smoltcp 允许多个 UDP socket bind 同一端口(由上层保证),`ax-net` 在此层实现 `SO_REUSEADDR` 语义: @@ -191,7 +201,26 @@ UDP bind 仲裁规则([wrapper.rs#L59-L71](net/ax-net/src/wrapper.rs#L59-L71) - **Wildcard bind**(`0.0.0.0:53`):如果任何地址已占用同一端口则拒绝。 - **`SO_REUSEADDR`**:设置后跳过仲裁。 -`new_socket` Event 用于通知 `accept()` 阻塞等待和 poll 就绪检测:当 TCP SYN pre-create 或 DNS socket 创建时,`add()` 方法调用 `new_socket.notify(1)` 唤醒等待者。 +`add()` 方法调用 `self.new_socket.notify(1)`,用于在 smoltcp 内部唤醒等待 `SocketSet` 变更的轮询者(smoltcp `wait`/`wake` 机制通过此 Event 传递)。 + +## TCP 端口绑定仲裁 + +除了 `ListenTable`(管理 `listen()` 端口),`ax-net` 还有独立的 `TCP_BOUND_PORTS`([tcp.rs](net/ax-net/src/tcp.rs))来追踪已 bind 但尚未 listen 的端口: + +```rust +static TCP_BOUND_PORTS: LazyLock>>>> = + LazyLock::new(|| Mutex::new(HashMap::new())); + +fn register_tcp_bound(endpoint: IpListenEndpoint) -> AxResult; +fn unregister_tcp_bound(endpoint: IpListenEndpoint); +``` + +两个系统协同工作: +- `LISTEN_TABLE`:管理已进入 `listen()` 状态的端口及 SYN 队列,防止同一端口被多次 listen。 +- `TCP_BOUND_PORTS`:追踪已 bind(可能尚未 listen)的端口和地址,防止地址冲突的重复 bind。`register_tcp_bound()` 在 `bind()`、`listen()`、`connect()` 中被调用;`unregister_tcp_bound()` 在 `shutdown()`、`Drop` 和错误回退中被调用。 +- `tcp_port_available(port)`:同时检查两个表,确保 `get_ephemeral_port()` 不会分配到已被 bind 或已在 listen 的端口。 + +`listen_addrs_conflict(a, b)` 判断两个 `IpListenEndpoint` 是否冲突:任一为 `None`(wildcard)即冲突,或两者值相等即冲突。 ## Socket 状态机 diff --git a/docs/docs/architecture/net/flows.md b/docs/docs/architecture/net/flows.md index ce1301553d..0749939d09 100644 --- a/docs/docs/architecture/net/flows.md +++ b/docs/docs/architecture/net/flows.md @@ -984,6 +984,22 @@ Connection Manager([vsock/connection_manager.rs](net/ax-net/src/vsock/connecti Vsock stream 的 send/recv 直接从 `Connection` 的 ring buffer 读写数据。 +### 11.1 Driver 事件处理(vsock-poll worker) + +`vsock-poll` 是独立的 vsock 轮询线程([device/vsock.rs](net/ax-net/src/device/vsock.rs)),引用计数管理(`start_vsock_poll`/`stop_vsock_poll`),自适应轮询频率(100μs → 500μs → 2ms → 10ms 的四级退避)。 + +`handle_vsock_event()` 将 driver 的 4 类事件分派给 `VSOCK_CONN_MANAGER`: + +| 事件 | handler | 动作 | +| --- | --- | --- | +| `VsockEvent::ConnectionRequest` | `on_connection_request()` | 查 `listen_queues` → 创建新 `Connection`(state=Connected) → 推入 `AcceptQueue` → `wake()` 唤醒 accept 等待者 | +| `VsockEvent::Received` | `on_data_received()` | 查 `connections` → `push_rx_data()` 写入 RX ring buffer → `wake_rx()` 唤醒 recv 等待者 | +| `VsockEvent::Disconnected` | `on_disconnected()` | 查 `connections` → state=Closed, rx_closed=true, tx_closed=true → `wake_rx()` 通知 recv 端 EOF | +| `VsockEvent::Connected` | `on_connected()` | 查 `connections` → state=Connected → `wake_connect()` 唤醒 connect 等待者 | +| `VsockEvent::CreditUpdate` | `on_credit_update()` | 查 `connections` → `tx_wait_queue_notify()` 唤醒因 buffer 空间不足而等待的 send 调用 | + +`VsockEvent::Received` 的特殊处理:如果 `Connection` RX buffer 已满(`rx_buffer_free() == 0`),事件重新推入 `PENDING_EVENTS` 全局队列,下次 poll 再尝试,避免数据丢失。 + --- ## 12. ICMP Loopback Echo 流程 diff --git a/docs/docs/architecture/net/overview.md b/docs/docs/architecture/net/overview.md index 3db998f593..447dc78ccd 100644 --- a/docs/docs/architecture/net/overview.md +++ b/docs/docs/architecture/net/overview.md @@ -21,7 +21,7 @@ TGOSKits 的网络能力收敛在 `net/ax-net`。它是 ArceOS、StarryOS 和 Ax | Vsock stream | `rdif-vsock` 驱动 + connection manager + ring buffer stream | 需要 `vsock` feature | | DHCPv4 client | 内核态 `DhcpState` 状态机,per-interface,启动阻塞等待 | 基础完成 | | DNS resolver | smoltcp `socket::dns`,自动过滤不可路由 server,5s 超时 | 完整 | -| ARP | `EthernetDevice` 内部 `HashMap` + 300s TTL + ARP-pending 队列 | 完整 | +| ARP | `EthernetDevice` 内部 `neighbors: HashMap`(已解析条目, 300s TTL)+ `pending_neighbors: HashMap`(等待 ARP reply 条目, 1s 重试) + `pending_packets: PacketBuffer`(暂存待 ARP 解析后发送的包) | 完整 | | 多 NIC 路由 | `RouteTable` 最长前缀匹配 + metric 排序 + per-interface 替换 | 完整 | | IRQ 感知 | `EthernetIrqRegistrar` + `EthernetIrqAction` + IRQ→wake 转换 | 完整 | | Loopback | 自包含 `LoopbackDevice`,直接 buffer 回环,不封装 Ethernet 帧 | 完整 | @@ -36,6 +36,7 @@ TGOSKits 的网络能力收敛在 `net/ax-net`。它是 ArceOS、StarryOS 和 Ax | `{ifname}-rx` worker | 每网卡一个,从 driver 收包压入 `RouterQueues::rx` 有界队列 | `device.rx_wake.wait()` | | `{ifname}-tx` worker | 每网卡一个,从 `DeviceHandle::tx_queue` 取包调用 driver send | `device.tx_wake.wait_until()` | | 调用者线程 | 应用/内核线程调用 socket API | `StateLock::lock()`、`block_on(poll_io())` | +| `vsock-poll` worker | vsock 设备轮询,事件分发到 `VSOCK_CONN_MANAGER` | 自适应频率 sleep(100μs→10ms) | ### 全局锁顺序 diff --git a/docs/docs/architecture/net/protocol.md b/docs/docs/architecture/net/protocol.md new file mode 100644 index 0000000000..16c3fdeb48 --- /dev/null +++ b/docs/docs/architecture/net/protocol.md @@ -0,0 +1,136 @@ +--- +sidebar_position: 2 +sidebar_label: "协议栈映射" +--- + +# 协议栈映射 + +`ax-net` 实现了标准 TCP/IP 四层模型的完整协议栈,以下说明各层在代码中的具体承担者、封装格式和处理路径。 + +```mermaid +flowchart TB + subgraph App["应用层 (Application)"] + direction LR + Starry["starry-kernel:\nsyscall socket API"] + Arce["ax-api / ax-posix-api:\nPOSIX socket"] + Axv["Axvisor:\n结构化配置"] + end + + subgraph Transport["传输层 (Transport)"] + direction LR + Tcp["TcpSocket\n(tcp.rs)"] + Udp["UdpSocket\n(udp.rs)"] + Raw["RawSocket\n(raw.rs)"] + Unix["UnixSocket\n(unix/)"] + Vsock["VsockSocket\n(vsock/)"] + end + + subgraph Internet["网络层 (Internet)"] + direction LR + SmolIface["smoltcp Interface\n(service.rs)"] + Route["Router / RouteTable\n(router.rs)"] + IpProto["IP / ICMP / DHCP"] + end + + subgraph Link["链路层 (Link)"] + direction LR + EthDev["EthernetDevice\n(device/ethernet.rs)"] + LoopDev["LoopbackDevice\n(device/loopback.rs)"] + RdNetDrv["RdNetDriver\n(device/driver.rs)"] + Hardware["rd-net → 物理网卡"] + end + + App --> Transport + Transport --> Internet + Internet --> Link + SmolIface --> Route + Route --> EthDev + Route --> LoopDev + EthDev --> RdNetDrv + RdNetDrv --> Hardware +``` + +## 各层职责与封装格式 + +| TCP/IP 层 | ax-net 组件 | 封装格式 | 职责 | +| --- | --- | --- | --- | +| **应用层** | `Socket` enum + `SocketOps` trait([socket.rs](net/ax-net/src/socket.rs)) | 用户数据(字节流/数据报) | 暴露 TCP/UDP/raw/unix/vsock socket API,管理 socket options、超时、非阻塞、poll readiness | +| **传输层** | smoltcp `socket::tcp` / `socket::udp` / `socket::raw` | TCP segment / UDP datagram | 连接管理(三次握手/四次挥手)、可靠传输(重传/ACK/窗口)、端口复用、corking | +| **网络层** | smoltcp `Interface` + `Router`([router.rs](net/ax-net/src/router.rs)) + `RouteTable` | IP packet(IPv4 header + payload) | IP 路由决策、分片/重组、ICMP 处理、ARP 缓存、DHCP 状态机、IP 地址管理 | +| **链路层** | `EthernetDevice`([ethernet.rs](net/ax-net/src/device/ethernet.rs)) + `LoopbackDevice` + `RdNetDriver` | Ethernet frame(MAC header + ethertype + payload) | MAC 编址、ARP 封帧与解帧、IRQ 处理、邻居表(300s TTL)、pending packet 队列 | + +## 数据封装沿各层的具体转换 + +以 **TCP 发送** 为例,数据从应用到物理网卡经过以下封装步骤: + +``` +应用层: 用户调用 send(data) → TcpSocket::send() + │ + ▼ 写入 smoltcp socket TX buffer (TCP_TX_BUF_LEN=64KiB) +传输层: smoltcp TCP 状态机构造 TCP segment + │ [src_port, dst_port, seq, ack, flags, window, payload] + ▼ smoltcp 通过 Router::transmit() 输出 +网络层: smoltcp 添加 IPv4 header(或 IPv6) + │ [version, ihl, tos, total_len, id, flags, ttl, proto=TCP, + │ checksum, src_ip, dst_ip, options..., payload=TCP segment] + │ IP packet 写入 Router.tx_buffer + ▼ Router::dispatch() 查路由表选择出接口 +链路层: EthernetDevice::send() 查 ARP 表获取 dst_mac + │ 添加 Ethernet header + │ [dst_mac(6B), src_mac(6B), ethertype=0x0800(IPv4), payload=IP packet] + ▼ EthernetDriver::transmit() +物理层: rd-net 驱动将帧写入硬件 TX ring → DMA 发送 +``` + +以 **TCP 接收** 为例,数据从物理网卡到应用的解封装: + +``` +物理层: 硬件 DMA → RX ring → 中断/轮询 + ▼ EthernetDriver::receive() +链路层: EthernetDevice::recv() 解析 EthernetFrame + │ 提取 dst_mac, src_mac, ethertype + │ ethertype=0x0806(ARP) → 更新 neighbors 表 + │ ethertype=0x0800(IPv4) → 提取 IP payload + ▼ 写入 smoltcp PacketBuffer +网络层: smoltcp Interface::poll() 从 Router::receive() 取 IP packet + │ 解析 IPv4 header → 提取 protocol=TCP, src_ip, dst_ip + │ protocol=ICMP → raw socket + ▼ 交付给对应的 socket +传输层: smoltcp TCP 状态机处理 TCP segment + │ 校验 seq/ack, 更新窗口, 提取 payload + │ payload 写入 socket RX buffer (TCP_RX_BUF_LEN=64KiB) + ▼ socket readiness 更新 → waker 唤醒 recv() 调用者 +应用层: 用户调用 recv(data) → TcpSocket::recv() → 从 smoltcp RX buffer 读取 +``` + +## 协议穿越各层的路由决策点 + +| 操作 | 发生层 | 决策方式 | +| --- | --- | --- | +| **端口选择** (ephemeral port) | 传输层 | `bind(port=0)` → 从 49152 递增尝试 | +| **路由决策** (出接口选择) | 网络层 | smoltcp 构造 IP 包时注入 src_addr → `dispatch()` 用 `select_route_for_source(dst, src)` 查 `RouteTable` → 选接口 | +| **ARP 解析** (IP→MAC) | 网络层↔链路层 | `EthernetDevice::send()` 查 `neighbors: HashMap` → 未命中则广播 ARP request | +| **DHCP** | 网络层↔链路层 | `DhcpState` 在 `Service::poll()` 中独立于 smoltcp 运行,直接操作 `Router::send_on_device()` | +| **DNS** | 应用层 | `dns_query()` 创建临时 smoltcp `dns::Socket`,通过 smoltcp UDP 发送 DNS 查询 | + +## smoltcp 承担的协议与 ax-net 补充的部分 + +| 协议 | smoltcp 原生支持 | ax-net 补充 | +| --- | --- | --- | +| **IPv4** | 包头构造、分片、校验和 | 多接口地址管理、静态/DHCP 地址分配、`InterfaceId` 元数据 | +| **IPv6** | 基本包头、多播 | IPv6→IPv4 映射(v4-mapped)、`IPV6_V6ONLY` socket option | +| **TCP** | 状态机、重传、窗口、keep-alive | `ListenTable` 端口槽位表、SYN pre-create、`TcpInfo` 快照、`SO_BINDTODEVICE` | +| **UDP** | 数据报、端口 | 端口冲突仲裁(`SocketSetWrapper::udp_binds`)、MSG_MORE corking | +| **ICMP** | 基础 | Loopback ICMP echo reply 模拟 | +| **ARP** | 无 | `EthernetDevice::neighbors` 表、TTL 300s、ARP-pending 队列 drain | +| **DHCP** | 无 | 完整 Discover→Offer→Request→ACK 状态机、per-interface `DhcpState`、指数退避 | +| **DNS** | `dns::Socket`、A 查询 | server 路由过滤、超时管理、`DnsSocketGuard` | +| **路由** | 无 | `RouteTable` 最长前缀匹配、metric 排序、`select_route_if` 接口可用性过滤 | + +## 非 TCP/IP 协议路径 + +| 协议 | 承载方式 | 数据路径 | +| --- | --- | --- | +| **Unix domain stream** | `ringbuf::HeapRb` 双向 buffer | 不经过 smoltcp,直接内存拷贝,含 cmsg 管道 | +| **Unix domain datagram** | `async_channel::unbounded()` | 不经过 smoltcp,无界队列 + Packet 结构体(含 data + cmsg + sender addr) | +| **Vsock stream** | `Connection` 的 ring buffer | 经过 `rdif-vsock` 驱动,不经过 smoltcp IP 层 | From 7df66c72f714e633e1ddb0f3c613ac48013add06 Mon Sep 17 00:00:00 2001 From: ZCShou <72115@163.com> Date: Mon, 15 Jun 2026 08:26:28 +0800 Subject: [PATCH 08/31] fix(ax-net): improve socket readiness, loopback routing, and raw socket peer filtering --- net/ax-net/src/lib.rs | 9 +++- net/ax-net/src/raw.rs | 89 ++++++++++++++++++++++++++++++--------- net/ax-net/src/router.rs | 62 +++------------------------ net/ax-net/src/service.rs | 7 +-- net/ax-net/src/tcp.rs | 11 ++++- net/ax-net/src/udp.rs | 13 ++++-- 6 files changed, 106 insertions(+), 85 deletions(-) diff --git a/net/ax-net/src/lib.rs b/net/ax-net/src/lib.rs index 5a84b0e261..b9f2d103f8 100644 --- a/net/ax-net/src/lib.rs +++ b/net/ax-net/src/lib.rs @@ -385,7 +385,10 @@ fn poll_until_idle() { } while POLL_AGAIN.swap(false, Ordering::AcqRel) { - while poll_once() {} + while poll_once() { + #[cfg(not(test))] + ax_task::yield_now(); + } } POLLING_INTERFACES.store(false, Ordering::Release); if !POLL_AGAIN.load(Ordering::Acquire) { @@ -402,6 +405,10 @@ pub fn poll_interfaces() { request_poll(); } +pub(crate) fn poll_interfaces_now() { + poll_until_idle(); +} + /// Request network polling. /// /// This is the lightweight entry used by socket and device paths. diff --git a/net/ax-net/src/raw.rs b/net/ax-net/src/raw.rs index 4c34ed2f17..77d6843611 100644 --- a/net/ax-net/src/raw.rs +++ b/net/ax-net/src/raw.rs @@ -31,7 +31,7 @@ use crate::{ general::GeneralOptions, get_control, interface_by_id, options::{Configurable, GetSocketOption, SetSocketOption}, - request_poll, + poll_interfaces_now, request_poll, }; pub(crate) fn new_raw_socket( @@ -53,6 +53,7 @@ pub struct RawSocket { local_addr: RwLock>, peer_addr: RwLock>, loopback_rx: Mutex)>>, + deferred_rx: Mutex)>>, ttl: RwLock>, rx_closed: AtomicBool, tx_closed: AtomicBool, @@ -71,6 +72,7 @@ impl RawSocket { local_addr: RwLock::new(None), peer_addr: RwLock::new(None), loopback_rx: Mutex::new(None), + deferred_rx: Mutex::new(None), ttl: RwLock::new(None), rx_closed: AtomicBool::new(false), tx_closed: AtomicBool::new(false), @@ -135,6 +137,10 @@ impl RawSocket { } } } + + fn source_matches_peer(&self, source: IpAddress) -> bool { + self.peer_addr.read().is_none_or(|peer| source == peer) + } } fn is_loopback_address(addr: IpAddress) -> bool { @@ -266,7 +272,7 @@ impl SocketOps for RawSocket { self.general.send_poller_with(self, extra_nb, || { request_poll(); - self.with_smol_socket(|socket| { + let written = self.with_smol_socket(|socket| { if !socket.can_send() { return Err(AxError::WouldBlock); } @@ -361,7 +367,9 @@ impl SocketOps for RawSocket { *self.loopback_rx.lock() = Some((local, reply)); } Ok(written) - }) + })?; + poll_interfaces_now(); + Ok(written) }) } @@ -377,24 +385,45 @@ impl SocketOps for RawSocket { self.with_smol_socket(|socket| { loop { if let Some((source, packet)) = if options.flags.contains(RecvFlags::PEEK) { + self.deferred_rx.lock().clone() + } else { + self.deferred_rx.lock().take() + } { + if !self.source_matches_peer(source) { + *self.deferred_rx.lock() = Some((source, packet)); + return Err(AxError::WouldBlock); + } + + if let Some(from) = options.from.as_deref_mut() { + *from = SocketAddrEx::Ip(SocketAddr::new(source.into(), 0)); + } + + let written = dst.write(&packet)?; + return Ok(if options.flags.contains(RecvFlags::TRUNCATE) { + packet.len() + } else { + written + }); + } + + let loopback_packet = if options.flags.contains(RecvFlags::PEEK) { self.loopback_rx.lock().clone() } else { self.loopback_rx.lock().take() - } { - let peer_mismatch = - matches!(*self.peer_addr.read(), Some(peer) if source != peer); - if !peer_mismatch { - if let Some(from) = options.from.as_deref_mut() { - *from = SocketAddrEx::Ip(SocketAddr::new(source.into(), 0)); - } - - let written = dst.write(&packet)?; - return Ok(if options.flags.contains(RecvFlags::TRUNCATE) { - packet.len() - } else { - written - }); + }; + if let Some((source, packet)) = loopback_packet + && self.source_matches_peer(source) + { + if let Some(from) = options.from.as_deref_mut() { + *from = SocketAddrEx::Ip(SocketAddr::new(source.into(), 0)); } + + let written = dst.write(&packet)?; + return Ok(if options.flags.contains(RecvFlags::TRUNCATE) { + packet.len() + } else { + written + }); } let packet = if options.flags.contains(RecvFlags::PEEK) { @@ -403,6 +432,7 @@ impl SocketOps for RawSocket { if let Some(peer) = *self.peer_addr.read() && source != peer { + *self.deferred_rx.lock() = Some((source, packet.to_vec())); return Err(AxError::WouldBlock); } packet @@ -411,9 +441,8 @@ impl SocketOps for RawSocket { }; let (source, packet) = self.parse_ip_packet(packet)?; - if let Some(peer) = *self.peer_addr.read() - && source != peer - { + if !self.source_matches_peer(source) { + *self.deferred_rx.lock() = Some((source, packet.to_vec())); continue; } @@ -473,12 +502,30 @@ impl Pollable for RawSocket { }); events.set( IoEvents::IN, - events.contains(IoEvents::IN) || self.loopback_rx.lock().is_some(), + events.contains(IoEvents::IN) + || self + .loopback_rx + .lock() + .as_ref() + .is_some_and(|(source, _)| self.source_matches_peer(*source)) + || self + .deferred_rx + .lock() + .as_ref() + .is_some_and(|(source, _)| self.source_matches_peer(*source)), ); events } fn register(&self, context: &mut Context<'_>, events: IoEvents) { + self.with_smol_socket(|socket| { + if events.contains(IoEvents::IN) { + socket.register_recv_waker(context.waker()); + } + if events.contains(IoEvents::OUT) { + socket.register_send_waker(context.waker()); + } + }); if events.intersects(IoEvents::IN | IoEvents::OUT) { self.general.register_waker(context.waker()); } diff --git a/net/ax-net/src/router.rs b/net/ax-net/src/router.rs index 9cba4449ac..0eb238f15b 100644 --- a/net/ax-net/src/router.rs +++ b/net/ax-net/src/router.rs @@ -78,6 +78,8 @@ impl Rule { } type PacketBuffer = smoltcp::storage::PacketBuffer<'static, InterfaceId>; +// TX metadata is created before route lookup; dispatch() selects the real +// egress interface from the packet destination and route table. const TX_INTERFACE_PLACEHOLDER: InterfaceId = InterfaceId::new(0); struct BoundedPacketQueue { @@ -363,9 +365,6 @@ impl Router { pub fn start_tx_workers(&self) { for device in &self.devices { - if device.interface_id == InterfaceId::LOOPBACK { - continue; - } let device = device.clone(); let name = format!("{}-tx", device.name); ax_task::spawn_with_name(move || device_tx_worker(device), name); @@ -374,9 +373,6 @@ impl Router { pub fn start_rx_workers(&self) { for device in &self.devices { - if device.interface_id == InterfaceId::LOOPBACK { - continue; - } let device = device.clone(); let name = format!("{}-rx", device.name); ax_task::spawn_with_name(move || device_rx_worker(device), name); @@ -433,11 +429,10 @@ impl Router { pub fn poll( &mut self, - timestamp: Instant, + _timestamp: Instant, sockets: &mut SocketSet<'_>, mut snoop: impl FnMut(InterfaceId, &[u8]), ) { - self.poll_loopback(timestamp, sockets, &mut snoop); while !self.rx_buffer.is_full() { let Some(packet) = self.queues.rx.pop() else { break; @@ -455,48 +450,13 @@ impl Router { } } - fn poll_loopback( - &mut self, - timestamp: Instant, - sockets: &mut SocketSet<'_>, - snoop: &mut dyn FnMut(InterfaceId, &[u8]), - ) { - while !self.rx_buffer.is_full() { - let mut received = false; - for device in &self.devices { - if device.interface_id != InterfaceId::LOOPBACK { - continue; - } - let mut loopback_snoop = |packet: &[u8]| { - snoop_tcp_packet(packet, sockets); - snoop(InterfaceId::LOOPBACK, packet); - }; - received |= device.inner.lock().recv( - device.interface_id, - &mut self.rx_buffer, - timestamp, - &mut loopback_snoop, - ); - } - if !received { - break; - } - } - } - pub fn send_on_device( &mut self, dev: usize, next_hop: IpAddress, packet: &[u8], - timestamp: Instant, + _timestamp: Instant, ) -> bool { - if self.devices[dev].interface_id == InterfaceId::LOOPBACK { - return self.devices[dev] - .inner - .lock() - .send(next_hop, packet, timestamp); - } self.devices[dev].enqueue_tx(next_hop, packet) } @@ -559,8 +519,7 @@ impl Router { }; let dev = &self.devices[route.dev]; - poll_next |= - Self::dispatch_packet(dev, route.next_hop, packet.into_inner()); + poll_next |= dev.enqueue_tx(route.next_hop, packet.into_inner()); } } IpVersion::Ipv6 => { @@ -583,22 +542,13 @@ impl Router { }; let dev = &self.devices[route.dev]; - poll_next |= - Self::dispatch_packet(dev, route.next_hop, packet.into_inner()); + poll_next |= dev.enqueue_tx(route.next_hop, packet.into_inner()); } } } } poll_next } - - fn dispatch_packet(device: &DeviceHandle, next_hop: IpAddress, packet: &[u8]) -> bool { - if device.interface_id == InterfaceId::LOOPBACK { - device.inner.lock().send(next_hop, packet, now()) - } else { - device.enqueue_tx(next_hop, packet) - } - } } fn device_tx_worker(device: Arc) { diff --git a/net/ax-net/src/service.rs b/net/ax-net/src/service.rs index 7ffe9df567..dd5769b9a3 100644 --- a/net/ax-net/src/service.rs +++ b/net/ax-net/src/service.rs @@ -8,7 +8,7 @@ use ax_errno::{AxResult, ax_err_type}; use ax_hal::time::{NANOS_PER_MICROS, TimeValue, monotonic_time_nanos, wall_time_nanos}; use ax_task::future::sleep_until; use smoltcp::{ - iface::{Interface, SocketSet}, + iface::{Interface, PollResult, SocketSet}, phy::ChecksumCapabilities, time::{Duration as SmolDuration, Instant}, wire::{ @@ -510,9 +510,10 @@ impl Service { for event in dhcp_events { self.handle_dhcp_event(event); } - self.iface.poll(timestamp, &mut self.router, sockets); + let socket_state_changed = + self.iface.poll(timestamp, &mut self.router, sockets) == PollResult::SocketStateChanged; let dhcp_poll_next = self.poll_dhcp(timestamp); - self.router.dispatch(timestamp) || dhcp_poll_next + self.router.dispatch(timestamp) || dhcp_poll_next || socket_state_changed } pub fn next_poll_at(&mut self, sockets: &SocketSet) -> Option { diff --git a/net/ax-net/src/tcp.rs b/net/ax-net/src/tcp.rs index d67c9e04ab..8ee6b795fb 100644 --- a/net/ax-net/src/tcp.rs +++ b/net/ax-net/src/tcp.rs @@ -27,7 +27,7 @@ use crate::{ general::GeneralOptions, get_control, get_service, interface_by_id, options::{Configurable, GetSocketOption, SetSocketOption, TcpInfo, TcpInfoOptions, TcpState}, - request_poll, + poll_interfaces_now, request_poll, state::*, }; @@ -668,6 +668,14 @@ impl Pollable for TcpSocket { } fn register(&self, context: &mut Context<'_>, events: IoEvents) { + self.with_smol_socket(|socket| { + if events.intersects(IoEvents::IN | IoEvents::RDHUP) { + socket.register_recv_waker(context.waker()); + } + if events.contains(IoEvents::OUT) { + socket.register_send_waker(context.waker()); + } + }); if events.intersects(IoEvents::IN | IoEvents::OUT | IoEvents::RDHUP) { self.general.register_waker(context.waker()); } @@ -682,6 +690,7 @@ impl Drop for TcpSocket { if let Err(err) = self.shutdown(Shutdown::Both) { warn!("TCP socket {}: shutdown failed: {}", self.handle, err); } + poll_interfaces_now(); self.unregister_bound_endpoint(); SOCKET_SET.remove(self.handle); // This is crucial for the close messages to be sent. diff --git a/net/ax-net/src/udp.rs b/net/ax-net/src/udp.rs index d646e16340..ff318f22b5 100644 --- a/net/ax-net/src/udp.rs +++ b/net/ax-net/src/udp.rs @@ -24,7 +24,7 @@ use crate::{ general::GeneralOptions, get_control, interface_by_id, options::{Configurable, GetSocketOption, SetSocketOption}, - request_poll, + poll_interfaces_now, request_poll, }; /// Buffered state for MSG_MORE corking: captures the target endpoint @@ -391,8 +391,7 @@ impl SocketOps for UdpSocket { Ok(cur_read) } })?; - // Flush TX so loopback packets reach the receiver immediately. - request_poll(); + poll_interfaces_now(); Ok(result) }) } @@ -511,6 +510,14 @@ impl Pollable for UdpSocket { } fn register(&self, context: &mut Context<'_>, events: IoEvents) { + self.with_smol_socket(|socket| { + if events.contains(IoEvents::IN) { + socket.register_recv_waker(context.waker()); + } + if events.contains(IoEvents::OUT) { + socket.register_send_waker(context.waker()); + } + }); if events.intersects(IoEvents::IN | IoEvents::OUT) { self.general.register_waker(context.waker()); } From 5467eef7aaff3fdcc897b9a54ef8b75c531492af Mon Sep 17 00:00:00 2001 From: ZCShou <72115@163.com> Date: Mon, 15 Jun 2026 08:54:35 +0800 Subject: [PATCH 09/31] feat(starry): add dual-net QEMU test case for multi-interface parallel fetch validation --- apps/starry/qemu/dual-net/c/CMakeLists.txt | 6 + apps/starry/qemu/dual-net/c/dual-net-tests.sh | 106 ++++++++++++++++++ apps/starry/qemu/dual-net/c/prebuild.sh | 5 + apps/starry/qemu/dual-net/qemu-riscv64.toml | 32 ++++++ apps/starry/qemu/dual-net/qemu-x86_64.toml | 30 +++++ scripts/axbuild/src/starry/test.rs | 95 ++++++++++++++++ 6 files changed, 274 insertions(+) create mode 100644 apps/starry/qemu/dual-net/c/CMakeLists.txt create mode 100644 apps/starry/qemu/dual-net/c/dual-net-tests.sh create mode 100644 apps/starry/qemu/dual-net/c/prebuild.sh create mode 100644 apps/starry/qemu/dual-net/qemu-riscv64.toml create mode 100644 apps/starry/qemu/dual-net/qemu-x86_64.toml diff --git a/apps/starry/qemu/dual-net/c/CMakeLists.txt b/apps/starry/qemu/dual-net/c/CMakeLists.txt new file mode 100644 index 0000000000..0a5c60b03c --- /dev/null +++ b/apps/starry/qemu/dual-net/c/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.20) + +project(dual-net-test NONE) + +install(PROGRAMS dual-net-tests.sh DESTINATION usr/bin) +install(PROGRAMS ${STARRY_STAGING_ROOT}/usr/bin/curl DESTINATION usr/bin) diff --git a/apps/starry/qemu/dual-net/c/dual-net-tests.sh b/apps/starry/qemu/dual-net/c/dual-net-tests.sh new file mode 100644 index 0000000000..a2e2707860 --- /dev/null +++ b/apps/starry/qemu/dual-net/c/dual-net-tests.sh @@ -0,0 +1,106 @@ +#!/bin/sh +set -eu + +fail() { + echo "DUAL_NET_TEST_FAILED: $*" + exit 1 +} + +need_cmd() { + command -v "$1" >/dev/null 2>&1 || fail "missing command $1" +} + +now_ms() { + ns="$(date +%s%N 2>/dev/null || true)" + case "$ns" in + *[!0-9]* | "") + echo "$(($(date +%s) * 1000))" + ;; + *) + echo "$((ns / 1000000))" + ;; + esac +} + +iface_addr_contains() { + iface="$1" + expected="$2" + ifconfig "$iface" 2>&1 | tee "/tmp/dual-net-$iface.ifconfig" + ip addr show "$iface" 2>&1 | tee "/tmp/dual-net-$iface.ipaddr" + grep -qF "$expected" "/tmp/dual-net-$iface.ifconfig" || + grep -qF "$expected" "/tmp/dual-net-$iface.ipaddr" +} + +fetch_with_iface() { + iface="$1" + host="$2" + tag="$3" + out="/tmp/dual-net-$tag.bin" + meta="/tmp/dual-net-$tag.meta" + start="$(now_ms)" + curl --interface "$iface" \ + --connect-timeout 10 \ + --max-time 60 \ + --fail \ + --silent \ + --show-error \ + "http://$host:18382/payload.bin?iface=$iface&tag=$tag" \ + -o "$out" + end="$(now_ms)" + bytes="$(wc -c < "$out" | tr -d ' ')" + elapsed="$((end - start))" + printf '%s %s %s\n' "$elapsed" "$bytes" "$out" > "$meta" +} + +wait_fetch() { + pid="$1" + tag="$2" + if ! wait "$pid"; then + fail "fetch $tag failed" + fi + read -r elapsed bytes out < "/tmp/dual-net-$tag.meta" + [ "$bytes" -ge 1048576 ] || fail "fetch $tag too small: $bytes" + echo "DUAL_NET_FETCH_${tag}_MS=$elapsed BYTES=$bytes" + rm -f "$out" "/tmp/dual-net-$tag.meta" +} + +echo "DUAL_NET_TEST_BEGIN" +need_cmd ifconfig +need_cmd ip +need_cmd curl + +if iface_addr_contains eth0 10.0.2.15; then + echo "DUAL_NET_ETH0_ADDR_OK" +else + fail "eth0 did not get 10.0.2.15" +fi + +if iface_addr_contains eth1 10.0.3.15; then + echo "DUAL_NET_ETH1_ADDR_OK" +else + fail "eth1 did not get 10.0.3.15" +fi + +single_start="$(now_ms)" +fetch_with_iface eth0 10.0.2.2 eth0_single +fetch_with_iface eth1 10.0.3.2 eth1_single +single_end="$(now_ms)" +read -r eth0_single_ms eth0_single_bytes _ < /tmp/dual-net-eth0_single.meta +read -r eth1_single_ms eth1_single_bytes _ < /tmp/dual-net-eth1_single.meta +echo "DUAL_NET_FETCH_ETH0_SINGLE_MS=$eth0_single_ms BYTES=$eth0_single_bytes" +echo "DUAL_NET_FETCH_ETH1_SINGLE_MS=$eth1_single_ms BYTES=$eth1_single_bytes" +echo "DUAL_NET_FETCH_SERIAL_MS=$((single_end - single_start))" +rm -f /tmp/dual-net-eth0_single.bin /tmp/dual-net-eth1_single.bin +rm -f /tmp/dual-net-eth0_single.meta /tmp/dual-net-eth1_single.meta + +parallel_start="$(now_ms)" +fetch_with_iface eth0 10.0.2.2 ETH0_PARALLEL & +pid0="$!" +fetch_with_iface eth1 10.0.3.2 ETH1_PARALLEL & +pid1="$!" +wait_fetch "$pid0" ETH0_PARALLEL +wait_fetch "$pid1" ETH1_PARALLEL +parallel_end="$(now_ms)" +echo "DUAL_NET_FETCH_PARALLEL_MS=$((parallel_end - parallel_start))" + +echo "DUAL_NET_TEST_PASSED" diff --git a/apps/starry/qemu/dual-net/c/prebuild.sh b/apps/starry/qemu/dual-net/c/prebuild.sh new file mode 100644 index 0000000000..a508354592 --- /dev/null +++ b/apps/starry/qemu/dual-net/c/prebuild.sh @@ -0,0 +1,5 @@ +#!/bin/sh +set -eu + +apk add curl +test -x "$STARRY_STAGING_ROOT/usr/bin/curl" diff --git a/apps/starry/qemu/dual-net/qemu-riscv64.toml b/apps/starry/qemu/dual-net/qemu-riscv64.toml new file mode 100644 index 0000000000..4d86adcaf1 --- /dev/null +++ b/apps/starry/qemu/dual-net/qemu-riscv64.toml @@ -0,0 +1,32 @@ +args = [ + "-nographic", + "-m", + "512M", + "-cpu", + "rv64", + "-device", + "virtio-blk-pci,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-riscv64-alpine.img", + "-device", + "virtio-net-pci,netdev=net0", + "-netdev", + "user,id=net0,net=10.0.2.0/24,dhcpstart=10.0.2.15", + "-device", + "virtio-net-pci,netdev=net1", + "-netdev", + "user,id=net1,net=10.0.3.0/24,dhcpstart=10.0.3.15", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/dual-net-tests.sh" +success_regex = ["(?m)^DUAL_NET_TEST_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^lockdep fatal violation\\s*$", "(?m)^DUAL_NET_TEST_FAILED:"] +timeout = 240 + +[host_http_server] +bind = "127.0.0.1" +port = 18382 +body_size = 1048576 +body_byte = 68 diff --git a/apps/starry/qemu/dual-net/qemu-x86_64.toml b/apps/starry/qemu/dual-net/qemu-x86_64.toml new file mode 100644 index 0000000000..bacc712310 --- /dev/null +++ b/apps/starry/qemu/dual-net/qemu-x86_64.toml @@ -0,0 +1,30 @@ +args = [ + "-nographic", + "-m", + "512M", + "-device", + "virtio-blk-pci,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-x86_64-alpine.img", + "-device", + "virtio-net-pci,netdev=net0", + "-netdev", + "user,id=net0,net=10.0.2.0/24,dhcpstart=10.0.2.15", + "-device", + "virtio-net-pci,netdev=net1", + "-netdev", + "user,id=net1,net=10.0.3.0/24,dhcpstart=10.0.3.15", +] +uefi = false +to_bin = false +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/dual-net-tests.sh" +success_regex = ["(?m)^DUAL_NET_TEST_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^lockdep fatal violation\\s*$", "(?m)^DUAL_NET_TEST_FAILED:"] +timeout = 240 + +[host_http_server] +bind = "127.0.0.1" +port = 18382 +body_size = 1048576 +body_byte = 68 diff --git a/scripts/axbuild/src/starry/test.rs b/scripts/axbuild/src/starry/test.rs index 358896080c..2789d79979 100644 --- a/scripts/axbuild/src/starry/test.rs +++ b/scripts/axbuild/src/starry/test.rs @@ -2409,6 +2409,101 @@ mod tests { } } + #[test] + fn dual_net_qemu_case_exercises_two_interfaces_and_parallel_fetches() { + let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let case_dir = workspace_root.join("apps/starry/qemu/dual-net"); + let script_path = case_dir.join("c/dual-net-tests.sh"); + let prebuild_path = case_dir.join("c/prebuild.sh"); + let cmake_path = case_dir.join("c/CMakeLists.txt"); + + assert!( + script_path.is_file(), + "{} must contain the guest dual-net probe", + script_path.display() + ); + assert!( + prebuild_path.is_file() && cmake_path.is_file(), + "{} must use the C pipeline so curl is installed before boot", + case_dir.display() + ); + + for arch in ["x86_64", "riscv64"] { + let config_path = case_dir.join(format!("qemu-{arch}.toml")); + assert!( + config_path.is_file(), + "{} must provide a QEMU runtime config", + config_path.display() + ); + + let config: toml::Value = + toml::from_str(&fs::read_to_string(&config_path).unwrap()).unwrap(); + let args = config + .get("args") + .and_then(toml::Value::as_array) + .unwrap() + .iter() + .filter_map(toml::Value::as_str) + .collect::>(); + for expected in [ + "virtio-net-pci,netdev=net0", + "virtio-net-pci,netdev=net1", + "user,id=net0,net=10.0.2.0/24,dhcpstart=10.0.2.15", + "user,id=net1,net=10.0.3.0/24,dhcpstart=10.0.3.15", + ] { + assert!( + args.iter().any(|arg| arg.contains(expected)), + "{} must include `{expected}`", + config_path.display() + ); + } + assert_eq!( + config.get("shell_init_cmd").and_then(toml::Value::as_str), + Some("/usr/bin/dual-net-tests.sh") + ); + let http = config + .get("host_http_server") + .and_then(toml::Value::as_table) + .expect("dual-net case must start a host HTTP fixture"); + assert_eq!( + http.get("port").and_then(toml::Value::as_integer), + Some(18382) + ); + assert!( + http.get("body_size") + .and_then(toml::Value::as_integer) + .is_some_and(|size| size >= 1024 * 1024), + "dual-net case must fetch a payload large enough to expose obvious regressions" + ); + } + + let script = fs::read_to_string(&script_path).unwrap(); + for expected in [ + "now_ms()", + "iface_addr_contains eth0 10.0.2.15", + "iface_addr_contains eth1 10.0.3.15", + "curl --interface \"$iface\"", + "fetch_with_iface eth0 10.0.2.2", + "fetch_with_iface eth1 10.0.3.2", + "DUAL_NET_FETCH_PARALLEL_MS", + "DUAL_NET_TEST_PASSED", + "DUAL_NET_TEST_FAILED", + ] { + assert!( + script.contains(expected), + "{} must contain `{expected}`", + script_path.display() + ); + } + + let prebuild = fs::read_to_string(&prebuild_path).unwrap(); + assert!( + prebuild.contains("apk add curl"), + "{} must install curl during asset preparation, not at guest runtime", + prebuild_path.display() + ); + } + #[test] fn lua_qemu_case_installs_lua_before_boot() { let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); From 9004d8254f14c56848309502f79e21e8ce7e7212 Mon Sep 17 00:00:00 2001 From: ZCShou <72115@163.com> Date: Mon, 15 Jun 2026 09:09:18 +0800 Subject: [PATCH 10/31] fix(rdrive): track probed PCI devices by address instead of vendor/device ID --- drivers/rdrive/src/probe/pci/mod.rs | 17 ++++------------- net/ax-net/Cargo.toml | 1 + net/ax-net/src/service.rs | 22 ++++++++++++++++++++++ scripts/axbuild/src/starry/app.rs | 25 +++++++++++++++++++++++-- scripts/axbuild/src/starry/mod.rs | 8 +++++++- 5 files changed, 57 insertions(+), 16 deletions(-) diff --git a/drivers/rdrive/src/probe/pci/mod.rs b/drivers/rdrive/src/probe/pci/mod.rs index 395e47e4bd..27790e8110 100644 --- a/drivers/rdrive/src/probe/pci/mod.rs +++ b/drivers/rdrive/src/probe/pci/mod.rs @@ -17,12 +17,6 @@ static PCIE: Once>> = Once::new(); pub type FnOnProbe = fn(ProbePci<'_>) -> Result<(), OnProbeError>; -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -struct Id { - vendor: u16, - device: u16, -} - pub fn new_driver_generic( mmio_base: usize, mmio_size: usize, @@ -148,7 +142,7 @@ impl DerefMut for EndpointRc { struct PcieEnumterator { ctrl: Device, - probed: BTreeSet, + probed: BTreeSet, } impl PcieEnumterator { @@ -184,11 +178,8 @@ impl PcieEnumterator { ) -> Result<(), ProbeError> { let intx_route = endpoint.intx_route; let endpoint = endpoint.endpoint; - let id = Id { - vendor: endpoint.vendor_id(), - device: endpoint.device_id(), - }; - if self.probed.contains(&id) { + let address = endpoint.address(); + if self.probed.contains(&address) { return Ok(()); } @@ -212,7 +203,7 @@ impl PcieEnumterator { let plat_dev = PlatformDevice::new(desc); match (pci_probe)(ProbePci::new(info, &mut endpoint, plat_dev)) { Ok(_) => { - self.probed.insert(id); + self.probed.insert(address); return Ok(()); } Err(e) => match e { diff --git a/net/ax-net/Cargo.toml b/net/ax-net/Cargo.toml index 7fc1c137c3..b5c7cb61b9 100644 --- a/net/ax-net/Cargo.toml +++ b/net/ax-net/Cargo.toml @@ -47,6 +47,7 @@ features = [ "socket-tcp", "socket-dhcpv4", "socket-dns", + "iface-max-addr-count-8", # "fragmentation-buffer-size-65536", "proto-ipv4-fragmentation", # "reassembly-buffer-size-65536", "reassembly-buffer-count-32", # "assembler-max-segment-count-32", diff --git a/net/ax-net/src/service.rs b/net/ax-net/src/service.rs index dd5769b9a3..2267200ae0 100644 --- a/net/ax-net/src/service.rs +++ b/net/ax-net/src/service.rs @@ -804,4 +804,26 @@ mod tests { service.dhcp[1].address = Some(Ipv4Cidr::new(Ipv4Address::new(192, 0, 2, 10), 24)); assert!(service.dhcp_configured()); } + + #[test] + fn interface_address_table_handles_loopback_and_two_ethernet_addresses() { + let routes = Arc::new(spin::RwLock::new(RouteTable::new())); + let router = Router::new(routes.clone()); + let control = Arc::new(NetControl::new(Vec::new(), routes, Vec::new())); + let mut service = Service::new(router, control); + + let lo = Ipv4Cidr::new(Ipv4Address::new(127, 0, 0, 1), 8); + let eth0 = Ipv4Cidr::new(Ipv4Address::new(10, 0, 2, 15), 24); + let eth1 = Ipv4Cidr::new(Ipv4Address::new(10, 0, 3, 15), 24); + + service.iface.update_ip_addrs(|ip_addrs| { + ip_addrs.push(lo.into()).unwrap(); + }); + Service::set_interface_ipv4(&mut service.iface, None, Some(eth0)); + Service::set_interface_ipv4(&mut service.iface, None, Some(eth1)); + + assert!(service.iface.ip_addrs().contains(&IpCidr::Ipv4(lo))); + assert!(service.iface.ip_addrs().contains(&IpCidr::Ipv4(eth0))); + assert!(service.iface.ip_addrs().contains(&IpCidr::Ipv4(eth1))); + } } diff --git a/scripts/axbuild/src/starry/app.rs b/scripts/axbuild/src/starry/app.rs index 2ce6a480ab..4e65ca985a 100644 --- a/scripts/axbuild/src/starry/app.rs +++ b/scripts/axbuild/src/starry/app.rs @@ -14,7 +14,10 @@ use crate::{ context::starry_target_for_arch_checked, rootfs::inject, support::process::ProcessExt, - test::{case::TestQemuCase, qemu as qemu_test}, + test::{ + case::{HostHttpServerConfig, TestQemuCase}, + qemu as qemu_test, + }, }; #[derive(Args, Debug, Clone)] @@ -121,6 +124,7 @@ pub(crate) struct StarryAppQemuCase { pub(crate) rootfs_path: PathBuf, pub(crate) test_commands: Vec, pub(crate) host_symbolize_success_regex: Vec, + pub(crate) host_http_server: Option, pub(crate) subcases: Vec, } @@ -381,6 +385,9 @@ pub(crate) async fn prepare_qemu_app_case( .as_ref() .map(|fields| fields.test_case.host_symbolize_success_regex.clone()) .unwrap_or_default(), + host_http_server: fields + .as_ref() + .and_then(|fields| fields.test_case.host_http_server.clone()), subcases: fields .map(|fields| fields.test_case.subcases) .unwrap_or_default(), @@ -399,7 +406,7 @@ pub(crate) fn app_qemu_test_case( qemu_config_path, test_commands: case.test_commands.clone(), host_symbolize_success_regex: case.host_symbolize_success_regex.clone(), - host_http_server: None, + host_http_server: case.host_http_server.clone(), subcases: case.subcases.clone(), grouped_subcase_filter: None, }) @@ -1547,6 +1554,13 @@ fail_regex = [] rootfs_path: PathBuf::from("/tmp/rootfs.img"), test_commands: Vec::new(), host_symbolize_success_regex: vec!["symbolized".to_string()], + host_http_server: Some(HostHttpServerConfig { + bind: "127.0.0.1".to_string(), + port: 18382, + body: "fixture".to_string(), + body_size: None, + body_byte: b'X', + }), subcases: Vec::new(), }; @@ -1555,6 +1569,13 @@ fail_regex = [] assert_eq!(test_case.case_dir, case_dir); assert_eq!(test_case.qemu_config_path, qemu_config_path); assert_eq!(test_case.host_symbolize_success_regex, vec!["symbolized"]); + assert_eq!( + test_case + .host_http_server + .as_ref() + .map(|config| (config.bind.as_str(), config.port)), + Some(("127.0.0.1", 18382)) + ); } #[test] diff --git a/scripts/axbuild/src/starry/mod.rs b/scripts/axbuild/src/starry/mod.rs index cf67e5f4cd..0c96998197 100644 --- a/scripts/axbuild/src/starry/mod.rs +++ b/scripts/axbuild/src/starry/mod.rs @@ -11,7 +11,7 @@ use ostool::{ use crate::{ context::{AppContext, ResolvedStarryRequest, SnapshotPersistence, StarryCliArgs}, - test::{case as qemu_case, qemu}, + test::{case as qemu_case, host_http::HostHttpServerGuard, qemu}, }; pub(crate) mod apk; @@ -631,6 +631,12 @@ impl Starry { ); println!(" rootfs: {}", prepared_assets.rootfs_path.display()); + let _host_http_server = test_case + .host_http_server + .as_ref() + .map(|config| HostHttpServerGuard::start(config, &test_case.name)) + .transpose()?; + let result = self .app .qemu(cargo, request.build_info_path, Some(qemu)) From d780574c2374fbfbd3bb542f7e6cbd5047e6c3ef Mon Sep 17 00:00:00 2001 From: ZCShou <72115@163.com> Date: Mon, 15 Jun 2026 10:03:25 +0800 Subject: [PATCH 11/31] fix(ax-net): flush pending TCP output before socket removal in Drop --- net/ax-net/src/tcp.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/net/ax-net/src/tcp.rs b/net/ax-net/src/tcp.rs index 8ee6b795fb..327bd25f68 100644 --- a/net/ax-net/src/tcp.rs +++ b/net/ax-net/src/tcp.rs @@ -690,7 +690,7 @@ impl Drop for TcpSocket { if let Err(err) = self.shutdown(Shutdown::Both) { warn!("TCP socket {}: shutdown failed: {}", self.handle, err); } - poll_interfaces_now(); + self.flush_pending_output_before_remove(); self.unregister_bound_endpoint(); SOCKET_SET.remove(self.handle); // This is crucial for the close messages to be sent. @@ -698,6 +698,20 @@ impl Drop for TcpSocket { } } +impl TcpSocket { + fn flush_pending_output_before_remove(&self) { + const CLOSE_FLUSH_POLLS: usize = 64; + + for _ in 0..CLOSE_FLUSH_POLLS { + poll_interfaces_now(); + if self.with_smol_socket(|socket| socket.send_queue() == 0) { + return; + } + ax_task::yield_now(); + } + } +} + fn saturating_u32(value: usize) -> u32 { value.min(u32::MAX as usize) as u32 } From 11384d51c345fd0080dc7f6d360e812cfd6f48d8 Mon Sep 17 00:00:00 2001 From: ZCShou <72115@163.com> Date: Mon, 15 Jun 2026 11:54:55 +0800 Subject: [PATCH 12/31] feat(ax-net): add TCP orphan socket reaping and loopback fast path --- net/ax-net/src/consts.rs | 12 ++++ net/ax-net/src/device/loopback.rs | 73 +++++++---------------- net/ax-net/src/lib.rs | 1 + net/ax-net/src/orphan.rs | 99 +++++++++++++++++++++++++++++++ net/ax-net/src/router.rs | 40 +++++++++++-- net/ax-net/src/service.rs | 49 +++++++++++++++ net/ax-net/src/tcp.rs | 45 ++++++++------ 7 files changed, 246 insertions(+), 73 deletions(-) create mode 100644 net/ax-net/src/orphan.rs diff --git a/net/ax-net/src/consts.rs b/net/ax-net/src/consts.rs index 441b928977..5c2f74e879 100644 --- a/net/ax-net/src/consts.rs +++ b/net/ax-net/src/consts.rs @@ -9,6 +9,18 @@ pub const RAW_TX_BUF_LEN: usize = 64 * 1024; pub const LISTEN_QUEUE_SIZE: usize = 512; pub const SOCKET_BUFFER_SIZE: usize = 64; + +/// Per-device TX queue capacity. +/// +/// Sized to absorb bursty traffic without drops while keeping memory bounded. +/// 128 slots × 1500 bytes = 192KB per network device (acceptable for embedded). +/// +/// Rationale: +/// - At 1Gbps, 128 packets = ~1.5ms of buffering +/// - Handles typical burst scenarios (ARP resolution, TCP slow start) +/// - Reduces packet loss under momentary TX worker scheduling delays +pub const DEVICE_TX_QUEUE_SIZE: usize = 128; + /// Number of outbound packets that can be queued while waiting for ARP /// resolution of the next hop. /// diff --git a/net/ax-net/src/device/loopback.rs b/net/ax-net/src/device/loopback.rs index b3ca9069c0..ad9a8c87d3 100644 --- a/net/ax-net/src/device/loopback.rs +++ b/net/ax-net/src/device/loopback.rs @@ -1,33 +1,18 @@ -use alloc::vec; use core::task::Waker; -use axpoll::PollSet; -use smoltcp::{ - storage::{PacketBuffer, PacketMetadata}, - time::Instant, - wire::IpAddress, -}; +use smoltcp::{time::Instant, wire::IpAddress}; -use crate::{ - config::InterfaceId, - consts::{SOCKET_BUFFER_SIZE, STANDARD_MTU}, - device::Device, -}; +use crate::{config::InterfaceId, device::Device}; + +/// Loopback device for local traffic. +/// +/// Unlike Ethernet devices, loopback uses a fast path that bypasses device +/// workers: packets are injected directly into the router's RX queue on send. +pub struct LoopbackDevice; -pub struct LoopbackDevice { - buffer: PacketBuffer<'static, ()>, - poll: PollSet, -} impl LoopbackDevice { pub fn new() -> Self { - let buffer = PacketBuffer::new( - vec![PacketMetadata::EMPTY; SOCKET_BUFFER_SIZE], - vec![0u8; STANDARD_MTU * SOCKET_BUFFER_SIZE], - ); - Self { - buffer, - poll: PollSet::new(), - } + Self } } @@ -38,39 +23,23 @@ impl Device for LoopbackDevice { fn recv( &mut self, - interface_id: InterfaceId, - buffer: &mut PacketBuffer, + _interface_id: InterfaceId, + _buffer: &mut smoltcp::storage::PacketBuffer, _timestamp: Instant, - snoop: &mut dyn FnMut(&[u8]), + _snoop: &mut dyn FnMut(&[u8]), ) -> bool { - self.buffer.dequeue().ok().is_some_and(|(_, rx_buf)| { - snoop(rx_buf); - buffer - .enqueue(rx_buf.len(), interface_id) - .unwrap() - .copy_from_slice(rx_buf); - true - }) + // Loopback uses fast path: packets go directly to RouterQueues::rx + // This recv() is never called by device workers + false } - fn send(&mut self, next_hop: IpAddress, packet: &[u8], _timestamp: Instant) -> bool { - match self.buffer.enqueue(packet.len(), ()) { - Ok(tx_buf) => { - tx_buf.copy_from_slice(packet); - self.poll.wake(); - true - } - Err(_) => { - warn!( - "Loopback device buffer is full, dropping packet to {}", - next_hop - ); - false - } - } + fn send(&mut self, _next_hop: IpAddress, _packet: &[u8], _timestamp: Instant) -> bool { + // Fast path: loopback packets are injected directly in Router::dispatch() + // See Router::dispatch_loopback() + true } - fn register_waker(&self, waker: &Waker) { - self.poll.register(waker); + fn register_waker(&self, _waker: &Waker) { + // No async operations needed for loopback fast path } } diff --git a/net/ax-net/src/lib.rs b/net/ax-net/src/lib.rs index b9f2d103f8..06307a810d 100644 --- a/net/ax-net/src/lib.rs +++ b/net/ax-net/src/lib.rs @@ -28,6 +28,7 @@ mod general; mod listen_table; /// Socket option types and the [`Configurable`](options::Configurable) trait. pub mod options; +mod orphan; /// Raw socket implementation. pub mod raw; mod router; diff --git a/net/ax-net/src/orphan.rs b/net/ax-net/src/orphan.rs new file mode 100644 index 0000000000..7e35eff077 --- /dev/null +++ b/net/ax-net/src/orphan.rs @@ -0,0 +1,99 @@ +//! Orphan socket management for TCP connections. +//! +//! When a user closes a TCP socket (via Drop), the socket enters the orphan state: +//! - Unbound from the user-facing API (handle not accessible to new operations) +//! - smoltcp socket handle remains active in the protocol stack +//! - Connection teardown (FIN handshake, TIME_WAIT) continues in background +//! - Removed after smoltcp reaches Closed, or after TIME_WAIT/max linger expires +//! +//! This ensures RFC 793 compliance and prevents premature connection termination. + +use alloc::vec::Vec; + +use ax_sync::Mutex; +use smoltcp::{ + iface::{SocketHandle, SocketSet}, + socket::tcp, + time::Instant, +}; +use spin::LazyLock; + +/// Orphaned TCP socket awaiting final cleanup. +struct OrphanSocket { + handle: SocketHandle, + orphaned_at: Instant, +} + +/// Global orphan socket pool. +/// +/// Accessed by: +/// - TcpSocket::drop() to add orphans +/// - net-poll worker to reap finished orphans +static ORPHAN_SOCKETS: LazyLock>> = + LazyLock::new(|| Mutex::new(Vec::new())); + +/// Move a TCP socket to the orphan pool. +/// +/// Called from TcpSocket::drop() after shutdown and endpoint cleanup. +pub(crate) fn add_orphan(handle: SocketHandle, timestamp: Instant) { + ORPHAN_SOCKETS.lock().push(OrphanSocket { + handle, + orphaned_at: timestamp, + }); +} + +/// Reap finished orphan sockets. +/// +/// Called from net-poll worker on every poll cycle. +/// Removes sockets whose background TCP teardown no longer needs stack state. +pub(crate) fn reap_orphans(timestamp: Instant, sockets: &mut SocketSet<'_>) { + const ORPHAN_MAX_LINGER: i64 = 60_000_000; // 60 seconds in microseconds + + ORPHAN_SOCKETS.lock().retain(|orphan| { + let keep = { + let socket = sockets.get_mut::(orphan.handle); + match socket.state() { + tcp::State::Closed => false, // Fully closed, safe to remove + tcp::State::TimeWait => { + // TIME_WAIT should expire naturally (smoltcp default: 10s) + // But force cleanup after max linger to prevent leaks + let elapsed = timestamp.total_micros() - orphan.orphaned_at.total_micros(); + elapsed < ORPHAN_MAX_LINGER + } + tcp::State::LastAck | tcp::State::FinWait1 | tcp::State::FinWait2 => { + // Still tearing down, keep alive + true + } + tcp::State::Closing => true, + _ => { + // Unexpected state for orphan (Listen/SynSent/SynReceived/Established) + // Force remove after max linger + let elapsed = timestamp.total_micros() - orphan.orphaned_at.total_micros(); + if elapsed >= ORPHAN_MAX_LINGER { + warn!( + "Orphan socket {} in unexpected state {:?} after {}s, force removing", + orphan.handle, + socket.state(), + elapsed / 1_000_000 + ); + false + } else { + true + } + } + } + }; + + if !keep { + sockets.remove(orphan.handle); + debug!("Reaped orphan socket {}", orphan.handle); + } + keep + }); +} + +/// Get current orphan socket count (for diagnostics). +#[allow(dead_code)] +pub(crate) fn orphan_count() -> usize { + ORPHAN_SOCKETS.lock().len() +} diff --git a/net/ax-net/src/router.rs b/net/ax-net/src/router.rs index 0eb238f15b..7a7dfdaf6d 100644 --- a/net/ax-net/src/router.rs +++ b/net/ax-net/src/router.rs @@ -31,7 +31,7 @@ use spin::RwLock; use crate::{ LISTEN_TABLE, config::{DeviceBinding, InterfaceId, RouteInfo}, - consts::{SOCKET_BUFFER_SIZE, STANDARD_MTU}, + consts::{DEVICE_TX_QUEUE_SIZE, SOCKET_BUFFER_SIZE, STANDARD_MTU}, device::{ArpEntry, Device}, }; @@ -156,7 +156,7 @@ impl DeviceHandle { name, inner: Arc::new(Mutex::new(device)), rx_queue: queues.rx.clone(), - tx_queue: Arc::new(BoundedPacketQueue::new(SOCKET_BUFFER_SIZE)), + tx_queue: Arc::new(BoundedPacketQueue::new(DEVICE_TX_QUEUE_SIZE)), rx_wake: Arc::new(WaitQueue::new()), tx_wake: Arc::new(WaitQueue::new()), rx_waker: Waker::from(Arc::new(DeviceRxWake { @@ -365,6 +365,10 @@ impl Router { pub fn start_tx_workers(&self) { for device in &self.devices { + // Skip loopback: it uses fast path (no worker needed) + if device.interface_id == InterfaceId::LOOPBACK { + continue; + } let device = device.clone(); let name = format!("{}-tx", device.name); ax_task::spawn_with_name(move || device_tx_worker(device), name); @@ -373,6 +377,10 @@ impl Router { pub fn start_rx_workers(&self) { for device in &self.devices { + // Skip loopback: packets injected directly in dispatch + if device.interface_id == InterfaceId::LOOPBACK { + continue; + } let device = device.clone(); let name = format!("{}-rx", device.name); ax_task::spawn_with_name(move || device_rx_worker(device), name); @@ -519,7 +527,19 @@ impl Router { }; let dev = &self.devices[route.dev]; - poll_next |= dev.enqueue_tx(route.next_hop, packet.into_inner()); + // Loopback fast path: inject directly to RX queue + if dev.interface_id == InterfaceId::LOOPBACK { + let rx = RxPacket { + interface_id: InterfaceId::LOOPBACK, + bytes: packet.into_inner().to_vec().into_boxed_slice(), + }; + if self.queues.rx.push(rx).is_err() { + warn!("Loopback: RX queue full, dropping packet to {}", dst_addr); + } + poll_next = true; + } else { + poll_next |= dev.enqueue_tx(route.next_hop, packet.into_inner()); + } } } IpVersion::Ipv6 => { @@ -542,7 +562,19 @@ impl Router { }; let dev = &self.devices[route.dev]; - poll_next |= dev.enqueue_tx(route.next_hop, packet.into_inner()); + // Loopback fast path + if dev.interface_id == InterfaceId::LOOPBACK { + let rx = RxPacket { + interface_id: InterfaceId::LOOPBACK, + bytes: packet.into_inner().to_vec().into_boxed_slice(), + }; + if self.queues.rx.push(rx).is_err() { + warn!("Loopback: RX queue full, dropping packet to {}", dst_addr); + } + poll_next = true; + } else { + poll_next |= dev.enqueue_tx(route.next_hop, packet.into_inner()); + } } } } diff --git a/net/ax-net/src/service.rs b/net/ax-net/src/service.rs index 2267200ae0..870cd55d0f 100644 --- a/net/ax-net/src/service.rs +++ b/net/ax-net/src/service.rs @@ -1,3 +1,48 @@ +//! Network service and control plane. +//! +//! # Lock Ordering Rules +//! +//! To prevent deadlocks, acquire coarser locks before finer locks: +//! +//! 1. **SERVICE** (`Mutex`) +//! - Most coarse-grained, protects entire protocol stack +//! - Held during `Service::poll()` and waker registration +//! +//! 2. **SOCKET_SET.inner** (`Mutex`) +//! - smoltcp socket set (all TCP/UDP/raw/DNS sockets) +//! - Acquired during poll, socket operations, and state queries +//! - Never acquire SERVICE while holding this lock +//! +//! 3. **TCP_BOUND_PORTS** (`Mutex>>`) +//! - Tracks TCP bind() registrations +//! - Hold duration: registration/unregistration only +//! +//! 4. **Per-port LISTEN_TABLE entries** (`Arc>>`) +//! - Most granular, one mutex per port +//! +//! # Correct Patterns +//! +//! ```ignore +//! // ✓ SERVICE → SOCKET_SET (poll path) +//! fn poll_interfaces_now() { +//! get_service().poll(&mut SOCKET_SET.inner.lock()) +//! } +//! +//! // ✓ SOCKET_SET → LISTEN_TABLE (accept readiness check) +//! fn TcpSocket::poll_listener() { +//! let sockets = SOCKET_SET.inner.lock(); +//! LISTEN_TABLE.can_accept(port, &sockets) +//! } +//! ``` +//! +//! # Forbidden Patterns +//! +//! ```ignore +//! // ✗ SOCKET_SET → SERVICE (deadlock risk) +//! let sockets = SOCKET_SET.inner.lock(); +//! get_service().do_something(); // WRONG +//! ``` + use alloc::{boxed::Box, format, string::String, vec, vec::Vec}; use core::{ pin::Pin, @@ -513,6 +558,10 @@ impl Service { let socket_state_changed = self.iface.poll(timestamp, &mut self.router, sockets) == PollResult::SocketStateChanged; let dhcp_poll_next = self.poll_dhcp(timestamp); + + // Reap orphaned TCP sockets using the SocketSet already held by poll_once(). + crate::orphan::reap_orphans(timestamp, sockets); + self.router.dispatch(timestamp) || dhcp_poll_next || socket_state_changed } diff --git a/net/ax-net/src/tcp.rs b/net/ax-net/src/tcp.rs index 327bd25f68..ac41b3db13 100644 --- a/net/ax-net/src/tcp.rs +++ b/net/ax-net/src/tcp.rs @@ -27,7 +27,7 @@ use crate::{ general::GeneralOptions, get_control, get_service, interface_by_id, options::{Configurable, GetSocketOption, SetSocketOption, TcpInfo, TcpInfoOptions, TcpState}, - poll_interfaces_now, request_poll, + request_poll, state::*, }; @@ -687,28 +687,39 @@ impl Pollable for TcpSocket { impl Drop for TcpSocket { fn drop(&mut self) { + let should_orphan = self.with_smol_socket(|socket| { + matches!( + socket.state(), + smol::State::Established + | smol::State::CloseWait + | smol::State::FinWait1 + | smol::State::FinWait2 + | smol::State::Closing + | smol::State::LastAck + | smol::State::TimeWait + ) || socket.send_queue() > 0 + }); + + // Initiate graceful shutdown (send FIN if connected) if let Err(err) = self.shutdown(Shutdown::Both) { warn!("TCP socket {}: shutdown failed: {}", self.handle, err); } - self.flush_pending_output_before_remove(); - self.unregister_bound_endpoint(); - SOCKET_SET.remove(self.handle); - // This is crucial for the close messages to be sent. - request_poll(); - } -} -impl TcpSocket { - fn flush_pending_output_before_remove(&self) { - const CLOSE_FLUSH_POLLS: usize = 64; + // Unbind from API layer (port registry, etc.) + self.unregister_bound_endpoint(); - for _ in 0..CLOSE_FLUSH_POLLS { - poll_interfaces_now(); - if self.with_smol_socket(|socket| socket.send_queue() == 0) { - return; - } - ax_task::yield_now(); + if should_orphan { + // Keep the smoltcp socket alive after the user-facing handle is gone. + let timestamp = smoltcp::time::Instant::from_micros_const( + (ax_hal::time::monotonic_time_nanos() / 1_000) as i64, + ); + crate::orphan::add_orphan(self.handle, timestamp); + } else { + SOCKET_SET.remove(self.handle); } + + // Wake net-poll worker to process teardown + crate::request_poll(); } } From 1386555c4e19eacd40106b2cb294d3de5c8d7b42 Mon Sep 17 00:00:00 2001 From: ZCShou <72115@163.com> Date: Mon, 15 Jun 2026 13:16:58 +0800 Subject: [PATCH 13/31] fix(ax-net): respect device binding in route selection and allow no-gateway static IP configs --- net/ax-net/src/lib.rs | 12 ++++---- net/ax-net/src/listen_table.rs | 9 ++++++ net/ax-net/src/raw.rs | 10 +++++-- net/ax-net/src/router.rs | 6 ---- net/ax-net/src/service.rs | 15 +++++++++- net/ax-net/src/tcp.rs | 52 ++++++++++++++++++++++++++++++---- net/ax-net/src/udp.rs | 49 +++++++++++++++++++++++++++----- 7 files changed, 127 insertions(+), 26 deletions(-) diff --git a/net/ax-net/src/lib.rs b/net/ax-net/src/lib.rs index 9e29c683be..16bab0a8e2 100644 --- a/net/ax-net/src/lib.rs +++ b/net/ax-net/src/lib.rs @@ -154,9 +154,6 @@ pub fn init_network(mut net_devs: EthernetDeviceList, config: NetworkConfig) { if static_cfg.prefix_len > 32 { panic!("Invalid static IP for {}: prefix length > 32", cfg.name); } - if static_cfg.gateway.is_unspecified() { - panic!("Invalid gateway for {}: unspecified address", cfg.name); - } } for (i, dns) in cfg.dns_servers.iter().enumerate() { if dns.is_unspecified() { @@ -231,7 +228,10 @@ pub fn init_network(mut net_devs: EthernetDeviceList, config: NetworkConfig) { let static_ip = cfg.and_then(|cfg| cfg.static_ip.as_ref()); let ipv4 = static_ip.map(|cfg| Ipv4Cidr::new(Ipv4Address::from(cfg.ip.octets()), cfg.prefix_len)); - let gateway = static_ip.map(|cfg| Ipv4Address::from(cfg.gateway.octets())); + let gateway = static_ip.and_then(|cfg| { + (!cfg.gateway.is_unspecified()).then(|| Ipv4Address::from(cfg.gateway.octets())) + }); + let dhcp_enabled = cfg.is_none_or(|cfg| cfg.dhcp); let eth_dev = router.add_device(id, Box::new(EthernetDevice::new(name.clone(), dev, ipv4))); info!("{name}:"); @@ -251,9 +251,11 @@ pub fn init_network(mut net_devs: EthernetDeviceList, config: NetworkConfig) { if let Some(gateway) = gateway { info!(" gw: {}", gateway); } - } else { + } else if dhcp_enabled { dhcp_ifaces.push((id, eth_dev, name.clone(), mac, metric)); info!(" mode: dhcp"); + } else { + info!(" mode: none"); } if let Some(cfg) = cfg { dns.extend( diff --git a/net/ax-net/src/listen_table.rs b/net/ax-net/src/listen_table.rs index 42d9d24ebc..3e03fdc9b6 100644 --- a/net/ax-net/src/listen_table.rs +++ b/net/ax-net/src/listen_table.rs @@ -63,6 +63,12 @@ impl ListenTableEntryInner { .map(|pending| pending.accepted.handle) .collect() } + + fn has_pending(&self, src: IpEndpoint, dst: IpEndpoint) -> bool { + self.syn_queue.iter().any(|pending| { + pending.accepted.local_endpoint == dst && pending.accepted.remote_endpoint == src + }) + } } type ListenTableEntry = Arc>>>; @@ -192,6 +198,9 @@ impl ListenTable { warn!("SYN queue overflow!"); return; } + if entry.has_pending(src, dst) { + return; + } let mut socket = smoltcp::socket::tcp::Socket::new( SocketBuffer::new(vec![0; TCP_RX_BUF_LEN]), diff --git a/net/ax-net/src/raw.rs b/net/ax-net/src/raw.rs index 77d6843611..b30e49e242 100644 --- a/net/ax-net/src/raw.rs +++ b/net/ax-net/src/raw.rs @@ -120,7 +120,9 @@ impl RawSocket { if is_loopback_address(remote) { return Ok(remote); } - Ok(get_control().select_route(&remote)?.source) + Ok(get_control() + .select_route_with_binding(&remote, self.general.device_binding())? + .source) } fn parse_ip_packet<'a>(&self, packet: &'a [u8]) -> AxResult<(IpAddress, &'a [u8])> { @@ -243,7 +245,11 @@ impl SocketOps for RawSocket { let remote_addr = remote_addr.into_ip()?; let remote = self.check_ip_version(remote_addr.ip().into())?; if self.local_addr.read().is_none() { - *self.local_addr.write() = Some(get_control().select_route(&remote)?.source); + *self.local_addr.write() = Some( + get_control() + .select_route_with_binding(&remote, self.general.device_binding())? + .source, + ); } *self.peer_addr.write() = Some(remote); let local = (*self.local_addr.read()).expect("raw socket local address"); diff --git a/net/ax-net/src/router.rs b/net/ax-net/src/router.rs index 1a7e3d4198..d9df17201e 100644 --- a/net/ax-net/src/router.rs +++ b/net/ax-net/src/router.rs @@ -514,12 +514,6 @@ impl Router { } } - pub fn wake_rx_workers(&self) { - for device in &self.devices { - device.rx_wake.notify_one(true); - } - } - pub fn wake_all_devices(&self) { for device in &self.devices { device.inner.lock().wake_rx(); diff --git a/net/ax-net/src/service.rs b/net/ax-net/src/service.rs index e307795c43..1ab668ca5c 100644 --- a/net/ax-net/src/service.rs +++ b/net/ax-net/src/service.rs @@ -191,10 +191,24 @@ impl NetControl { } pub fn select_route(&self, dst_addr: &IpAddress) -> AxResult { + self.select_route_with_binding(dst_addr, DeviceBinding::default()) + } + + pub fn select_route_with_binding( + &self, + dst_addr: &IpAddress, + binding: DeviceBinding, + ) -> AxResult { let state = self.state.read(); let routes = self.routes.read(); let route = routes .select_route_if(dst_addr, |interface_id| { + if binding + .bound_if + .is_some_and(|bound_if| bound_if != interface_id) + { + return false; + } state .interfaces .iter() @@ -630,7 +644,6 @@ impl Service { let mut dhcp_events = Vec::new(); let mut dhcp_server_replies = Vec::new(); - self.router.wake_rx_workers(); { let dhcp = &mut self.dhcp; let dhcp_server = &mut self.dhcp_server; diff --git a/net/ax-net/src/tcp.rs b/net/ax-net/src/tcp.rs index ebcf4fbb67..fc38a70c70 100644 --- a/net/ax-net/src/tcp.rs +++ b/net/ax-net/src/tcp.rs @@ -410,7 +410,9 @@ impl SocketOps for TcpSocket { let binding = get_control().local_binding_for(&endpoint)?; self.register_bound_endpoint(endpoint)?; *self.bound_endpoint.lock() = endpoint; - self.general.set_device_binding(binding); + if binding.bound_if.is_some() { + self.general.set_device_binding(binding); + } debug!("TCP socket {}: binding to {}", self.handle, local_addr); Ok(()) }) @@ -459,7 +461,9 @@ impl SocketOps for TcpSocket { if register_bound { self.bound_registered.store(true, Ordering::Release); } - self.general.set_device_binding(binding); + if binding.bound_if.is_some() { + self.general.set_device_binding(binding); + } debug!("listening on {}", bound_endpoint); Ok(()) })?; @@ -793,11 +797,18 @@ impl TcpSocket { // Record original bind state before modifying let was_unbound_or_unspecified = bound_endpoint.addr.is_none_or(|addr| addr.is_unspecified()); + let had_explicit_device_binding = self.general.device_binding().bound_if.is_some(); // Fill source address if unbound or bound to 0.0.0.0 if bound_endpoint.addr.is_none_or(|addr| addr.is_unspecified()) { - bound_endpoint.addr = - Some(get_control().select_route(&remote_endpoint.addr)?.source); + bound_endpoint.addr = Some( + get_control() + .select_route_with_binding( + &remote_endpoint.addr, + self.general.device_binding(), + )? + .source, + ); } if bound_endpoint.port == 0 { bound_endpoint.port = get_ephemeral_port()?; @@ -841,7 +852,7 @@ impl TcpSocket { // Only set device binding if was originally unbound or bound to 0.0.0.0 // Binding to a specific IP should lock the interface - if was_unbound_or_unspecified { + if !had_explicit_device_binding && was_unbound_or_unspecified { self.general .set_device_binding(get_control().local_binding_for(&bound_endpoint)?); } @@ -1035,4 +1046,35 @@ mod tests { } ); } + + #[test] + fn connect_rejects_unroutable_bound_device() { + let _guard = network_test_guard(); + init_split_route_network(); + + let socket = TcpSocket::new(); + let nonblocking = true; + socket + .set_option(SetSocketOption::NonBlocking(&nonblocking)) + .unwrap(); + socket.bind_device(LOCAL_IF).unwrap(); + socket + .bind(SocketAddrEx::Ip(SocketAddr::new( + IpAddr::V4(Ipv4Addr::UNSPECIFIED), + 0, + ))) + .unwrap(); + + assert!( + socket + .start_connect(SocketAddr::new(IpAddr::V4(PEER_ADDR), 80)) + .is_err() + ); + assert_eq!( + socket.general.device_binding(), + DeviceBinding { + bound_if: Some(LOCAL_IF) + } + ); + } } diff --git a/net/ax-net/src/udp.rs b/net/ax-net/src/udp.rs index ff318f22b5..dfc920ed98 100644 --- a/net/ax-net/src/udp.rs +++ b/net/ax-net/src/udp.rs @@ -94,6 +94,12 @@ impl UdpSocket { None => Err(AxError::NotConnected), } } + + fn source_for_remote(&self, remote: &IpAddress) -> AxResult { + Ok(get_control() + .select_route_with_binding(remote, self.general.device_binding())? + .source) + } } impl Configurable for UdpSocket { @@ -169,7 +175,9 @@ impl SocketOps for UdpSocket { self.with_smol_socket(|socket| socket.close()); return Err(err); } - self.general.set_device_binding(binding); + if binding.bound_if.is_some() { + self.general.set_device_binding(binding); + } *guard = Some(local_endpoint); info!("UDP socket {}: bound on {}", self.handle, endpoint); @@ -194,13 +202,13 @@ impl SocketOps for UdpSocket { let (src, should_update_binding) = if let Some(local_ep) = *local { if local_ep.addr.is_unspecified() { // Bound to 0.0.0.0, use route decision - (get_control().select_route(&remote_addr.addr)?.source, true) + (self.source_for_remote(&remote_addr.addr)?, true) } else { // Bound to specific IP, use that address and keep interface (local_ep.addr, false) } } else { - (get_control().select_route(&remote_addr.addr)?.source, true) + (self.source_for_remote(&remote_addr.addr)?, true) }; *guard = Some((remote_addr, src)); @@ -242,12 +250,12 @@ impl SocketOps for UdpSocket { // Use bound address if bound to specific IP let src = if let Some(local_ep) = *self.local_addr.read() { if local_ep.addr.is_unspecified() { - get_control().select_route(&addr.addr)?.source + self.source_for_remote(&addr.addr)? } else { local_ep.addr } } else { - get_control().select_route(&addr.addr)?.source + self.source_for_remote(&addr.addr)? }; (addr, src) } @@ -292,12 +300,12 @@ impl SocketOps for UdpSocket { // Use bound address if bound to specific IP, otherwise route decision let src = if let Some(local_ep) = *self.local_addr.read() { if local_ep.addr.is_unspecified() { - get_control().select_route(&addr.addr)?.source + self.source_for_remote(&addr.addr)? } else { local_ep.addr } } else { - get_control().select_route(&addr.addr)?.source + self.source_for_remote(&addr.addr)? }; Some((addr, src)) } @@ -613,4 +621,31 @@ mod tests { } ); } + + #[test] + fn connect_rejects_unroutable_bound_device() { + let _guard = network_test_guard(); + init_split_route_network(); + + let socket = UdpSocket::new(); + socket.bind_device(LOCAL_IF).unwrap(); + socket + .bind(SocketAddrEx::Ip(SocketAddr::new( + IpAddr::V4(Ipv4Addr::UNSPECIFIED), + 0, + ))) + .unwrap(); + + assert!( + socket + .connect(SocketAddrEx::Ip(SocketAddr::new(IpAddr::V4(PEER_ADDR), 53))) + .is_err() + ); + assert_eq!( + socket.general.device_binding(), + DeviceBinding { + bound_if: Some(LOCAL_IF) + } + ); + } } From 613abb8b47c58bbdb0b25753e97d3422ba5f1bad Mon Sep 17 00:00:00 2001 From: ZCShou <72115@163.com> Date: Mon, 15 Jun 2026 13:49:55 +0800 Subject: [PATCH 14/31] fix(ax-net): support per-address TCP/UDP listening and replace poll_interfaces_now with request_poll --- net/ax-net/src/lib.rs | 9 +- net/ax-net/src/listen_table.rs | 156 ++++++++++++++++++++++++++------- net/ax-net/src/orphan.rs | 100 +++++++++++++++------ net/ax-net/src/raw.rs | 4 +- net/ax-net/src/router.rs | 52 ++++++----- net/ax-net/src/service.rs | 37 +++++--- net/ax-net/src/tcp.rs | 46 +++++----- net/ax-net/src/udp.rs | 23 +++-- net/ax-net/src/wrapper.rs | 84 ++++++++++++++++-- 9 files changed, 362 insertions(+), 149 deletions(-) diff --git a/net/ax-net/src/lib.rs b/net/ax-net/src/lib.rs index 16bab0a8e2..984d6d6118 100644 --- a/net/ax-net/src/lib.rs +++ b/net/ax-net/src/lib.rs @@ -395,10 +395,7 @@ fn poll_until_idle() { } while POLL_AGAIN.swap(false, Ordering::AcqRel) { - while poll_once() { - #[cfg(not(test))] - ax_task::yield_now(); - } + while poll_once() {} } POLLING_INTERFACES.store(false, Ordering::Release); if !POLL_AGAIN.load(Ordering::Acquire) { @@ -415,10 +412,6 @@ pub fn poll_interfaces() { request_poll(); } -pub(crate) fn poll_interfaces_now() { - poll_until_idle(); -} - /// Request network polling. /// /// This is the lightweight entry used by socket and device paths. diff --git a/net/ax-net/src/listen_table.rs b/net/ax-net/src/listen_table.rs index 3e03fdc9b6..b30bb95891 100644 --- a/net/ax-net/src/listen_table.rs +++ b/net/ax-net/src/listen_table.rs @@ -1,5 +1,5 @@ use alloc::{boxed::Box, collections::VecDeque, sync::Arc, vec, vec::Vec}; -use core::{ops::DerefMut, task::Waker}; +use core::task::Waker; use ax_errno::{AxError, AxResult}; use ax_sync::Mutex; @@ -71,7 +71,7 @@ impl ListenTableEntryInner { } } -type ListenTableEntry = Arc>>>; +type ListenTableEntry = Arc>>; pub struct ListenTable { tcp: Box<[ListenTableEntry]>, @@ -89,60 +89,84 @@ impl ListenTable { Self { tcp } } - pub fn can_listen(&self, port: u16) -> bool { - self.tcp[port as usize].lock().is_none() + pub fn can_listen(&self, listen_endpoint: IpListenEndpoint) -> bool { + self.tcp[listen_endpoint.port as usize] + .lock() + .iter() + .all(|entry| !listen_addrs_conflict(entry.listen_endpoint.addr, listen_endpoint.addr)) } pub fn listen(&self, listen_endpoint: IpListenEndpoint, backlog: usize) -> AxResult { let port = listen_endpoint.port; assert_ne!(port, 0); - let mut entry = self.tcp[port as usize].lock(); - if entry.is_none() { - *entry = Some(Box::new(ListenTableEntryInner::new( - listen_endpoint, - backlog, - ))); - Ok(()) - } else { - warn!("socket already listening on port {port}"); - Err(AxError::AddrInUse) + let mut entries = self.tcp[port as usize].lock(); + if entries + .iter() + .any(|entry| listen_addrs_conflict(entry.listen_endpoint.addr, listen_endpoint.addr)) + { + warn!("socket already listening on {}", listen_endpoint); + return Err(AxError::AddrInUse); } + entries.push(ListenTableEntryInner::new(listen_endpoint, backlog)); + Ok(()) } - pub fn unlisten(&self, port: u16) { - debug!("TCP socket unlisten on {}", port); - let handles = self.tcp[port as usize] - .lock() - .take() - .map(|entry| (*entry).into_handles()) - .unwrap_or_default(); + pub fn unlisten(&self, listen_endpoint: IpListenEndpoint) { + debug!("TCP socket unlisten on {}", listen_endpoint); + let handles = { + let mut entries = self.tcp[listen_endpoint.port as usize].lock(); + let Some(idx) = entries + .iter() + .position(|entry| entry.listen_endpoint == listen_endpoint) + else { + return; + }; + entries.swap_remove(idx).into_handles() + }; for handle in handles { SOCKET_SET.remove(handle); } } - fn listen_entry(&self, port: u16) -> Arc>>> { + fn listen_entry(&self, port: u16) -> Arc>> { self.tcp[port as usize].clone() } // Callers pass the locked SocketSet to keep the global order: // SERVICE -> SOCKET_SET -> listen entry. - pub fn can_accept(&self, port: u16, sockets: &SocketSet<'_>) -> AxResult { - if let Some(entry) = self.listen_entry(port).lock().as_ref() { - Ok(entry + pub fn can_accept( + &self, + listen_endpoint: IpListenEndpoint, + sockets: &SocketSet<'_>, + ) -> AxResult { + let entries = self.listen_entry(listen_endpoint.port); + let table = entries.lock(); + if let Some(entry) = table + .iter() + .find(|entry| entry.listen_endpoint == listen_endpoint) + { + return Ok(entry .syn_queue .iter() - .any(|pending| is_acceptable(sockets, pending.accepted.handle))) - } else { + .any(|pending| is_acceptable(sockets, pending.accepted.handle))); + } + { warn!("accept before listen"); Err(AxError::InvalidInput) } } - pub fn accept(&self, port: u16, sockets: &mut SocketSet<'_>) -> AxResult { - let entry = self.listen_entry(port); - let mut table = entry.lock(); - let Some(entry) = table.deref_mut() else { + pub fn accept( + &self, + listen_endpoint: IpListenEndpoint, + sockets: &mut SocketSet<'_>, + ) -> AxResult { + let entries = self.listen_entry(listen_endpoint.port); + let mut table = entries.lock(); + let Some(entry) = table + .iter_mut() + .find(|entry| entry.listen_endpoint == listen_endpoint) + else { warn!("accept before listen"); return Err(AxError::InvalidInput); }; @@ -171,8 +195,18 @@ impl ListenTable { Err(AxError::WouldBlock) } - pub fn register_accept_waker(&self, port: u16, sockets: &mut SocketSet<'_>, waker: &Waker) { - if let Some(entry) = self.listen_entry(port).lock().as_ref() { + pub fn register_accept_waker( + &self, + listen_endpoint: IpListenEndpoint, + sockets: &mut SocketSet<'_>, + waker: &Waker, + ) { + let entries = self.listen_entry(listen_endpoint.port); + let table = entries.lock(); + if let Some(entry) = table + .iter() + .find(|entry| entry.listen_endpoint == listen_endpoint) + { entry.accept_poll.register(waker); let accept_waker = Waker::from(entry.accept_poll.clone()); for pending in &entry.syn_queue { @@ -189,7 +223,12 @@ impl ListenTable { dst: IpEndpoint, sockets: &mut SocketSet<'_>, ) { - if let Some(entry) = self.listen_entry(dst.port).lock().deref_mut() { + let entries = self.listen_entry(dst.port); + let mut table = entries.lock(); + if let Some(entry) = table + .iter_mut() + .find(|entry| entry.can_accept_endpoint(dst)) + { if !entry.can_accept_endpoint(dst) { return; } @@ -230,6 +269,13 @@ impl ListenTable { } } +fn listen_addrs_conflict( + a: Option, + b: Option, +) -> bool { + a.is_none() || b.is_none() || a == b +} + fn is_acceptable(sockets: &SocketSet<'_>, handle: SocketHandle) -> bool { let socket: &tcp::Socket = sockets.get(handle); match socket.state() { @@ -243,3 +289,45 @@ fn is_closed_without_data(sockets: &SocketSet<'_>, handle: SocketHandle) -> bool let socket: &tcp::Socket = sockets.get(handle); matches!(socket.state(), State::Closed) && socket.recv_queue() == 0 } + +#[cfg(test)] +mod tests { + use smoltcp::wire::{IpAddress, Ipv4Address}; + + use super::*; + + fn endpoint(addr: Option, port: u16) -> IpListenEndpoint { + IpListenEndpoint { + addr: addr.map(IpAddress::Ipv4), + port, + } + } + + #[test] + fn allows_same_port_on_distinct_specific_addresses() { + let table = ListenTable::new(); + let first = endpoint(Some(Ipv4Address::new(192, 0, 2, 10)), 8080); + let second = endpoint(Some(Ipv4Address::new(198, 51, 100, 20)), 8080); + + assert!(table.can_listen(first)); + table.listen(first, 16).unwrap(); + assert!(table.can_listen(second)); + table.listen(second, 16).unwrap(); + + table.unlisten(first); + assert!(table.can_listen(first)); + assert!(!table.can_listen(second)); + } + + #[test] + fn wildcard_listener_conflicts_with_specific_addresses() { + let table = ListenTable::new(); + let wildcard = endpoint(None, 8081); + let specific = endpoint(Some(Ipv4Address::new(192, 0, 2, 10)), 8081); + + table.listen(wildcard, 16).unwrap(); + + assert!(!table.can_listen(specific)); + assert_eq!(table.listen(specific, 16), Err(AxError::AddrInUse)); + } +} diff --git a/net/ax-net/src/orphan.rs b/net/ax-net/src/orphan.rs index bd5be9d41b..d36337ee0c 100644 --- a/net/ax-net/src/orphan.rs +++ b/net/ax-net/src/orphan.rs @@ -24,6 +24,16 @@ struct OrphanSocket { orphaned_at: Instant, } +impl OrphanSocket { + fn linger_micros(&self, timestamp: Instant) -> i64 { + timestamp.total_micros() - self.orphaned_at.total_micros() + } + + fn linger_expired(&self, timestamp: Instant) -> bool { + self.linger_micros(timestamp) >= ORPHAN_MAX_LINGER + } +} + /// Global orphan socket pool. /// /// Accessed by: @@ -32,6 +42,9 @@ struct OrphanSocket { static ORPHAN_SOCKETS: LazyLock>> = LazyLock::new(|| Mutex::new(Vec::new())); +const ORPHAN_MAX_LINGER: i64 = 60_000_000; // 60 seconds in microseconds +const ORPHAN_MAX_SOCKETS: usize = 1024; + /// Move a TCP socket to the orphan pool. /// /// Called from TcpSocket::drop() after shutdown and endpoint cleanup. @@ -45,28 +58,21 @@ pub(crate) fn add_orphan(handle: SocketHandle, timestamp: Instant) { /// Reap finished orphan sockets. /// /// Called from net-poll worker on every poll cycle. -/// Removes sockets whose background TCP teardown no longer needs stack state. +/// Removes orphan sockets after their background TCP teardown completes. +/// +/// # Removal Conditions +/// +/// - **Closed**: immediate removal (connection fully closed) +/// - **TimeWait**: removed after smoltcp timeout (~10s, max 60s) +/// - **FinWait1/FinWait2/LastAck/Closing**: kept until smoltcp transitions to Closed (max 60s) +/// - **Unexpected states** (Listen/SynSent/Established): force remove after 60s +/// +/// # Overflow Protection +/// +/// If the orphan pool exceeds 1024 sockets, closed or max-linger-expired entries +/// are removed first. Connections still inside the linger window are preserved +/// so normal FIN/TIME_WAIT teardown can complete. pub(crate) fn reap_orphans(timestamp: Instant, sockets: &mut SocketSet<'_>) { - const ORPHAN_MAX_LINGER: i64 = 60_000_000; // 60 seconds in microseconds - const ORPHAN_MAX_SOCKETS: usize = 1024; - - let overflow = { - let mut orphans = ORPHAN_SOCKETS.lock(); - let overflow = orphans.len().saturating_sub(ORPHAN_MAX_SOCKETS); - if overflow == 0 { - Vec::new() - } else { - orphans - .drain(..overflow) - .map(|orphan| orphan.handle) - .collect() - } - }; - for handle in overflow { - sockets.remove(handle); - warn!("Reaped orphan socket {handle} because orphan pool is full"); - } - ORPHAN_SOCKETS.lock().retain(|orphan| { let keep = { let socket = sockets.get_mut::(orphan.handle); @@ -75,19 +81,18 @@ pub(crate) fn reap_orphans(timestamp: Instant, sockets: &mut SocketSet<'_>) { tcp::State::TimeWait => { // TIME_WAIT should expire naturally (smoltcp default: 10s) // But force cleanup after max linger to prevent leaks - let elapsed = timestamp.total_micros() - orphan.orphaned_at.total_micros(); - elapsed < ORPHAN_MAX_LINGER + !orphan.linger_expired(timestamp) } tcp::State::LastAck | tcp::State::FinWait1 | tcp::State::FinWait2 => { - // Still tearing down, keep alive - true + // Still tearing down, but keep a hard resource bound. + !orphan.linger_expired(timestamp) } - tcp::State::Closing => true, + tcp::State::Closing => !orphan.linger_expired(timestamp), _ => { // Unexpected state for orphan (Listen/SynSent/SynReceived/Established) // Force remove after max linger - let elapsed = timestamp.total_micros() - orphan.orphaned_at.total_micros(); - if elapsed >= ORPHAN_MAX_LINGER { + let elapsed = orphan.linger_micros(timestamp); + if orphan.linger_expired(timestamp) { warn!( "Orphan socket {} in unexpected state {:?} after {}s, force removing", orphan.handle, @@ -108,6 +113,45 @@ pub(crate) fn reap_orphans(timestamp: Instant, sockets: &mut SocketSet<'_>) { } keep }); + + reap_overflow(timestamp, sockets, ORPHAN_MAX_SOCKETS); +} + +fn reap_overflow(timestamp: Instant, sockets: &mut SocketSet<'_>, limit: usize) { + let overflow = { + let orphans = ORPHAN_SOCKETS.lock(); + orphans.len().saturating_sub(limit) + }; + if overflow == 0 { + return; + } + + let mut removed = Vec::new(); + ORPHAN_SOCKETS.lock().retain(|orphan| { + if removed.len() >= overflow { + return true; + } + let socket = sockets.get_mut::(orphan.handle); + if socket.state() == tcp::State::Closed || orphan.linger_expired(timestamp) { + removed.push(orphan.handle); + false + } else { + true + } + }); + + for handle in &removed { + sockets.remove(*handle); + warn!("Reaped orphan socket {handle} because orphan pool is full"); + } + + let remaining_overflow = ORPHAN_SOCKETS.lock().len().saturating_sub(limit); + if remaining_overflow > 0 { + warn!( + "Orphan socket pool exceeds limit by {remaining_overflow}; keeping sockets that are \ + still tearing down" + ); + } } /// Get current orphan socket count (for diagnostics). diff --git a/net/ax-net/src/raw.rs b/net/ax-net/src/raw.rs index b30e49e242..ec913c562a 100644 --- a/net/ax-net/src/raw.rs +++ b/net/ax-net/src/raw.rs @@ -31,7 +31,7 @@ use crate::{ general::GeneralOptions, get_control, interface_by_id, options::{Configurable, GetSocketOption, SetSocketOption}, - poll_interfaces_now, request_poll, + request_poll, }; pub(crate) fn new_raw_socket( @@ -374,7 +374,7 @@ impl SocketOps for RawSocket { } Ok(written) })?; - poll_interfaces_now(); + request_poll(); Ok(written) }) } diff --git a/net/ax-net/src/router.rs b/net/ax-net/src/router.rs index d9df17201e..cda7ca7749 100644 --- a/net/ax-net/src/router.rs +++ b/net/ax-net/src/router.rs @@ -486,15 +486,7 @@ impl Router { ) -> bool { let device = &self.devices[dev]; if device.interface_id == InterfaceId::LOOPBACK { - let rx = RxPacket { - interface_id: InterfaceId::LOOPBACK, - bytes: packet.to_vec().into_boxed_slice(), - }; - if self.queues.rx.push(rx).is_err() { - warn!("Loopback: RX queue full, dropping packet to {}", next_hop); - return false; - } - return true; + return inject_loopback_rx(&self.queues.rx, next_hop, packet); } device.enqueue_tx(next_hop, packet) } @@ -559,16 +551,9 @@ impl Router { }; let dev = &self.devices[route.dev]; - // Loopback fast path: inject directly to RX queue if dev.interface_id == InterfaceId::LOOPBACK { - let rx = RxPacket { - interface_id: InterfaceId::LOOPBACK, - bytes: packet.into_inner().to_vec().into_boxed_slice(), - }; - if self.queues.rx.push(rx).is_err() { - warn!("Loopback: RX queue full, dropping packet to {}", dst_addr); - } - poll_next = true; + poll_next |= + inject_loopback_rx(&self.queues.rx, dst_addr, packet.into_inner()); } else { poll_next |= dev.enqueue_tx(route.next_hop, packet.into_inner()); } @@ -599,16 +584,9 @@ impl Router { }; let dev = &self.devices[route.dev]; - // Loopback fast path if dev.interface_id == InterfaceId::LOOPBACK { - let rx = RxPacket { - interface_id: InterfaceId::LOOPBACK, - bytes: packet.into_inner().to_vec().into_boxed_slice(), - }; - if self.queues.rx.push(rx).is_err() { - warn!("Loopback: RX queue full, dropping packet to {}", dst_addr); - } - poll_next = true; + poll_next |= + inject_loopback_rx(&self.queues.rx, dst_addr, packet.into_inner()); } else { poll_next |= dev.enqueue_tx(route.next_hop, packet.into_inner()); } @@ -620,6 +598,26 @@ impl Router { } } +/// Injects a loopback packet into the router RX queue. +/// +/// Returns `true` when the packet was queued; callers should continue polling +/// so smoltcp can immediately consume the injected RX packet. +fn inject_loopback_rx( + rx_queue: &BoundedPacketQueue, + dst_addr: IpAddress, + packet: &[u8], +) -> bool { + let rx = RxPacket { + interface_id: InterfaceId::LOOPBACK, + bytes: packet.to_vec().into_boxed_slice(), + }; + if rx_queue.push(rx).is_err() { + warn!("Loopback: RX queue full, dropping packet to {}", dst_addr); + return false; + } + true +} + fn device_tx_worker(device: Arc) { loop { if let Some(packet) = device.tx_queue.pop() { diff --git a/net/ax-net/src/service.rs b/net/ax-net/src/service.rs index 1ab668ca5c..b39255f8bb 100644 --- a/net/ax-net/src/service.rs +++ b/net/ax-net/src/service.rs @@ -2,45 +2,58 @@ //! //! # Lock Ordering Rules //! -//! To prevent deadlocks, acquire coarser locks before finer locks: +//! To prevent deadlocks, locks must be acquired from outermost to innermost. +//! Never acquire an outer lock while holding an inner lock. +//! +//! **Lock hierarchy (outermost → innermost):** //! //! 1. **SERVICE** (`Mutex`) -//! - Most coarse-grained, protects entire protocol stack +//! - Outermost, protects entire protocol stack //! - Held during `Service::poll()` and waker registration //! //! 2. **SOCKET_SET.inner** (`Mutex`) //! - smoltcp socket set (all TCP/UDP/raw/DNS sockets) //! - Acquired during poll, socket operations, and state queries -//! - Never acquire SERVICE while holding this lock +//! - ⚠️ Never acquire SERVICE while holding this lock //! //! 3. **TCP_BOUND_PORTS** (`Mutex>>`) //! - Tracks TCP bind() registrations //! - Hold duration: registration/unregistration only //! -//! 4. **Per-port LISTEN_TABLE entries** (`Arc>>`) -//! - Most granular, one mutex per port +//! 4. **Per-port LISTEN_TABLE buckets** (`Arc>>`) +//! - Innermost, most granular (one mutex per TCP port) +//! +//! **Acquisition order rule:** +//! ```text +//! SERVICE → SOCKET_SET → TCP_BOUND_PORTS → LISTEN_TABLE +//! (outer) (inner) +//! ``` //! //! # Correct Patterns //! //! ```ignore -//! // ✓ SERVICE → SOCKET_SET (poll path) -//! fn poll_interfaces_now() { -//! get_service().poll(&mut SOCKET_SET.inner.lock()) +//! // ✓ Lightweight trigger: socket paths request the dedicated worker. +//! fn socket_operation() { +//! request_poll() //! } //! -//! // ✓ SOCKET_SET → LISTEN_TABLE (accept readiness check) +//! // ✓ Outer → Inner: SOCKET_SET → LISTEN_TABLE (accept readiness) //! fn TcpSocket::poll_listener() { //! let sockets = SOCKET_SET.inner.lock(); -//! LISTEN_TABLE.can_accept(port, &sockets) +//! LISTEN_TABLE.can_accept(endpoint, &sockets) //! } //! ``` //! //! # Forbidden Patterns //! //! ```ignore -//! // ✗ SOCKET_SET → SERVICE (deadlock risk) +//! // ✗ Inner → Outer: SOCKET_SET → SERVICE (DEADLOCK!) +//! let sockets = SOCKET_SET.inner.lock(); +//! get_service().do_something(); // WRONG: reverse order +//! +//! // ✗ Holding any lock while calling wake() (may re-enter via async I/O) //! let sockets = SOCKET_SET.inner.lock(); -//! get_service().do_something(); // WRONG +//! waker.wake(); // WRONG: potential self-deadlock //! ``` use alloc::{boxed::Box, format, string::String, vec, vec::Vec}; diff --git a/net/ax-net/src/tcp.rs b/net/ax-net/src/tcp.rs index fc38a70c70..0541e1cd02 100644 --- a/net/ax-net/src/tcp.rs +++ b/net/ax-net/src/tcp.rs @@ -27,7 +27,7 @@ use crate::{ general::GeneralOptions, get_control, get_service, interface_by_id, options::{Configurable, GetSocketOption, SetSocketOption, TcpInfo, TcpInfoOptions, TcpState}, - poll_interfaces_now, request_poll, + request_poll, state::*, }; @@ -268,11 +268,11 @@ impl TcpSocket { fn poll_listener(&self) -> IoEvents { let mut events = IoEvents::empty(); - let port = self.bound_endpoint().unwrap().port; + let endpoint = self.bound_endpoint().unwrap(); let sockets = SOCKET_SET.inner.lock(); events.set( IoEvents::IN, - LISTEN_TABLE.can_accept(port, &sockets).unwrap(), + LISTEN_TABLE.can_accept(endpoint, &sockets).unwrap(), ); events } @@ -392,10 +392,9 @@ impl SocketOps for TcpSocket { if local_addr.port() == 0 { local_addr.set_port(get_ephemeral_port()?); } - if !self.general.reuse_address() && !LISTEN_TABLE.can_listen(local_addr.port()) { - return Err(AxError::AddrInUse); + if self.bound_endpoint.lock().port != 0 { + return Err(AxError::InvalidInput); } - let endpoint = IpListenEndpoint { addr: if local_addr.ip().is_unspecified() { None @@ -404,8 +403,8 @@ impl SocketOps for TcpSocket { }, port: local_addr.port(), }; - if self.bound_endpoint.lock().port != 0 { - return Err(AxError::InvalidInput); + if !self.general.reuse_address() && !LISTEN_TABLE.can_listen(endpoint) { + return Err(AxError::AddrInUse); } let binding = get_control().local_binding_for(&endpoint)?; self.register_bound_endpoint(endpoint)?; @@ -421,13 +420,11 @@ impl SocketOps for TcpSocket { fn connect(&self, remote_addr: SocketAddrEx) -> AxResult { let remote_addr = remote_addr.into_ip()?; self.start_connect(remote_addr)?; - - // Hack: let the server listen - ax_task::yield_now(); + request_poll(); // Here our state must be `CONNECTING`, and only one thread can run here. self.general.send_poller(self, || { - poll_interfaces_now(); + request_poll(); let events = self.poll_connect(); if !events.contains(IoEvents::OUT) { Err(AxError::WouldBlock) @@ -478,12 +475,12 @@ impl SocketOps for TcpSocket { ax_bail!(InvalidInput, "not listening"); } - let bound_port = self.bound_endpoint()?.port; + let bound_endpoint = self.bound_endpoint()?; self.general.recv_poller(self, || { - poll_interfaces_now(); + request_poll(); let accepted = { let mut sockets = SOCKET_SET.inner.lock(); - LISTEN_TABLE.accept(bound_port, &mut sockets)? + LISTEN_TABLE.accept(bound_endpoint, &mut sockets)? }; Ok({ let socket = TcpSocket::new_connected( @@ -504,7 +501,7 @@ impl SocketOps for TcpSocket { // SAFETY: `self.handle` should be initialized in a connected socket. let extra_nb = options.flags.contains(crate::SendFlags::DONTWAIT); let result = self.general.send_poller_with(self, extra_nb, || { - poll_interfaces_now(); + request_poll(); self.with_smol_socket(|socket| { if !socket.is_active() { Err(AxError::NotConnected) @@ -523,11 +520,8 @@ impl SocketOps for TcpSocket { } }) }); - // Poll again after writing so the data is transmitted through the - // network stack immediately. For loopback, this causes loopback.send() - // to run, which wakes any epoll wakers registered on the peer socket. if result.is_ok() { - poll_interfaces_now(); + request_poll(); } result } @@ -541,7 +535,7 @@ impl SocketOps for TcpSocket { } let extra_nb = options.flags.contains(RecvFlags::DONTWAIT); self.general.recv_poller_with(self, extra_nb, || { - poll_interfaces_now(); + request_poll(); self.with_smol_socket(|socket| { if socket.recv_queue() > 0 { if options.flags.contains(RecvFlags::PEEK) { @@ -588,7 +582,7 @@ impl SocketOps for TcpSocket { if available > 0 { return Ok(available); } - poll_interfaces_now(); + request_poll(); Ok(self.with_smol_socket(|socket| socket.recv_queue())) } @@ -651,7 +645,7 @@ impl SocketOps for TcpSocket { // listener if let Ok(guard) = self.state.lock(State::Listening) { guard.transit(State::Closed, || { - LISTEN_TABLE.unlisten(self.bound_endpoint()?.port); + LISTEN_TABLE.unlisten(self.bound_endpoint()?); self.unregister_bound_endpoint(); *self.bound_endpoint.lock() = empty_endpoint(); request_poll(); @@ -681,8 +675,9 @@ impl Pollable for TcpSocket { if self.is_listening() && events.intersects(IoEvents::IN | IoEvents::RDHUP) { let port = self.bound_endpoint.lock().port; if port != 0 { + let endpoint = *self.bound_endpoint.lock(); let mut sockets = SOCKET_SET.inner.lock(); - LISTEN_TABLE.register_accept_waker(port, &mut sockets, context.waker()); + LISTEN_TABLE.register_accept_waker(endpoint, &mut sockets, context.waker()); } } self.with_smol_socket(|socket| { @@ -912,7 +907,8 @@ fn unregister_tcp_bound(endpoint: IpListenEndpoint) { } fn tcp_port_available(port: u16) -> bool { - LISTEN_TABLE.can_listen(port) && !TCP_BOUND_PORTS.lock().contains_key(&port) + LISTEN_TABLE.can_listen(IpListenEndpoint { addr: None, port }) + && !TCP_BOUND_PORTS.lock().contains_key(&port) } fn listen_addrs_conflict( diff --git a/net/ax-net/src/udp.rs b/net/ax-net/src/udp.rs index dfc920ed98..b8170eed04 100644 --- a/net/ax-net/src/udp.rs +++ b/net/ax-net/src/udp.rs @@ -24,7 +24,7 @@ use crate::{ general::GeneralOptions, get_control, interface_by_id, options::{Configurable, GetSocketOption, SetSocketOption}, - poll_interfaces_now, request_poll, + request_poll, }; /// Buffered state for MSG_MORE corking: captures the target endpoint @@ -399,7 +399,7 @@ impl SocketOps for UdpSocket { Ok(cur_read) } })?; - poll_interfaces_now(); + request_poll(); Ok(result) }) } @@ -545,13 +545,20 @@ fn get_ephemeral_port() -> AxResult { static CURR: Mutex = Mutex::new(PORT_START); let mut curr = CURR.lock(); - let port = *curr; - if *curr == PORT_END { - *curr = PORT_START; - } else { - *curr += 1; + let mut tries = 0; + while tries <= PORT_END - PORT_START { + let port = *curr; + if *curr == PORT_END { + *curr = PORT_START; + } else { + *curr += 1; + } + if SOCKET_SET.udp_port_available(IpAddress::Ipv4(Ipv4Addr::UNSPECIFIED), port) { + return Ok(port); + } + tries += 1; } - Ok(port) + ax_bail!(AddrInUse, "no available ports") } #[cfg(test)] diff --git a/net/ax-net/src/wrapper.rs b/net/ax-net/src/wrapper.rs index 584b54f870..1a6cfe61e0 100644 --- a/net/ax-net/src/wrapper.rs +++ b/net/ax-net/src/wrapper.rs @@ -56,18 +56,25 @@ impl<'a> SocketSetWrapper<'a> { addr: (!addr.is_unspecified()).then_some(addr), port, }; - let wildcard = UdpBindKey { addr: None, port }; let mut binds = self.udp_binds.lock(); - if binds.contains_key(&key) || (key.addr.is_some() && binds.contains_key(&wildcard)) { - return Err(AxError::AddrInUse); - } - if key.addr.is_none() && binds.keys().any(|bind| bind.port == port) { + if !udp_bind_available(&binds, key) { return Err(AxError::AddrInUse); } binds.insert(key, handle); Ok(()) } + pub fn udp_port_available(&self, addr: IpAddress, port: u16) -> bool { + if port == 0 { + return true; + } + let key = UdpBindKey { + addr: (!addr.is_unspecified()).then_some(addr), + port, + }; + udp_bind_available(&self.udp_binds.lock(), key) + } + pub fn udp_unbind(&self, handle: SocketHandle) { self.udp_binds .lock() @@ -80,3 +87,70 @@ impl<'a> SocketSetWrapper<'a> { debug!("socket {}: destroyed", handle); } } + +fn udp_bind_available(binds: &HashMap, key: UdpBindKey) -> bool { + let wildcard = UdpBindKey { + addr: None, + port: key.port, + }; + if binds.contains_key(&key) || (key.addr.is_some() && binds.contains_key(&wildcard)) { + return false; + } + key.addr.is_some() || !binds.keys().any(|bind| bind.port == key.port) +} + +#[cfg(test)] +mod tests { + use alloc::vec; + + use smoltcp::{ + iface::SocketSet, + socket::udp, + storage::PacketMetadata, + wire::{IpAddress, Ipv4Address}, + }; + + use super::*; + + fn key(addr: Option, port: u16) -> UdpBindKey { + UdpBindKey { + addr: addr.map(IpAddress::Ipv4), + port, + } + } + + fn handle() -> SocketHandle { + let mut sockets = SocketSet::new(vec![]); + sockets.add(udp::Socket::new( + udp::PacketBuffer::new(vec![PacketMetadata::EMPTY; 1], vec![0; 8]), + udp::PacketBuffer::new(vec![PacketMetadata::EMPTY; 1], vec![0; 8]), + )) + } + + #[test] + fn udp_bind_rules_allow_distinct_specific_addresses() { + let mut binds = HashMap::new(); + binds.insert(key(Some(Ipv4Address::new(192, 0, 2, 10)), 5353), handle()); + + assert!(udp_bind_available( + &binds, + key(Some(Ipv4Address::new(198, 51, 100, 20)), 5353) + )); + assert!(!udp_bind_available( + &binds, + key(Some(Ipv4Address::new(192, 0, 2, 10)), 5353) + )); + assert!(!udp_bind_available(&binds, key(None, 5353))); + } + + #[test] + fn udp_bind_rules_reject_specific_after_wildcard() { + let mut binds = HashMap::new(); + binds.insert(key(None, 5354), handle()); + + assert!(!udp_bind_available( + &binds, + key(Some(Ipv4Address::new(192, 0, 2, 10)), 5354) + )); + } +} From 72bf89025251f8b68ad7559ffabb1f725cd5cf1d Mon Sep 17 00:00:00 2001 From: ZCShou <72115@163.com> Date: Mon, 15 Jun 2026 14:11:04 +0800 Subject: [PATCH 15/31] fix(ax-net): refactor orphan reaping and add TCP snooping on loopback dispatch --- net/ax-net/src/orphan.rs | 83 ++++++++++++++++++--------------------- net/ax-net/src/router.rs | 33 +++++++++++++--- net/ax-net/src/service.rs | 2 +- 3 files changed, 67 insertions(+), 51 deletions(-) diff --git a/net/ax-net/src/orphan.rs b/net/ax-net/src/orphan.rs index d36337ee0c..fcfb7f76e9 100644 --- a/net/ax-net/src/orphan.rs +++ b/net/ax-net/src/orphan.rs @@ -34,6 +34,12 @@ impl OrphanSocket { } } +#[derive(Clone, Copy)] +enum ReapReason { + Closed, + Expired, +} + /// Global orphan socket pool. /// /// Accessed by: @@ -73,21 +79,30 @@ pub(crate) fn add_orphan(handle: SocketHandle, timestamp: Instant) { /// are removed first. Connections still inside the linger window are preserved /// so normal FIN/TIME_WAIT teardown can complete. pub(crate) fn reap_orphans(timestamp: Instant, sockets: &mut SocketSet<'_>) { - ORPHAN_SOCKETS.lock().retain(|orphan| { - let keep = { + let mut removed = Vec::new(); + let remaining_overflow = { + let mut orphans = ORPHAN_SOCKETS.lock(); + orphans.retain(|orphan| { let socket = sockets.get_mut::(orphan.handle); - match socket.state() { - tcp::State::Closed => false, // Fully closed, safe to remove + let state = socket.state(); + let reason = match state { + tcp::State::Closed => Some(ReapReason::Closed), tcp::State::TimeWait => { // TIME_WAIT should expire naturally (smoltcp default: 10s) // But force cleanup after max linger to prevent leaks - !orphan.linger_expired(timestamp) + orphan + .linger_expired(timestamp) + .then_some(ReapReason::Expired) } tcp::State::LastAck | tcp::State::FinWait1 | tcp::State::FinWait2 => { // Still tearing down, but keep a hard resource bound. - !orphan.linger_expired(timestamp) + orphan + .linger_expired(timestamp) + .then_some(ReapReason::Expired) } - tcp::State::Closing => !orphan.linger_expired(timestamp), + tcp::State::Closing => orphan + .linger_expired(timestamp) + .then_some(ReapReason::Expired), _ => { // Unexpected state for orphan (Listen/SynSent/SynReceived/Established) // Force remove after max linger @@ -99,53 +114,31 @@ pub(crate) fn reap_orphans(timestamp: Instant, sockets: &mut SocketSet<'_>) { socket.state(), elapsed / 1_000_000 ); - false + Some(ReapReason::Expired) } else { - true + None } } - } - }; + }; - if !keep { - sockets.remove(orphan.handle); - debug!("Reaped orphan socket {}", orphan.handle); - } - keep - }); - - reap_overflow(timestamp, sockets, ORPHAN_MAX_SOCKETS); -} - -fn reap_overflow(timestamp: Instant, sockets: &mut SocketSet<'_>, limit: usize) { - let overflow = { - let orphans = ORPHAN_SOCKETS.lock(); - orphans.len().saturating_sub(limit) + if let Some(reason) = reason { + removed.push((orphan.handle, reason)); + false + } else { + true + } + }); + orphans.len().saturating_sub(ORPHAN_MAX_SOCKETS) }; - if overflow == 0 { - return; - } - let mut removed = Vec::new(); - ORPHAN_SOCKETS.lock().retain(|orphan| { - if removed.len() >= overflow { - return true; + for (handle, reason) in removed { + sockets.remove(handle); + match reason { + ReapReason::Closed => debug!("Reaped closed orphan socket {}", handle), + ReapReason::Expired => warn!("Reaped expired orphan socket {}", handle), } - let socket = sockets.get_mut::(orphan.handle); - if socket.state() == tcp::State::Closed || orphan.linger_expired(timestamp) { - removed.push(orphan.handle); - false - } else { - true - } - }); - - for handle in &removed { - sockets.remove(*handle); - warn!("Reaped orphan socket {handle} because orphan pool is full"); } - let remaining_overflow = ORPHAN_SOCKETS.lock().len().saturating_sub(limit); if remaining_overflow > 0 { warn!( "Orphan socket pool exceeds limit by {remaining_overflow}; keeping sockets that are \ diff --git a/net/ax-net/src/router.rs b/net/ax-net/src/router.rs index cda7ca7749..0f488510d6 100644 --- a/net/ax-net/src/router.rs +++ b/net/ax-net/src/router.rs @@ -522,7 +522,7 @@ impl Router { } } - pub fn dispatch(&mut self, _timestamp: Instant) -> bool { + pub fn dispatch(&mut self, _timestamp: Instant, sockets: &mut SocketSet<'_>) -> bool { let mut poll_next = false; while let Ok((_, packet)) = self.tx_buffer.dequeue() { match IpVersion::of_packet(packet).expect("got invalid IP packet") { @@ -552,8 +552,12 @@ impl Router { let dev = &self.devices[route.dev]; if dev.interface_id == InterfaceId::LOOPBACK { - poll_next |= - inject_loopback_rx(&self.queues.rx, dst_addr, packet.into_inner()); + poll_next |= inject_loopback_rx_direct( + &mut self.rx_buffer, + dst_addr, + packet.into_inner(), + sockets, + ); } else { poll_next |= dev.enqueue_tx(route.next_hop, packet.into_inner()); } @@ -585,8 +589,12 @@ impl Router { let dev = &self.devices[route.dev]; if dev.interface_id == InterfaceId::LOOPBACK { - poll_next |= - inject_loopback_rx(&self.queues.rx, dst_addr, packet.into_inner()); + poll_next |= inject_loopback_rx_direct( + &mut self.rx_buffer, + dst_addr, + packet.into_inner(), + sockets, + ); } else { poll_next |= dev.enqueue_tx(route.next_hop, packet.into_inner()); } @@ -598,6 +606,21 @@ impl Router { } } +fn inject_loopback_rx_direct( + rx_buffer: &mut PacketBuffer, + dst_addr: IpAddress, + packet: &[u8], + sockets: &mut SocketSet<'_>, +) -> bool { + snoop_tcp_packet(packet, sockets); + let Ok(dst) = rx_buffer.enqueue(packet.len(), InterfaceId::LOOPBACK) else { + warn!("Loopback: RX buffer full, dropping packet to {}", dst_addr); + return false; + }; + dst.copy_from_slice(packet); + true +} + /// Injects a loopback packet into the router RX queue. /// /// Returns `true` when the packet was queued; callers should continue polling diff --git a/net/ax-net/src/service.rs b/net/ax-net/src/service.rs index b39255f8bb..c3642a3f5c 100644 --- a/net/ax-net/src/service.rs +++ b/net/ax-net/src/service.rs @@ -693,7 +693,7 @@ impl Service { // Reap orphaned TCP sockets using the SocketSet already held by poll_once(). crate::orphan::reap_orphans(timestamp, sockets); - self.router.dispatch(timestamp) + self.router.dispatch(timestamp, sockets) || dhcp_poll_next || dhcp_server_sent || socket_state_changed From de8c66d96cbf6d34fd19131980db4d983a8ddba4 Mon Sep 17 00:00:00 2001 From: ZCShou <72115@163.com> Date: Mon, 15 Jun 2026 14:13:18 +0800 Subject: [PATCH 16/31] fix(ax-net): remove redundant check and unused new_socket event, clarify ephemeral port comment --- net/ax-net/src/listen_table.rs | 3 --- net/ax-net/src/tcp.rs | 2 ++ net/ax-net/src/wrapper.rs | 4 ---- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/net/ax-net/src/listen_table.rs b/net/ax-net/src/listen_table.rs index b30bb95891..1d326b2a21 100644 --- a/net/ax-net/src/listen_table.rs +++ b/net/ax-net/src/listen_table.rs @@ -229,9 +229,6 @@ impl ListenTable { .iter_mut() .find(|entry| entry.can_accept_endpoint(dst)) { - if !entry.can_accept_endpoint(dst) { - return; - } if entry.syn_queue.len() >= entry.backlog { // SYN queue is full, drop the packet warn!("SYN queue overflow!"); diff --git a/net/ax-net/src/tcp.rs b/net/ax-net/src/tcp.rs index 0541e1cd02..7b082f9ad7 100644 --- a/net/ax-net/src/tcp.rs +++ b/net/ax-net/src/tcp.rs @@ -907,6 +907,8 @@ fn unregister_tcp_bound(endpoint: IpListenEndpoint) { } fn tcp_port_available(port: u16) -> bool { + // Ephemeral ports are selected conservatively: avoid any port that has a + // listener or bound socket on any local address. LISTEN_TABLE.can_listen(IpListenEndpoint { addr: None, port }) && !TCP_BOUND_PORTS.lock().contains_key(&port) } diff --git a/net/ax-net/src/wrapper.rs b/net/ax-net/src/wrapper.rs index 1a6cfe61e0..8a6a4ff604 100644 --- a/net/ax-net/src/wrapper.rs +++ b/net/ax-net/src/wrapper.rs @@ -2,7 +2,6 @@ use alloc::vec; use ax_errno::{AxError, AxResult}; use ax_sync::Mutex; -use event_listener::Event; use hashbrown::HashMap; use smoltcp::{ iface::{SocketHandle, SocketSet}, @@ -18,7 +17,6 @@ struct UdpBindKey { pub(crate) struct SocketSetWrapper<'a> { pub inner: Mutex>, - pub new_socket: Event, udp_binds: Mutex>, } @@ -26,7 +24,6 @@ impl<'a> SocketSetWrapper<'a> { pub fn new() -> Self { Self { inner: Mutex::new(SocketSet::new(vec![])), - new_socket: Event::new(), udp_binds: Mutex::new(HashMap::new()), } } @@ -34,7 +31,6 @@ impl<'a> SocketSetWrapper<'a> { pub fn add>(&self, socket: T) -> SocketHandle { let handle = self.inner.lock().add(socket); debug!("socket {}: created", handle); - self.new_socket.notify(1); handle } From 09698f7aa974312182b25e9ebc9170049dfbc5df Mon Sep 17 00:00:00 2001 From: ZCShou <72115@163.com> Date: Mon, 15 Jun 2026 14:40:46 +0800 Subject: [PATCH 17/31] fix(ax-net): replace heap-allocated queued packets with fixed-size inline storage --- net/ax-net/src/router.rs | 86 +++++++++++++++++++++++++++++++--------- 1 file changed, 68 insertions(+), 18 deletions(-) diff --git a/net/ax-net/src/router.rs b/net/ax-net/src/router.rs index 0f488510d6..44551e33f2 100644 --- a/net/ax-net/src/router.rs +++ b/net/ax-net/src/router.rs @@ -121,12 +121,39 @@ impl BoundedPacketQueue { struct TxPacket { next_hop: IpAddress, - bytes: Box<[u8]>, + bytes: QueuedPacket, } struct RxPacket { interface_id: InterfaceId, - bytes: Box<[u8]>, + bytes: QueuedPacket, +} + +/// Fixed-size packet storage for bounded router queues. +/// +/// Keeping packets inline avoids per-packet heap allocation while preserving a +/// predictable memory ceiling from the queue capacity constants. +struct QueuedPacket { + bytes: [u8; STANDARD_MTU], + len: usize, +} + +impl QueuedPacket { + fn new(packet: &[u8]) -> Option { + if packet.len() > STANDARD_MTU { + return None; + } + let mut bytes = [0; STANDARD_MTU]; + bytes[..packet.len()].copy_from_slice(packet); + Some(Self { + bytes, + len: packet.len(), + }) + } + + fn as_slice(&self) -> &[u8] { + &self.bytes[..self.len] + } } struct RouterQueues { @@ -166,10 +193,16 @@ impl DeviceHandle { } fn enqueue_tx(&self, next_hop: IpAddress, packet: &[u8]) -> bool { - let tx = TxPacket { - next_hop, - bytes: packet.to_vec().into_boxed_slice(), + let Some(bytes) = QueuedPacket::new(packet) else { + warn!( + "{}: packet to {} exceeds MTU ({} bytes), dropping", + self.name, + next_hop, + packet.len() + ); + return false; }; + let tx = TxPacket { next_hop, bytes }; if self.tx_queue.push(tx).is_err() { warn!( "{}: TX queue is full, dropping packet to {}", @@ -464,16 +497,14 @@ impl Router { let Some(packet) = self.queues.rx.pop() else { break; }; - snoop_tcp_packet(&packet.bytes, sockets); - snoop(packet.interface_id, &packet.bytes); - let Ok(dst) = self - .rx_buffer - .enqueue(packet.bytes.len(), packet.interface_id) - else { + let bytes = packet.bytes.as_slice(); + snoop_tcp_packet(bytes, sockets); + snoop(packet.interface_id, bytes); + let Ok(dst) = self.rx_buffer.enqueue(bytes.len(), packet.interface_id) else { warn!("Router RX buffer is full, dropping packet"); break; }; - dst.copy_from_slice(&packet.bytes); + dst.copy_from_slice(bytes); } } @@ -630,9 +661,17 @@ fn inject_loopback_rx( dst_addr: IpAddress, packet: &[u8], ) -> bool { + let Some(bytes) = QueuedPacket::new(packet) else { + warn!( + "Loopback: packet to {} exceeds MTU ({} bytes), dropping", + dst_addr, + packet.len() + ); + return false; + }; let rx = RxPacket { interface_id: InterfaceId::LOOPBACK, - bytes: packet.to_vec().into_boxed_slice(), + bytes, }; if rx_queue.push(rx).is_err() { warn!("Loopback: RX queue full, dropping packet to {}", dst_addr); @@ -644,10 +683,11 @@ fn inject_loopback_rx( fn device_tx_worker(device: Arc) { loop { if let Some(packet) = device.tx_queue.pop() { - let poll_next = device - .inner - .lock() - .send(packet.next_hop, &packet.bytes, now()); + let poll_next = + device + .inner + .lock() + .send(packet.next_hop, packet.bytes.as_slice(), now()); if poll_next { crate::request_poll(); } @@ -675,7 +715,17 @@ fn device_rx_worker(device: Arc) { while let Ok((interface_id, packet)) = rx_buffer.dequeue() { let rx = RxPacket { interface_id, - bytes: packet.to_vec().into_boxed_slice(), + bytes: match QueuedPacket::new(packet) { + Some(bytes) => bytes, + None => { + warn!( + "{}: RX packet exceeds MTU ({} bytes), dropping", + device.name, + packet.len() + ); + continue; + } + }, }; if device.rx_queue.push(rx).is_err() { warn!("{}: RX queue is full, dropping packet", device.name); From a2cddc9cc39fd24cd999b6aa500d1c9bbd7b2033 Mon Sep 17 00:00:00 2001 From: ZCShou <72115@163.com> Date: Mon, 15 Jun 2026 14:56:50 +0800 Subject: [PATCH 18/31] docs(net): document loopback fast path, orphan reaping, DHCP server, and OOB RX support --- docs/docs/architecture/net/api.md | 30 ++++++- docs/docs/architecture/net/architecture.md | 97 +++++++++++++--------- docs/docs/architecture/net/flows.md | 39 +++++---- docs/docs/architecture/net/overview.md | 7 +- 4 files changed, 118 insertions(+), 55 deletions(-) diff --git a/docs/docs/architecture/net/api.md b/docs/docs/architecture/net/api.md index 539821a71b..0e384815d8 100644 --- a/docs/docs/architecture/net/api.md +++ b/docs/docs/architecture/net/api.md @@ -366,7 +366,35 @@ let udp = ax_net::udp::UdpSocket::new(); udp.bind_device(eth1.id)?; ``` -绑定具体本地地址时,`ax-net` 会根据地址所属接口设置 `DeviceBinding`。例如 `UdpSocket::bind()` 调用 `get_control().local_binding_for(&endpoint)?`([udp.rs#L157](net/ax-net/src/udp.rs#L157))从监听地址反查接口。未绑定接口的 connect/sendto 会按 route decision 自动选择源地址和出接口。 +绑定具体本地地址时,`ax-net` 会根据地址所属接口设置 `DeviceBinding`。例如 `UdpSocket::bind()` 调用 `get_control().local_binding_for(&endpoint)?`([udp.rs](net/ax-net/src/udp.rs))从监听地址反查接口。未绑定接口的 connect/sendto 会按 route decision 自动选择源地址和出接口。 + +## 动态设备注册与 OOB RX + +用于运行时添加 Ethernet 设备(如 Wi-Fi AP 模式启动后),以及 SDIO Wi-Fi 等 out-of-band RX 设备: + +```rust +pub struct NetConfig { + pub name: String, + pub ip: [u8; 4], + pub prefix_len: u8, + pub dhcp_server_client_ip: Option<[u8; 4]>, + pub dedicated_poll: bool, // true = 使用 notify_oob_rx() 驱动 RX +} + +pub fn register_device_with_config(dev: Box, config: NetConfig); +pub fn notify_oob_rx(); // SDIO Wi-Fi 收到数据后调用 +pub fn eth0_ipv4_config() -> Option; +``` + +`register_device_with_config()` 在 `Service` 中调用 `register_static_device()` 添加设备和路由,可选启用 DHCP server(`enable_dhcp_server()`)。`dedicated_poll = true` 时设备用 `EthernetDevice::new_oob_rx()` 创建,RX 就绪由驱动线程调用 `notify_oob_rx()` 唤醒独立 poll task(`{ifname}-oob-poll`),不经过 Ethernet IRQ 框架。 + +## Per-address Listen 支持 + +`ListenTable` 和 `SocketSetWrapper` 支持 Linux 语义的同端口不同地址 listen/bind: + +- `LISTEN_TABLE.listen(endpoint, backlog)`:`ListenTableEntry` 是 `Vec`,每个 entry 存储完整的 `IpListenEndpoint`。 +- `listen_addrs_conflict(a, b)`:wildcard(`None`)与所有地址冲突;两个 `Some(addr)` 仅当相等时冲突。 +- `SocketSetWrapper::udp_bind_available()`:UDP 同端口不同地址允许共存,wildcard 与所有冲突。 ## TCP listen/accept diff --git a/docs/docs/architecture/net/architecture.md b/docs/docs/architecture/net/architecture.md index fa8844c932..e6ac17a5ca 100644 --- a/docs/docs/architecture/net/architecture.md +++ b/docs/docs/architecture/net/architecture.md @@ -80,6 +80,8 @@ flowchart TB | [state.rs](net/ax-net/src/state.rs) | socket 状态机锁 | `StateLock`, `StateGuard` | | [listen_table.rs](net/ax-net/src/listen_table.rs) | TCP listen/accept 表与 SYN 预创建 | `ListenTable` | | [tcp.rs](net/ax-net/src/tcp.rs) / [udp.rs](net/ax-net/src/udp.rs) / [raw.rs](net/ax-net/src/raw.rs) | IP socket 实现 | `TcpSocket`, `UdpSocket`, `RawSocket` | +| [orphan.rs](net/ax-net/src/orphan.rs) | TCP orphan socket 回收(RFC 793 TIME_WAIT) | `add_orphan`, `reap_orphans` | +| [dhcp_server.rs](net/ax-net/src/dhcp_server.rs) | 最简 DHCP 服务器(SoftAP 模式) | `DhcpServer` | | [unix/](net/ax-net/src/unix/) | Unix domain socket | `UnixSocket`, `Transport` | | [vsock/](net/ax-net/src/vsock/) | 可选 vsock 支持(`vsock` feature) | `VsockSocket`, `VsockTransport` | | [device/](net/ax-net/src/device/) | loopback、Ethernet、rd-net、vsock 设备适配 | `Device`, `EthernetDevice`, `RdNetDriver` | @@ -190,18 +192,17 @@ pub fn select_route_if(&self, dst, mut is_usable: impl FnMut(InterfaceId) -> boo ```rust pub(crate) struct SocketSetWrapper<'a> { pub inner: Mutex>, // smoltcp SocketSet - pub new_socket: Event, // 通知 accept()/poll() 有新 socket udp_binds: Mutex>, // UDP 端口占用表 } ``` -UDP bind 仲裁规则([wrapper.rs#L59-L71](net/ax-net/src/wrapper.rs#L59-L71)): +UDP bind 仲裁规则([wrapper.rs](net/ax-net/src/wrapper.rs) `udp_bind_available()`): - **精确 bind**(如 `192.168.1.1:53`):如果已有同地址同端口则拒绝;如果有 wildcard `0.0.0.0:53` 则拒绝。 - **Wildcard bind**(`0.0.0.0:53`):如果任何地址已占用同一端口则拒绝。 - **`SO_REUSEADDR`**:设置后跳过仲裁。 -`add()` 方法调用 `self.new_socket.notify(1)`,用于在 smoltcp 内部唤醒等待 `SocketSet` 变更的轮询者(smoltcp `wait`/`wake` 机制通过此 Event 传递)。 +`udp_port_available()` 暴露给 ephemeral port 分配器(UDP 和 TCP),避免分配到已占用的端口。 ## TCP 端口绑定仲裁 @@ -367,12 +368,12 @@ RX 队列是所有设备共享的(`RouterQueues::rx`),因为 smoltcp 从 ### TX Dispatch -`Router::dispatch()`([router.rs#L523-L565](net/ax-net/src/router.rs#L523-L565))从 `tx_buffer` 取出 smoltcp 发出的 IP 包: +`Router::dispatch()`([router.rs](net/ax-net/src/router.rs))接收 `&mut SocketSet`,从 `tx_buffer` 取出 smoltcp 发出的 IP 包: - **IPv4 广播**(dst=255.255.255.255):复制到所有非 loopback Ethernet 设备。 -- **IPv4 单播**:按 `select_route_for_source()` 选路由(要求源地址匹配),将包推入对应设备 TX 队列。 +- **IPv4 单播**:按 `select_route_for_source()` 选路由(要求源地址匹配)。如果出接口是 loopback,走快速路径 `inject_loopback_rx_direct()`(snoop TCP + 直接写入 `rx_buffer`);否则推入对应设备 TX 队列。 - **IPv6 多播**:复制到所有非 loopback Ethernet 设备。 -- **IPv6 单播**:按 `select_route()` 选路由。 +- **IPv6 单播**:按 `select_route_for_source()` 选路由,同样有 loopback 快速路径。 ### RX 数据流 @@ -437,47 +438,48 @@ IRQ 处理链: `RdNetDriver`([driver.rs](net/ax-net/src/device/driver.rs))是 `EthernetDriver` trait 的 `rd-net` 适配实现。内部持有 `rd_net::TxQueue` 和 `rd_net::RxQueue`,通过预取 `RX_PREFETCH_TARGET=1` 个包到 `pending_rx: VecDeque` 减少锁竞争。`alloc_tx_buffer()` 返回 `VecTxBuffer`(栈友好),`transmit()` 执行硬件发送。 -## Loopback 设备 +## Loopback 快速路径 -`LoopbackDevice`([loopback.rs](net/ax-net/src/device/loopback.rs))实现本地回环: +`LoopbackDevice`([loopback.rs](net/ax-net/src/device/loopback.rs))是一个零状态占位类型(`pub struct LoopbackDevice;`),其 `Device` trait 实现全部是 no-op。真正的 loopback 数据路径在 `Router` 中以**快速路径**实现,绕过设备 worker 和 `LoopbackDevice::send()/recv()`: -- `send()`:将 IP 包写入内部 `PacketBuffer`(64 槽 × 1500 字节),调用 `poll.wake()` 通知 poller。 -- `recv()`:从内部 buffer 出队,将 IP 包写入 smoltcp `rx_buffer`。不封装/解封装 Ethernet 帧。 -- `register_waker()`:将 waker 注册到内部 `PollSet`。 +- **TX 方向**(`Router::dispatch()`,[router.rs](net/ax-net/src/router.rs)):当路由决策选中 loopback 接口时,调用 `inject_loopback_rx_direct()` 直接将 IP 包写入 `Router.rx_buffer`(smoltcp 的 RX token 来源),并在写入前执行 `snoop_tcp_packet()` 预创建 TCP listen socket。这样回环包在**同一个 `Service::poll()` 周期内**被 smoltcp 消费,无需等待设备 worker 调度。 +- **DHCP/特殊发包**(`Router::send_on_device()`):对 loopback 设备调用 `inject_loopback_rx()` 写入共享 `RouterQueues::rx` 有界队列,由下一轮 `Router::poll()` 消费。 +- **Worker 跳过**:`start_tx_workers()` 和 `start_rx_workers()` 显式跳过 `InterfaceId::LOOPBACK`,不为 loopback 创建线程。 + +这种设计避免了在 loopback 路径上引入设备线程调度延迟,对 riscv64 单核等慢速目标尤其重要。 ## 数据面 poll 流程 -`Service::poll()`([service.rs#L494-L509](net/ax-net/src/service.rs#L494-L509))在每个 poll 周期执行: +`Service::poll()`([service.rs](net/ax-net/src/service.rs))在每个 poll 周期执行: ```rust pub fn poll(&mut self, sockets: &mut SocketSet) -> bool { let timestamp = now(); let mut dhcp_events = Vec::new(); - // 1. 唤醒所有 RX worker,让它们有机会推新数据到队列 - self.router.wake_rx_workers(); - // 2. Router::poll():从 RX 队列消费包到 smoltcp buffer,同时分发 DHCP 包 - { - let dhcp = &mut self.dhcp; - self.router.poll(timestamp, sockets, |interface_id, packet| { - for state in dhcp.iter_mut() { - if let Some(event) = state.process_packet(interface_id, packet, timestamp) { - dhcp_events.push(event); - } - } - }); - } - // 3. 处理 DHCP 事件(更新地址、路由、DNS) + let mut dhcp_server_replies = Vec::new(); + // 1. Router::poll():从 RX 队列消费包到 smoltcp buffer,同时 snoop 回调分发 + // DHCP client 和 DHCP server 的入站包 + self.router.poll(timestamp, sockets, |interface_id, packet| { + // DHCP client: DhcpState::process_packet() + // DHCP server: DhcpServer::process_packet() -> reply bytes + }); + // 2. 处理 DHCP client 事件(更新地址、路由、DNS) for event in dhcp_events { self.handle_dhcp_event(event); } + // 3. DHCP server 回复通过 send_on_device() 广播出去 + for (dev, reply) in dhcp_server_replies { self.router.send_on_device(...); } // 4. smoltcp Interface::poll():协议状态机推进 - self.iface.poll(timestamp, &mut self.router, sockets); - // 5. DHCP 发包定时器 + let socket_state_changed = self.iface.poll(timestamp, &mut self.router, sockets); + // 5. DHCP client 发包定时器 let dhcp_poll_next = self.poll_dhcp(timestamp); - // 6. TX dispatch:将 smoltcp 输出的包路由到设备 - self.router.dispatch(timestamp) || dhcp_poll_next + // 6. 回收已完成 teardown 的 orphan TCP socket + crate::orphan::reap_orphans(timestamp, sockets); + // 7. TX dispatch:将 smoltcp 输出的包路由到设备(含 loopback 快速路径) + self.router.dispatch(timestamp, sockets) + || dhcp_poll_next || dhcp_server_sent || socket_state_changed } ``` -返回值 `bool` 指示是否需要立即再 poll 一次(用于 `poll_until_idle` 循环)。`next_poll_at()` 调用 `Interface::poll_at()` 获取 smoltcp 建议的下次 poll 时间(用于 `net-poll` worker 的 idle 休眠决策)。 +关键变更:`dispatch()` 现在接收 `&mut SocketSet`,使 loopback 快速路径能在注入 RX buffer 前执行 `snoop_tcp_packet()`。`reap_orphans()` 在持有 SocketSet 锁时执行,避免额外锁获取。 ## net-poll Worker @@ -495,7 +497,7 @@ fn net_poll_worker() { } ``` -`poll_until_idle()` 使用 `POLLING_INTERFACES` CAS 锁防止重入(同一时刻最多一个 poll 进行中),内部循环调用 `poll_once()` 直到 `POLL_AGAIN` 为 false。空闲时 `next_poll_delay()` 返回最多 100ms 的 idle poll interval。 +`poll_until_idle()` 使用 `POLLING_INTERFACES` CAS 锁防止重入(同一时刻最多一个 poll 进行中),内部循环调用 `poll_once()` 直到 `POLL_AGAIN` 为 false。循环内**不 yield**——net-poll worker 是专用线程,在有工作(`poll_once() == true`)时持续批量处理以最大化吞吐。空闲时 `next_poll_delay()` 返回最多 100ms 的 idle poll interval。 ## DHCP 状态机 @@ -515,6 +517,28 @@ Discovering ──Offer──→ Requesting ──ACK──→ Bound `dhcp_transaction_id(mac)` 将 MAC 地址低 32 位与 `wall_time_nanos()` 混合生成 transaction ID。 +## TCP Orphan Socket 回收 + +当用户关闭 TCP socket(`TcpSocket::drop()`)时,如果连接仍处于活跃 teardown 状态(Established/CloseWait/FinWait*/LastAck/Closing/TimeWait)或有未发送数据,socket 不会立即从 smoltcp `SocketSet` 移除,而是转为 **orphan** 状态,由后台回收([orphan.rs](net/ax-net/src/orphan.rs)): + +- **目的**:保证 RFC 793 合规的 FIN 四次挥手和 TIME_WAIT,避免对端连接悬挂。 +- **机制**:`Drop` 中调用 `socket.close()` 发起 FIN,然后 `add_orphan()` 将 `SocketHandle` 移入全局 `ORPHAN_SOCKETS: Mutex>`。 +- **回收**:`reap_orphans()` 在每个 `Service::poll()` 周期中调用,检查每个 orphan 的 smoltcp TCP state: + - `Closed` → 立即移除(`ReapReason::Closed`) + - `TimeWait/FinWait1/FinWait2/LastAck/Closing` → 保留直到 smoltcp 自然超时,但最长 60 秒(`ORPHAN_MAX_LINGER`)后强制移除(`ReapReason::Expired`) + - 其他意外状态 → 60 秒后强制移除并 warn +- **溢出保护**:`ORPHAN_MAX_SOCKETS = 1024`,超限时只 warn 不强制清除正在 teardown 的连接,避免 FIN 丢失。 + +## DHCP 服务器(SoftAP 模式) + +`DhcpServer`([dhcp_server.rs](net/ax-net/src/dhcp_server.rs))是最简 IPv4 DHCP 服务器,用于 Wi-Fi SoftAP 模式给单个客户端分配地址: + +- 单客户端、单地址租约(`leased_to: Option`),足够 AP 验证 ping/ssh。 +- 不依赖 smoltcp 的 DHCP socket,手工解析/封装 `DhcpRepr → UdpRepr → Ipv4Repr`。 +- 处理 Discover→Offer、Request→Ack,回复广播 IPv4 包。 +- 通过 `Service::enable_dhcp_server(dev, server_ip, client_ip, subnet_mask)` 启用,回复包通过 `Router::send_on_device()` 广播。 +- 入站包在 `Router::poll()` 的 snoop 回调中分发,与 DHCP client 路径完全独立。 + ## DNS 解析流程 `dns_query()`([lib.rs#L506-L536](net/ax-net/src/lib.rs#L506-L536)): @@ -582,24 +606,23 @@ pub trait Device: Send + Sync { 两个实现: -- `LoopbackDevice`([device/loopback.rs#L16](net/ax-net/src/device/loopback.rs#L16)):用 `PacketBuffer` 自环,`send()` 直接 enqueue 并 wake。 -- `EthernetDevice`([device/ethernet.rs#L106](net/ax-net/src/device/ethernet.rs#L106)):维护 ARP 邻居表(`NEIGHBOR_TTL = 300s`,见 [ethernet.rs#L133](net/ax-net/src/device/ethernet.rs#L133))、pending packet 队列、Ethernet 帧封装/解析,并对接 IRQ 适配。 +- `LoopbackDevice`([device/loopback.rs](net/ax-net/src/device/loopback.rs)):零状态占位类型,`Device` trait 全为 no-op。真正的回环在 `Router::dispatch()` 的快速路径中完成(见 [Loopback 快速路径](#loopback-快速路径))。 +- `EthernetDevice`([device/ethernet.rs](net/ax-net/src/device/ethernet.rs)):维护 ARP 邻居表(`NEIGHBOR_TTL = 300s`)、pending packet 队列、Ethernet 帧封装/解析,并对接 IRQ 适配。支持两种 RX 就绪模式:IRQ 驱动(`new()`)和 out-of-band 驱动(`new_oob_rx()`,用于 SDIO Wi-Fi 等自带中断线程的设备)。 `EthernetDevice` 之下是能力边界 trait `EthernetDriver`([device/driver.rs#L70-L82](net/ax-net/src/device/driver.rs#L70-L82)),由 `RdNetDriver`([device/driver.rs#L128](net/ax-net/src/device/driver.rs#L128))包装 `rd-net` 的 `TxQueue` / `RxQueue` 实现。这样 `ax-net` 避免直接依赖 FDT、PCI、MMIO、DMA、VirtIO 或平台 IRQ ABI。 ## SocketSetWrapper -全局 socket 容器 `SocketSetWrapper`([wrapper.rs#L16-L24](net/ax-net/src/wrapper.rs#L16-L24))除了持有 smoltcp `SocketSet`,还维护 UDP 端口冲突仲裁(`udp_binds`): +全局 socket 容器 `SocketSetWrapper`([wrapper.rs](net/ax-net/src/wrapper.rs))除了持有 smoltcp `SocketSet`,还维护 UDP 端口冲突仲裁(`udp_binds`): ```rust pub(crate) struct SocketSetWrapper<'a> { pub inner: Mutex>, - pub new_socket: Event, udp_binds: Mutex>, } ``` -`udp_bind()`([wrapper.rs#L48-L68](net/ax-net/src/wrapper.rs#L48-L68))实现 Linux 语义的端口复用规则:具体地址与通配地址互斥,相同地址端口冲突返回 `AddrInUse`(除非 `SO_REUSEADDR`)。 +`udp_bind()` 实现 Linux 语义的端口复用规则:具体地址与通配地址互斥,相同地址端口冲突返回 `AddrInUse`(除非 `SO_REUSEADDR`)。`udp_port_available()` 暴露给 ephemeral port 分配器使用。 ## 为什么不是 multi-smoltcp domain diff --git a/docs/docs/architecture/net/flows.md b/docs/docs/architecture/net/flows.md index 0749939d09..12f922669f 100644 --- a/docs/docs/architecture/net/flows.md +++ b/docs/docs/architecture/net/flows.md @@ -80,7 +80,7 @@ let lo_ip = Ipv4Cidr::new(Ipv4Address::new(127,0,0,1), 8); router.add_rule(Rule::new(lo_ip.into(), None, lo_dev, lo_id, lo_ip.address().into(), 0)); ``` -Loopback 设备注册后,立即安装直连路由 `127.0.0.0/8 → lo_dev`,源地址 `127.0.0.1`,metric 0。`LoopbackDevice` 的 `send()`/`recv()` 直接在内部 buffer 中回环,不经过 Ethernet 帧封装。 +Loopback 设备注册后,立即安装直连路由 `127.0.0.0/8 → lo_dev`,源地址 `127.0.0.1`,metric 0。`LoopbackDevice` 是零状态占位类型,真正的回环数据路径在 `Router::dispatch()` 中以快速路径实现(见 [loopback 快速路径](architecture.md#loopback-快速路径))。 **步骤 3 — 注册 Ethernet 设备**([lib.rs#L205-L292](net/ax-net/src/lib.rs#L205-L292)) @@ -304,7 +304,7 @@ sequenceDiagram Note over RouterTx: IP packet 进入 Router.tx_buffer App->>App: request_poll() → 唤醒 net-poll Note over net-poll: === poll_until_idle() → dispatch() === - Router->>Router: dispatch(timestamp) + Router->>Router: dispatch(timestamp, sockets) loop tx_buffer.dequeue() Router->>Router: parse IP version alt IPv4 unicast @@ -374,9 +374,9 @@ fn transmit(&mut self, _timestamp: Instant) -> Option> { `TxToken::consume()` 将 smoltcp 构造的 IP 包写入 `Router.tx_buffer`,metadata 设为 `TX_INTERFACE_PLACEHOLDER`(占位符,出接口由 `dispatch()` 决定)。 -**步骤 3 — Router::dispatch() 选择出接口**([router.rs#L498-L557](net/ax-net/src/router.rs#L498-L557)) +**步骤 3 — Router::dispatch() 选择出接口**([router.rs](net/ax-net/src/router.rs)) -`dispatch()` 在 smoltcp `Interface::poll()` 返回后执行,从 `tx_buffer` 取出 IP 包: +`dispatch()` 接收 `&mut SocketSet`,在 smoltcp `Interface::poll()` 返回后执行,从 `tx_buffer` 取出 IP 包: ```rust while let Ok((_, packet)) = self.tx_buffer.dequeue() { @@ -397,10 +397,16 @@ while let Ok((_, packet)) = self.tx_buffer.dequeue() { let routes = self.table.read(); let Some(route) = routes.select_route_for_source(&dst_addr, &src_addr) else { warn!("No route found for source {} destination {}", src_addr, dst_addr); - continue; // 丢包但不 panic + continue; }; let dev = &self.devices[route.dev]; - poll_next |= dev.enqueue_tx(route.next_hop, packet.into_inner()); + if dev.interface_id == InterfaceId::LOOPBACK { + // Loopback 快速路径:直接注入 rx_buffer + snoop TCP + poll_next |= inject_loopback_rx_direct( + &mut self.rx_buffer, dst_addr, packet.into_inner(), sockets); + } else { + poll_next |= dev.enqueue_tx(route.next_hop, packet.into_inner()); + } } } IpVersion::Ipv6 => { /* 类似逻辑 */ } @@ -411,7 +417,7 @@ while let Ok((_, packet)) = self.tx_buffer.dequeue() { 关键点: - **出接口不由 socket binding 决定**,而是由 IP 包的 `(源地址, 目的地址)` 在路由表中查 `select_route_for_source()` 决定。 -- 源地址由 smoltcp 根据 socket binding 或 connect 时的路由查询结果注入。 +- **Loopback 快速路径**:当出接口是 loopback 时,`inject_loopback_rx_direct()` 在写入 `rx_buffer` 前执行 `snoop_tcp_packet()`,使回环 TCP SYN 在**同一个 `Service::poll()` 周期内**被 smoltcp 消费,不经过设备 worker。 - 广播包发往所有 Ethernet 设备。 - 查不到路由时 `warn!` + `continue`,不 panic。 @@ -545,23 +551,23 @@ sequenceDiagram end ``` -`TcpSocket::connect()`([tcp.rs#L415-L432](net/ax-net/src/tcp.rs#L415-L432)): +`TcpSocket::connect()`([tcp.rs](net/ax-net/src/tcp.rs)): 1. `start_connect()` 查路由获取源地址,在 smoltcp socket 上调用 `connect()`。 -2. **`ax_task::yield_now()`** — 这是一个 hack:让出当前任务,让 net-poll worker 有机会处理 SYN 发送和 SYN-ACK 接收。 +2. `request_poll()` 唤醒 net-poll worker 处理 SYN 发送和 SYN-ACK 接收。 3. 在 `send_poller` 闭包中循环 `poll_connect()`,每次 `request_poll()` 触发协议栈推进。 4. 连接成功返回 `Ok(())`,连接失败返回对应错误(`ConnectionRefused` 等)。 ### 5.2 Listen + Accept -`listen()`([tcp.rs#L434-L462](net/ax-net/src/tcp.rs#L434-L462)): +`listen()`([tcp.rs](net/ax-net/src/tcp.rs)): 1. 状态转换 `Idle → Listening`。 2. 分配端口(如果未绑定)。 -3. 在 `LISTEN_TABLE` 注册 `(port, backlog)`。 -4. 设置 `DeviceBinding`。 +3. 在 `LISTEN_TABLE` 注册 `(IpListenEndpoint, backlog)`。`ListenTable` 支持同端口不同地址 listen(per-address listen),用 `Vec` 存储。 +4. 设置 `DeviceBinding`(仅当 `bound_if.is_some()` 时)。 -`accept()`([tcp.rs#L464-L487](net/ax-net/src/tcp.rs#L464-L487)): +`accept()`([tcp.rs](net/ax-net/src/tcp.rs)): ```rust fn accept(&self) -> AxResult { @@ -837,12 +843,13 @@ fn poll_once() -> bool { ``` 1. Service lock (get_service()) 2. SocketSet lock (SOCKET_SET.inner.lock()) - ├─ Router::poll() — drain RX queue → rx_buffer + ├─ Router::poll() — drain RX queue → rx_buffer,snoop DHCP client/server ├─ smoltcp Interface::poll(timestamp, router, sockets) │ ├─ Router::receive() / Router::transmit() (phy::Device trait) │ └─ TCP/UDP/ICMP state machine - ├─ Router::dispatch() — tx_buffer → per-device tx_queue - └─ poll_dhcp() — DHCP retry packets + ├─ poll_dhcp() — DHCP client retry packets + ├─ reap_orphans() — 回收已完成 teardown 的 orphan TCP socket + └─ Router::dispatch(timestamp, sockets) — tx_buffer → 设备/loopback 快速路径 3. 释放 SocketSet lock 4. 释放 Service lock 5. wake socket waiters (在锁外部) diff --git a/docs/docs/architecture/net/overview.md b/docs/docs/architecture/net/overview.md index 447dc78ccd..3ae9ccc820 100644 --- a/docs/docs/architecture/net/overview.md +++ b/docs/docs/architecture/net/overview.md @@ -24,7 +24,11 @@ TGOSKits 的网络能力收敛在 `net/ax-net`。它是 ArceOS、StarryOS 和 Ax | ARP | `EthernetDevice` 内部 `neighbors: HashMap`(已解析条目, 300s TTL)+ `pending_neighbors: HashMap`(等待 ARP reply 条目, 1s 重试) + `pending_packets: PacketBuffer`(暂存待 ARP 解析后发送的包) | 完整 | | 多 NIC 路由 | `RouteTable` 最长前缀匹配 + metric 排序 + per-interface 替换 | 完整 | | IRQ 感知 | `EthernetIrqRegistrar` + `EthernetIrqAction` + IRQ→wake 转换 | 完整 | -| Loopback | 自包含 `LoopbackDevice`,直接 buffer 回环,不封装 Ethernet 帧 | 完整 | +| Loopback | 零状态 `LoopbackDevice` + `Router::dispatch()` 快速路径 inline 注入 `rx_buffer`,不经设备 worker | 完整 | +| TCP orphan 回收 | `orphan.rs`:Drop 后保留 smoltcp socket 直到 FIN/TIME_WAIT 完成,RFC 793 合规 | 完整 | +| DHCP 服务器(SoftAP) | `dhcp_server.rs`:最简单客户端 DHCP 服务器,Discover→Offer、Request→Ack | 完整 | +| OOB RX(SDIO Wi-Fi) | `EthernetDevice::new_oob_rx()` + `notify_oob_rx()` + 独立 poll task | 完整 | +| 动态设备注册 | `register_device_with_config()` 运行时添加静态 IP 设备(Wi-Fi AP) | 完整 | ## 线程与锁模型 @@ -37,6 +41,7 @@ TGOSKits 的网络能力收敛在 `net/ax-net`。它是 ArceOS、StarryOS 和 Ax | `{ifname}-tx` worker | 每网卡一个,从 `DeviceHandle::tx_queue` 取包调用 driver send | `device.tx_wake.wait_until()` | | 调用者线程 | 应用/内核线程调用 socket API | `StateLock::lock()`、`block_on(poll_io())` | | `vsock-poll` worker | vsock 设备轮询,事件分发到 `VSOCK_CONN_MANAGER` | 自适应频率 sleep(100μs→10ms) | +| `{ifname}-oob-poll` | OOB RX 设备(如 SDIO Wi-Fi)的专用 poll task | `OOB_RX_SIGNAL.wait()` | ### 全局锁顺序 From f808c89ae6dcf7ec2f8f71d14fa2c2d0b30d8b08 Mon Sep 17 00:00:00 2001 From: ZCShou <72115@163.com> Date: Mon, 15 Jun 2026 16:18:20 +0800 Subject: [PATCH 19/31] fix(poll): wrap ppoll timeout in interruptible to handle signal interruption --- os/StarryOS/kernel/src/syscall/io_mpx/poll.rs | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/os/StarryOS/kernel/src/syscall/io_mpx/poll.rs b/os/StarryOS/kernel/src/syscall/io_mpx/poll.rs index 5b582fd0d4..8e9842b043 100644 --- a/os/StarryOS/kernel/src/syscall/io_mpx/poll.rs +++ b/os/StarryOS/kernel/src/syscall/io_mpx/poll.rs @@ -7,7 +7,10 @@ use core::{ use ax_errno::{AxError, AxResult}; use ax_runtime::hal::time::TimeValue; -use ax_task::{current, future, future::block_on}; +use ax_task::{ + current, future, + future::{block_on, interruptible}, +}; use axpoll::{IoEvents, Pollable}; use linux_raw_sys::general::{POLLNVAL, RLIMIT_NOFILE, pollfd, timespec}; use starry_signal::SignalSet; @@ -127,25 +130,25 @@ fn do_poll( let fds = FdPollSet(fds); with_blocked_signals(sigmask, || { - match block_on(future::timeout( - timeout, - poll_fn(|cx| { - let mut res = collect_ready_poll_events(&fds, &revent_indices, poll_fds); - if res > 0 { - return Poll::Ready(Ok(res as _)); - } - - fds.register(cx, IoEvents::empty()); - - res = collect_ready_poll_events(&fds, &revent_indices, poll_fds); - if res > 0 { - return Poll::Ready(Ok(res as _)); - } - Poll::Pending - }), - )) { - Ok(r) => r, - Err(_) => Ok(0), + let wait = poll_fn(|cx| { + let mut res = collect_ready_poll_events(&fds, &revent_indices, poll_fds); + if res > 0 { + return Poll::Ready(Ok(res as _)); + } + + fds.register(cx, IoEvents::empty()); + + res = collect_ready_poll_events(&fds, &revent_indices, poll_fds); + if res > 0 { + return Poll::Ready(Ok(res as _)); + } + Poll::Pending + }); + + match block_on(interruptible(future::timeout(timeout, wait))) { + Ok(Ok(r)) => r, + Ok(Err(_)) => Ok(0), + Err(err) => Err(err.into()), } }) } @@ -181,7 +184,6 @@ pub fn sys_ppoll( let timeout = nullable!(timeout.get_as_ref())? .map(|ts| ts.try_into_time_value()) .transpose()?; - // TODO: handle signal let res = do_poll( &mut poll_fds, timeout, From 50a316ddfdc9aed0d9c51d81319601651da8597d Mon Sep 17 00:00:00 2001 From: ZCShou <72115@163.com> Date: Mon, 15 Jun 2026 16:49:41 +0800 Subject: [PATCH 20/31] docs(net): restructure architecture docs into focused topics and reflect inline queue changes --- docs/docs/architecture/net/api.md | 52 +- docs/docs/architecture/net/architecture.md | 833 ++++++-------------- docs/docs/architecture/net/configuration.md | 18 +- docs/docs/architecture/net/control.md | 143 ++++ docs/docs/architecture/net/dependencies.md | 299 ------- docs/docs/architecture/net/devices.md | 226 ++++++ docs/docs/architecture/net/flows.md | 44 +- docs/docs/architecture/net/integration.md | 2 +- docs/docs/architecture/net/overview.md | 62 +- docs/docs/architecture/net/protocol.md | 136 ---- docs/docs/architecture/net/sockets.md | 150 ++++ docs/docs/architecture/net/testing.md | 23 +- 12 files changed, 864 insertions(+), 1124 deletions(-) create mode 100644 docs/docs/architecture/net/control.md delete mode 100644 docs/docs/architecture/net/dependencies.md create mode 100644 docs/docs/architecture/net/devices.md delete mode 100644 docs/docs/architecture/net/protocol.md create mode 100644 docs/docs/architecture/net/sockets.md diff --git a/docs/docs/architecture/net/api.md b/docs/docs/architecture/net/api.md index 0e384815d8..3daff3790d 100644 --- a/docs/docs/architecture/net/api.md +++ b/docs/docs/architecture/net/api.md @@ -1,5 +1,5 @@ --- -sidebar_position: 4 +sidebar_position: 6 sidebar_label: "API 参考" --- @@ -395,53 +395,3 @@ pub fn eth0_ipv4_config() -> Option; - `LISTEN_TABLE.listen(endpoint, backlog)`:`ListenTableEntry` 是 `Vec`,每个 entry 存储完整的 `IpListenEndpoint`。 - `listen_addrs_conflict(a, b)`:wildcard(`None`)与所有地址冲突;两个 `Some(addr)` 仅当相等时冲突。 - `SocketSetWrapper::udp_bind_available()`:UDP 同端口不同地址允许共存,wildcard 与所有冲突。 - -## TCP listen/accept - -TCP 的 listen/accept 由全局 `LISTEN_TABLE`([lib.rs#L89](net/ax-net/src/lib.rs#L89))管理。`ListenTable`([listen_table.rs#L67-L103](net/ax-net/src/listen_table.rs#L67-L103))为每个端口维护一个 SYN 队列。RX 路径中 `snoop_tcp_packet()`([router.rs#L622](net/ax-net/src/router.rs#L622))在收到首个 SYN 时预创建 smoltcp socket 并入队([listen_table.rs#L165-L200](net/ax-net/src/listen_table.rs#L165-L200)),从而在 smoltcp `Interface::poll` 之前就让连接进入 accepted 候选状态。`accept()` 扫描 SYN 队列返回已建立的连接([listen_table.rs#L133-L164](net/ax-net/src/listen_table.rs#L133-L164))。 - -## DNS API - -```rust -pub fn dns_servers() -> Vec; // lib.rs L480 -pub fn dns_query(name: &str) -> AxResult>; // lib.rs L486 -pub fn dns_query_timeout(name: &str, timeout: Duration) -> AxResult>; // lib.rs L494 -``` - -`dns_servers()` 只返回地址列表。内部仍保留 DNS 来源接口、metric 和来源类型,用于 DHCP 更新和接口状态变化。 - -## 设备适配 API - -`ax-net` 对外导出 Ethernet 设备适配类型([lib.rs#L67-L84](net/ax-net/src/lib.rs#L67-L84)): - -```rust -pub struct EthernetDeviceList; // = Vec> driver.rs L84 -pub trait EthernetDriver; // driver.rs L70 -pub struct RdNetDriver; // driver.rs L128 -``` - -IRQ adapter 类型([device/ethernet.rs#L27-L81](net/ax-net/src/device/ethernet.rs#L27-L81))让 `ax-runtime` 把 HAL IRQ registration 适配到网络领域,而不把 `ax-hal::irq` ABI 泄漏到 `ax-net` public trait: - -```rust -pub struct EthernetIrqAction { /* data + handler fn pointer */ } -pub enum EthernetIrqOutcome { Handled, Wake } -pub trait EthernetIrqRegistrar: Send + Sync { - fn register_shared(&self, name: &str, irq: usize, action: EthernetIrqAction) - -> Result, EthernetIrqRegistrationError>; -} -pub enum EthernetIrqRegistrationError { InvalidIrq, Busy, Unsupported, Other } -pub fn set_ethernet_irq_registrar(registrar: &'static dyn EthernetIrqRegistrar); -``` - -`EthernetDevice::new()`([ethernet.rs#L136-L174](net/ax-net/src/device/ethernet.rs#L136-L174))在创建时若 registrar 已安装且设备有 IRQ,会通过 `register_shared()` 注册共享中断处理函数 `handle_ethernet_irq`([ethernet.rs#L116-L123](net/ax-net/src/device/ethernet.rs#L116-L123)),收到 RX/TX 事件时 `wake()` RX worker 与 `net-poll`。 - -## 已删除或不应使用的入口 - -以下旧入口不应再使用: - -- `eth0_ipv4_config()` -- 旧全局单网口 env 配置语义 -- `u32 device_mask` -- 旧 benchmark public API - -所有调用方应迁移到接口 registry API 和 `DeviceBinding` / `InterfaceId`。 diff --git a/docs/docs/architecture/net/architecture.md b/docs/docs/architecture/net/architecture.md index e6ac17a5ca..ddc0f8bf3a 100644 --- a/docs/docs/architecture/net/architecture.md +++ b/docs/docs/architecture/net/architecture.md @@ -1,670 +1,341 @@ --- -sidebar_position: 3 -sidebar_label: "架构设计" +sidebar_position: 2 +sidebar_label: "总体架构" --- -# 网络栈总体架构 +# 总体架构 -`ax-net` 使用单协议栈、多接口、多设备模型。所有 TCP/UDP/raw socket 仍共享一个 smoltcp `Interface` 和一个 `SocketSet`,但接口、路由、DNS、DHCP 和设备收发已经从单 `eth0` 假设扩展为多接口模型。 +`ax-net` 是 ArceOS/StarryOS 的网络协议栈 crate。它以 smoltcp 作为单一 TCP/IP 协议核心,在外层补齐多接口、多设备、路由、DNS、DHCP、Linux socket 语义和设备驱动适配。 + +整体设计可以概括为: + +- **单协议栈核心**:所有 IP socket 共享一个 smoltcp `Interface` 和一个全局 `SocketSet`。 +- **多设备适配层**:`Router` 对 smoltcp 暴露一个 `phy::Device`,内部聚合 loopback 和多个 Ethernet 设备。 +- **控制面与数据面分离**:接口 registry、路由表和 DNS registry 独立于收发路径,可返回只读快照。 +- **专用 poll worker**:socket 热路径只请求 poll,由 `net-poll` worker 独占推进 smoltcp。 +- **兼容 POSIX/Linux socket 语义**:支持 bind/listen/connect/accept、poll readiness、`SO_BINDTODEVICE`、TCP orphan teardown、Unix domain socket 和可选 vsock。 + +## 核心设计 + +`ax-net` 的核心选择是 **Single Interface + Router as Device**:不为每个网卡创建独立 smoltcp `Interface`,而是让所有 IP socket 共享一个 smoltcp `Interface` 和一个全局 `SocketSet`,再用 `Router` 作为虚拟 `Device` 聚合 loopback 与多个 Ethernet 设备。 + +这种结构保留了 socket 语义的一致性: + +| 语义 | 单协议栈核心中的处理方式 | +| --- | --- | +| wildcard listen (`0.0.0.0:port`) | 一个 `ListenTable` entry 覆盖所有可用接口 | +| per-address listen/bind | `IpListenEndpoint` + `DeviceBinding` 做地址/接口约束 | +| ephemeral port 分配 | 在全局 TCP/UDP 端口表中仲裁,避免跨接口重复占用 | +| route change / DHCP 更新 | `RouteTable` 和接口地址原子更新,socket 不需要迁移 | +| poll readiness | 全局 `SocketSet` 中统一计算 readiness 并唤醒调用者 | + +如果改成每设备一个 `Interface`/`SocketSet`,这些语义需要在多个协议栈实例之间重新仲裁,例如 wildcard listen 要在所有接口创建 listener,accept queue 要跨实例聚合,端口冲突也要引入中心协调。当前模型更符合多宿主主机上的普通 socket 语义。 + +### 总体拓扑 ```mermaid flowchart TB - subgraph Public["public API"] - ApiSocket["tcp / udp / raw / unix / vsock"] - ApiIface["interfaces() / routes() / DNS APIs"] - ApiPoll["request_poll() / poll_interfaces()"] + subgraph Api["Public API"] + Init["init_network() / init_vsock()"] + Query["interfaces() / routes() / arp_entries()"] + DnsApi["dns_servers() / dns_query()"] + Sockets["TcpSocket / UdpSocket / RawSocket / UnixSocket / VsockSocket"] + PollApi["request_poll() / poll_interfaces()"] end - subgraph Control["control plane"] - Registry["Interface registry"] + subgraph Control["Control plane"] + NetControl["NetControl"] + IfaceRegistry["Interface registry"] Routes["RouteTable"] - Dns["DnsRegistry entries"] + DnsRegistry["DNS registry"] Binding["DeviceBinding"] end - subgraph Core["single protocol core"] - SocketSet["SocketSetWrapper"] + subgraph Core["Single protocol core"] Service["Service"] - Smol["smoltcp Interface"] - Dhcp["per-interface DhcpState"] - Router["Router as smoltcp Device"] + SmolIface["smoltcp Interface"] + SocketSet["SocketSetWrapper"] + Listen["ListenTable"] + Orphan["TCP orphan reaper"] + DhcpClient["DHCP client states"] + DhcpServer["DHCP server"] end - subgraph Queues["device queues"] - RxQ["bounded RX queue"] - TxQ0["bounded TX queue: eth0"] - TxQ1["bounded TX queue: eth1"] - Wake["poll wake flag / WaitQueue"] + subgraph MultiDev["Router as MultiDevice"] + Router["Router implements smoltcp::phy::Device"] + RxBuffer["Router.rx_buffer"] + TxBuffer["Router.tx_buffer"] + RxQueue["shared RX queue"] + TxQueues["per-device TX queues"] end - subgraph Devices["device workers"] - Lo["LoopbackDevice"] - Eth0["EthernetDevice eth0"] - Eth1["EthernetDevice eth1"] - RdNet["rd-net / rdif-eth driver"] + subgraph DeviceLayer["Device layer"] + Loopback["LoopbackDevice"] + Ethernet["EthernetDevice"] + RdNet["RdNetDriver"] + Hardware["rd-net / physical NIC"] end - Public --> Control - ApiSocket --> SocketSet - ApiPoll --> Service + Api --> Control + Sockets --> SocketSet + PollApi --> Service Control --> Service - Service --> Smol - Service --> Dhcp - Smol --> Router - Router --> RxQ - Router --> TxQ0 - Router --> TxQ1 - RxQ --> Eth0 - RxQ --> Eth1 - TxQ0 --> Eth0 - TxQ1 --> Eth1 - Eth0 --> RdNet - Eth1 --> RdNet - Lo --> Router + Service --> SmolIface + Service --> DhcpClient + Service --> DhcpServer + Service --> Orphan + SmolIface --> Router + SocketSet --> SmolIface + Listen --> SocketSet + Router --> RxBuffer + Router --> TxBuffer + Router --> RxQueue + Router --> TxQueues + Router --> Routes + TxQueues --> Ethernet + RxQueue --> Router + Loopback --> Router + Ethernet --> RdNet + RdNet --> Hardware ``` -## 源码组织 +### 组件分层 -整个 crate 源于 [net/ax-net/src/](net/ax-net/src/),入口为 [net/ax-net/src/lib.rs](net/ax-net/src/lib.rs)。模块划分与职责如下: +| 层级 | 主要职责 | 关键源码 | 详细文档 | +| --- | --- | --- | --- | +| Public API | 初始化、接口查询、DNS、socket facade、poll trigger | [lib.rs](net/ax-net/src/lib.rs), [socket.rs](net/ax-net/src/socket.rs), [options.rs](net/ax-net/src/options.rs) | [API 参考](api.md) | +| Control plane | 接口 registry、路由决策、DNS 来源、运行期配置提交 | [service.rs](net/ax-net/src/service.rs), [config.rs](net/ax-net/src/config.rs), [router.rs](net/ax-net/src/router.rs) | [控制面](control.md) | +| Single protocol core | 一个 smoltcp `Interface`、全局 `SocketSet`、socket backend、DHCP、orphan 回收、poll 调度 | [service.rs](net/ax-net/src/service.rs), [wrapper.rs](net/ax-net/src/wrapper.rs), [tcp.rs](net/ax-net/src/tcp.rs), [udp.rs](net/ax-net/src/udp.rs), [listen_table.rs](net/ax-net/src/listen_table.rs), [orphan.rs](net/ax-net/src/orphan.rs) | 本文、[Socket 系统](sockets.md) | +| Multi-device Router | smoltcp `Device` 适配、TX 路由、RX 汇聚、loopback 快速路径 | [router.rs](net/ax-net/src/router.rs) | [多设备实现](devices.md) | +| Device layer | Ethernet 封装/解封装、ARP、IRQ/OOB RX、rd-net 适配 | [device/](net/ax-net/src/device/) | [多设备实现](devices.md) | +| Configuration | 静态网络配置、DHCP、MTU、缓冲区、feature | [config.rs](net/ax-net/src/config.rs), [consts.rs](net/ax-net/src/consts.rs), `Cargo.toml` | [配置参考](configuration.md) | +| Integration and tests | OS 集成、启动流程、测试范围 | `ax-runtime`, `starry-kernel`, `ax-api` | [集成](integration.md), [测试](testing.md) | -| 模块 | 角色 | 关键类型 | -| --- | --- | --- | -| [lib.rs](net/ax-net/src/lib.rs) | public facade,初始化网络、启动 poll worker、导出 API | `init_network`, `request_poll`, `net_poll_worker` | -| [config.rs](net/ax-net/src/config.rs) | 配置与接口信息类型 | `InterfaceId`, `NetworkConfig`, `InterfaceInfo`, `DeviceBinding` | -| [service.rs](net/ax-net/src/service.rs) | 控制面 + 协议核心调度 | `Service`, `NetControl`, `DhcpState` | -| [router.rs](net/ax-net/src/router.rs) | 路由表、有界队列、smoltcp `Device` 适配 | `Router`, `RouteTable`, `RouteDecision` | -| [wrapper.rs](net/ax-net/src/wrapper.rs) | 全局 `SocketSet` 包装与端口冲突仲裁 | `SocketSetWrapper` | -| [socket.rs](net/ax-net/src/socket.rs) | 统一 socket 抽象 | `SocketOps`, `Socket`, `SocketAddrEx` | -| [options.rs](net/ax-net/src/options.rs) | socket 选项与 `Configurable` trait | `GetSocketOption`, `SetSocketOption`, `TcpInfo` | -| [general.rs](net/ax-net/src/general.rs) | 通用 socket 选项、非阻塞/超时/poll helper | `GeneralOptions` | -| [state.rs](net/ax-net/src/state.rs) | socket 状态机锁 | `StateLock`, `StateGuard` | -| [listen_table.rs](net/ax-net/src/listen_table.rs) | TCP listen/accept 表与 SYN 预创建 | `ListenTable` | -| [tcp.rs](net/ax-net/src/tcp.rs) / [udp.rs](net/ax-net/src/udp.rs) / [raw.rs](net/ax-net/src/raw.rs) | IP socket 实现 | `TcpSocket`, `UdpSocket`, `RawSocket` | -| [orphan.rs](net/ax-net/src/orphan.rs) | TCP orphan socket 回收(RFC 793 TIME_WAIT) | `add_orphan`, `reap_orphans` | -| [dhcp_server.rs](net/ax-net/src/dhcp_server.rs) | 最简 DHCP 服务器(SoftAP 模式) | `DhcpServer` | -| [unix/](net/ax-net/src/unix/) | Unix domain socket | `UnixSocket`, `Transport` | -| [vsock/](net/ax-net/src/vsock/) | 可选 vsock 支持(`vsock` feature) | `VsockSocket`, `VsockTransport` | -| [device/](net/ax-net/src/device/) | loopback、Ethernet、rd-net、vsock 设备适配 | `Device`, `EthernetDevice`, `RdNetDriver` | -| [consts.rs](net/ax-net/src/consts.rs) | 缓冲区大小等常量 | `STANDARD_MTU`, `SOCKET_BUFFER_SIZE` | - -## 全局单例 - -`ax-net` 通过若干 `Once` / `LazyLock` 全局单例持有协议核心与控制面,定义在 [net/ax-net/src/lib.rs#L89-L103](net/ax-net/src/lib.rs#L89-L103): - -```rust -static LISTEN_TABLE: LazyLock = LazyLock::new(ListenTable::new); -static SOCKET_SET: LazyLock = LazyLock::new(SocketSetWrapper::new); - -static SERVICE: Once> = Once::new(); -static NET_CONTROL: Once> = Once::new(); -static POLLING_INTERFACES: AtomicBool = AtomicBool::new(false); -static POLL_AGAIN: AtomicBool = AtomicBool::new(false); -static NET_POLL_REQUESTED: AtomicBool = AtomicBool::new(false); -static NET_POLL_WAKE: WaitQueue = WaitQueue::new(); -static NET_POLL_DEVICE_WAKER: LazyLock = - LazyLock::new(|| Waker::from(Arc::new(NetPollWake))); -``` +### TCP/IP 分层映射 -- `SOCKET_SET` 是所有 IP socket(TCP/UDP/raw/DNS)共享的 smoltcp `SocketSet`。 -- `SERVICE` 持有 `Service`(smoltcp `Interface` + `Router` + DHCP 状态),被 mutex 保护。 -- `NET_CONTROL` 持有只读控制面(接口 registry、路由表、DNS),可在不持有 `Service` 锁的情况下被查询。 -- `NET_POLL_WAKE` 是 `net-poll` worker 的等待队列,`request_poll()` 通过它唤醒 worker。 +| TCP/IP 层 | ax-net 组件 | 主要职责 | +| --- | --- | --- | +| 应用层 | `SocketOps`, `Socket`, `Configurable` | socket API、options、poll readiness、地址类型统一 | +| 传输层 | smoltcp TCP/UDP/raw socket + `tcp.rs`/`udp.rs`/`raw.rs` | TCP 状态机、UDP datagram、raw packet、端口仲裁和 Linux 语义补齐 | +| 网络层 | smoltcp `Interface`, `Router`, `RouteTable`, DHCP/DNS 辅助 | IP packet 处理、路由、接口地址、DHCP client/server、DNS 查询 | +| 链路层 | `EthernetDevice`, `LoopbackDevice`, `RdNetDriver` | Ethernet frame、ARP、IRQ/OOB RX、rd-net 驱动适配 | -## 接口标识 +smoltcp 负责 TCP/IP 协议核心;`ax-net` 负责多接口、多设备、设备生命周期、socket 兼容语义和 OS 集成。 -`InterfaceId(u32)` 是 `ax-net` 内部接口 ID,同时也是 StarryOS 暴露给 Linux ABI 的 ifindex 数值来源。定义在 [net/ax-net/src/config.rs#L8-L38](net/ax-net/src/config.rs#L8-L38): +## Public API -```rust -#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct InterfaceId(u32); +Public API 是上层 OS 模块进入 `ax-net` 的边界,主要定义在 [lib.rs](net/ax-net/src/lib.rs)、[socket.rs](net/ax-net/src/socket.rs) 和 [options.rs](net/ax-net/src/options.rs)。它不暴露 smoltcp 的内部类型,而是提供面向 ArceOS/StarryOS 的稳定能力: -impl InterfaceId { - pub const LOOPBACK: Self = Self(1); - pub const fn new(raw: u32) -> Self { Self(raw) } - pub const fn get(self) -> u32 { self.0 } - /// Convert to Linux ifindex (i32). - pub const fn to_linux_ifindex(self) -> i32 { self.0 as i32 } - pub const fn from_linux_ifindex(ifindex: i32) -> Option { /* ... */ } -} -``` +| API 类别 | 代表接口 | 架构作用 | +| --- | --- | --- | +| 初始化 | `init_network()`、`init_vsock()` | 建立 `Service`、`Router`、`NetControl`、设备 worker 和 net-poll worker | +| 接口查询 | `interfaces()`、`interface_by_name()`、`ipv4_config()`、`default_routes()`、`arp_entries()` | 从控制面或设备层返回只读快照 | +| DNS | `dns_servers()`、`dns_query()`、`dns_query_timeout()` | 读取 DNS registry,并通过临时 smoltcp DNS socket 查询 | +| Socket facade | `TcpSocket`、`UdpSocket`、`RawSocket`、`UnixSocket`、`VsockSocket` | 为 syscall/POSIX 层提供统一 socket backend | +| Poll 触发 | `request_poll()`、`poll_interfaces()` | 唤醒专用 net-poll worker,避免应用线程同步驱动协议栈 | +| Socket options | `GetSocketOption`、`SetSocketOption`、`Configurable` | 覆盖通用 `SO_*`、`TCP_*`、`IP_*` 选项 | -- `InterfaceId::LOOPBACK == 1`,固定对应 `lo`。 -- Ethernet 接口从 `2` 开始,默认发现顺序为 `eth0`、`eth1`(见 [lib.rs](net/ax-net/src/lib.rs#L221) 中 `InterfaceId::new((order as u32) + 2)`)。 -- `InterfaceInfo`([config.rs#L58-L70](net/ax-net/src/config.rs#L58-L70))是对外只读快照,包含 ID、名称、类型、MAC、IPv4、MTU、flags 和 metric。 - -```rust -pub struct InterfaceInfo { - pub id: InterfaceId, - pub name: String, - pub kind: InterfaceKind, - pub mac: Option, - pub ipv4: Option, - pub mtu: usize, - pub flags: InterfaceFlags, - pub metric: u32, -} -``` +Public API 的职责是做边界收敛:上层不需要知道某个 socket 是否由 smoltcp、Unix transport 或 vsock transport 实现,也不需要直接操作 `Service`、`Router` 或 `SocketSet`。具体 API 列表见 [API 参考](api.md)。 ## 控制面 -控制面由 `NetControl`([service.rs#L45-L50](net/ax-net/src/service.rs#L45-L50))统一持有: - -```rust -pub struct NetControl { - state: RwLock, - pub(crate) routes: SharedRouteTable, -} -``` - -内部 `ControlState` 保存 `Vec`(接口 registry)与 `Vec`(DNS 来源信息)。控制面提供: - -- `interfaces()` / `interface_by_name()` / `interface_by_id()`:名称与 ID 查询([service.rs#L81-L107](net/ax-net/src/service.rs#L81-L107))。 -- `select_route()`:按目的地址查路由,并校验目标接口 `UP`([service.rs#L147-L174](net/ax-net/src/service.rs#L147-L174))。 -- `local_binding_for()`:把本地监听地址映射为 `DeviceBinding`([service.rs#L122-L135](net/ax-net/src/service.rs#L122-L135))。 -- `commit_interface_update()`:DHCP ACK 等运行期更新通过一次性事务提交接口地址、smoltcp IP 地址、路由和 DNS,避免外部看到半更新状态([service.rs#L175-L198](net/ax-net/src/service.rs#L175-L198))。 - -控制面查询不进入设备锁,也不直接推进 smoltcp poll。`select_route_if`([router.rs#L245](net/ax-net/src/router.rs#L245))会传入一个 `is_usable` 闭包,跳过未 `UP` 的接口: - -```rust -pub fn select_route_if(&self, dst, mut is_usable: impl FnMut(InterfaceId) -> bool) - -> Option -{ - self.rules.iter() - .find(|rule| rule.filter.contains_addr(dst) && is_usable(rule.interface_id)) - .map(|rule| RouteDecision { /* ... */ }) -} -``` - -`RouteTable` 还提供 `select_route_for_source(dst, src)`([router.rs#L262](net/ax-net/src/router.rs#L262)),用于在 TX dispatch 时同时匹配目的地址和源地址。例如,多宿主(multi-homed)主机有两个接口分别有不同源 IP,发送到同一目的地的包需根据 smoltcp 注入的源地址选择正确的出接口。 - -### 路由排序策略 - -`sort_rules()` 的三级排序([router.rs#L235-L240](net/ax-net/src/router.rs#L235-L240)): - -1. **最长前缀优先**:`b.filter.prefix_len().cmp(&a.filter.prefix_len())` — 更长的前缀排在前面 -2. **低 metric 优先**:同前缀长度时,`a.metric.cmp(&b.metric)` — metric 更小的优先 -3. **插入顺序稳定**:同前缀同 metric 时,`a.order.cmp(&b.order)` — order 由 `next_order` 自增产生 - -这使得 `select_route_if` 使用 `find()` 返回第一个匹配项(即最高优先级的路由)。 - -`SocketSetWrapper`([wrapper.rs](net/ax-net/src/wrapper.rs))在 smoltcp 原生 `SocketSet` 之上增加了 UDP 端口冲突仲裁。原生 smoltcp 允许多个 UDP socket bind 同一端口(由上层保证),`ax-net` 在此层实现 `SO_REUSEADDR` 语义: - -```rust -pub(crate) struct SocketSetWrapper<'a> { - pub inner: Mutex>, // smoltcp SocketSet - udp_binds: Mutex>, // UDP 端口占用表 -} -``` - -UDP bind 仲裁规则([wrapper.rs](net/ax-net/src/wrapper.rs) `udp_bind_available()`): - -- **精确 bind**(如 `192.168.1.1:53`):如果已有同地址同端口则拒绝;如果有 wildcard `0.0.0.0:53` 则拒绝。 -- **Wildcard bind**(`0.0.0.0:53`):如果任何地址已占用同一端口则拒绝。 -- **`SO_REUSEADDR`**:设置后跳过仲裁。 - -`udp_port_available()` 暴露给 ephemeral port 分配器(UDP 和 TCP),避免分配到已占用的端口。 - -## TCP 端口绑定仲裁 - -除了 `ListenTable`(管理 `listen()` 端口),`ax-net` 还有独立的 `TCP_BOUND_PORTS`([tcp.rs](net/ax-net/src/tcp.rs))来追踪已 bind 但尚未 listen 的端口: - -```rust -static TCP_BOUND_PORTS: LazyLock>>>> = - LazyLock::new(|| Mutex::new(HashMap::new())); - -fn register_tcp_bound(endpoint: IpListenEndpoint) -> AxResult; -fn unregister_tcp_bound(endpoint: IpListenEndpoint); -``` - -两个系统协同工作: -- `LISTEN_TABLE`:管理已进入 `listen()` 状态的端口及 SYN 队列,防止同一端口被多次 listen。 -- `TCP_BOUND_PORTS`:追踪已 bind(可能尚未 listen)的端口和地址,防止地址冲突的重复 bind。`register_tcp_bound()` 在 `bind()`、`listen()`、`connect()` 中被调用;`unregister_tcp_bound()` 在 `shutdown()`、`Drop` 和错误回退中被调用。 -- `tcp_port_available(port)`:同时检查两个表,确保 `get_ephemeral_port()` 不会分配到已被 bind 或已在 listen 的端口。 - -`listen_addrs_conflict(a, b)` 判断两个 `IpListenEndpoint` 是否冲突:任一为 `None`(wildcard)即冲突,或两者值相等即冲突。 - -## Socket 状态机 - -`StateLock`([state.rs](net/ax-net/src/state.rs))为 socket 提供基于 CAS 的轻量状态转换锁: - -``` -Idle ──bind──→ Idle (不变,但注册 endpoint) -Idle ──listen──→ Listening -Idle ──connect──→ Connecting ──SYN/SYN+ACK──→ Connected -Listening ──accept──→ Listening (不变,但生成新 socket) -Connected ──close──→ Closed -``` - -核心 API: - -```rust -pub fn lock(&self, expect: State) -> Result, State> { - // CAS: expect → Busy,失败返回当前状态 -} -pub fn transit(self, new: State, f: impl FnOnce() -> AxResult) -> AxResult { - // 执行 f(),成功则写 new 状态,失败则回退到原始状态 -} -``` - -`StateLock` 保证绑定/监听/连接等操作的原子性:并发 bind+connect 在同一 socket 上只能有一个成功。 - -## TCP Listen Table 与 SYN Pre-create - -`ListenTable`([listen_table.rs](net/ax-net/src/listen_table.rs))是 TCP 监听的核心数据结构。它包含 65536 个槽位(每端口一个 `Arc>>>`),每个活跃端口存储: - -```rust -struct ListenTableEntryInner { - listen_endpoint: IpListenEndpoint, // 监听地址和端口 - backlog: usize, // SYN 队列容量(clamp 到 LISTEN_QUEUE_SIZE=512) - syn_queue: VecDeque, // 预创建的连接 -} -``` +控制面负责“网络配置如何被发现、保存、查询和用于决策”。它不直接收发 packet,也不推进 smoltcp poll;数据面只在需要路由、接口地址或 DNS 信息时读取控制面快照。 -### SYN 预创建机制 - -`snoop_tcp_packet()`([router.rs](net/ax-net/src/router.rs#L474-L500))在 RX 路径中拦截 TCP SYN 包: - -1. 解析 TCP 包头,检查 SYN 标志。 -2. 查 `LISTEN_TABLE` 是否存在匹配的 listening entry。 -3. 如果 SYN queue 未满,**预创建**一个 `TcpSocket` 并调用 `socket.listen()`,将其加入 `syn_queue`。 -4. smoltcp 随后 `Interface::poll()` 完成三次握手。 -5. `accept()` 时直接从前端取出已完成握手的 socket,无需阻塞等待。 - -SYN queue 满时丢弃 SYN 包(打 warning),由客户端重传机制保证可靠性。 - -### accept() 流程 - -`ListenTable::accept()`([listen_table.rs#L133-L157](net/ax-net/src/listen_table.rs#L133-L157)): - -1. 遍历 `syn_queue`。 -2. 跳过已关闭且无未读数据的 socket(`is_closed_without_data`),移除并销毁。 -3. 返回第一个已完成握手的 socket(`is_acceptable` 检查 TCP state 为 Established/CloseWait/FinWait1/FinWait2 等可 accept 状态)。 -4. 为减少慢速枚举,`idx > 0` 时打印 warning。 - -## 通用 Poll 与超时 - -`GeneralOptions`([general.rs](net/ax-net/src/general.rs))为所有 socket 类型提供通用设施: - -```rust -pub(crate) struct GeneralOptions { - nonblock: AtomicBool, // O_NONBLOCK - reuse_address: AtomicBool, // SO_REUSEADDR - send_timeout_nanos: AtomicU64, // SO_SNDTIMEO - recv_timeout_nanos: AtomicU64, // SO_RCVTIMEO - bound_if: AtomicU32, // SO_BINDTODEVICE (InterfaceId) - socket_type: AtomicI32, // SOCK_STREAM/DGRAM/RAW - domain: i32, // AF_INET/AF_UNIX/AF_VSOCK - protocol: i32, // IPPROTO_TCP/UDP/ICMP -} -``` - -提供的 poll helper: - -- `send_poller()` / `recv_poller()`:阻塞等待 I/O 就绪或超时。内部流程:检查 `nonblock` → 调用 `register_waker()` 注册到 `Service` → `block_on(poll_io())` 自旋等待 → 超时检查 → 执行操作 → 成功或返回错误。 -- `send_poller_with()`:支持指定是否检测 HUP 事件。 -- `register_waker()`:根据 `DeviceBinding` 将调用者 waker 注册到对应设备的 `PollSet`。 - -### socket.domain / socket.protocol / socket_type 常量 - -每个 socket 创建时固化: - -| socket 类型 | socket_type (SOCK_\*) | domain (AF_\*) | protocol (IPPROTO_\*) | -| --- | --- | --- | --- | -| `TcpSocket` | 1 (STREAM) | 2 (INET) | 6 (TCP) | -| `UdpSocket` | 2 (DGRAM) | 2 (INET) | 17 (UDP) | -| `RawSocket` | 3 (RAW) | 2 (INET) | 根据 `IpProtocol` | -| `UnixSocket` (stream) | 1 (STREAM) | 1 (UNIX) | 0 | -| `UnixSocket` (dgram) | 2 (DGRAM) | 1 (UNIX) | 0 | -| `VsockSocket` | 1 (STREAM) | 40 (VSOCK) | 0 | - -## Router 内部结构 - -`Router`([router.rs](net/ax-net/src/router.rs))是 smoltcp 与多设备之间的适配层: - -```rust -pub struct Router { - rx_buffer: PacketBuffer, // smoltcp Device RX buffer(64 个 MTU 槽位) - tx_buffer: PacketBuffer, // smoltcp Device TX buffer - queues: Arc, // 全局 RX 队列(所有设备共享) - devices: Vec>, // 设备列表(按 add_device() 顺序) - table: SharedRouteTable, // 路由表 -} -``` +```mermaid +flowchart TB + subgraph Sources["配置来源"] + Static["NetworkConfig / InterfaceConfig"] + DhcpAck["DHCP ACK"] + SocketBind["bind(addr) / SO_BINDTODEVICE"] + end -### BoundedPacketQueue + subgraph State["NetControl"] + Registry["Interface registry"] + Routes["SharedRouteTable"] + Dns["DNS registry"] + end -`BoundedPacketQueue` 是有界 MPSC 队列,容量默认 `SOCKET_BUFFER_SIZE=64`: + subgraph Consumers["消费者"] + Query["interfaces() / default_routes() / dns_servers()"] + Dispatch["Router::dispatch() route lookup"] + Socket["socket bind/connect/listen"] + DnsQuery["dns_query() server selection"] + end -```rust -struct BoundedPacketQueue { - inner: Mutex>, - capacity: usize, - len: AtomicUsize, // 无锁长度读取,用于 is_empty() 检查 -} + Static --> Registry + Static --> Routes + Static --> Dns + DhcpAck --> Commit["commit_interface_update()"] + Commit --> Registry + Commit --> Routes + Commit --> Dns + SocketBind --> Binding["DeviceBinding"] + Binding --> Socket + Registry --> Query + Routes --> Query + Dns --> Query + Routes --> Dispatch + Routes --> DnsQuery ``` -- `push()`:加锁,满则返回 `Err(T)`(上层丢弃并打 warning)。 -- `pop()`:加锁,空返回 `None`。 -- `is_empty()`:原子读 `len`,无锁。 - -### DeviceHandle - -每设备一个 `DeviceHandle`,持有设备锁、收发队列和唤醒机制: - -```rust -struct DeviceHandle { - interface_id: InterfaceId, - name: String, - inner: Arc>>, // 设备 trait object - rx_queue: Arc>, // 指向 RouterQueues::rx(共享) - tx_queue: Arc>, // 独立 TX 队列 - rx_wake: Arc, // 唤醒 RX worker - tx_wake: Arc, // 唤醒 TX worker - rx_waker: Waker, // 驱动→rx_wake 的转换器 -} -``` +控制面由 `NetControl` 持有: -RX 队列是所有设备共享的(`RouterQueues::rx`),因为 smoltcp 从同一个 `Router::rx_buffer` 消费;TX 队列每设备独立,由 `dispatch()` 按路由决策分发。 +- `ControlState`:接口 registry 和 DNS server entries。 +- `SharedRouteTable`:路由规则,按最长前缀、metric、插入顺序排序。 +- `DeviceBinding`:表达 `SO_BINDTODEVICE` 或本地地址绑定推导出的接口约束。 -### TX Dispatch +### 初始化注册 -`Router::dispatch()`([router.rs](net/ax-net/src/router.rs))接收 `&mut SocketSet`,从 `tx_buffer` 取出 smoltcp 发出的 IP 包: +`init_network()` 根据 `NetworkConfig` 和发现到的 Ethernet 设备创建接口 registry: -- **IPv4 广播**(dst=255.255.255.255):复制到所有非 loopback Ethernet 设备。 -- **IPv4 单播**:按 `select_route_for_source()` 选路由(要求源地址匹配)。如果出接口是 loopback,走快速路径 `inject_loopback_rx_direct()`(snoop TCP + 直接写入 `rx_buffer`);否则推入对应设备 TX 队列。 -- **IPv6 多播**:复制到所有非 loopback Ethernet 设备。 -- **IPv6 单播**:按 `select_route_for_source()` 选路由,同样有 loopback 快速路径。 +- `lo` 固定为 `InterfaceId::LOOPBACK`。 +- Ethernet 接口从 ifindex 2 开始按发现顺序分配 `InterfaceId`。 +- 静态 IPv4、DHCP 开关、metric、DNS server 和默认路由在初始化时写入 `NetControl`。 +- `Router` 使用同一份 `SharedRouteTable`,所以控制面的路由更新会直接影响后续 TX dispatch。 -### RX 数据流 +### 运行期更新 -`Router::poll()`([router.rs#L476-L490](net/ax-net/src/router.rs#L476-L490)): +DHCP client 在 `Service::poll()` 中运行。收到 DHCP ACK 后,`Service` 通过 `commit_interface_update()` 一次性提交: -1. 从 `RouterQueues::rx` 循环出队,填充 `rx_buffer`。 -2. 每包先调 `snoop_tcp_packet()` 检查 TCP SYN,预创建 listen socket。 -3. 再调 snoop 回调(DHCP packet 分发)。 -4. `rx_buffer` 满则停止出队(剩余包留在队列供下次 poll)。 +- 接口 IPv4 地址和 flags。 +- smoltcp `Interface` 的 IP address 列表。 +- 当前接口的 IPv4 路由。 +- 当前接口贡献的 DNS server。 -## Ethernet 设备实现 +这里使用事务式更新,是为了避免外部查询看到“地址已更新但路由/DNS 还没更新”的中间状态。 -`EthernetDevice`([ethernet.rs](net/ax-net/src/device/ethernet.rs))是 `Device` trait 的主要实现: +### 查询路径 -```rust -pub struct EthernetDevice { - name: String, - inner: Arc, // 共享 IRQ 状态 - neighbors: HashMap, // ARP 缓存 - pending_neighbors: HashMap, // 等待 ARP reply - ip: Option, // 本机 IP - pending_packets: PacketBuffer, // ARP-pending 发包队列(128 个槽位) -} -``` +`interfaces()`、`interface_by_name()`、`interface_by_id()`、`ipv4_config()`、`default_routes()` 和 `dns_servers()` 都返回快照。调用方拿到的是当时的只读视图,不持有内部锁,也不应该假设快照会随 DHCP 或接口状态变化自动更新。 -### ARP 处理 +### 路由选择 -`EthernetDevice::recv()` 处理入站 Ethernet 帧: +路由表按以下优先级排序: -1. 调用 `EthernetDriver::receive()` 从硬件获取一帧。 -2. 解析 `EthernetFrame`,按 `EthernetProtocol` 分派: - - **ARP**:更新 neighbor 表(reply 和 gratuitous request),从 `pending_packets` 释放等待的包。 - - **IPv4/IPv6**:encapsulation 检查,将 payload 写入 smoltcp `PacketBuffer`。 +1. 最长前缀优先。 +2. 同前缀时低 metric 优先。 +3. 同前缀同 metric 时保留插入顺序。 -`send()` 路径: +普通查询使用 `select_route(dst)`,会过滤未 `UP` 的接口。TX dispatch 使用 `select_route_for_source(dst, src)`,同时匹配 smoltcp 已经选出的源地址,避免多宿主主机从错误接口发出带另一接口源地址的包。 -1. 查 `neighbors` 表,有则直接发送。 -2. 无则检查 `pending_neighbors`:如果已发送 ARP request 未超时,将包写入 `pending_packets`。 -3. 否则发送 ARP request,记录到 `pending_neighbors`。 -4. Neighbor TTL = 300s(与 Linux 一致),ARP retry = 1s。 +### 绑定约束 -### IRQ 模型 +`DeviceBinding` 是控制面和 socket 语义的连接点: -`EthernetIrqState` 管理 IRQ 注册和驱动锁: +- `bind(具体本地地址)` 会推导该地址所属接口。 +- `SO_BINDTODEVICE` 会显式限制 socket 只使用某个接口。 +- wildcard bind 不绑定具体接口,由路由表在发送时选择。 -```rust -struct EthernetIrqState { - irq: Option, - irq_registration: spin::Once>, - driver: SpinNoIrq>, - poll_ready: PollSet, -} -``` +这些约束会影响 socket readiness 注册、端口/listen 语义和路由可用性过滤。更细的规则见[控制面](control.md)。 -IRQ 处理链: -1. `ax-runtime` 通过 `set_ethernet_irq_registrar()` 注册 `EthernetIrqRegistrar`。 -2. `EthernetDevice::new()` 中通过 `registrar.register_shared(name, irq, action)` 注册 IRQ handler。 -3. IRQ 触发时调用 `handle_ethernet_irq()`:运行 `driver.handle_irq()`,如果返回 `RX_READY` 则 `poll_ready.wake()` 唤醒 RX worker。 -4. 如果没有注册 IRQ registrar,回退到纯 poll 模式(RX worker 的 `rx_wake.wait()` 可被 `request_poll()` 路径唤醒)。 - -### RdNetDriver - -`RdNetDriver`([driver.rs](net/ax-net/src/device/driver.rs))是 `EthernetDriver` trait 的 `rd-net` 适配实现。内部持有 `rd_net::TxQueue` 和 `rd_net::RxQueue`,通过预取 `RX_PREFETCH_TARGET=1` 个包到 `pending_rx: VecDeque` 减少锁竞争。`alloc_tx_buffer()` 返回 `VecTxBuffer`(栈友好),`transmit()` 执行硬件发送。 - -## Loopback 快速路径 - -`LoopbackDevice`([loopback.rs](net/ax-net/src/device/loopback.rs))是一个零状态占位类型(`pub struct LoopbackDevice;`),其 `Device` trait 实现全部是 no-op。真正的 loopback 数据路径在 `Router` 中以**快速路径**实现,绕过设备 worker 和 `LoopbackDevice::send()/recv()`: - -- **TX 方向**(`Router::dispatch()`,[router.rs](net/ax-net/src/router.rs)):当路由决策选中 loopback 接口时,调用 `inject_loopback_rx_direct()` 直接将 IP 包写入 `Router.rx_buffer`(smoltcp 的 RX token 来源),并在写入前执行 `snoop_tcp_packet()` 预创建 TCP listen socket。这样回环包在**同一个 `Service::poll()` 周期内**被 smoltcp 消费,无需等待设备 worker 调度。 -- **DHCP/特殊发包**(`Router::send_on_device()`):对 loopback 设备调用 `inject_loopback_rx()` 写入共享 `RouterQueues::rx` 有界队列,由下一轮 `Router::poll()` 消费。 -- **Worker 跳过**:`start_tx_workers()` 和 `start_rx_workers()` 显式跳过 `InterfaceId::LOOPBACK`,不为 loopback 创建线程。 - -这种设计避免了在 loopback 路径上引入设备线程调度延迟,对 riscv64 单核等慢速目标尤其重要。 - -## 数据面 poll 流程 - -`Service::poll()`([service.rs](net/ax-net/src/service.rs))在每个 poll 周期执行: - -```rust -pub fn poll(&mut self, sockets: &mut SocketSet) -> bool { - let timestamp = now(); - let mut dhcp_events = Vec::new(); - let mut dhcp_server_replies = Vec::new(); - // 1. Router::poll():从 RX 队列消费包到 smoltcp buffer,同时 snoop 回调分发 - // DHCP client 和 DHCP server 的入站包 - self.router.poll(timestamp, sockets, |interface_id, packet| { - // DHCP client: DhcpState::process_packet() - // DHCP server: DhcpServer::process_packet() -> reply bytes - }); - // 2. 处理 DHCP client 事件(更新地址、路由、DNS) - for event in dhcp_events { self.handle_dhcp_event(event); } - // 3. DHCP server 回复通过 send_on_device() 广播出去 - for (dev, reply) in dhcp_server_replies { self.router.send_on_device(...); } - // 4. smoltcp Interface::poll():协议状态机推进 - let socket_state_changed = self.iface.poll(timestamp, &mut self.router, sockets); - // 5. DHCP client 发包定时器 - let dhcp_poll_next = self.poll_dhcp(timestamp); - // 6. 回收已完成 teardown 的 orphan TCP socket - crate::orphan::reap_orphans(timestamp, sockets); - // 7. TX dispatch:将 smoltcp 输出的包路由到设备(含 loopback 快速路径) - self.router.dispatch(timestamp, sockets) - || dhcp_poll_next || dhcp_server_sent || socket_state_changed -} -``` +## Single protocol core -关键变更:`dispatch()` 现在接收 `&mut SocketSet`,使 loopback 快速路径能在注入 RX buffer 前执行 `snoop_tcp_packet()`。`reap_orphans()` 在持有 SocketSet 锁时执行,避免额外锁获取。 +Single protocol core 是 `ax-net` 的协议状态中心,由 `Service`、smoltcp `Interface`、全局 `SocketSetWrapper`、`ListenTable`、DHCP 状态和 TCP orphan 回收共同组成。 -## net-poll Worker +### Socket system -`net_poll_worker`([lib.rs#L479-L489](net/ax-net/src/lib.rs#L479-L489))是协议核心的唯一驱动线程: +IP socket 共享 smoltcp `SocketSet`,但 Linux/POSIX 语义由 `ax-net` 自己补齐: -```rust -fn net_poll_worker() { - loop { - let delay = next_poll_delay(); // smoltcp poll_at() 建议延迟 - let timed_out = NET_POLL_WAKE.wait_timeout_until(delay, - || NET_POLL_REQUESTED.load(Acquire)); // 等 request_poll() 或超时 - if !timed_out { NET_POLL_REQUESTED.store(false, Release); } - poll_until_idle(); // 循环 poll 直到无新事件 - } -} -``` +- `SocketOps` 统一 TCP/UDP/raw/Unix/vsock backend。 +- `GeneralOptions` 维护非阻塞、超时、`SO_REUSEADDR`、`SO_BINDTODEVICE` 等通用选项。 +- `SocketSetWrapper` 增加 UDP bind 冲突仲裁。 +- `TCP_BOUND_PORTS` 和 `ListenTable` 共同维护 TCP bind/listen 端口语义。 +- `ListenTable` 在 RX snoop 阶段预创建 TCP socket,支持 accept queue 和 per-address listen。 +- `orphan.rs` 在用户关闭仍处于 teardown 的 TCP socket 后继续推进 FIN/TIME_WAIT,避免破坏 TCP 关闭语义。 -`poll_until_idle()` 使用 `POLLING_INTERFACES` CAS 锁防止重入(同一时刻最多一个 poll 进行中),内部循环调用 `poll_once()` 直到 `POLL_AGAIN` 为 false。循环内**不 yield**——net-poll worker 是专用线程,在有工作(`poll_once() == true`)时持续批量处理以最大化吞吐。空闲时 `next_poll_delay()` 返回最多 100ms 的 idle poll interval。 +Unix domain socket 和 vsock 不走 smoltcp IP 层,但通过同一个 public socket facade 暴露给上层。细节见[Socket 系统](sockets.md)。 -## DHCP 状态机 +### 专用 poll worker -`DhcpState`([service.rs#L269-L510](net/ax-net/src/service.rs#L269-L510))维护 per-interface DHCP 客户端: +smoltcp 的 `Interface::poll()` 由 `net-poll` worker 独占推进。socket 操作、设备 IRQ/OOB RX 和 DNS 等路径只调用 `request_poll()` 触发唤醒,不在应用线程里同步驱动整个协议栈。 +```mermaid +sequenceDiagram + participant App as socket caller + participant Wake as request_poll() + participant Worker as net-poll worker + participant Service as Service::poll() + participant Smol as smoltcp Interface + + App->>Wake: send/recv/connect/accept needs progress + Wake->>Worker: notify NET_POLL_WAKE + Worker->>Service: poll_until_idle() + Service->>Smol: Interface::poll() + Service->>Service: DHCP/orphan/TX dispatch + Smol-->>App: readiness waker wakes blocked caller ``` -Discovering ──Offer──→ Requesting ──ACK──→ Bound - │ │ │ - └──timeout→retry── └──timeout→retry── └──NAK→Discovering (reset) -``` - -- **Discovering**:广播 DHCPDISCOVER,指数退避重试(1,2,4,8... 最多 16 秒间隔)。 -- **Requesting**:收到 DHCPOFFER 后广播 DHCPREQUEST。 -- **Bound**:收到 DHCPACK,状态转为 Bound。NAK 触发 reset → Discovering。 - -`process_packet()` 按 ingress `InterfaceId` 匹配、解析 IPv4→UDP→DHCP,校验 `transaction_id` 和 `client_hardware_address` 防止误收。`poll_packet()` 在非 Bound 状态且重试时间到达时构造 DHCP 发包。 -`dhcp_transaction_id(mac)` 将 MAC 地址低 32 位与 `wall_time_nanos()` 混合生成 transaction ID。 +这个模型避免应用线程和协议栈驱动线程互相抢协议栈锁,也保证 TCP 重传、keepalive、DHCP 和设备收包不会依赖某个应用线程继续运行。 -## TCP Orphan Socket 回收 +## Router as MultiDevice -当用户关闭 TCP socket(`TcpSocket::drop()`)时,如果连接仍处于活跃 teardown 状态(Established/CloseWait/FinWait*/LastAck/Closing/TimeWait)或有未发送数据,socket 不会立即从 smoltcp `SocketSet` 移除,而是转为 **orphan** 状态,由后台回收([orphan.rs](net/ax-net/src/orphan.rs)): +`Router` 是 single protocol core 和真实设备之间的适配层。它位于 smoltcp 的 `phy::Device` 边界上,对上提供一个 IP medium 设备,对下管理多个 `DeviceHandle`。 -- **目的**:保证 RFC 793 合规的 FIN 四次挥手和 TIME_WAIT,避免对端连接悬挂。 -- **机制**:`Drop` 中调用 `socket.close()` 发起 FIN,然后 `add_orphan()` 将 `SocketHandle` 移入全局 `ORPHAN_SOCKETS: Mutex>`。 -- **回收**:`reap_orphans()` 在每个 `Service::poll()` 周期中调用,检查每个 orphan 的 smoltcp TCP state: - - `Closed` → 立即移除(`ReapReason::Closed`) - - `TimeWait/FinWait1/FinWait2/LastAck/Closing` → 保留直到 smoltcp 自然超时,但最长 60 秒(`ORPHAN_MAX_LINGER`)后强制移除(`ReapReason::Expired`) - - 其他意外状态 → 60 秒后强制移除并 warn -- **溢出保护**:`ORPHAN_MAX_SOCKETS = 1024`,超限时只 warn 不强制清除正在 teardown 的连接,避免 FIN 丢失。 +| 子组件 | 职责 | +| --- | --- | +| `Router.rx_buffer` | smoltcp 从这里取 RX IP packet | +| `Router.tx_buffer` | smoltcp 把待发送 IP packet 写到这里 | +| `RouterQueues::rx` | 所有 Ethernet RX worker 共享的有界 RX 队列 | +| `DeviceHandle.tx_queue` | 每个真实设备独立的有界 TX 队列 | +| `RouteTable` | TX dispatch 的出接口和 next-hop 决策依据 | +| loopback fast path | 回环包直接写入 `rx_buffer`,不经过设备 worker | -## DHCP 服务器(SoftAP 模式) +`Router::poll()` 负责把设备 RX 队列推进到 smoltcp RX buffer;`Router::dispatch()` 负责把 smoltcp TX buffer 中的包按路由分发到 loopback 或真实设备。更细的 worker、队列和 ARP 行为见[多设备实现](devices.md)。 -`DhcpServer`([dhcp_server.rs](net/ax-net/src/dhcp_server.rs))是最简 IPv4 DHCP 服务器,用于 Wi-Fi SoftAP 模式给单个客户端分配地址: +## Device layer -- 单客户端、单地址租约(`leased_to: Option`),足够 AP 验证 ping/ssh。 -- 不依赖 smoltcp 的 DHCP socket,手工解析/封装 `DhcpRepr → UdpRepr → Ipv4Repr`。 -- 处理 Discover→Offer、Request→Ack,回复广播 IPv4 包。 -- 通过 `Service::enable_dhcp_server(dev, server_ip, client_ip, subnet_mask)` 启用,回复包通过 `Router::send_on_device()` 广播。 -- 入站包在 `Router::poll()` 的 snoop 回调中分发,与 DHCP client 路径完全独立。 +设备层把不同来源的网络设备统一成 `ax-net` 内部 `Device` trait: -## DNS 解析流程 - -`dns_query()`([lib.rs#L506-L536](net/ax-net/src/lib.rs#L506-L536)): - -1. `dns_servers()` 获取按 (metric, interface_id, server_ip) 排序的去重 DNS server 列表。 -2. 过滤不可路由的 server(通过 `select_route()` 检查可达性)。 -3. 在 `SOCKET_SET` 中创建 `dns::Socket`,发起 A 记录查询。 -4. 循环 `request_poll()` + `yield_now()`,检查 `get_query_result()`。 -5. 默认 5 秒超时(`DNS_DEFAULT_TIMEOUT`),超时返回 `ETIMEDOUT`。 -6. `DnsSocketGuard` 在 drop 时从 `SOCKET_SET` 移除 DNS socket。 +| 设备类型 | 主要源码 | 作用 | +| --- | --- | --- | +| `LoopbackDevice` | [device/loopback.rs](net/ax-net/src/device/loopback.rs) | 零状态占位;真实回环数据路径由 `Router` 快速路径完成 | +| `EthernetDevice` | [device/ethernet.rs](net/ax-net/src/device/ethernet.rs) | Ethernet frame 解析/封装、ARP neighbor 表、pending packet、IRQ/OOB RX | +| `RdNetDriver` | [device/driver.rs](net/ax-net/src/device/driver.rs) | 将 `rd-net` RX/TX queue 适配为 `EthernetDriver` | +| `VsockDevice` | [device/vsock.rs](net/ax-net/src/device/vsock.rs) | 可选 vsock 设备注册和事件入口 | -执行顺序: +这一层是协议栈和硬件驱动框架的能力边界。`ax-net` 不直接依赖 FDT、PCI、MMIO、DMA 或平台 IRQ ABI,而是通过 `EthernetDriver`、`EthernetIrqRegistrar` 和 OOB RX 通知机制接入实际设备。 -```text -Service::poll() - -> Router::poll() drain RX queue - -> DHCP packet snoop - -> smoltcp Interface::poll() - -> poll DHCP retry - -> Router::dispatch() route TX packets -``` +## 数据面流程 -`Router`([router.rs#L318-L326](net/ax-net/src/router.rs#L318-L326))是 smoltcp `phy::Device` 适配层。它对 smoltcp 实现 `phy::Device` trait([router.rs#L672](net/ax-net/src/router.rs#L672)),对外只暴露 IP 层(`Medium::Ip`): - -```rust -impl smoltcp::phy::Device for Router { - type RxToken<'a> = RxToken<'a>; - type TxToken<'a> = TxToken<'a>; - fn receive(&mut self, _timestamp: Instant) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> { /* ... */ } - fn transmit(&mut self, _timestamp: Instant) -> Option> { /* ... */ } - fn capabilities(&self) -> DeviceCapabilities { - let mut caps = DeviceCapabilities::default(); - caps.medium = Medium::Ip; - caps.max_transmission_unit = STANDARD_MTU; - caps.max_burst_size = Some(SOCKET_BUFFER_SIZE); - caps - } -} -``` +### RX 路径 -设备 worker 不直接访问 `Router` 本体,只和 `RouterQueues` / per-device TX queue 交互: - -- RX worker([router.rs#L575](net/ax-net/src/router.rs#L575))从 rd-net 设备读取 packet,复制到有界 RX queue,并 `request_poll()`。 -- smoltcp `RxToken`([router.rs#L656](net/ax-net/src/router.rs#L656))从 RX buffer 读 packet,并保留 ingress `InterfaceId` 元数据。 -- smoltcp `TxToken`([router.rs#L642](net/ax-net/src/router.rs#L642))只写入 TX buffer;真实出接口由 `Router::dispatch()`([router.rs#L498](net/ax-net/src/router.rs#L498))根据 IP 包源/目的地址和路由表选择。 -- TX worker([router.rs#L559](net/ax-net/src/router.rs#L559))从对应设备 TX queue 发包。 - -## 设备能力边界(Device trait) - -`ax-net` 不直接依赖硬件驱动框架。所有设备实现统一的内部 `Device` trait([device/mod.rs#L33-L55](net/ax-net/src/device/mod.rs#L33-L55)): - -```rust -pub trait Device: Send + Sync { - fn name(&self) -> &str; - fn recv(&mut self, interface_id: InterfaceId, - buffer: &mut PacketBuffer, timestamp: Instant, - snoop: &mut dyn FnMut(&[u8])) -> bool; - /// Sends a packet to the next hop. Returns true if receive became ready. - fn send(&mut self, next_hop: IpAddress, packet: &[u8], timestamp: Instant) -> bool; - fn set_ipv4_addr(&mut self, _addr: Option) {} - fn arp_entries(&self, _timestamp: Instant) -> Vec { Vec::new() } - fn register_waker(&self, waker: &Waker); -} +```mermaid +flowchart LR + Hw["NIC / rd-net RX"] --> Eth["EthernetDevice::recv()"] + Eth --> Arp["ARP / Ethernet 解封装"] + Arp --> RxQ["RouterQueues::rx"] + RxQ --> RouterPoll["Router::poll()"] + RouterPoll --> Snoop["DHCP / TCP SYN snoop"] + Snoop --> RxBuf["Router.rx_buffer"] + RxBuf --> Smol["smoltcp Interface::poll()"] + Smol --> Sock["SocketSet socket RX buffer"] + Sock --> App["recv()/accept()/poll()"] ``` -两个实现: - -- `LoopbackDevice`([device/loopback.rs](net/ax-net/src/device/loopback.rs)):零状态占位类型,`Device` trait 全为 no-op。真正的回环在 `Router::dispatch()` 的快速路径中完成(见 [Loopback 快速路径](#loopback-快速路径))。 -- `EthernetDevice`([device/ethernet.rs](net/ax-net/src/device/ethernet.rs)):维护 ARP 邻居表(`NEIGHBOR_TTL = 300s`)、pending packet 队列、Ethernet 帧封装/解析,并对接 IRQ 适配。支持两种 RX 就绪模式:IRQ 驱动(`new()`)和 out-of-band 驱动(`new_oob_rx()`,用于 SDIO Wi-Fi 等自带中断线程的设备)。 +RX worker 只负责从真实设备取包并推入共享 RX 队列。协议处理发生在 `Service::poll()` 内:先由 `Router::poll()` 把 RX 队列 drain 到 `rx_buffer`,再由 smoltcp `Interface::poll()` 交付给 TCP/UDP/raw socket。 -`EthernetDevice` 之下是能力边界 trait `EthernetDriver`([device/driver.rs#L70-L82](net/ax-net/src/device/driver.rs#L70-L82)),由 `RdNetDriver`([device/driver.rs#L128](net/ax-net/src/device/driver.rs#L128))包装 `rd-net` 的 `TxQueue` / `RxQueue` 实现。这样 `ax-net` 避免直接依赖 FDT、PCI、MMIO、DMA、VirtIO 或平台 IRQ ABI。 +### TX 路径 -## SocketSetWrapper - -全局 socket 容器 `SocketSetWrapper`([wrapper.rs](net/ax-net/src/wrapper.rs))除了持有 smoltcp `SocketSet`,还维护 UDP 端口冲突仲裁(`udp_binds`): - -```rust -pub(crate) struct SocketSetWrapper<'a> { - pub inner: Mutex>, - udp_binds: Mutex>, -} +```mermaid +flowchart LR + App["send()/connect()"] --> Sock["SocketSet socket TX buffer"] + Sock --> Smol["smoltcp Interface::poll()"] + Smol --> TxBuf["Router.tx_buffer"] + TxBuf --> Dispatch["Router::dispatch()"] + Dispatch --> Route["RouteTable select_route_for_source()"] + Route --> Loop["loopback: direct rx_buffer injection"] + Route --> TxQ["Ethernet: per-device TX queue"] + TxQ --> Eth["EthernetDevice::send()"] + Eth --> Arp["ARP / next-hop MAC"] + Arp --> Hw["rd-net TX / NIC"] ``` -`udp_bind()` 实现 Linux 语义的端口复用规则:具体地址与通配地址互斥,相同地址端口冲突返回 `AddrInUse`(除非 `SO_REUSEADDR`)。`udp_port_available()` 暴露给 ephemeral port 分配器使用。 +普通 Ethernet 发送会进入设备 TX worker,由 `EthernetDevice` 完成 ARP 和 Ethernet frame 封装。loopback 不走外部设备队列:`Router::dispatch()` 选中 `InterfaceId::LOOPBACK` 时直接把 IP packet 注入 `rx_buffer`,因此本机回环连接可在同一个 poll 周期内继续推进。 -## 为什么不是 multi-smoltcp domain - -多个 smoltcp 实例可以让不同 NIC 的协议处理并行,但会引入更复杂的 socket 语义: - -- TCP wildcard listen 需要在多个 `SocketSet` 创建 listener 并聚合 accept queue。 -- UDP wildcard bind 需要聚合多个 backend 的 recv readiness。 -- 端口冲突检查需要跨 domain 统一仲裁。 -- 动态路由切换会涉及 socket backend 迁移或重新绑定。 -- `SO_BINDTODEVICE`、AF_PACKET、raw socket 和 multicast 的接口集合语义会更复杂。 - -因此当前 `ax-net` 优先保留单协议栈语义,用多接口 registry、路由决策和设备队列解耦补齐功能和部分性能优化。 - -## Socket backend 分层 - -`Socket` 枚举([socket.rs#L253-L265](net/ax-net/src/socket.rs#L253-L265))聚合 5 类 backend,统一实现 `SocketOps` + `Configurable`: - -| Backend | 类型 | 协议核心 | 源码 | -| --- | --- | --- | --- | -| `TcpSocket` | SOCK_STREAM / AF_INET | smoltcp TCP socket | [tcp.rs#L50](net/ax-net/src/tcp.rs#L50) | -| `UdpSocket` | SOCK_DGRAM / AF_INET | smoltcp UDP socket | [udp.rs#L50](net/ax-net/src/udp.rs#L50) | -| `RawSocket` | SOCK_RAW / AF_INET | smoltcp raw socket | [raw.rs#L50](net/ax-net/src/raw.rs#L50) | -| `UnixSocket` | SOCK_STREAM/SOCK_DGRAM / AF_UNIX | 自包含 `Transport`(不经 smoltcp) | [unix/mod.rs#L70](net/ax-net/src/unix/mod.rs#L70) | -| `VsockSocket` | SOCK_STREAM / AF_VSOCK | `rdif-vsock` 驱动 + ring buffer | [vsock/mod.rs#L68](net/ax-net/src/vsock/mod.rs#L68) | - -IP 类 socket 通过 `SOCKET_SET.add()` 注册到全局 `SocketSet`,获得 `SocketHandle`;Unix / vsock 不依赖 smoltcp,各自维护连接状态。 - -## Socket 状态机 - -IP socket 使用 `StateLock`([state.rs#L32-L57](net/ax-net/src/state.rs#L32-L57))做无锁状态转换,避免在 socket 上长期持锁: - -```rust -pub(crate) enum State { Idle, Busy, Connecting, Connected, Listening, Closed } - -pub struct StateLock(AtomicU8); -impl StateLock { - pub fn lock(&self, expect: State) -> Result, State> { - // CAS: expect -> Busy - } -} -``` +### DHCP 和 DNS -`StateGuard::transit()`([state.rs#L65-L76](net/ax-net/src/state.rs#L65-L76))在执行操作时把状态临时置为 `Busy`,操作成功才提交新状态,失败回滚。这样并发 `connect` / `accept` 不会互相破坏。 +DHCP client/server 都位于 `Service::poll()` 调度内,但不依赖 smoltcp 的普通 socket API: -非阻塞与超时由 `GeneralOptions`([general.rs#L113-L145](net/ax-net/src/general.rs#L113-L145))统一处理,基于 `axpoll::poll_io` 与 `block_on`/`timeout` 实现 `send_poller_with` / `recv_poller_with`。 +- DHCP client 由 per-interface `DhcpState` 维护 Discover/Request/Bound 状态,收到 ACK 后通过 `commit_interface_update()` 更新地址、路由和 DNS。 +- DHCP server 位于 [dhcp_server.rs](net/ax-net/src/dhcp_server.rs),面向 SoftAP 场景,手工解析/封装 DHCP/UDP/IPv4 包,并通过 `Router::send_on_device()` 发包。 +- DNS 查询使用 smoltcp `dns::Socket` 临时加入全局 `SocketSet`,查询完成后由 guard 自动移除。 diff --git a/docs/docs/architecture/net/configuration.md b/docs/docs/architecture/net/configuration.md index bfd2804f55..a187ea4aba 100644 --- a/docs/docs/architecture/net/configuration.md +++ b/docs/docs/architecture/net/configuration.md @@ -1,9 +1,9 @@ --- -sidebar_position: 3 +sidebar_position: 7 sidebar_label: "配置参考" --- -# 配置与 Feature +# 配置参考 `ax-net` 的配置目标是用结构化数据表达接口意图,而不是依赖旧的全局单网口环境变量。每个 Ethernet 接口可以独立使用静态 IPv4 或 DHCP,并携带 metric 和 DNS 配置。 @@ -53,6 +53,7 @@ pub const RAW_TX_BUF_LEN: usize = 64 * 1024; pub const LISTEN_QUEUE_SIZE: usize = 512; pub const SOCKET_BUFFER_SIZE: usize = 64; pub const ETHERNET_MAX_PENDING_PACKETS: usize = 128; +pub const DEVICE_TX_QUEUE_SIZE: usize = 128; ``` `ETHERNET_MAX_PENDING_PACKETS` 取 128 而非 32,是为了容纳应用启动时多个并发 TCP 连接的首个 SYN 突发,以及长连接在 `NEIGHBOR_TTL` 过期后重新进入 ARP-pending 队列的 burst(见源码注释 [consts.rs#L14-L27](net/ax-net/src/consts.rs#L14-L27))。 @@ -132,7 +133,7 @@ pub struct StaticIpConfig { - `dhcp = true` 且 `static_ip.is_some()`。 - 静态 IP 为 unspecified。 - prefix 大于 32。 -- gateway 为 unspecified。 +- gateway 可以为 unspecified,表示不安装默认路由。 - DNS server 为 unspecified。 - 显式配置没有匹配任何设备。 - 接口名冲突。 @@ -206,6 +207,8 @@ let config = NetworkConfig { ## 接口命名与 ID 分配 +> 以下为运行时常量与默认值。 + `InterfaceId` 数值的分配规则: | InterfaceId | 接口 | 说明 | @@ -221,7 +224,7 @@ let config = NetworkConfig { - 直连路由的 metric 与接口配置相同。 - 默认路由的 metric 与接口配置相同。 - 路由表排序:最长前缀匹配 > 低 metric 优先 > 同 metric 稳定插入顺序。 -- 未配置 metric 的接口默认为 `u32::MAX`。 +- 未显式配置的 Ethernet 接口默认启用 DHCP,metric 默认为 100;fallback DNS 来源使用 `u32::MAX`,因此会排在接口级 DNS 之后。 - 同一目的地址存在多个匹配路由时,优先选择 metric 较低且接口 `UP` 的路由。 ## TCP Keep-Alive 默认值 @@ -278,10 +281,11 @@ const TCP_INFO_DEFAULT_REORDERING: u32 = 3; | --- | --- | --- | | `Router::rx_buffer` | [router.rs#L326](net/ax-net/src/router.rs#L326) | `SOCKET_BUFFER_SIZE` 个 MTU 槽位 × 1500 字节 | | `Router::tx_buffer` | [router.rs#L330](net/ax-net/src/router.rs#L330) | 同上 | -| `RouterQueues::rx` | [router.rs#L335](net/ax-net/src/router.rs#L335) | `SOCKET_BUFFER_SIZE` 个 `RxPacket` | -| `DeviceHandle::tx_queue` | [router.rs#L140](net/ax-net/src/router.rs#L140) | `SOCKET_BUFFER_SIZE` 个 `TxPacket` | +| `RouterQueues::rx` | [router.rs](net/ax-net/src/router.rs) | `SOCKET_BUFFER_SIZE` 个 inline `RxPacket`(每包最多 MTU) | +| `DeviceHandle::tx_queue` | [router.rs](net/ax-net/src/router.rs) | `DEVICE_TX_QUEUE_SIZE` 个 inline `TxPacket`(每包最多 MTU) | | `EthernetDevice::pending_packets` | [ethernet.rs#L123](net/ax-net/src/device/ethernet.rs#L123) | `ETHERNET_MAX_PENDING_PACKETS`(128)个 IP 包 | -| `LoopbackDevice::buffer` | [loopback.rs#L17](net/ax-net/src/device/loopback.rs#L17) | `SOCKET_BUFFER_SIZE` 个 MTU 槽位 × 1500 字节 | + +`LoopbackDevice` 不维护独立 buffer。普通 smoltcp loopback TX 在 `Router::dispatch()` 中直接注入 `Router::rx_buffer`;`send_on_device()` 的 loopback 特殊发包才进入共享 `RouterQueues::rx`,且同样使用 inline `QueuedPacket`。 ## Unix Stream 缓冲区 diff --git a/docs/docs/architecture/net/control.md b/docs/docs/architecture/net/control.md new file mode 100644 index 0000000000..a83fedb7df --- /dev/null +++ b/docs/docs/architecture/net/control.md @@ -0,0 +1,143 @@ +--- +sidebar_position: 3 +sidebar_label: "控制面" +--- + +# 控制面 + +控制面是 `ax-net` 的接口管理和路由决策层,与数据面完全分离,查询不进入设备锁也不推进 smoltcp poll。 + +## NetControl + +控制面由 `NetControl`([service.rs](net/ax-net/src/service.rs))统一持有: + +```rust +pub struct NetControl { + state: RwLock, + pub(crate) routes: SharedRouteTable, +} +``` + +内部 `ControlState` 保存 `Vec`(接口 registry)与 `Vec`(DNS 来源信息)。 + +### 接口查询 + +- `interfaces()` / `interface_by_name()` / `interface_by_id()`:返回只读快照,不持锁。 +- `ipv4_config(name)`:按名称查接口 IPv4 配置。 +- `default_routes()`:返回当前默认路由列表。 + +### 路由决策 + +`select_route(dst_addr)`:按目的地址查路由,校验目标接口 `UP`。底层调用 `RouteTable::select_route_if()`,传入 `is_usable` 闭包过滤未 `UP` 的接口。 + +`select_route_for_source(dst, src)`:在 TX dispatch 时同时匹配目的地址和源地址。多宿主主机(两个接口有不同源 IP)发送到同一目的地时,需根据 smoltcp 注入的源地址选择正确的出接口。 + +```rust +pub fn select_route_if(&self, dst, mut is_usable: impl FnMut(InterfaceId) -> bool) + -> Option +{ + self.rules.iter() + .find(|rule| rule.filter.contains_addr(dst) && is_usable(rule.interface_id)) + .map(|rule| RouteDecision { /* ... */ }) +} +``` + +### 接口-地址绑定 + +`local_binding_for(endpoint)`:把本地监听地址映射为 `DeviceBinding`。若 `endpoint.addr` 是某个接口的 IP,返回 `bound_if = Some(interface_id)`;若是 wildcard,返回 `DeviceBinding::default()`。 + +### 运行期更新 + +`commit_interface_update(update, routes)`:DHCP ACK 等运行期更新通过一次性事务提交接口地址、smoltcp IP 地址、路由和 DNS,避免外部看到半更新状态。接口查询只需持有 `NetControl.state` 读锁;更新时获取写锁。 + +## RouteTable + +`RouteTable`([router.rs](net/ax-net/src/router.rs))是路由表的实现: + +```rust +#[derive(Debug)] +pub struct Rule { + pub filter: IpCidr, + pub via: Option, + pub dev: usize, + pub interface_id: InterfaceId, + pub src: IpAddress, + pub metric: u32, + pub order: u64, +} +``` + +### 排序策略 + +`sort_rules()` 的三级排序: + +1. **最长前缀优先**:`b.filter.prefix_len().cmp(&a.filter.prefix_len())` +2. **低 metric 优先**:同前缀长度时,`a.metric.cmp(&b.metric)` +3. **插入顺序稳定**:同前缀同 metric 时,`a.order.cmp(&b.order)` + +`select_route_if` 使用 `find()` 返回第一个匹配项(即最高优先级的路由)。 + +### 规则管理 + +- `add_rule(rule)`:添加单条规则,自动分配 `order` 并重排。 +- `remove_ipv4_rules_for_interface(id)`:移除某个接口的所有 IPv4 规则。 +- `replace_ipv4_rules_for_interface(id, new_rules)`:原子替换某个接口的 IPv4 规则(DHCP ACK 后使用)。 +- `default_routes()`:返回所有默认路由(`0.0.0.0/0`)。 + +## 接口标识 + +`InterfaceId(u32)` 是内部接口 ID,同时也是 StarryOS Linux ABI 的 ifindex 来源: + +```rust +pub struct InterfaceId(u32); + +impl InterfaceId { + pub const LOOPBACK: Self = Self(1); + pub const fn new(raw: u32) -> Self { Self(raw) } + pub const fn to_linux_ifindex(self) -> i32 { self.0 as i32 } + pub const fn from_linux_ifindex(ifindex: i32) -> Option { /* ... */ } +} +``` + +- `InterfaceId::LOOPBACK == 1`,固定对应 `lo`。 +- Ethernet 接口从 `2` 开始,按发现顺序递增。 + +`InterfaceInfo` 是对外只读快照,包含 ID、名称、类型、MAC、IPv4、MTU、flags 和 metric。 + +```rust +pub struct InterfaceInfo { + pub id: InterfaceId, + pub name: String, + pub kind: InterfaceKind, + pub mac: Option, + pub ipv4: Option, + pub mtu: usize, + pub flags: InterfaceFlags, + pub metric: u32, +} +``` + +## DeviceBinding + +`DeviceBinding`([config.rs](net/ax-net/src/config.rs))对应 Linux `SO_BINDTODEVICE`: + +```rust +pub struct DeviceBinding { + pub bound_if: Option, +} +``` + +- `None`:未绑定接口,按 route decision 选择出接口。 +- `Some(id)`:只允许对应接口参与 route/waker/device 选择。 + +## DNS 注册 + +DNS server 来源分三类,内部记录为 `DnsServerEntry`: + +| 来源 | 说明 | +| --- | --- | +| DHCP | DHCP ACK 下发,绑定来源接口和 metric | +| Static | `InterfaceConfig::dns_servers` | +| Fallback | `NetworkConfig::default_dns_servers` | + +`dns_servers()` 返回按 (metric, interface_id, server_ip) 排序的去重地址列表。DHCP ACK 后 `commit_interface_update` 会替换对应接口的 DNS entry。 diff --git a/docs/docs/architecture/net/dependencies.md b/docs/docs/architecture/net/dependencies.md deleted file mode 100644 index 88d4654a96..0000000000 --- a/docs/docs/architecture/net/dependencies.md +++ /dev/null @@ -1,299 +0,0 @@ ---- -sidebar_position: 2 -sidebar_label: "依赖关系" ---- - -# 网络组件依赖关系 - -`ax-net` 是一个共享网络栈 crate,不包含具体硬件驱动。它位于 `net/ax-net`,上接 ArceOS、StarryOS、Axvisor,下接 `rd-net` / `rdif-eth` 设备能力。 - -## 分层关系 - -```mermaid -flowchart TB - subgraph Upper["上层系统"] - ArceApi["ax-api / ax-posix-api"] - AxStd["ax-std / ax-libc"] - Starry["starry-kernel"] - Axvisor["Axvisor"] - end - - subgraph Runtime["运行时接入"] - AxRuntime["ax-runtime"] - IrqAdapter["runtime IRQ adapter"] - end - - subgraph Net["net/ax-net"] - Public["public socket + interface API"] - Service["Service + Router + SocketSet"] - DeviceAdapt["device adapter"] - end - - subgraph Protocol["协议与同步"] - Smol["smoltcp"] - Poll["axpoll"] - Task["ax-task"] - Sync["ax-sync / ax-kspin"] - end - - subgraph Driver["设备与驱动"] - RdNet["rd-net"] - RdifEth["rdif-eth"] - AxDriver["ax-driver / rdrive"] - Virtio["virtio-net / SoC net driver"] - end - - Upper --> Net - AxRuntime --> Net - IrqAdapter --> Net - Net --> Smol - Net --> Poll - Net --> Task - Net --> Sync - Net --> RdNet - RdNet --> RdifEth - AxDriver --> RdifEth - Virtio --> AxDriver -``` - -## 直接依赖 - -依赖声明在 [net/ax-net/Cargo.toml](net/ax-net/Cargo.toml): - -| 依赖 | 用途 | -| --- | --- | -| `smoltcp` | TCP/IP、UDP、raw、DHCPv4、DNS、ARP cache 和 `Device` token 模型 | -| `rd-net` | Ethernet 设备抽象和 RX/TX buffer trait | -| `rdif-vsock` | 可选 vsock 设备能力(`vsock` feature) | -| `ax-task` | worker、wait queue、future helper、sleep/yield | -| `axpoll` | readiness 和 `Pollable` | -| `ax-sync` / `ax-kspin` | mutex、spin lock 和同步原语 | -| `ax-hal` | monotonic/wall clock 等 HAL 能力 | -| `ax-io` | socket read/write buffer trait | -| `ax-errno` | `AxError` / `AxResult` | -| `spin` | `Once`、`LazyLock`、`RwLock` | -| `hashbrown` / `ringbuf` / `async-channel` | socket 和辅助数据结构 | -| `event-listener` / `bitflags` / `enum_dispatch` / `async-trait` | 事件通知、标志位、trait 分派 | - -`smoltcp` 在 `Cargo.toml` 中固定启用一组能力(`medium-ethernet`、`medium-ip`、`proto-ipv4`、`proto-ipv6`、`socket-{raw,icmp,udp,tcp,dhcpv4,dns}`、`async`、`alloc`、`log`)。`ax-net` 自身只用 `Medium::Ip` 层,Ethernet 帧处理由 `EthernetDevice` 自行完成。 - -## 能力边界(EthernetDriver) - -`ax-net` 不直接依赖硬件驱动框架,而是通过能力边界 trait `EthernetDriver`([net/ax-net/src/device/driver.rs#L70-L82](net/ax-net/src/device/driver.rs#L70-L82))对接网卡: - -```rust -pub trait EthernetDriver: Send + Sync { - fn device_name(&self) -> &str; - fn irq_num(&self) -> Option; - fn enable_irq(&mut self); - fn disable_irq(&mut self); - fn mac_address(&self) -> [u8; 6]; - fn alloc_tx_buffer(&mut self, size: usize) -> NetDeviceResult>; - fn recycle_tx_buffers(&mut self) -> NetDeviceResult; - fn transmit(&mut self, tx_buf: &mut dyn NetTxBuffer) -> NetDeviceResult; - fn receive(&mut self) -> NetDeviceResult>; - fn recycle_rx_buffer(&mut self, rx_buf: &mut dyn NetRxBuffer) -> NetDeviceResult; - fn handle_irq(&mut self) -> NetIrqEvents; -} -``` - -`RdNetDriver`([driver.rs#L128](net/ax-net/src/device/driver.rs#L128))实现该 trait,内部包装 `rd-net` 的 `TxQueue` / `RxQueue`。`EthernetDeviceList` 即 `Vec>`([driver.rs#L84](net/ax-net/src/device/driver.rs#L84)),由 `ax-runtime` 收集后传入 `init_network()`。 - -## 主要消费者 - -| 消费方 | 使用方式 | -| --- | --- | -| `ax-runtime` | 收集 Ethernet/vsock 设备,构造 `NetworkConfig`,调用 `init_network()` / `init_vsock()` | -| `ax-api` | 提供 ArceOS socket API,复用 `SocketOps` | -| `ax-posix-api` | 提供 POSIX socket、poll、epoll 等适配 | -| `ax-std` / `ax-libc` | 面向应用暴露标准库和 libc 网络接口 | -| `starry-kernel` | Linux socket syscall、ioctl、AF_PACKET、AF_NETLINK 等 ABI 层 | -| Axvisor | 通过结构化配置表达管理面/服务面接口需求 | - -## 与 `ax-runtime` 的边界 - -`ax-runtime` 负责平台设备与网络栈之间的装配: - -- 从 rdrive / rdif registry 获取 Ethernet 设备。 -- 包装为 `RdNetDriver` / `EthernetDeviceList`。 -- 注册网络 IRQ adapter。 -- 构造 `NetworkConfig`。 -- 调用 `ax_net::init_network(net_devs, config)`。 - -`ax-runtime` 不维护第二套接口状态,不直接修改路由表,也不解析 Linux socket 语义。 - -## 与 StarryOS 的边界 - -StarryOS 负责 Linux ABI: - -- `socket()`、`bind()`、`connect()`、`sendmsg()`、`recvmsg()` syscall 参数解析。 -- `ifreq`、`sockaddr_in`、`sockaddr_ll`、`sockaddr_nl` 编解码。 -- Linux errno 映射。 -- root net namespace 可见性过滤。 -- `SO_BINDTODEVICE` 到 `InterfaceId` 的转换。 -- AF_PACKET 的 ifindex 绑定和兼容行为。 - -StarryOS 不缓存独立 IPv4/gateway/MAC 状态。接口信息统一来自: - -- `ax_net::interfaces()` -- `ax_net::interface_by_name()` -- `ax_net::interface_by_id()` -- `ax_net::ipv4_config()` -- `ax_net::arp_entries()` - -## 与驱动框架的边界 - -具体网卡驱动属于 `drivers/`、`ax-driver`、`rdrive` 和 `rdif-*`。`ax-net` 只关心 Ethernet 能力边界: - -```text -driver core - -> rdif-eth / rd-net - -> ax-runtime collects devices - -> ax-net EthernetDevice -``` - -这种边界避免网络协议栈直接依赖 FDT、PCI、MMIO、DMA、VirtIO 或平台 IRQ ABI。 - -## 与 smoltcp 的边界 - -smoltcp 提供协议核心,但不直接知道 TGOSKits 的多接口 registry。`ax-net` 在 smoltcp 外层维护: - -- `InterfaceId` -- `InterfaceInfo` -- `NetworkConfig` -- `RouteTable` -- per-interface DHCP metadata -- DNS 来源信息 -- StarryOS ifindex 映射 - -`Router` 是两者之间的适配层:对 smoltcp 它是一个 `phy::Device`([router.rs#L672](net/ax-net/src/router.rs#L672));对 TGOSKits 它知道多个设备、路由和接口元数据。`Service`([service.rs#L210-L218](net/ax-net/src/service.rs#L210-L218))持有 smoltcp `Interface` 并驱动其 poll: - -```rust -pub struct Service { - pub iface: Interface, - router: Router, - control: Arc, - timeout: Option + Send>>>, - dhcp: Vec, -} -``` - -`Service::new()`([service.rs#L450-L462](net/ax-net/src/service.rs#L450-L462))使用 `HardwareAddress::Ip` 创建 smoltcp `Interface`,把 `Router` 作为 `phy::Device` 传入。 - -## 内部模块依赖关系 - -`ax-net` crate 内部模块间调用关系: - -```mermaid -flowchart TB - lib[lib.rs
Public API + 全局单例 + init_network] - - service[service.rs
Service + NetControl + DhcpState] - router[router.rs
Router + RouteTable + DeviceHandle] - wrapper[wrapper.rs
SocketSetWrapper] - listen[listen_table.rs
ListenTable] - - tcp[tcp.rs
TcpSocket] - udp[udp.rs
UdpSocket] - raw[raw.rs
RawSocket] - unix[unix/
UnixSocket + Transport] - vsock[vsock/
VsockSocket + VsockTransport] - - general[general.rs
GeneralOptions] - options[options.rs
Configurable + Get/SetSocketOption] - state[state.rs
StateLock] - socket[socket.rs
SocketOps + SocketAddrEx] - - config[config.rs
InterfaceId + NetworkConfig] - consts[consts.rs
缓冲区常量] - device[device/
EthernetDevice + LoopbackDevice + RdNetDriver] - - lib --> service - lib --> router - lib --> wrapper - lib --> listen - lib --> tcp - lib --> udp - lib --> raw - lib --> unix - lib --> vsock - lib --> config - lib --> device - - tcp --> general - tcp --> options - tcp --> state - tcp --> socket - tcp --> config - tcp --> consts - tcp --> wrapper - tcp --> listen - tcp --> service - - udp --> general - udp --> options - udp --> socket - udp --> config - udp --> consts - udp --> wrapper - udp --> service - - raw --> general - raw --> options - raw --> state - raw --> socket - raw --> config - raw --> consts - raw --> wrapper - raw --> service - - unix --> general - unix --> options - unix --> socket - - vsock --> general - vsock --> options - vsock --> state - vsock --> socket - vsock --> device - vsock --> service - - service --> router - service --> config - service --> consts - service --> device - service --> wrapper - - router --> config - router --> consts - router --> device - router --> listen - - device --> config - device --> consts -``` - -### 关键数据流方向 - -| 方向 | 数据 | 载体 | -| --- | --- | --- | -| 设备 → Router | RX 包(bytes + InterfaceId) | `BoundedPacketQueue` | -| Router → smoltcp | RX payload(IP 包裸内容) | `Router.rx_buffer`(`PacketBuffer`) | -| smoltcp → Router | TX IP 包(完整 IP packet) | `Router.tx_buffer`(`PacketBuffer`) | -| Router → 设备 | TX 包(next_hop + bytes) | `DeviceHandle.tx_queue`(`BoundedPacketQueue`) | -| 设备 → Service | DHCP 事件 | `DhcpEvent` 枚举(通过 snoop callback) | -| Service → NetControl | 接口/routing/DNS 更新 | `commit_interface_update()` + `RouteTable::replace_ipv4_rules_for_interface()` | -| Socket → Service | Poll 请求 | `request_poll()` + `NET_POLL_REQUESTED` atomic + `NET_POLL_WAKE` | -| Service → Socket | I/O 就绪通知 | `register_waker()` 注册 → smoltcp readiness → waker 唤醒 | - -### 对外提供的 trait 与类型 - -| trait/类型 | 定义位置 | 消费者 | -| --- | --- | --- | -| `SocketOps` | [socket.rs](net/ax-net/src/socket.rs) | `ax-api`, `ax-posix-api`, `starry-kernel` | -| `Configurable` | [options.rs](net/ax-net/src/options.rs) | 同上 + 各 socket backend | -| `Pollable` | 通过 `axpoll` | `axpoll` 消费者 | -| `EthernetDriver` | [device/driver.rs](net/ax-net/src/device/driver.rs) | `ax-runtime` | -| `EthernetIrqRegistrar` | [device/ethernet.rs](net/ax-net/src/device/ethernet.rs) | `ax-runtime` | -| `VsockDriver` | [device/vsock.rs](net/ax-net/src/device/vsock.rs) | `ax-runtime`(`vsock` feature) | -| `UnixNamespace` | [unix/namespace.rs](net/ax-net/src/unix/namespace.rs) | `starry-kernel` | diff --git a/docs/docs/architecture/net/devices.md b/docs/docs/architecture/net/devices.md new file mode 100644 index 0000000000..58acbf5560 --- /dev/null +++ b/docs/docs/architecture/net/devices.md @@ -0,0 +1,226 @@ +--- +sidebar_position: 5 +sidebar_label: "多设备实现" +--- + +# 多设备实现 + +本文详述 `ax-net` 的 Router 多设备适配、设备驱动层、数据面 poll 流程和协议处理实现。架构总览见[总体架构](architecture.md),Socket 层见 [Socket 系统](sockets.md)。 + +## Router 内部结构 + +`Router`([router.rs](net/ax-net/src/router.rs))是 smoltcp 与多设备之间的适配层。它的职责更接近 "MultiDevice adapter":对 smoltcp 暴露一个 `phy::Device`,内部聚合 loopback 和多个 Ethernet 设备,并在 TX 路径根据路由选择真实出接口。 + +```rust +pub struct Router { + rx_buffer: PacketBuffer, // smoltcp Device RX buffer + tx_buffer: PacketBuffer, // smoltcp Device TX buffer + queues: Arc, // 全局 RX 队列(所有设备共享) + devices: Vec>, // 设备列表(按 add_device() 顺序) + table: SharedRouteTable, // 路由表 +} +``` + +### BoundedPacketQueue + +`BoundedPacketQueue` 是有界 MPSC 队列,容量默认 `SOCKET_BUFFER_SIZE=64`: + +```rust +struct BoundedPacketQueue { + inner: Mutex>, + capacity: usize, + len: AtomicUsize, // 无锁长度读取,用于 is_empty() 检查 +} +``` + +- `push()`:加锁,满则返回 `Err(T)`(上层丢弃并打 warning)。 +- `pop()`:加锁,空返回 `None`。 +- `is_empty()`:原子读 `len`,无锁。 + +### DeviceHandle + +每设备一个 `DeviceHandle`,持有设备锁、收发队列和唤醒机制: + +```rust +struct DeviceHandle { + interface_id: InterfaceId, + name: String, + inner: Arc>>, + rx_queue: Arc>, // 指向 RouterQueues::rx(共享) + tx_queue: Arc>, // 独立 TX 队列 + rx_wake: Arc, + tx_wake: Arc, + rx_waker: Waker, +} +``` + +RX 队列是所有设备共享的(`RouterQueues::rx`),因为 smoltcp 从同一个 `Router::rx_buffer` 消费;TX 队列每设备独立,由 `dispatch()` 按路由决策分发。 + +### TX Dispatch + +`Router::dispatch()` 接收 `&mut SocketSet`,从 `tx_buffer` 取出 smoltcp 发出的 IP 包: + +- **IPv4 广播**(dst=255.255.255.255):复制到所有非 loopback Ethernet 设备。 +- **IPv4 单播**:按 `select_route_for_source()` 选路由(要求源地址匹配)。如果出接口是 loopback,走快速路径 `inject_loopback_rx_direct()`(snoop TCP + 直接写入 `rx_buffer`);否则推入对应设备 TX 队列。 +- **IPv6 多播**:复制到所有非 loopback Ethernet 设备。 +- **IPv6 单播**:按 `select_route_for_source()` 选路由,同样有 loopback 快速路径。 + +### RX 数据流 + +`Router::poll()` 从共享 RX 队列消费包到 smoltcp `rx_buffer`,每个包先调 `snoop_tcp_packet()` 检查 TCP SYN 预创建,再调 snoop 回调(DHCP 分发)。 + +## Ethernet 设备实现 + +`EthernetDevice`([ethernet.rs](net/ax-net/src/device/ethernet.rs))是 `Device` trait 的主要实现: + +```rust +pub struct EthernetDevice { + name: String, + inner: Arc, + neighbors: HashMap, + pending_neighbors: HashMap, + ip: Option, + pending_packets: PacketBuffer, +} +``` + +### ARP 处理 + +`EthernetDevice::recv()` 处理入站 Ethernet 帧: + +1. 调用 `EthernetDriver::receive()` 从硬件获取一帧。 +2. 解析 `EthernetFrame`,按 `EthernetProtocol` 分派: + - **ARP**:更新 neighbor 表(reply 和 gratuitous request),从 `pending_packets` 释放等待的包。 + - **IPv4**:encapsulation 检查,将 payload 写入 smoltcp `PacketBuffer`。 + +`send()` 路径: + +1. 查 `neighbors` 表,有则直接发送。 +2. 无则检查 `pending_neighbors`:如果已发送 ARP request 未超时,将包写入 `pending_packets`。 +3. 否则发送 ARP request,记录到 `pending_neighbors`。 +4. Neighbor TTL = 300s(与 Linux 一致),ARP retry = 1s。 + +### IRQ 模型 + +`EthernetIrqState` 管理 IRQ 注册和驱动锁。支持两种 RX 就绪模式: + +- **IRQ 驱动**(`new()`):通过 `EthernetIrqRegistrar` 注册硬件 IRQ,IRQ 触发时 `handle_ethernet_irq()` → `poll_ready.wake()` 唤醒 RX worker。 +- **OOB 驱动**(`new_oob_rx()`):用于 SDIO Wi-Fi 等自带中断线程的设备。RX 就绪由驱动线程调用 `notify_oob_rx()` 唤醒独立 poll task(`{ifname}-oob-poll`),不经过 Ethernet IRQ 框架。 + +### RdNetDriver + +`RdNetDriver`([driver.rs](net/ax-net/src/device/driver.rs))是 `EthernetDriver` trait 的 `rd-net` 适配实现。内部持有 `rd_net::TxQueue` 和 `rd_net::RxQueue`,通过预取 `RX_PREFETCH_TARGET=1` 个包到 `pending_rx: VecDeque` 减少锁竞争。 + +## Loopback 快速路径 + +`LoopbackDevice`([loopback.rs](net/ax-net/src/device/loopback.rs))是零状态占位类型(`pub struct LoopbackDevice;`),`Device` trait 全为 no-op。真正的 loopback 数据路径在 `Router` 中以快速路径实现: + +- **TX 方向**(`Router::dispatch()`):路由选中 loopback 时,`inject_loopback_rx_direct()` 直接将 IP 包写入 `Router.rx_buffer`,并在写入前执行 `snoop_tcp_packet()` 预创建 TCP listen socket。回环包在**同一个 `Service::poll()` 周期内**被 smoltcp 消费。 +- **DHCP/特殊发包**(`Router::send_on_device()`):对 loopback 调用 `inject_loopback_rx()` 写入共享 `RouterQueues::rx`,由下一轮 `Router::poll()` 消费。 +- **Worker 跳过**:`start_tx_workers()` 和 `start_rx_workers()` 显式跳过 `InterfaceId::LOOPBACK`。 + +## 数据面 poll 流程 + +`Service::poll()` 在每个 poll 周期执行: + +```rust +pub fn poll(&mut self, sockets: &mut SocketSet) -> bool { + let timestamp = now(); + // 1. Router::poll():drain RX queue → rx_buffer,snoop DHCP client/server + // 2. 处理 DHCP client 事件(更新地址、路由、DNS) + // 3. DHCP server 回复通过 send_on_device() 广播 + // 4. smoltcp Interface::poll():协议状态机推进 + // 5. DHCP client 发包定时器 + // 6. 回收已完成 teardown 的 orphan TCP socket + // 7. TX dispatch:路由到设备(含 loopback 快速路径) +} +``` + +关键:`dispatch()` 接收 `&mut SocketSet`,使 loopback 快速路径能在注入 RX buffer 前执行 `snoop_tcp_packet()`。`reap_orphans()` 在持有 SocketSet 锁时执行,删除 smoltcp socket 的动作放在 orphan 锁外执行。 + +## net-poll Worker + +`net_poll_worker` 是协议核心的唯一驱动线程: + +```rust +fn net_poll_worker() { + loop { + let delay = next_poll_delay(); + let timed_out = NET_POLL_WAKE.wait_timeout_until(delay, + || NET_POLL_REQUESTED.load(Acquire)); + if !timed_out { NET_POLL_REQUESTED.store(false, Release); } + poll_until_idle(); + } +} +``` + +`poll_until_idle()` 使用 `POLLING_INTERFACES` CAS 锁防止重入,循环内**不 yield**——net-poll worker 是专用线程,在有工作时持续批量处理以最大化吞吐。 + +## DHCP 状态机 + +`DhcpState`([service.rs](net/ax-net/src/service.rs))维护 per-interface DHCP 客户端: + +``` +Discovering ──Offer──→ Requesting ──ACK──→ Bound + │ │ │ + └──timeout→retry── └──timeout→retry── └──NAK→Discovering (reset) +``` + +- **Discovering**:广播 DHCPDISCOVER,指数退避重试(1,2,4,8... 最多 16 秒间隔)。 +- **Requesting**:收到 DHCPOFFER 后广播 DHCPREQUEST。 +- **Bound**:收到 DHCPACK,状态转为 Bound。NAK 触发 reset → Discovering。 + +`process_packet()` 按 ingress `InterfaceId` 匹配,校验 `transaction_id` 和 `client_hardware_address` 防止误收。 + +## TCP Orphan Socket 回收 + +当用户关闭 TCP socket(`TcpSocket::drop()`)时,如果连接仍处于活跃 teardown 状态或有未发送数据,socket 转为 orphan 状态([orphan.rs](net/ax-net/src/orphan.rs)): + +- **目的**:保证 RFC 793 合规的 FIN 四次挥手和 TIME_WAIT,避免对端连接悬挂。 +- **回收**:`reap_orphans()` 在每个 `Service::poll()` 周期中调用: + - `Closed` → 立即移除 + - `TimeWait/FinWait*/LastAck/Closing` → 最长 60 秒后强制移除 + - 其他意外状态 → 60 秒后强制移除并 warn +- **溢出保护**:`ORPHAN_MAX_SOCKETS = 1024`,超限时只 warn 不强制清除正在 teardown 的连接。 + +## DHCP 服务器(SoftAP 模式) + +`DhcpServer`([dhcp_server.rs](net/ax-net/src/dhcp_server.rs))是最简 IPv4 DHCP 服务器,用于 Wi-Fi SoftAP 模式: + +- 单客户端、单地址租约,处理 Discover→Offer、Request→Ack。 +- 不依赖 smoltcp 的 DHCP socket,手工解析/封装 `DhcpRepr → UdpRepr → Ipv4Repr`。 +- 入站包在 `Router::poll()` 的 snoop 回调中分发,与 DHCP client 路径完全独立。 + +## DNS 解析流程 + +`dns_query()`([lib.rs](net/ax-net/src/lib.rs)): + +1. `dns_servers()` 获取按 (metric, interface_id, server_ip) 排序的去重 DNS server 列表。 +2. 过滤不可路由的 server(通过 `select_route()` 检查可达性)。 +3. 在 `SOCKET_SET` 中创建 `dns::Socket`,发起 A 记录查询。 +4. 循环 `request_poll()` + `yield_now()`,检查 `get_query_result()`。 +5. 默认 5 秒超时,超时返回 `ETIMEDOUT`。 +6. `DnsSocketGuard` 在 drop 时从 `SOCKET_SET` 移除 DNS socket。 + +## 设备能力边界(Device trait) + +`ax-net` 不直接依赖硬件驱动框架。所有设备实现统一的内部 `Device` trait([device/mod.rs](net/ax-net/src/device/mod.rs)): + +```rust +pub trait Device: Send + Sync { + fn name(&self) -> &str; + fn recv(&mut self, interface_id: InterfaceId, + buffer: &mut PacketBuffer, timestamp: Instant, + snoop: &mut dyn FnMut(&[u8])) -> bool; + fn send(&mut self, next_hop: IpAddress, packet: &[u8], timestamp: Instant) -> bool; + fn set_ipv4_addr(&mut self, _addr: Option) {} + fn arp_entries(&self, _timestamp: Instant) -> Vec { Vec::new() } + fn register_waker(&self, waker: &Waker); + fn wake_rx(&self) {} +} +``` + +两个实现: + +- `LoopbackDevice`:零状态占位类型,`Device` trait 全为 no-op。真正的回环在 `Router::dispatch()` 的快速路径中完成。 +- `EthernetDevice`:维护 ARP 邻居表(`NEIGHBOR_TTL = 300s`)、pending packet 队列、Ethernet 帧封装/解析,并对接 IRQ 适配。支持 IRQ 驱动和 OOB 驱动两种 RX 模式。 diff --git a/docs/docs/architecture/net/flows.md b/docs/docs/architecture/net/flows.md index 12f922669f..62da27d7fe 100644 --- a/docs/docs/architecture/net/flows.md +++ b/docs/docs/architecture/net/flows.md @@ -1,5 +1,5 @@ --- -sidebar_position: 5 +sidebar_position: 8 sidebar_label: "运行时流程" --- @@ -167,7 +167,7 @@ sequenceDiagram end DevLock-->>RxW: unlock loop rx_buffer.dequeue() - RxW->>RxQ: push(RxPacket{interface_id, bytes}) + RxW->>RxQ: push(RxPacket{interface_id, QueuedPacket}) alt 队列满 RxW->>RxW: warn + drop packet end @@ -227,7 +227,11 @@ while rx_buffer.is_empty() ```rust while let Ok((interface_id, packet)) = rx_buffer.dequeue() { - let rx = RxPacket { interface_id, bytes: packet.to_vec().into_boxed_slice() }; + let Some(bytes) = QueuedPacket::new(packet) else { + warn!("{}: RX packet exceeds MTU, dropping packet", device.name); + continue; + }; + let rx = RxPacket { interface_id, bytes }; if device.rx_queue.push(rx).is_err() { warn!("{}: RX queue is full, dropping packet", device.name); break; @@ -237,7 +241,7 @@ while let Ok((interface_id, packet)) = rx_buffer.dequeue() { } ``` -`rx_queue` 是 `BoundedPacketQueue`,容量 `SOCKET_BUFFER_SIZE = 64`([consts.rs#L12](net/ax-net/src/consts.rs#L12))。队列满时丢弃剩余包并 break,避免在队列已满时继续从 driver 拷贝。 +`rx_queue` 是 `BoundedPacketQueue`,容量 `SOCKET_BUFFER_SIZE = 64`。`RxPacket` 内部使用 `[u8; STANDARD_MTU] + len` 的 inline `QueuedPacket`,不为每个包分配 `Box<[u8]>`。队列满时丢弃剩余包并 break,避免在队列已满时继续从 driver 拷贝。 **步骤 4 — Router drain 共享 RX 队列** @@ -246,13 +250,14 @@ while let Ok((interface_id, packet)) = rx_buffer.dequeue() { ```rust while !self.rx_buffer.is_full() { let Some(packet) = self.queues.rx.pop() else { break; }; - snoop_tcp_packet(&packet.bytes, sockets); // 预创建 listen socket - snoop(packet.interface_id, &packet.bytes); // DHCP snoop 回调 - let Ok(dst) = self.rx_buffer.enqueue(packet.bytes.len(), packet.interface_id) else { + let bytes = packet.bytes.as_slice(); + snoop_tcp_packet(bytes, sockets); // 预创建 listen socket + snoop(packet.interface_id, bytes); // DHCP snoop 回调 + let Ok(dst) = self.rx_buffer.enqueue(bytes.len(), packet.interface_id) else { warn!("Router RX buffer is full, dropping packet"); break; }; - dst.copy_from_slice(&packet.bytes); + dst.copy_from_slice(bytes); } ``` @@ -278,12 +283,12 @@ while !self.rx_buffer.is_full() { ``` driver RX ring → [copy] PacketBuffer (local, capacity=1, MTU=1500) - → [copy] BoundedPacketQueue (per-device, capacity=64) + → [copy] BoundedPacketQueue (shared RX queue, capacity=64, no heap allocation per packet) → [copy] Router.rx_buffer (PacketBuffer, shared, capacity from smoltcp config) → [zero-copy ref] RxToken → smoltcp Interface::poll ``` -每次 hop 最多一次内存拷贝。最坏路径有 3 次拷贝(driver → local buffer → queue → router buffer),但通过有界队列控制了背压。 +每次 hop 最多一次内存拷贝。最坏路径有 3 次拷贝(driver → local buffer → queue → router buffer),但通过有界 inline queue 控制了背压并避免每包堆分配。 --- @@ -431,7 +436,7 @@ while let Ok((_, packet)) = self.tx_buffer.dequeue() { fn device_tx_worker(device: Arc) { loop { if let Some(packet) = device.tx_queue.pop() { - let poll_next = device.inner.lock().send(packet.next_hop, &packet.bytes, now()); + let poll_next = device.inner.lock().send(packet.next_hop, packet.bytes.as_slice(), now()); if poll_next { crate::request_poll(); } } else { device.tx_wake.wait_until(|| !device.tx_queue.is_empty()); @@ -471,7 +476,7 @@ socket.send(data) → [copy] smoltcp socket TX buffer → [smoltcp 内部] IP packet 构造 → [TxToken::consume → copy] Router.tx_buffer - → [dispatch → copy] DeviceHandle.tx_queue + → [dispatch → copy] DeviceHandle.tx_queue (QueuedPacket inline, no heap allocation per packet) → [EthernetDevice::send → copy] driver TX buffer ``` @@ -539,14 +544,13 @@ sequenceDiagram Control-->>Sock: RouteDecision { source, dev, next_hop } Sock->>Sock: state: Idle → Connecting Sock->>SockSet: smoltcp socket.connect(remote, local) - Sock->>Sock: yield_now() (让 net-poll 处理 SYN) Sock->>Poll: request_poll() loop poll_connect() Sock->>SockSet: socket.state() == Connected? alt Connected Sock-->>App: Ok else still connecting - Sock->>Poll: request_poll() → yield + Sock->>Poll: register waker + request_poll() → yield end end ``` @@ -571,12 +575,12 @@ sequenceDiagram ```rust fn accept(&self) -> AxResult { - let bound_port = self.bound_endpoint()?.port; + let bound_endpoint = self.bound_endpoint()?; self.general.recv_poller(self, || { request_poll(); let accepted = { let mut sockets = SOCKET_SET.inner.lock(); - LISTEN_TABLE.accept(bound_port, &mut sockets)? + LISTEN_TABLE.accept(bound_endpoint, &mut sockets)? }; Ok(TcpSocket::new_connected(accepted.handle, ...).into()) }) @@ -858,7 +862,7 @@ fn poll_once() -> bool { **严格禁止**: - 禁止在持有设备锁(`DeviceHandle.inner`)时进入 `Service` 或 `SocketSet` 锁。 -- 禁止在持有 control-plane 写锁(`NetControl.state.write()`)时进入 smoltcp poll。 +- 禁止在持有 control 写锁(`NetControl.state.write()`)时进入 smoltcp poll。 - 锁的获取和释放严格嵌套,不允许跨层级持有。 ### Waker 注册路径 @@ -885,7 +889,7 @@ socket.send/recv/connect returns WouldBlock | --- | --- | --- | --- | | RX | IRQ → rx_wake → rx_worker → rx_queue | 设备锁 → RX 队列(无 Service 锁) | driver recv + 1 次 copy | | TX | socket.send → smoltcp buffer → dispatch → tx_queue | Service 锁 → SocketSet 锁 → 路由表读锁 | smoltcp TCP 状态机 + ARP | -| TCP connect | socket.connect → smoltcp connect → yield → poll | Service 锁 → SocketSet 锁 | RTT × 1.5(SYN + SYN-ACK) | +| TCP connect | socket.connect → smoltcp connect → request_poll → poll | Service 锁 → SocketSet 锁 | RTT × 1.5(SYN + SYN-ACK) | | DHCP | poll_dhcp → send_on_device → RX → process_packet | Service 锁 → 路由表写锁 | DHCP server 响应时间 | | DNS | dns_query_timeout → add socket → poll loop | Service 锁 → SocketSet 锁 | DNS server RTT | | Poll | request_poll → net-poll worker → poll_until_idle | POLLING_INTERFACES 原子 → Service 锁 → SocketSet 锁 | smoltcp poll duration | @@ -1039,7 +1043,7 @@ sequenceDiagram Eth->>Router: packet 进入 rx_buffer Router->>Router: snoop_tcp_packet(bytes) Router->>ListenTbl: incoming_tcp_packet(src, dst, sockets) - ListenTbl->>ListenTbl: 查 port → ListenTableEntryInner + ListenTbl->>ListenTbl: 查 port bucket → 匹配目的地址的 ListenTableEntryInner ListenTbl->>SockSet: add(new TcpSocket) ListenTbl->>ListenTbl: socket.listen(dst.port) ListenTbl->>ListenTbl: syn_queue.push_back(PendingTcp) @@ -1052,7 +1056,7 @@ sequenceDiagram Smol->>Smol: socket.state → Established Smol->>SockSet: socket can_accept() = true - App->>ListenTbl: accept(port, sockets) + App->>ListenTbl: accept(bound_endpoint, sockets) ListenTbl->>ListenTbl: 遍历 syn_queue ListenTbl->>SockSet: is_acceptable(handle)? alt Established diff --git a/docs/docs/architecture/net/integration.md b/docs/docs/architecture/net/integration.md index 8de9756c23..a9503229d7 100644 --- a/docs/docs/architecture/net/integration.md +++ b/docs/docs/architecture/net/integration.md @@ -1,5 +1,5 @@ --- -sidebar_position: 7 +sidebar_position: 9 sidebar_label: "系统集成" --- diff --git a/docs/docs/architecture/net/overview.md b/docs/docs/architecture/net/overview.md index 3ae9ccc820..79a6d5d752 100644 --- a/docs/docs/architecture/net/overview.md +++ b/docs/docs/architecture/net/overview.md @@ -4,11 +4,31 @@ sidebar_label: "概览" --- # 网络栈概览 - TGOSKits 的网络能力收敛在 `net/ax-net`。它是 ArceOS、StarryOS 和 Axvisor 共享的统一网络栈,向上提供 TCP、UDP、raw socket、Unix domain socket、可选 vsock、DNS、DHCP、ARP、接口查询和 readiness/poll 能力,向下通过 `rd-net` / `rdif-eth` 消费 Ethernet 设备。 +## 源码 源码位于 [net/ax-net/src/](net/ax-net/src/),入口 [lib.rs](net/ax-net/src/lib.rs)。Socket backend 包括 IP 类([tcp.rs](net/ax-net/src/tcp.rs)、[udp.rs](net/ax-net/src/udp.rs)、[raw.rs](net/ax-net/src/raw.rs),基于 smoltcp)、[unix/](net/ax-net/src/unix/)(自包含 stream/dgram,不经 smoltcp)和可选的 [vsock/](net/ax-net/src/vsock/)(基于 `rdif-vsock` 驱动,含 connection manager 与 ring buffer)。 +| 模块 | 角色 | 关键类型 | +| --- | --- | --- | +| [lib.rs](net/ax-net/src/lib.rs) | public facade,初始化网络、启动 poll worker、导出 API | `init_network`, `request_poll`, `net_poll_worker` | +| [config.rs](net/ax-net/src/config.rs) | 配置与接口信息类型 | `InterfaceId`, `NetworkConfig`, `InterfaceInfo`, `DeviceBinding` | +| [service.rs](net/ax-net/src/service.rs) | 控制面 + 协议核心调度 | `Service`, `NetControl`, `DhcpState` | +| [router.rs](net/ax-net/src/router.rs) | 路由表、有界队列、smoltcp `Device` 适配 | `Router`, `RouteTable`, `RouteDecision` | +| [wrapper.rs](net/ax-net/src/wrapper.rs) | 全局 `SocketSet` 包装与端口冲突仲裁 | `SocketSetWrapper` | +| [socket.rs](net/ax-net/src/socket.rs) | 统一 socket 抽象 | `SocketOps`, `Socket`, `SocketAddrEx` | +| [options.rs](net/ax-net/src/options.rs) | socket 选项与 `Configurable` trait | `GetSocketOption`, `SetSocketOption`, `TcpInfo` | +| [general.rs](net/ax-net/src/general.rs) | 通用 socket 选项、非阻塞/超时/poll helper | `GeneralOptions` | +| [state.rs](net/ax-net/src/state.rs) | socket 状态机锁 | `StateLock`, `StateGuard` | +| [listen_table.rs](net/ax-net/src/listen_table.rs) | TCP listen/accept 表与 SYN 预创建 | `ListenTable` | +| [tcp.rs](net/ax-net/src/tcp.rs) / [udp.rs](net/ax-net/src/udp.rs) / [raw.rs](net/ax-net/src/raw.rs) | IP socket 实现 | `TcpSocket`, `UdpSocket`, `RawSocket` | +| [orphan.rs](net/ax-net/src/orphan.rs) | TCP orphan socket 回收(RFC 793 TIME_WAIT) | `add_orphan`, `reap_orphans` | +| [dhcp_server.rs](net/ax-net/src/dhcp_server.rs) | 最简 DHCP 服务器(SoftAP 模式) | `DhcpServer` | +| [unix/](net/ax-net/src/unix/) | Unix domain socket | `UnixSocket`, `Transport` | +| [vsock/](net/ax-net/src/vsock/) | 可选 vsock 支持(`vsock` feature) | `VsockSocket`, `VsockTransport` | +| [device/](net/ax-net/src/device/) | loopback、Ethernet、rd-net、vsock 设备适配 | `Device`, `EthernetDevice`, `RdNetDriver` | +| [consts.rs](net/ax-net/src/consts.rs) | 缓冲区大小等常量 | `STANDARD_MTU`, `SOCKET_BUFFER_SIZE` | + ## 能力矩阵 | 能力 | 实现方式 | 状态 | @@ -24,13 +44,21 @@ TGOSKits 的网络能力收敛在 `net/ax-net`。它是 ArceOS、StarryOS 和 Ax | ARP | `EthernetDevice` 内部 `neighbors: HashMap`(已解析条目, 300s TTL)+ `pending_neighbors: HashMap`(等待 ARP reply 条目, 1s 重试) + `pending_packets: PacketBuffer`(暂存待 ARP 解析后发送的包) | 完整 | | 多 NIC 路由 | `RouteTable` 最长前缀匹配 + metric 排序 + per-interface 替换 | 完整 | | IRQ 感知 | `EthernetIrqRegistrar` + `EthernetIrqAction` + IRQ→wake 转换 | 完整 | -| Loopback | 零状态 `LoopbackDevice` + `Router::dispatch()` 快速路径 inline 注入 `rx_buffer`,不经设备 worker | 完整 | +| Loopback | 零状态 `LoopbackDevice` + `Router::dispatch()` 快速路径 inline 注入 `rx_buffer`,不经设备 worker 和队列分配 | 完整 | | TCP orphan 回收 | `orphan.rs`:Drop 后保留 smoltcp socket 直到 FIN/TIME_WAIT 完成,RFC 793 合规 | 完整 | | DHCP 服务器(SoftAP) | `dhcp_server.rs`:最简单客户端 DHCP 服务器,Discover→Offer、Request→Ack | 完整 | | OOB RX(SDIO Wi-Fi) | `EthernetDevice::new_oob_rx()` + `notify_oob_rx()` + 独立 poll task | 完整 | | 动态设备注册 | `register_device_with_config()` 运行时添加静态 IP 设备(Wi-Fi AP) | 完整 | -## 线程与锁模型 +## 设计原则 + +- **单协议栈语义优先**:所有 TCP/UDP/raw socket 共享一个 smoltcp `Interface` 和 `SocketSet`,端口冲突、listen 聚合、wildcard bind 等语义自然正确。 +- **控制面与数据面分离**:接口查询(`interfaces()`、`interface_by_name()`)走只读 `NetControl`,不进入设备锁或 smoltcp poll。 +- **异步 poll 解耦热路径**:socket 操作只调用轻量 `request_poll()` 唤醒 worker,不在调用者上下文同步驱动整个协议栈。 +- **能力边界隔离**:`ax-net` 通过 `EthernetDriver` trait 对接网卡驱动,不直接依赖 FDT、PCI、MMIO、DMA 或平台 IRQ ABI。 +- **Linux ABI 友好**:`InterfaceId` 直接映射 Linux ifindex,`DeviceBinding` 对应 `SO_BINDTODEVICE`,socket option 覆盖主流 `getsockopt`/`setsockopt` 语义。 + +### 线程与锁模型 `ax-net` 使用多线程模型,但协议核心串行: @@ -61,15 +89,11 @@ SERVICE (Mutex) - `ListenTable` 条目锁在 `SOCKET_SET` 锁内获取,保证 accept/snoop 的一致性。 - 设备锁(`DeviceHandle.inner`)在 poll 路径的最内层获取。 -## 核心设计取舍 +## 核心方案 -当前网络栈的核心取舍是:**保持单 `smoltcp::iface::Interface + SocketSet` 协议栈实例**,不拆成多 smoltcp domain。多网口能力通过接口 registry、路由表、每接口配置、设备队列和 `Router` 适配层实现。 +`ax-net` 采用 **单 smoltcp `Interface` + 多设备 `Router`** 架构。详细设计论证见[架构设计 — Single Interface + Multi-Device Router](architecture.md#single-interface--multi-device-router)。 -这一选择的根本原因是 socket 语义的稳定性。多协议栈实例会让 TCP wildcard listen、UDP wildcard bind、端口冲突仲裁、动态路由切换在多个 `SocketSet` 之间重复实现,引入难以测试的竞态。单实例串行 poll 配合设备队列解耦,在保持语义简单的同时获得多网口支持。 - -## 与主流实现对比 - -### 与 Linux 网络栈对比 +### 与 Linux 实现对比 | 维度 | Linux | ax-net | | --- | --- | --- | @@ -78,8 +102,8 @@ SERVICE (Mutex) | 多 NIC | 独立 netdev + per-device NAPI queue | 单 `Router`(smoltcp `Device`)+ per-device 有界 RX/TX queue | | ARP/邻居发现 | 内核 neighbour table + GC | `EthernetDevice` 内部 `HashMap` + `NEIGHBOR_TTL=300s` | | DHCP | 用户态 dhclient / systemd-networkd | 内核态 `DhcpState` 状态机,bootstrap 阻塞启动 | -| Socket 缓冲区 | 动态可调 sk_buff 链 | 固定大小 `PacketBuffer`(TX/RX 各 64 KiB) | -| Zero-copy | `MSG_ZEROCOPY` / `io_uring` | 不支持,RX/TX 均有一次 copy | +| Socket 缓冲区 | 动态可调 sk_buff 链 | 固定大小 `PacketBuffer` + 有界 inline packet queue | +| Zero-copy | `MSG_ZEROCOPY` / `io_uring` | 不支持端到端 zero-copy;Router 队列无每包堆分配,loopback 快速路径少一次队列 hop | ### 与 smoltcp 原生使用对比 @@ -98,15 +122,7 @@ smoltcp 原生使用需要一个 `phy::Device` + 一个 `Interface`。`ax-net` | 主要场景 | 嵌入式 MCU(RAM < 64 KiB) | 服务器级 unikernel(128 MiB+) | | 线程模型 | NO_SYS 单线程 / SYS 多线程 | 多线程,per-device worker + net-poll worker | | socket API | 有限 POSIX 子集 | `SocketOps` trait + `Configurable`,覆盖 SO_\*/TCP_\*/IP_\* | -| 多接口 | 基本不支持 | 原生多 NIC + 路由表 + metric | - -## 设计原则 - -- **单协议栈语义优先**:所有 TCP/UDP/raw socket 共享一个 smoltcp `Interface` 和 `SocketSet`,端口冲突、listen 聚合、wildcard bind 等语义自然正确。 -- **控制面与数据面分离**:接口查询(`interfaces()`、`interface_by_name()`)走只读 `NetControl`,不进入设备锁或 smoltcp poll。 -- **异步 poll 解耦热路径**:socket 操作只调用轻量 `request_poll()` 唤醒 worker,不在调用者上下文同步驱动整个协议栈。 -- **能力边界隔离**:`ax-net` 通过 `EthernetDriver` trait 对接网卡驱动,不直接依赖 FDT、PCI、MMIO、DMA 或平台 IRQ ABI。 -- **Linux ABI 友好**:`InterfaceId` 直接映射 Linux ifindex,`DeviceBinding` 对应 `SO_BINDTODEVICE`,socket option 覆盖主流 `getsockopt`/`setsockopt` 语义。 +| 多接口 | 原生 `netif` + 全局 PCB/socket 管理 | 单 smoltcp 实例 + `Router` 聚合多 NIC + 路由表 + metric | ## 当前限制 @@ -114,9 +130,9 @@ smoltcp 原生使用需要一个 `phy::Device` + 一个 `Interface`。`ax-net` 单 smoltcp 实例意味着 TCP/UDP 协议状态机在同一 `net-poll` worker 上串行执行。设备队列解耦可以减少收发阻塞,但不能让多核并行处理协议状态机。Linux 的 per-CPU softirq 在这里没有对应物。 -### 收发路径有拷贝 +### 收发路径仍有拷贝 -RX worker 从 driver buffer 复制到有界队列中的 packet bytes,TX worker 同样从队列复制到 driver buffer。真正 zero-copy 需要 `rd-net` buffer ownership、packet pool 和 smoltcp token 适配改造。当前每次收发各有一次内存拷贝。 +RX worker 从 driver buffer 复制到有界队列中的 inline packet,Router 再复制到 smoltcp `PacketBuffer`;TX 方向从 smoltcp `PacketBuffer` 复制到 per-device inline queue,再由 driver 发送。当前队列不再为每个包分配 `Box<[u8]>`,loopback dispatch 也直接写 `rx_buffer`,但端到端 zero-copy 仍需要 `rd-net` buffer ownership、packet pool 和 smoltcp token 适配改造。 ### DHCP 租约管理不完整 diff --git a/docs/docs/architecture/net/protocol.md b/docs/docs/architecture/net/protocol.md deleted file mode 100644 index 16c3fdeb48..0000000000 --- a/docs/docs/architecture/net/protocol.md +++ /dev/null @@ -1,136 +0,0 @@ ---- -sidebar_position: 2 -sidebar_label: "协议栈映射" ---- - -# 协议栈映射 - -`ax-net` 实现了标准 TCP/IP 四层模型的完整协议栈,以下说明各层在代码中的具体承担者、封装格式和处理路径。 - -```mermaid -flowchart TB - subgraph App["应用层 (Application)"] - direction LR - Starry["starry-kernel:\nsyscall socket API"] - Arce["ax-api / ax-posix-api:\nPOSIX socket"] - Axv["Axvisor:\n结构化配置"] - end - - subgraph Transport["传输层 (Transport)"] - direction LR - Tcp["TcpSocket\n(tcp.rs)"] - Udp["UdpSocket\n(udp.rs)"] - Raw["RawSocket\n(raw.rs)"] - Unix["UnixSocket\n(unix/)"] - Vsock["VsockSocket\n(vsock/)"] - end - - subgraph Internet["网络层 (Internet)"] - direction LR - SmolIface["smoltcp Interface\n(service.rs)"] - Route["Router / RouteTable\n(router.rs)"] - IpProto["IP / ICMP / DHCP"] - end - - subgraph Link["链路层 (Link)"] - direction LR - EthDev["EthernetDevice\n(device/ethernet.rs)"] - LoopDev["LoopbackDevice\n(device/loopback.rs)"] - RdNetDrv["RdNetDriver\n(device/driver.rs)"] - Hardware["rd-net → 物理网卡"] - end - - App --> Transport - Transport --> Internet - Internet --> Link - SmolIface --> Route - Route --> EthDev - Route --> LoopDev - EthDev --> RdNetDrv - RdNetDrv --> Hardware -``` - -## 各层职责与封装格式 - -| TCP/IP 层 | ax-net 组件 | 封装格式 | 职责 | -| --- | --- | --- | --- | -| **应用层** | `Socket` enum + `SocketOps` trait([socket.rs](net/ax-net/src/socket.rs)) | 用户数据(字节流/数据报) | 暴露 TCP/UDP/raw/unix/vsock socket API,管理 socket options、超时、非阻塞、poll readiness | -| **传输层** | smoltcp `socket::tcp` / `socket::udp` / `socket::raw` | TCP segment / UDP datagram | 连接管理(三次握手/四次挥手)、可靠传输(重传/ACK/窗口)、端口复用、corking | -| **网络层** | smoltcp `Interface` + `Router`([router.rs](net/ax-net/src/router.rs)) + `RouteTable` | IP packet(IPv4 header + payload) | IP 路由决策、分片/重组、ICMP 处理、ARP 缓存、DHCP 状态机、IP 地址管理 | -| **链路层** | `EthernetDevice`([ethernet.rs](net/ax-net/src/device/ethernet.rs)) + `LoopbackDevice` + `RdNetDriver` | Ethernet frame(MAC header + ethertype + payload) | MAC 编址、ARP 封帧与解帧、IRQ 处理、邻居表(300s TTL)、pending packet 队列 | - -## 数据封装沿各层的具体转换 - -以 **TCP 发送** 为例,数据从应用到物理网卡经过以下封装步骤: - -``` -应用层: 用户调用 send(data) → TcpSocket::send() - │ - ▼ 写入 smoltcp socket TX buffer (TCP_TX_BUF_LEN=64KiB) -传输层: smoltcp TCP 状态机构造 TCP segment - │ [src_port, dst_port, seq, ack, flags, window, payload] - ▼ smoltcp 通过 Router::transmit() 输出 -网络层: smoltcp 添加 IPv4 header(或 IPv6) - │ [version, ihl, tos, total_len, id, flags, ttl, proto=TCP, - │ checksum, src_ip, dst_ip, options..., payload=TCP segment] - │ IP packet 写入 Router.tx_buffer - ▼ Router::dispatch() 查路由表选择出接口 -链路层: EthernetDevice::send() 查 ARP 表获取 dst_mac - │ 添加 Ethernet header - │ [dst_mac(6B), src_mac(6B), ethertype=0x0800(IPv4), payload=IP packet] - ▼ EthernetDriver::transmit() -物理层: rd-net 驱动将帧写入硬件 TX ring → DMA 发送 -``` - -以 **TCP 接收** 为例,数据从物理网卡到应用的解封装: - -``` -物理层: 硬件 DMA → RX ring → 中断/轮询 - ▼ EthernetDriver::receive() -链路层: EthernetDevice::recv() 解析 EthernetFrame - │ 提取 dst_mac, src_mac, ethertype - │ ethertype=0x0806(ARP) → 更新 neighbors 表 - │ ethertype=0x0800(IPv4) → 提取 IP payload - ▼ 写入 smoltcp PacketBuffer -网络层: smoltcp Interface::poll() 从 Router::receive() 取 IP packet - │ 解析 IPv4 header → 提取 protocol=TCP, src_ip, dst_ip - │ protocol=ICMP → raw socket - ▼ 交付给对应的 socket -传输层: smoltcp TCP 状态机处理 TCP segment - │ 校验 seq/ack, 更新窗口, 提取 payload - │ payload 写入 socket RX buffer (TCP_RX_BUF_LEN=64KiB) - ▼ socket readiness 更新 → waker 唤醒 recv() 调用者 -应用层: 用户调用 recv(data) → TcpSocket::recv() → 从 smoltcp RX buffer 读取 -``` - -## 协议穿越各层的路由决策点 - -| 操作 | 发生层 | 决策方式 | -| --- | --- | --- | -| **端口选择** (ephemeral port) | 传输层 | `bind(port=0)` → 从 49152 递增尝试 | -| **路由决策** (出接口选择) | 网络层 | smoltcp 构造 IP 包时注入 src_addr → `dispatch()` 用 `select_route_for_source(dst, src)` 查 `RouteTable` → 选接口 | -| **ARP 解析** (IP→MAC) | 网络层↔链路层 | `EthernetDevice::send()` 查 `neighbors: HashMap` → 未命中则广播 ARP request | -| **DHCP** | 网络层↔链路层 | `DhcpState` 在 `Service::poll()` 中独立于 smoltcp 运行,直接操作 `Router::send_on_device()` | -| **DNS** | 应用层 | `dns_query()` 创建临时 smoltcp `dns::Socket`,通过 smoltcp UDP 发送 DNS 查询 | - -## smoltcp 承担的协议与 ax-net 补充的部分 - -| 协议 | smoltcp 原生支持 | ax-net 补充 | -| --- | --- | --- | -| **IPv4** | 包头构造、分片、校验和 | 多接口地址管理、静态/DHCP 地址分配、`InterfaceId` 元数据 | -| **IPv6** | 基本包头、多播 | IPv6→IPv4 映射(v4-mapped)、`IPV6_V6ONLY` socket option | -| **TCP** | 状态机、重传、窗口、keep-alive | `ListenTable` 端口槽位表、SYN pre-create、`TcpInfo` 快照、`SO_BINDTODEVICE` | -| **UDP** | 数据报、端口 | 端口冲突仲裁(`SocketSetWrapper::udp_binds`)、MSG_MORE corking | -| **ICMP** | 基础 | Loopback ICMP echo reply 模拟 | -| **ARP** | 无 | `EthernetDevice::neighbors` 表、TTL 300s、ARP-pending 队列 drain | -| **DHCP** | 无 | 完整 Discover→Offer→Request→ACK 状态机、per-interface `DhcpState`、指数退避 | -| **DNS** | `dns::Socket`、A 查询 | server 路由过滤、超时管理、`DnsSocketGuard` | -| **路由** | 无 | `RouteTable` 最长前缀匹配、metric 排序、`select_route_if` 接口可用性过滤 | - -## 非 TCP/IP 协议路径 - -| 协议 | 承载方式 | 数据路径 | -| --- | --- | --- | -| **Unix domain stream** | `ringbuf::HeapRb` 双向 buffer | 不经过 smoltcp,直接内存拷贝,含 cmsg 管道 | -| **Unix domain datagram** | `async_channel::unbounded()` | 不经过 smoltcp,无界队列 + Packet 结构体(含 data + cmsg + sender addr) | -| **Vsock stream** | `Connection` 的 ring buffer | 经过 `rdif-vsock` 驱动,不经过 smoltcp IP 层 | diff --git a/docs/docs/architecture/net/sockets.md b/docs/docs/architecture/net/sockets.md new file mode 100644 index 0000000000..f34855724b --- /dev/null +++ b/docs/docs/architecture/net/sockets.md @@ -0,0 +1,150 @@ +--- +sidebar_position: 4 +sidebar_label: "Socket 系统" +--- + +# Socket 系统 + +本文描述 `ax-net` 的 socket 抽象层:统一的 backend 分层、端口仲裁机制、状态机以及通用 poll 设施。 + +## Backend 分层 + +`Socket` 枚举([socket.rs](net/ax-net/src/socket.rs))聚合 5 类 backend,统一实现 `SocketOps` + `Configurable`: + +| Backend | 类型 | 协议核心 | 源码 | +| --- | --- | --- | --- | +| `TcpSocket` | SOCK_STREAM / AF_INET | smoltcp TCP socket | [tcp.rs](net/ax-net/src/tcp.rs) | +| `UdpSocket` | SOCK_DGRAM / AF_INET | smoltcp UDP socket | [udp.rs](net/ax-net/src/udp.rs) | +| `RawSocket` | SOCK_RAW / AF_INET | smoltcp raw socket | [raw.rs](net/ax-net/src/raw.rs) | +| `UnixSocket` | SOCK_STREAM/SOCK_DGRAM / AF_UNIX | 自包含 `Transport`(不经 smoltcp) | [unix/mod.rs](net/ax-net/src/unix/mod.rs) | +| `VsockSocket` | SOCK_STREAM / AF_VSOCK | `rdif-vsock` 驱动 + ring buffer | [vsock/mod.rs](net/ax-net/src/vsock/mod.rs) | + +IP 类 socket 通过 `SOCKET_SET.add()` 注册到全局 `SocketSet`,获得 `SocketHandle`;Unix / vsock 不依赖 smoltcp,各自维护连接状态。 + +## SocketSetWrapper + +`SocketSetWrapper`([wrapper.rs](net/ax-net/src/wrapper.rs))在 smoltcp 原生 `SocketSet` 之上增加了 UDP 端口冲突仲裁: + +```rust +pub(crate) struct SocketSetWrapper<'a> { + pub inner: Mutex>, + udp_binds: Mutex>, +} +``` + +UDP bind 仲裁规则(`udp_bind_available()`): + +- **精确 bind**(如 `192.168.1.1:53`):如果已有同地址同端口则拒绝;如果有 wildcard `0.0.0.0:53` 则拒绝。 +- **Wildcard bind**(`0.0.0.0:53`):如果任何地址已占用同一端口则拒绝。 +- **`SO_REUSEADDR`**:设置后跳过仲裁。 + +`udp_port_available()` 暴露给 ephemeral port 分配器,避免分配到已占用的端口。 + +## TCP 端口仲裁 + +除了 `ListenTable`(管理 `listen()` 端口),`ax-net` 还有独立的 `TCP_BOUND_PORTS`([tcp.rs](net/ax-net/src/tcp.rs))来追踪已 bind 但尚未 listen 的端口: + +```rust +static TCP_BOUND_PORTS: LazyLock>>>> = + LazyLock::new(|| Mutex::new(HashMap::new())); + +fn register_tcp_bound(endpoint: IpListenEndpoint) -> AxResult; +fn unregister_tcp_bound(endpoint: IpListenEndpoint); +``` + +两个系统协同工作: +- `LISTEN_TABLE`:管理已进入 `listen()` 状态的端口及 SYN 队列。 +- `TCP_BOUND_PORTS`:追踪已 bind(可能尚未 listen)的端口,防止地址冲突的重复 bind。`register_tcp_bound()` 在 `bind()`、`listen()`、`connect()` 中被调用;`unregister_tcp_bound()` 在 `shutdown()`、`Drop` 和错误回退中被调用。 +- `tcp_port_available(port)`:同时检查两个表,确保 `get_ephemeral_port()` 不会分配到已被占用的端口。 + +`listen_addrs_conflict(a, b)` 判断两个 `IpListenEndpoint` 是否冲突:任一为 `None`(wildcard)即冲突,或两者值相等即冲突。 + +## 状态机 + +`StateLock`([state.rs](net/ax-net/src/state.rs))为 socket 提供基于 CAS 的轻量状态转换锁: + +``` +Idle ──bind──→ Idle (不变,但注册 endpoint) +Idle ──listen──→ Listening +Idle ──connect──→ Connecting ──SYN/SYN+ACK──→ Connected +Listening ──accept──→ Listening (不变,但生成新 socket) +Connected ──close──→ Closed +``` + +核心 API: + +```rust +pub fn lock(&self, expect: State) -> Result, State> { + // CAS: expect → Busy,失败返回当前状态 +} +pub fn transit(self, new: State, f: impl FnOnce() -> AxResult) -> AxResult { + // 执行 f(),成功则写 new 状态,失败则回退到原始状态 +} +``` + +`StateLock` 保证绑定/监听/连接等操作的原子性:并发 bind+connect 在同一 socket 上只能有一个成功。 + +## ListenTable 与 SYN Pre-create + +`ListenTable`([listen_table.rs](net/ax-net/src/listen_table.rs))是 TCP 监听的核心数据结构。它包含 65536 个端口 bucket,每个端口是一个 `Arc>>`。同一端口可并存多个互不冲突的具体地址 listener,wildcard listener 会与该端口所有地址冲突: + +```rust +struct ListenTableEntryInner { + listen_endpoint: IpListenEndpoint, + backlog: usize, // clamp 到 LISTEN_QUEUE_SIZE=512 + syn_queue: VecDeque, + accept_poll: Arc, +} +``` + +### SYN 预创建 + +`snoop_tcp_packet()` 在 RX 路径中拦截 TCP SYN 包: + +1. 解析 TCP 包头,检查 SYN 标志。 +2. 按目的端口查 `LISTEN_TABLE`,匹配具体 listener。 +3. 如果 SYN queue 未满,预创建 `TcpSocket` 并调用 `socket.listen()`,加入 `syn_queue`。 +4. smoltcp `Interface::poll()` 完成三次握手。 +5. `accept()` 时直接从 `syn_queue` 取出已完成握手的 socket。 + +SYN queue 满时丢弃 SYN 包(warning),由客户端重传机制保证可靠性。 + +### accept() + +`ListenTable::accept()` 遍历 `syn_queue`,跳过已关闭且无未读数据的 socket,返回第一个可 accept 的连接(TCP state 为 Established/CloseWait/FinWait1/FinWait2 等)。 + +## 通用 Poll 与超时 + +`GeneralOptions`([general.rs](net/ax-net/src/general.rs))为所有 socket 类型提供通用设施: + +```rust +pub(crate) struct GeneralOptions { + nonblock: AtomicBool, // O_NONBLOCK + reuse_address: AtomicBool, // SO_REUSEADDR + send_timeout_nanos: AtomicU64, // SO_SNDTIMEO + recv_timeout_nanos: AtomicU64, // SO_RCVTIMEO + bound_if: AtomicU32, // SO_BINDTODEVICE (InterfaceId) + socket_type: AtomicI32, // SOCK_STREAM/DGRAM/RAW + domain: i32, // AF_INET/AF_UNIX/AF_VSOCK + protocol: i32, // IPPROTO_TCP/UDP/ICMP +} +``` + +poll helper: + +- `send_poller()` / `recv_poller()`:检查 `nonblock` → `register_waker()` → `block_on(poll_io())` 自旋等待 → 超时检查 → 执行操作。 +- `send_poller_with()`:支持指定是否检测 HUP 事件。 +- `register_waker()`:根据 `DeviceBinding` 将调用者 waker 注册到对应设备的 `PollSet`。 + +### socket 常量 + +每个 socket 创建时固化: + +| socket 类型 | SOCK_\* | AF_\* | IPPROTO_\* | +| --- | --- | --- | --- | +| `TcpSocket` | 1 (STREAM) | 2 (INET) | 6 (TCP) | +| `UdpSocket` | 2 (DGRAM) | 2 (INET) | 17 (UDP) | +| `RawSocket` | 3 (RAW) | 2 (INET) | 根据 `IpProtocol` | +| `UnixSocket` (stream) | 1 (STREAM) | 1 (UNIX) | 0 | +| `UnixSocket` (dgram) | 2 (DGRAM) | 1 (UNIX) | 0 | +| `VsockSocket` | 1 (STREAM) | 40 (VSOCK) | 0 | diff --git a/docs/docs/architecture/net/testing.md b/docs/docs/architecture/net/testing.md index fde5eb79c8..8ca8ef5fdf 100644 --- a/docs/docs/architecture/net/testing.md +++ b/docs/docs/architecture/net/testing.md @@ -1,6 +1,6 @@ --- -sidebar_position: 8 -sidebar_label: "测试与已知限制" +sidebar_position: 10 +sidebar_label: "测试与限制" --- # 测试与限制 @@ -19,7 +19,11 @@ sidebar_label: "测试与已知限制" - 接口 down / 不可用时跳过 route。 - default route 查询。 - bounded RX/TX queue 满时行为。 +- `QueuedPacket` 超 MTU 拒绝、队列路径无每包堆分配。 +- loopback dispatch 直接注入 `Router.rx_buffer`,并在注入前触发 TCP SYN snoop。 - DHCP bootstrap 和 per-interface 状态。 +- DHCP server Discover/Request 回复。 +- TCP orphan 在 Closed/Expired/overflow 场景下的回收。 - TCP/UDP bind 到具体本地地址后保留接口绑定。 - 未绑定 TCP/UDP connect 按 route decision 选择接口。 @@ -36,7 +40,7 @@ cargo test -p ax-net | 模块 | 关键测试点 | 优先级 | | --- | --- | --- | | `RouteTable` | 添加/删除/替换规则、排序验证、默认路由查询、`select_route_if` with mock filter | 高 | -| `ListenTable` | port 冲突、backlog 上限、`can_listen`/`listen`/`unlisten`、SYN 队列溢出、`can_accept`/`accept` | 高 | +| `ListenTable` | port 冲突、per-address listen、wildcard 冲突、backlog 上限、`can_listen`/`listen`/`unlisten`、SYN 队列溢出、`can_accept`/`accept` | 高 | | `SocketSetWrapper` | UDP bind 冲突仲裁(精确/wildcard/SO_REUSEADDR)、add/remove | 中 | | `StateLock` | Idle→Busy CAS、transit 成功/失败回退、状态读取一致性 | 中 | | `GeneralOptions` | nonblock/send_timeout/recv_timeout、device_binding 读写、Atomic 一致性 | 中 | @@ -46,6 +50,8 @@ cargo test -p ax-net | `UnixSocket` | stream pair send/recv、cmsg 传递、datagram pair、abstract namespace bind/connect | 中 | | `VsockSocket` | bind/listen/connect/accept 状态转换、send/recv | 低(需 `vsock` feature) | | `DhcpState` | Discovering→Offer→Requesting→ACK→Bound 状态机、NAK→reset、`process_packet` 校验 | 中 | +| `DhcpServer` | Discover→Offer、Request→Ack、错误 xid/mac/interface 过滤、单客户端租约覆盖 | 中 | +| `orphan` | Closed 立即回收、teardown 超时回收、overflow 只回收 Closed 且保留正在 teardown 的 socket | 高 | | DNS | `dns_query_timeout` 超时、不可路由 server 过滤、`DnsSocketGuard` drop | 中 | ## 接口路由测试 @@ -129,6 +135,11 @@ Test: bind_device Test: SO_REUSEADDR 两个 TcpSocket 都设 SO_REUSEADDR → 都可以 bind 同一端口 + +Test: per-address listen + listen(127.0.0.1:8080) 与 listen(10.0.2.15:8080) 可以共存 + listen(0.0.0.0:8080) 与任一具体地址 listener 冲突 + SYN 目的地址只进入匹配 listener 的 syn_queue ``` ## 已知 Debug 定位指南 @@ -140,8 +151,8 @@ Test: SO_REUSEADDR 1. DHCP 未完成 — 检查 `dhcp_configured()` 返回值。 2. 默认路由未提交 — 检查 `default_routes()` 是否非空。 3. 接口未 UP — 检查 `interface_by_id(id).flags` 包含 `UP`。 -4. `next_poll_delay()` 使用 wall clock 而非 monotonic clock — 可能导致 poll 延迟。 -5. RX worker 沉睡 — `device.rx_wake` 未被通知。 +4. RX worker 沉睡 — `device.rx_wake` 未被通知。 +5. 设备 TX/RX queue 满 — 检查是否有 worker 未运行或 driver 长时间阻塞。 检查方法: @@ -172,5 +183,5 @@ info!("arp entries: {:?}", ax_net::arp_entries()); 1. 检查 `NET_POLL_REQUESTED` atomic 标志。 2. 检查 `NET_POLL_WAKE` 是否有竞争。 -3. 检查 `poll_until_idle()` 中的 CAS 锁是否一直失败。 +3. 检查 `poll_until_idle()` 中的 CAS 锁是否一直失败;正常情况下只有 `net-poll` worker 会持续驱动协议栈,socket 热路径只负责 `request_poll()`。 4. 检查 `next_poll_delay()` 返回值 — 如果 smoltcp `poll_at()` 返回 `None`,idle poll interval 为 100ms。 From 02f32f61189875dbd90a33d125a0b8a7e93715ef Mon Sep 17 00:00:00 2001 From: ZCShou <72115@163.com> Date: Mon, 15 Jun 2026 16:59:08 +0800 Subject: [PATCH 21/31] test(socket-dataplane): add wait_readable helper for select-based readiness check --- .../syscall-test-socket-dataplane/src/main.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test-suit/starryos/qemu-smp1/system/syscall-test-socket-dataplane/src/main.c b/test-suit/starryos/qemu-smp1/system/syscall-test-socket-dataplane/src/main.c index 7913fbae9d..9f5a3db85d 100644 --- a/test-suit/starryos/qemu-smp1/system/syscall-test-socket-dataplane/src/main.c +++ b/test-suit/starryos/qemu-smp1/system/syscall-test-socket-dataplane/src/main.c @@ -206,6 +206,20 @@ static int make_tcp_sock(void) return fd; } +static int wait_readable(int fd, int timeout_ms) +{ + fd_set rfds; + FD_ZERO(&rfds); + FD_SET(fd, &rfds); + + struct timeval tv = { + .tv_sec = timeout_ms / 1000, + .tv_usec = (timeout_ms % 1000) * 1000, + }; + + return select(fd + 1, &rfds, NULL, NULL, &tv); +} + /* * 创建 UDP socket 并 bind 到 loopback 随机端口。 * 返回 fd, 通过 addr 返回实际绑定地址。 @@ -588,6 +602,9 @@ static void test_s11_sendto_msg_more_endpoint(void) /* A 应收到完整合并数据报 */ { + int ready = wait_readable(server_a, 1000); + CHECK(ready == 1, "A 在超时内变为可读"); + int flags_a = fcntl(server_a, F_GETFL, 0); fcntl(server_a, F_SETFL, flags_a | O_NONBLOCK); char buf[64] = {0}; From 8fc6e0a56f8189c318d094635ce77826bcfad5b3 Mon Sep 17 00:00:00 2001 From: ZCShou <72115@163.com> Date: Tue, 16 Jun 2026 08:19:24 +0800 Subject: [PATCH 22/31] docs(net): expand control plane and socket docs with inline source code examples --- docs/docs/architecture/net/control.md | 325 ++++++++++++++++++++------ docs/docs/architecture/net/sockets.md | 319 ++++++++++++++++++++----- 2 files changed, 524 insertions(+), 120 deletions(-) diff --git a/docs/docs/architecture/net/control.md b/docs/docs/architecture/net/control.md index a83fedb7df..aab0b5081e 100644 --- a/docs/docs/architecture/net/control.md +++ b/docs/docs/architecture/net/control.md @@ -5,139 +5,334 @@ sidebar_label: "控制面" # 控制面 -控制面是 `ax-net` 的接口管理和路由决策层,与数据面完全分离,查询不进入设备锁也不推进 smoltcp poll。 +控制面是 `ax-net` 的接口管理和路由决策层,与数据面完全分离。所有接口查询、路由查找和 DNS 查询通过 `NetControl` 进行,只读操作仅持 `RwLock` 读锁,不进入设备锁也不推进 smoltcp poll。 + +核心类型定义在 [config.rs](net/ax-net/src/config.rs),逻辑在 [service.rs](net/ax-net/src/service.rs) 和 [router.rs](net/ax-net/src/router.rs)。 + +## 接口标识 + +`InterfaceId(u32)` 贯穿整个网络栈,也是 StarryOS Linux ABI 的 `ifindex` 来源: + +```rust +// net/ax-net/src/config.rs +#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct InterfaceId(u32); + +impl InterfaceId { + pub const LOOPBACK: Self = Self(1); + pub const fn new(raw: u32) -> Self { Self(raw) } + pub const fn get(self) -> u32 { self.0 } + pub const fn to_linux_ifindex(self) -> i32 { self.0 as i32 } + pub const fn from_linux_ifindex(ifindex: i32) -> Option { + if ifindex > 0 { Some(Self(ifindex as u32)) } else { None } + } +} +``` + +- `InterfaceId::LOOPBACK == 1`,固定对应 `lo`。 +- Ethernet 接口从 `2` 开始,`InterfaceId::new((order as u32) + 2)` 递增。 +- `InterfaceId(0)` 是内部占位符 `TX_INTERFACE_PLACEHOLDER`,不对外暴露。 + +`InterfaceInfo` 是对外只读快照,由内部 `NetInterface` 通过 `to_info()` 转换: + +```rust +// config.rs +pub struct InterfaceInfo { + pub id: InterfaceId, + pub name: String, + pub kind: InterfaceKind, // Loopback | Ethernet + pub mac: Option, + pub ipv4: Option, + pub mtu: usize, + pub flags: InterfaceFlags, // UP | RUNNING | LOOPBACK | BROADCAST | MULTICAST + pub metric: u32, +} + +// service.rs (crate-internal) +pub(crate) struct NetInterface { + pub id: InterfaceId, + pub name: String, + pub kind: InterfaceKind, + pub mac: Option, + pub ipv4: Option, // smoltcp native type + pub gateway: Option, + pub mtu: usize, pub metric: u32, + pub flags: InterfaceFlags, +} +``` ## NetControl -控制面由 `NetControl`([service.rs](net/ax-net/src/service.rs))统一持有: +`NetControl` 在 `init_network()` 中构建,早于 `SERVICE` 初始化,通过 `Arc` 共享: ```rust +// service.rs +struct ControlState { + interfaces: Vec, + dns: Vec, +} + pub struct NetControl { state: RwLock, - pub(crate) routes: SharedRouteTable, + pub(crate) routes: SharedRouteTable, // = Arc> } + +// init_network() 中的构造顺序: +let control = Arc::new(NetControl::new(interfaces, routes, dns)); +let mut service = Service::new(router, control.clone()); +NET_CONTROL.call_once(|| control); +SERVICE.call_once(|| Mutex::new(service)); ``` -内部 `ControlState` 保存 `Vec`(接口 registry)与 `Vec`(DNS 来源信息)。 +`state` 使用 `spin::RwLock`,查询只持读锁。`routes` 与 `Service` 共享同一 `SharedRouteTable`。 ### 接口查询 -- `interfaces()` / `interface_by_name()` / `interface_by_id()`:返回只读快照,不持锁。 -- `ipv4_config(name)`:按名称查接口 IPv4 配置。 -- `default_routes()`:返回当前默认路由列表。 +返回只读快照,调用者不应假设快照永久有效(DHCP 可能更新地址/路由/DNS): + +```rust +// service.rs +pub fn interfaces(&self) -> Vec { + let state = self.state.read(); + state.interfaces.iter().map(NetInterface::to_info).collect() +} + +pub fn interface_by_name(&self, name: &str) -> Option { + let state = self.state.read(); + state.interfaces.iter().find(|i| i.name == name).map(NetInterface::to_info) +} + +pub fn interface_by_id(&self, id: InterfaceId) -> Option { + let state = self.state.read(); + state.interfaces.iter().find(|i| i.id == id).map(NetInterface::to_info) +} +``` ### 路由决策 -`select_route(dst_addr)`:按目的地址查路由,校验目标接口 `UP`。底层调用 `RouteTable::select_route_if()`,传入 `is_usable` 闭包过滤未 `UP` 的接口。 +`select_route()` 按目的地址查路由,`is_usable` 闭包跳过未 `UP` 的接口: + +```rust +// service.rs +pub fn select_route(&self, dst_addr: &IpAddress) -> AxResult { + let state = self.state.read(); + let routes = self.routes.read(); + let route = routes.select_route_if(dst_addr, |interface_id| { + state.interfaces.iter() + .find(|i| i.id == interface_id) + .is_some_and(|i| i.flags.contains(InterfaceFlags::UP)) + }).ok_or_else(|| ax_err_type!(NoSuchDeviceOrAddress, + format!("no route to destination {dst_addr}")))?; + Ok(route) +} +``` -`select_route_for_source(dst, src)`:在 TX dispatch 时同时匹配目的地址和源地址。多宿主主机(两个接口有不同源 IP)发送到同一目的地时,需根据 smoltcp 注入的源地址选择正确的出接口。 +`RouteDecision` 返回选中的设备索引、源地址、下一跳和 metric: ```rust -pub fn select_route_if(&self, dst, mut is_usable: impl FnMut(InterfaceId) -> bool) - -> Option -{ - self.rules.iter() - .find(|rule| rule.filter.contains_addr(dst) && is_usable(rule.interface_id)) - .map(|rule| RouteDecision { /* ... */ }) +// router.rs +#[derive(Debug, Clone, Copy)] +pub struct RouteDecision { + pub dev: usize, // Router::devices 索引 + pub interface_id: InterfaceId, + pub source: IpAddress, // smoltcp 注入 IP 包头的 src_ip + pub next_hop: IpAddress, // 直连时 = dst,有 gateway 时 = gateway + pub metric: u32, } ``` ### 接口-地址绑定 -`local_binding_for(endpoint)`:把本地监听地址映射为 `DeviceBinding`。若 `endpoint.addr` 是某个接口的 IP,返回 `bound_if = Some(interface_id)`;若是 wildcard,返回 `DeviceBinding::default()`。 +`local_binding_for()` 遍历接口,将监听地址映射为 `DeviceBinding`: + +```rust +// service.rs +pub fn local_binding_for(&self, endpoint: &IpListenEndpoint) -> AxResult { + match endpoint.addr { + Some(addr) => { + let state = self.state.read(); + let bound_if = state.interfaces.iter().find_map(|i| { + (i.ipv4.is_some_and(|ip| IpAddress::Ipv4(ip.address()) == addr)) + .then_some(i.id) + }); + bound_if.map(|id| DeviceBinding { bound_if: Some(id) }) + .ok_or_else(|| ax_err_type!(NoSuchDeviceOrAddress)) + } + None => Ok(DeviceBinding::default()), // wildcard → 不绑定接口 + } +} +``` ### 运行期更新 -`commit_interface_update(update, routes)`:DHCP ACK 等运行期更新通过一次性事务提交接口地址、smoltcp IP 地址、路由和 DNS,避免外部看到半更新状态。接口查询只需持有 `NetControl.state` 读锁;更新时获取写锁。 +`commit_interface_update()` 在 DHCP ACK 后以事务方式更新接口状态、DNS 和路由: + +```rust +// service.rs +fn commit_interface_update(&self, update: &NetworkStateUpdate, + routes: Vec) { + let mut state = self.state.write(); + // 1. 更新接口 IPv4/gateway + if let Some(i) = state.interfaces.iter_mut() + .find(|i| i.id == update.interface_id) + { + i.ipv4 = update.ipv4; + i.gateway = update.gateway; + } + // 2. 替换该接口同来源的 DNS 条目 + state.dns.retain(|e| e.interface_id != update.interface_id + || e.source != update.dns_source); + state.dns.extend(update.dns_servers.iter().copied().map(|s| DnsServerEntry { + server: s, interface_id: update.interface_id, + metric: update.metric, source: update.dns_source, + })); + // 3. 原子替换路由规则 + self.routes.write().replace_ipv4_rules_for_interface(update.interface_id, routes); +} +``` + +三步在同一写锁内完成,外部不会看到半更新状态。 ## RouteTable -`RouteTable`([router.rs](net/ax-net/src/router.rs))是路由表的实现: +`RouteTable` 在 [router.rs](net/ax-net/src/router.rs) 中实现,由 `Arc>`(`SharedRouteTable`)包装,被 `NetControl` 和 `Service` 共享。 ```rust +// router.rs #[derive(Debug)] pub struct Rule { - pub filter: IpCidr, - pub via: Option, - pub dev: usize, + pub filter: IpCidr, // 目标网段 + pub via: Option, // gateway(None = 直连) + pub dev: usize, // Router::devices 索引 pub interface_id: InterfaceId, - pub src: IpAddress, + pub src: IpAddress, // 源地址 pub metric: u32, - pub order: u64, + pub order: u64, // 插入顺序(next_order 自增) +} + +pub struct RouteTable { + rules: Vec, + next_order: u64, } ``` ### 排序策略 -`sort_rules()` 的三级排序: +`sort_rules()` 在每次 add/replace 后调用: -1. **最长前缀优先**:`b.filter.prefix_len().cmp(&a.filter.prefix_len())` -2. **低 metric 优先**:同前缀长度时,`a.metric.cmp(&b.metric)` -3. **插入顺序稳定**:同前缀同 metric 时,`a.order.cmp(&b.order)` +```rust +fn sort_rules(&mut self) { + self.rules.sort_by(|a, b| { + b.filter.prefix_len().cmp(&a.filter.prefix_len()) // 1. 最长前缀优先 + .then_with(|| a.metric.cmp(&b.metric)) // 2. 低 metric 优先 + .then_with(|| a.order.cmp(&b.order)) // 3. 插入顺序稳定 + }); +} +``` -`select_route_if` 使用 `find()` 返回第一个匹配项(即最高优先级的路由)。 +### 查询 -### 规则管理 +`select_route_if` 带 `is_usable` 闭包,`find()` 返回首个匹配项即优先级最高: -- `add_rule(rule)`:添加单条规则,自动分配 `order` 并重排。 -- `remove_ipv4_rules_for_interface(id)`:移除某个接口的所有 IPv4 规则。 -- `replace_ipv4_rules_for_interface(id, new_rules)`:原子替换某个接口的 IPv4 规则(DHCP ACK 后使用)。 -- `default_routes()`:返回所有默认路由(`0.0.0.0/0`)。 - -## 接口标识 +```rust +pub fn select_route_if(&self, dst: &IpAddress, + mut is_usable: impl FnMut(InterfaceId) -> bool) + -> Option +{ + self.rules.iter() + .find(|rule| rule.filter.contains_addr(dst) && is_usable(rule.interface_id)) + .map(|rule| RouteDecision { + dev: rule.dev, interface_id: rule.interface_id, + source: rule.src, next_hop: rule.via.unwrap_or(*dst), metric: rule.metric, + }) +} +``` -`InterfaceId(u32)` 是内部接口 ID,同时也是 StarryOS Linux ABI 的 ifindex 来源: +`select_route_for_source` 在 TX dispatch 时额外要求源地址匹配(多宿主场景): ```rust -pub struct InterfaceId(u32); - -impl InterfaceId { - pub const LOOPBACK: Self = Self(1); - pub const fn new(raw: u32) -> Self { Self(raw) } - pub const fn to_linux_ifindex(self) -> i32 { self.0 as i32 } - pub const fn from_linux_ifindex(ifindex: i32) -> Option { /* ... */ } +pub fn select_route_for_source(&self, dst: &IpAddress, source: &IpAddress) + -> Option +{ + self.rules.iter() + .find(|rule| rule.filter.contains_addr(dst) && &rule.src == source) + .map(|rule| RouteDecision { /* ... */ }) } ``` -- `InterfaceId::LOOPBACK == 1`,固定对应 `lo`。 -- Ethernet 接口从 `2` 开始,按发现顺序递增。 - -`InterfaceInfo` 是对外只读快照,包含 ID、名称、类型、MAC、IPv4、MTU、flags 和 metric。 +### 规则管理 ```rust -pub struct InterfaceInfo { - pub id: InterfaceId, - pub name: String, - pub kind: InterfaceKind, - pub mac: Option, - pub ipv4: Option, - pub mtu: usize, - pub flags: InterfaceFlags, - pub metric: u32, +pub fn add_rule(&mut self, mut rule: Rule) { + rule.order = self.next_order; self.next_order = self.next_order.saturating_add(1); + self.rules.push(rule); self.sort_rules(); +} + +pub fn default_routes(&self) -> Vec { + self.rules.iter() + .filter(|rule| matches!(rule.filter, + IpCidr::Ipv4(cidr) if cidr.address() == Ipv4Address::UNSPECIFIED + && cidr.prefix_len() == 0)) + .map(Rule::to_info).collect() +} + +pub fn replace_ipv4_rules_for_interface(&mut self, interface_id: InterfaceId, + mut new_rules: Vec) { + self.remove_ipv4_rules_for_interface(interface_id); + for rule in &mut new_rules { + rule.order = self.next_order; self.next_order = self.next_order.saturating_add(1); + } + self.rules.extend(new_rules); self.sort_rules(); } ``` ## DeviceBinding -`DeviceBinding`([config.rs](net/ax-net/src/config.rs))对应 Linux `SO_BINDTODEVICE`: +对应 Linux `SO_BINDTODEVICE`,存储在 `GeneralOptions::bound_if: AtomicU32`: ```rust +// config.rs +#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)] pub struct DeviceBinding { pub bound_if: Option, } ``` -- `None`:未绑定接口,按 route decision 选择出接口。 +- `None`:不绑定接口,route decision 选择出接口。 - `Some(id)`:只允许对应接口参与 route/waker/device 选择。 +设置/读取通过 `set_device_binding()` / `device_binding()` 操作 `AtomicU32`。 + ## DNS 注册 -DNS server 来源分三类,内部记录为 `DnsServerEntry`: +DNS server 分三类来源,记录为 `DnsServerEntry`: + +```rust +// config.rs +pub(crate) struct DnsServerEntry { + pub server: Ipv4Address, + pub interface_id: InterfaceId, + pub metric: u32, + pub source: DnsSource, // Dhcp | Static | Fallback +} +``` + +| 来源 | 创建时机 | metric | +| --- | --- | --- | +| DHCP | DHCP ACK 后 `commit_interface_update` | 对应接口 metric | +| Static | `init_network()` 从 `InterfaceConfig::dns_servers` | 对应接口 metric | +| Fallback | `init_network()` 从 `NetworkConfig::default_dns_servers` | `u32::MAX` | -| 来源 | 说明 | -| --- | --- | -| DHCP | DHCP ACK 下发,绑定来源接口和 metric | -| Static | `InterfaceConfig::dns_servers` | -| Fallback | `NetworkConfig::default_dns_servers` | +`dns_servers()` 排序去重返回纯地址列表: -`dns_servers()` 返回按 (metric, interface_id, server_ip) 排序的去重地址列表。DHCP ACK 后 `commit_interface_update` 会替换对应接口的 DNS entry。 +```rust +// service.rs +pub fn dns_servers(&self) -> Vec { + let state = self.state.read(); + let mut entries = state.dns.clone(); + entries.sort_by_key(|e| (e.metric, e.interface_id.get(), e.server.octets())); + let mut servers = Vec::new(); + for e in entries { if !servers.contains(&e.server) { servers.push(e.server); } } + servers +} +``` diff --git a/docs/docs/architecture/net/sockets.md b/docs/docs/architecture/net/sockets.md index f34855724b..9e6c56d7d1 100644 --- a/docs/docs/architecture/net/sockets.md +++ b/docs/docs/architecture/net/sockets.md @@ -5,63 +5,191 @@ sidebar_label: "Socket 系统" # Socket 系统 -本文描述 `ax-net` 的 socket 抽象层:统一的 backend 分层、端口仲裁机制、状态机以及通用 poll 设施。 +`ax-net` 的 socket 抽象层向上暴露统一 API,向下对接 smoltcp(IP 类 socket)或自包含传输(Unix/vsock)。本文从源码层描述 backend 分层、端口仲裁、状态机和通用 poll 设施。 ## Backend 分层 `Socket` 枚举([socket.rs](net/ax-net/src/socket.rs))聚合 5 类 backend,统一实现 `SocketOps` + `Configurable`: +```rust +// socket.rs +pub enum Socket { + Tcp(Box), + Udp(Box), + Raw(Box), + Unix(Box), + #[cfg(feature = "vsock")] + Vsock(Box), +} +``` + | Backend | 类型 | 协议核心 | 源码 | | --- | --- | --- | --- | | `TcpSocket` | SOCK_STREAM / AF_INET | smoltcp TCP socket | [tcp.rs](net/ax-net/src/tcp.rs) | | `UdpSocket` | SOCK_DGRAM / AF_INET | smoltcp UDP socket | [udp.rs](net/ax-net/src/udp.rs) | -| `RawSocket` | SOCK_RAW / AF_INET | smoltcp raw socket | [raw.rs](net/ax-net/src/raw.rs) | +| `RawSocket` | SOCK_RAW / AF_INET | smoltcp raw socket + ICMP loopback | [raw.rs](net/ax-net/src/raw.rs) | | `UnixSocket` | SOCK_STREAM/SOCK_DGRAM / AF_UNIX | 自包含 `Transport`(不经 smoltcp) | [unix/mod.rs](net/ax-net/src/unix/mod.rs) | | `VsockSocket` | SOCK_STREAM / AF_VSOCK | `rdif-vsock` 驱动 + ring buffer | [vsock/mod.rs](net/ax-net/src/vsock/mod.rs) | -IP 类 socket 通过 `SOCKET_SET.add()` 注册到全局 `SocketSet`,获得 `SocketHandle`;Unix / vsock 不依赖 smoltcp,各自维护连接状态。 +IP 类 socket(TCP/UDP/Raw/DNS)通过 `SOCKET_SET.add()` 注册到全局 `SocketSet`,获得 `SocketHandle`。Unix 和 vsock 不依赖 smoltcp,各自维护连接状态。 + +`SocketAddrEx` 统一地址类型: + +```rust +// socket.rs +pub enum SocketAddrEx { + Ip(SocketAddr), + Unix(UnixSocketAddr), + #[cfg(feature = "vsock")] + Vsock(VsockAddr), +} +``` + +`SocketOps` trait 是所有 backend 的统一接口: + +```rust +// socket.rs +pub trait SocketOps: Configurable { + fn bind(&self, local_addr: SocketAddrEx) -> AxResult; + fn connect(&self, remote_addr: SocketAddrEx) -> AxResult; + fn listen(&self, _backlog: usize) -> AxResult { Err(AxError::OperationNotSupported) } + fn accept(&self) -> AxResult { Err(AxError::OperationNotSupported) } + fn send(&self, src: impl Read + IoBuf, options: SendOptions) -> AxResult; + fn recv(&self, dst: impl Write + IoBufMut, options: RecvOptions<'_>) -> AxResult; + fn local_addr(&self) -> AxResult; + fn peer_addr(&self) -> AxResult; + fn shutdown(&self, how: Shutdown) -> AxResult; +} +``` + +TCP/UDP 实现完整接口;Raw socket 不实现 `listen`/`accept`;Unix/vsock 有各自独立的 transport trait。 ## SocketSetWrapper -`SocketSetWrapper`([wrapper.rs](net/ax-net/src/wrapper.rs))在 smoltcp 原生 `SocketSet` 之上增加了 UDP 端口冲突仲裁: +全局 socket 容器([wrapper.rs](net/ax-net/src/wrapper.rs))在 smoltcp `SocketSet` 之上增加 UDP 端口冲突仲裁: ```rust +// wrapper.rs +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +struct UdpBindKey { + addr: Option, + port: u16, +} + pub(crate) struct SocketSetWrapper<'a> { pub inner: Mutex>, udp_binds: Mutex>, } ``` -UDP bind 仲裁规则(`udp_bind_available()`): +`add()` 和 `with_socket_mut()` 是对 smoltcp API 的薄封装;`remove()` 额外调用 `udp_unbind()`。 + +### UDP 端口仲裁 -- **精确 bind**(如 `192.168.1.1:53`):如果已有同地址同端口则拒绝;如果有 wildcard `0.0.0.0:53` 则拒绝。 -- **Wildcard bind**(`0.0.0.0:53`):如果任何地址已占用同一端口则拒绝。 -- **`SO_REUSEADDR`**:设置后跳过仲裁。 +`udp_bind_available()` 实现 Linux `SO_REUSEADDR` 语义: -`udp_port_available()` 暴露给 ephemeral port 分配器,避免分配到已占用的端口。 +```rust +// wrapper.rs +fn udp_bind_available(binds: &HashMap, key: UdpBindKey) -> bool { + let wildcard = UdpBindKey { addr: None, port: key.port }; + // 精确 bind:同地址同端口冲突 || wildcard 已占用冲突 + if binds.contains_key(&key) || (key.addr.is_some() && binds.contains_key(&wildcard)) { + return false; + } + // Wildcard bind:任何地址已占用同一端口则冲突 + key.addr.is_some() || !binds.keys().any(|bind| bind.port == key.port) +} +``` + +| bind 类型 | 示例 | 拒绝条件 | +| --- | --- | --- | +| 精确 bind | `192.168.1.1:53` | 同地址同端口已存在,或 wildcard `0.0.0.0:53` 已存在 | +| Wildcard bind | `0.0.0.0:53` | 任何地址已占用同一端口 | +| `SO_REUSEADDR` | — | 跳过所有仲裁 | + +`udp_port_available()` 暴露给 ephemeral port 分配器使用。 ## TCP 端口仲裁 -除了 `ListenTable`(管理 `listen()` 端口),`ax-net` 还有独立的 `TCP_BOUND_PORTS`([tcp.rs](net/ax-net/src/tcp.rs))来追踪已 bind 但尚未 listen 的端口: +除了 `ListenTable`(管理 `listen()` 端口),`ax-net` 还有独立的 `TCP_BOUND_PORTS`([tcp.rs](net/ax-net/src/tcp.rs))追踪已 bind 但尚未 listen 的端口: ```rust +// tcp.rs static TCP_BOUND_PORTS: LazyLock>>>> = LazyLock::new(|| Mutex::new(HashMap::new())); -fn register_tcp_bound(endpoint: IpListenEndpoint) -> AxResult; -fn unregister_tcp_bound(endpoint: IpListenEndpoint); +fn register_tcp_bound(endpoint: IpListenEndpoint) -> AxResult { + if endpoint.port == 0 { return Ok(()); } + let mut bound_ports = TCP_BOUND_PORTS.lock(); + let bound_addrs = bound_ports.entry(endpoint.port).or_default(); + if bound_addrs.iter().any(|&addr| listen_addrs_conflict(addr, endpoint.addr)) { + return Err(AxError::AddrInUse); + } + bound_addrs.push(endpoint.addr); + Ok(()) +} + +fn unregister_tcp_bound(endpoint: IpListenEndpoint) { + if endpoint.port != 0 { + let mut bound_ports = TCP_BOUND_PORTS.lock(); + if let Some(bound_addrs) = bound_ports.get_mut(&endpoint.port) { + if let Some(idx) = bound_addrs.iter().position(|&addr| addr == endpoint.addr) { + bound_addrs.swap_remove(idx); + } + if bound_addrs.is_empty() { bound_ports.remove(&endpoint.port); } + } + } +} ``` -两个系统协同工作: -- `LISTEN_TABLE`:管理已进入 `listen()` 状态的端口及 SYN 队列。 -- `TCP_BOUND_PORTS`:追踪已 bind(可能尚未 listen)的端口,防止地址冲突的重复 bind。`register_tcp_bound()` 在 `bind()`、`listen()`、`connect()` 中被调用;`unregister_tcp_bound()` 在 `shutdown()`、`Drop` 和错误回退中被调用。 -- `tcp_port_available(port)`:同时检查两个表,确保 `get_ephemeral_port()` 不会分配到已被占用的端口。 +地址冲突判定: -`listen_addrs_conflict(a, b)` 判断两个 `IpListenEndpoint` 是否冲突:任一为 `None`(wildcard)即冲突,或两者值相等即冲突。 +```rust +fn listen_addrs_conflict(a: Option, b: Option) -> bool { + a.is_none() || b.is_none() || a == b +} +``` -## 状态机 +即 wildcard 与所有地址冲突,两个具体地址仅相等时冲突。这与 `ListenTable` 的同端口多地址 listen 语义一致。 -`StateLock`([state.rs](net/ax-net/src/state.rs))为 socket 提供基于 CAS 的轻量状态转换锁: +`tcp_port_available()` 同时检查两表: + +```rust +fn tcp_port_available(port: u16) -> bool { + LISTEN_TABLE.can_listen(IpListenEndpoint { addr: None, port }) + && !TCP_BOUND_PORTS.lock().contains_key(&port) +} +``` + +## Socket 状态机 + +`StateLock`([state.rs](net/ax-net/src/state.rs))提供基于 CAS 的轻量状态转换锁,所有 socket 类型共享: + +```rust +// state.rs +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum State { Idle = 0, Busy = 1, Connecting = 2, + Connected = 3, Listening = 4, Closed = 5 } + +pub struct StateLock(AtomicU8); + +impl StateLock { + pub fn get(&self) -> State { + self.0.load(Ordering::Acquire).try_into().expect("invalid state") + } + + pub fn lock(&self, expect: State) -> Result, State> { + match self.0.compare_exchange(expect as u8, State::Busy as u8, + Ordering::Acquire, Ordering::Acquire) { + Ok(_) => Ok(StateGuard(self, expect as u8)), + Err(old) => Err(old.try_into().expect("invalid state")), + } + } +} +``` + +状态转换图: ``` Idle ──bind──→ Idle (不变,但注册 endpoint) @@ -71,80 +199,161 @@ Listening ──accept──→ Listening (不变,但生成新 socket) Connected ──close──→ Closed ``` -核心 API: +`StateGuard::transit()` 在执行操作时临时置为 `Busy`,成功提交新状态,失败回退: ```rust -pub fn lock(&self, expect: State) -> Result, State> { - // CAS: expect → Busy,失败返回当前状态 -} -pub fn transit(self, new: State, f: impl FnOnce() -> AxResult) -> AxResult { - // 执行 f(),成功则写 new 状态,失败则回退到原始状态 +// state.rs +#[must_use] +pub struct StateGuard<'a>(&'a StateLock, u8); + +impl StateGuard<'_> { + pub fn transit(self, new: State, f: impl FnOnce() -> AxResult) -> AxResult { + match f() { + Ok(result) => { self.0.0.store(new as u8, Ordering::Release); Ok(result) } + Err(err) => { self.0.0.store(self.1, Ordering::Release); Err(err) } + } + } } ``` -`StateLock` 保证绑定/监听/连接等操作的原子性:并发 bind+connect 在同一 socket 上只能有一个成功。 +并发 `bind`/`connect`/`listen` 在同一 socket 上只有第一个成功——后续操作在 `lock(expect)` 时 CAS 失败。 -## ListenTable 与 SYN Pre-create +## ListenTable -`ListenTable`([listen_table.rs](net/ax-net/src/listen_table.rs))是 TCP 监听的核心数据结构。它包含 65536 个端口 bucket,每个端口是一个 `Arc>>`。同一端口可并存多个互不冲突的具体地址 listener,wildcard listener 会与该端口所有地址冲突: +`ListenTable`([listen_table.rs](net/ax-net/src/listen_table.rs))是 TCP 监听的核心数据结构。65536 个端口 bucket,每端口一个 `Arc>>`,支持同端口不同地址 listen: ```rust +// listen_table.rs struct ListenTableEntryInner { listen_endpoint: IpListenEndpoint, backlog: usize, // clamp 到 LISTEN_QUEUE_SIZE=512 syn_queue: VecDeque, accept_poll: Arc, } + +pub struct ListenTable { + tcp: Box<[ListenTableEntry]>, // [Arc>>; 65536] +} ``` -### SYN 预创建 +### listen/unlisten + +```rust +pub fn can_listen(&self, endpoint: IpListenEndpoint) -> bool { + self.tcp[endpoint.port as usize].lock().iter() + .all(|entry| !listen_addrs_conflict(entry.listen_endpoint.addr, endpoint.addr)) +} -`snoop_tcp_packet()` 在 RX 路径中拦截 TCP SYN 包: +pub fn listen(&self, endpoint: IpListenEndpoint, backlog: usize) -> AxResult { + let mut entries = self.tcp[endpoint.port as usize].lock(); + if entries.iter().any(|e| listen_addrs_conflict(e.listen_endpoint.addr, endpoint.addr)) { + return Err(AxError::AddrInUse); + } + entries.push(ListenTableEntryInner::new(endpoint, backlog)); + Ok(()) +} +``` -1. 解析 TCP 包头,检查 SYN 标志。 -2. 按目的端口查 `LISTEN_TABLE`,匹配具体 listener。 -3. 如果 SYN queue 未满,预创建 `TcpSocket` 并调用 `socket.listen()`,加入 `syn_queue`。 -4. smoltcp `Interface::poll()` 完成三次握手。 -5. `accept()` 时直接从 `syn_queue` 取出已完成握手的 socket。 +### SYN 预创建 -SYN queue 满时丢弃 SYN 包(warning),由客户端重传机制保证可靠性。 +`snoop_tcp_packet()` 在 `Router::poll()` 中拦截 TCP SYN 包(`tcp_packet.syn() && !tcp_packet.ack()`),调用 `incoming_tcp_packet()` 预创建 socket。 + +`incoming_tcp_packet()` 在 `syn_queue` 未满时创建新 `TcpSocket` 并调用 `socket.listen()`,推入队列供后续 `accept()` 获取。 ### accept() -`ListenTable::accept()` 遍历 `syn_queue`,跳过已关闭且无未读数据的 socket,返回第一个可 accept 的连接(TCP state 为 Established/CloseWait/FinWait1/FinWait2 等)。 +```rust +pub fn accept(&self, endpoint: IpListenEndpoint, sockets: &mut SocketSet) -> AxResult { + let entries = self.tcp[endpoint.port as usize].clone(); + let mut table = entries.lock(); + let entry = table.iter_mut() + .find(|e| e.listen_endpoint == endpoint) + .ok_or(AxError::InvalidInput)?; + + // 遍历 syn_queue,跳过 Closed 且无数据的 socket + let mut idx = 0; + while idx < entry.syn_queue.len() { + let handle = entry.syn_queue[idx].accepted.handle; + if is_closed_without_data(sockets, handle) { + entry.syn_queue.swap_remove_front(idx); + sockets.remove(handle); + continue; + } + if is_acceptable(sockets, handle) { + return Ok(entry.syn_queue.swap_remove_front(idx).unwrap().accepted); + } + idx += 1; + } + Err(AxError::WouldBlock) +} +``` + +可 accept 的状态:`Established | CloseWait | FinWait1 | FinWait2 | Closing | LastAck | TimeWait`。 ## 通用 Poll 与超时 -`GeneralOptions`([general.rs](net/ax-net/src/general.rs))为所有 socket 类型提供通用设施: +`GeneralOptions`([general.rs](net/ax-net/src/general.rs))为所有 socket 提供通用的非阻塞、超时和 poll 设施: ```rust +// general.rs pub(crate) struct GeneralOptions { nonblock: AtomicBool, // O_NONBLOCK reuse_address: AtomicBool, // SO_REUSEADDR - send_timeout_nanos: AtomicU64, // SO_SNDTIMEO + send_timeout_nanos: AtomicU64, // SO_SNDTIMEO (0 = 无限) recv_timeout_nanos: AtomicU64, // SO_RCVTIMEO - bound_if: AtomicU32, // SO_BINDTODEVICE (InterfaceId) - socket_type: AtomicI32, // SOCK_STREAM/DGRAM/RAW - domain: i32, // AF_INET/AF_UNIX/AF_VSOCK - protocol: i32, // IPPROTO_TCP/UDP/ICMP + bound_if: AtomicU32, // SO_BINDTODEVICE (InterfaceId, 0 = 未绑定) + socket_type: AtomicI32, // SOCK_STREAM(1)/DGRAM(2)/RAW(3) + domain: i32, // AF_INET(2)/AF_UNIX(1)/AF_VSOCK(40) + protocol: i32, // IPPROTO_TCP(6)/UDP(17)/ICMP(1) } ``` -poll helper: - -- `send_poller()` / `recv_poller()`:检查 `nonblock` → `register_waker()` → `block_on(poll_io())` 自旋等待 → 超时检查 → 执行操作。 -- `send_poller_with()`:支持指定是否检测 HUP 事件。 -- `register_waker()`:根据 `DeviceBinding` 将调用者 waker 注册到对应设备的 `PollSet`。 - -### socket 常量 +构造函数按 socket 类型固化 `(socket_type, domain, protocol)`: -每个 socket 创建时固化: - -| socket 类型 | SOCK_\* | AF_\* | IPPROTO_\* | +| socket | SOCK_* | AF_* | IPPROTO_* | | --- | --- | --- | --- | | `TcpSocket` | 1 (STREAM) | 2 (INET) | 6 (TCP) | | `UdpSocket` | 2 (DGRAM) | 2 (INET) | 17 (UDP) | | `RawSocket` | 3 (RAW) | 2 (INET) | 根据 `IpProtocol` | -| `UnixSocket` (stream) | 1 (STREAM) | 1 (UNIX) | 0 | -| `UnixSocket` (dgram) | 2 (DGRAM) | 1 (UNIX) | 0 | +| `UnixSocket` stream | 1 (STREAM) | 1 (UNIX) | 0 | +| `UnixSocket` dgram | 2 (DGRAM) | 1 (UNIX) | 0 | | `VsockSocket` | 1 (STREAM) | 40 (VSOCK) | 0 | + +### 核心 Poller + +`send_poller_with()` 和 `recv_poller_with()` 是通用的 poll 循环: + +```rust +// general.rs +pub fn send_poller_with AxResult, T>( + &self, pollable: &P, extra_nb: bool, mut f: F) -> AxResult +{ + loop { + // 尝试执行 f(),如果成功返回结果 + match f() { + Ok(result) => return Ok(result), + Err(AxError::WouldBlock) => { + // 检查 nonblock 模式 -> 直接返回 WouldBlock + if self.nonblock.load(Relaxed) || extra_nb { return Err(AxError::WouldBlock); } + } + Err(e) => return Err(e), + } + // 注册 waker 到对应设备 + self.register_waker(context.waker()); + // block_on 等待 I/O 事件或超时 + // ... + } +} +``` + +流程:`f()` 返回 `WouldBlock` → `register_waker()` 注册到 `Service` → `block_on(poll_io())` 等待 → 超时返回 `TimedOut` → 重新循环。 + +`register_waker()` 根据 `DeviceBinding` 选择性注册到匹配的设备: + +```rust +pub(crate) fn register_waker(&self, waker: &Waker) { + get_service().register_waker(self.device_binding(), waker); +} +``` + +绑定 `DeviceBinding { bound_if: Some(eth0) }` 时只注册到 eth0 的 waker,避免被无关设备唤醒。 From 9d12c58714bb525d9b9216546995707afc2b09e6 Mon Sep 17 00:00:00 2001 From: ZCShou <72115@163.com> Date: Tue, 16 Jun 2026 08:44:41 +0800 Subject: [PATCH 23/31] fix(ax-net): fix raw socket MSG_PEEK to deliver IP payload and defer non-peer packets without busy-looping --- net/ax-net/src/raw.rs | 123 ++++++++---------- .../syscall-test-raw-msg-peek/src/main.c | 20 +++ 2 files changed, 76 insertions(+), 67 deletions(-) diff --git a/net/ax-net/src/raw.rs b/net/ax-net/src/raw.rs index ec913c562a..945753a32b 100644 --- a/net/ax-net/src/raw.rs +++ b/net/ax-net/src/raw.rs @@ -143,6 +143,25 @@ impl RawSocket { fn source_matches_peer(&self, source: IpAddress) -> bool { self.peer_addr.read().is_none_or(|peer| source == peer) } + + fn deliver_packet( + &self, + source: IpAddress, + packet: &[u8], + dst: &mut (impl Write + IoBufMut), + options: &mut RecvOptions<'_>, + ) -> AxResult { + if let Some(from) = options.from.as_deref_mut() { + *from = SocketAddrEx::Ip(SocketAddr::new(source.into(), 0)); + } + + let written = dst.write(packet)?; + Ok(if options.flags.contains(RecvFlags::TRUNCATE) { + packet.len() + } else { + written + }) + } } fn is_loopback_address(addr: IpAddress) -> bool { @@ -389,81 +408,51 @@ impl SocketOps for RawSocket { self.general.recv_poller_with(self, extra_nb, || { request_poll(); self.with_smol_socket(|socket| { - loop { - if let Some((source, packet)) = if options.flags.contains(RecvFlags::PEEK) { - self.deferred_rx.lock().clone() - } else { - self.deferred_rx.lock().take() - } { - if !self.source_matches_peer(source) { - *self.deferred_rx.lock() = Some((source, packet)); - return Err(AxError::WouldBlock); - } - - if let Some(from) = options.from.as_deref_mut() { - *from = SocketAddrEx::Ip(SocketAddr::new(source.into(), 0)); - } - - let written = dst.write(&packet)?; - return Ok(if options.flags.contains(RecvFlags::TRUNCATE) { - packet.len() - } else { - written - }); - } - - let loopback_packet = if options.flags.contains(RecvFlags::PEEK) { - self.loopback_rx.lock().clone() - } else { - self.loopback_rx.lock().take() - }; - if let Some((source, packet)) = loopback_packet - && self.source_matches_peer(source) - { - if let Some(from) = options.from.as_deref_mut() { - *from = SocketAddrEx::Ip(SocketAddr::new(source.into(), 0)); - } - - let written = dst.write(&packet)?; - return Ok(if options.flags.contains(RecvFlags::TRUNCATE) { - packet.len() - } else { - written - }); + if let Some((source, packet)) = if options.flags.contains(RecvFlags::PEEK) { + self.deferred_rx.lock().clone() + } else { + self.deferred_rx.lock().take() + } { + if !self.source_matches_peer(source) { + *self.deferred_rx.lock() = Some((source, packet)); + return Err(AxError::WouldBlock); } + let (_, payload) = self.parse_ip_packet(&packet)?; + return self.deliver_packet(source, payload, &mut dst, &mut options); + } - let packet = if options.flags.contains(RecvFlags::PEEK) { - let packet = socket.peek().map_err(|_| AxError::WouldBlock)?; - let (source, _) = self.parse_ip_packet(packet)?; - if let Some(peer) = *self.peer_addr.read() - && source != peer - { - *self.deferred_rx.lock() = Some((source, packet.to_vec())); - return Err(AxError::WouldBlock); - } - packet - } else { - socket.recv().map_err(|_| AxError::WouldBlock)? - }; - let (source, packet) = self.parse_ip_packet(packet)?; - + if let Some((source, packet)) = if options.flags.contains(RecvFlags::PEEK) { + self.loopback_rx.lock().clone() + } else { + self.loopback_rx.lock().take() + } { if !self.source_matches_peer(source) { - *self.deferred_rx.lock() = Some((source, packet.to_vec())); - continue; + *self.loopback_rx.lock() = Some((source, packet)); + return Err(AxError::WouldBlock); } + return self.deliver_packet(source, &packet, &mut dst, &mut options); + } - if let Some(from) = options.from.as_deref_mut() { - *from = SocketAddrEx::Ip(SocketAddr::new(source.into(), 0)); + let wire_packet = if options.flags.contains(RecvFlags::PEEK) { + let packet = socket.peek().map_err(|_| AxError::WouldBlock)?; + let (source, _) = self.parse_ip_packet(packet)?; + if let Some(peer) = *self.peer_addr.read() + && source != peer + { + return Err(AxError::WouldBlock); } + packet + } else { + socket.recv().map_err(|_| AxError::WouldBlock)? + }; + let (source, packet) = self.parse_ip_packet(wire_packet)?; - let written = dst.write(packet)?; - // TODO: set options.truncated when user buffer < packet size. - return Ok(if options.flags.contains(RecvFlags::TRUNCATE) { - packet.len() - } else { - written - }); + if !self.source_matches_peer(source) { + *self.deferred_rx.lock() = Some((source, wire_packet.to_vec())); + return Err(AxError::WouldBlock); } + + self.deliver_packet(source, packet, &mut dst, &mut options) }) }) } diff --git a/test-suit/starryos/qemu-smp1/system/syscall-test-raw-msg-peek/src/main.c b/test-suit/starryos/qemu-smp1/system/syscall-test-raw-msg-peek/src/main.c index 1bdb77d36e..ccb10582e6 100644 --- a/test-suit/starryos/qemu-smp1/system/syscall-test-raw-msg-peek/src/main.c +++ b/test-suit/starryos/qemu-smp1/system/syscall-test-raw-msg-peek/src/main.c @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -43,6 +44,23 @@ static int make_raw_socket(void) { return fd; } +static int wait_readable(int fd, int timeout_ms) { + fd_set rfds; + FD_ZERO(&rfds); + FD_SET(fd, &rfds); + + struct timeval tv = { + .tv_sec = timeout_ms / 1000, + .tv_usec = (timeout_ms % 1000) * 1000, + }; + + int ret; + do { + ret = select(fd + 1, &rfds, NULL, NULL, &tv); + } while (ret < 0 && errno == EINTR); + return ret; +} + static int run_in_child(void (*func)(void)) { pid_t pid = fork(); if (pid < 0) { @@ -136,6 +154,8 @@ int main(void) { send_echo_request(send_fd, actual_peer, ident); + CHECK_RET(wait_readable(recv_fd, 1000), 1, + "receiver becomes readable before MSG_PEEK"); CHECK_ERR(recv(recv_fd, &(unsigned char){0}, 1, MSG_PEEK), EAGAIN, "MSG_PEEK rejects non-peer packet without consuming it"); From e33c055157e66aa641131636553190409b9c4248 Mon Sep 17 00:00:00 2001 From: ZCShou <72115@163.com> Date: Tue, 16 Jun 2026 09:14:26 +0800 Subject: [PATCH 24/31] docs(ax-net): add module-level docs and doc comments across the network stack --- net/ax-net/src/config.rs | 45 ++++++++++ net/ax-net/src/consts.rs | 15 ++++ net/ax-net/src/device/driver.rs | 52 +++++++++++ net/ax-net/src/device/ethernet.rs | 31 +++++++ net/ax-net/src/device/loopback.rs | 15 +++- net/ax-net/src/device/mod.rs | 34 +++++++ net/ax-net/src/device/vsock.rs | 21 ++++- net/ax-net/src/dhcp_server.rs | 45 ++++++---- net/ax-net/src/general.rs | 28 ++++++ net/ax-net/src/lib.rs | 48 ++++++++-- net/ax-net/src/listen_table.rs | 51 +++++++++++ net/ax-net/src/options.rs | 14 +++ net/ax-net/src/orphan.rs | 30 +++++-- net/ax-net/src/raw.rs | 38 ++++++++ net/ax-net/src/router.rs | 100 +++++++++++++++++++++ net/ax-net/src/socket.rs | 20 +++++ net/ax-net/src/state.rs | 21 +++++ net/ax-net/src/tcp.rs | 51 +++++++++++ net/ax-net/src/udp.rs | 39 ++++++++ net/ax-net/src/unix/dgram.rs | 33 +++++++ net/ax-net/src/unix/mod.rs | 26 ++++++ net/ax-net/src/unix/namespace.rs | 13 +++ net/ax-net/src/unix/stream.rs | 31 +++++++ net/ax-net/src/vsock/connection_manager.rs | 52 ++++++++--- net/ax-net/src/vsock/mod.rs | 14 +++ net/ax-net/src/vsock/stream.rs | 24 +++++ net/ax-net/src/wrapper.rs | 37 ++++++++ 27 files changed, 884 insertions(+), 44 deletions(-) diff --git a/net/ax-net/src/config.rs b/net/ax-net/src/config.rs index 23864090ff..4751676650 100644 --- a/net/ax-net/src/config.rs +++ b/net/ax-net/src/config.rs @@ -1,3 +1,21 @@ +//! Network interface and route configuration types. +//! +//! This module is the data model for ax-net's control plane. It is shared by +//! startup configuration, dynamic IPv4 updates, route table replacement, socket +//! device binding, DHCP client/server integration, and userspace interface +//! queries. +//! +//! # Design Notes +//! +//! `InterfaceId` is stable for the lifetime of the stack and is also exported +//! as the Linux ifindex. Route and binding structures refer to this identifier +//! rather than a device vector index so public state survives internal device +//! ordering details. +//! +//! `DeviceBinding` is deliberately small: sockets can bind to an interface, and +//! the service/router layer performs source-address and next-hop selection from +//! the route table. Socket implementations should not duplicate route logic. + use alloc::{string::String, vec::Vec}; use core::net::Ipv4Addr; @@ -70,8 +88,11 @@ pub struct InterfaceInfo { /// Interface matching rule for explicit configuration. #[derive(Debug, Clone)] pub enum InterfaceMatcher { + /// Match the Nth probed Ethernet device. ByOrder(usize), + /// Match a device by its Ethernet MAC address. ByMac(EthernetAddress), + /// Match a device by the name reported by its driver. ByDriverName(String), } @@ -87,58 +108,82 @@ pub struct NetworkConfig { /// Per-interface network configuration. #[derive(Debug, Clone)] pub struct InterfaceConfig { + /// Public interface name, for example `eth0`. pub name: String, + /// Rule used to bind this config to one probed device. pub match_by: InterfaceMatcher, + /// Static IPv4 configuration. Mutually exclusive with DHCP. pub static_ip: Option, + /// Whether DHCP client configuration is enabled. pub dhcp: bool, + /// Route metric used for routes installed from this interface. pub metric: u32, + /// Static DNS servers associated with this interface. pub dns_servers: Vec, } /// Static IP configuration. #[derive(Debug, Clone)] pub struct StaticIpConfig { + /// IPv4 address assigned to the interface. pub ip: Ipv4Addr, + /// CIDR prefix length. pub prefix_len: u8, + /// Default gateway; `0.0.0.0` means no gateway. pub gateway: Ipv4Addr, } /// Runtime IPv4 configuration of a network interface. #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct Ipv4InterfaceConfig { + /// Interface address and prefix. pub address: Ipv4Cidr, + /// Optional default gateway learned or configured for this interface. pub gateway: Option, } /// DNS server origin. #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum DnsSource { + /// Learned from DHCP. Dhcp, + /// Configured on a matching interface. Static, + /// Global fallback DNS server. Fallback, } /// Internal DNS server entry with origin metadata. #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub(crate) struct DnsServerEntry { + /// DNS server address. pub server: Ipv4Address, + /// Interface that owns or should route to this server. pub interface_id: InterfaceId, + /// Route/DNS priority; lower values are preferred. pub metric: u32, + /// Source used for priority and reporting decisions. pub source: DnsSource, } /// Public route snapshot. #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct RouteInfo { + /// Destination prefix. pub filter: smoltcp::wire::IpCidr, + /// Optional gateway/next hop. pub via: Option, + /// Egress interface. pub interface_id: InterfaceId, + /// Source address selected by this route. pub source: smoltcp::wire::IpAddress, + /// Route metric; lower values are preferred. pub metric: u32, } /// Ordinary socket interface binding. #[derive(Debug, Clone, Copy, Default, Eq, PartialEq)] pub struct DeviceBinding { + /// If set, route selection is constrained to this interface. pub bound_if: Option, } diff --git a/net/ax-net/src/consts.rs b/net/ax-net/src/consts.rs index 5c2f74e879..c7d59754ac 100644 --- a/net/ax-net/src/consts.rs +++ b/net/ax-net/src/consts.rs @@ -1,3 +1,18 @@ +//! Shared buffer sizes and queue limits. +//! +//! These constants define ax-net's default memory budget for protocol sockets, +//! smoltcp packet buffers, listen queues, pending ARP packets, and per-device +//! worker queues. They are intentionally centralized so embedded targets can +//! audit memory growth without chasing per-protocol magic numbers. +//! +//! # Sizing Policy +//! +//! The values favor predictable bounded memory over unbounded allocation. Socket +//! buffers are large enough for common POSIX workloads, while router/device +//! queues absorb short scheduling bursts without turning every packet path into +//! a heap allocation site. If a value is raised, consider the total cost across +//! all sockets or all devices, not only the cost of one queue. + pub const STANDARD_MTU: usize = 1500; pub const TCP_RX_BUF_LEN: usize = 64 * 1024; diff --git a/net/ax-net/src/device/driver.rs b/net/ax-net/src/device/driver.rs index e9d715e03f..7d7256a1fc 100644 --- a/net/ax-net/src/device/driver.rs +++ b/net/ax-net/src/device/driver.rs @@ -1,3 +1,23 @@ +//! Driver-facing network device contracts. +//! +//! This module is the boundary between ax-net and low-level NIC drivers. It +//! keeps the protocol stack independent from a concrete transport such as +//! `rd_net` by exposing small RX/TX buffer traits, IRQ readiness flags, and an +//! Ethernet driver trait consumed by higher-level device adapters. +//! +//! # Ownership Model +//! +//! Drivers own their DMA rings or transport queues. ax-net borrows one RX or TX +//! buffer at a time, fills or reads the packet bytes, and then returns control +//! to the driver through transmit/recycle calls. This avoids baking one NIC +//! descriptor model into the protocol stack. +//! +//! # Error Mapping +//! +//! `NetDeviceError` is intentionally small. Device adapters should translate +//! driver-specific failures into retry, bad-state, unsupported, or I/O classes +//! and keep policy decisions such as packet drops at the adapter/router layer. + use alloc::{boxed::Box, collections::VecDeque, string::String, vec::Vec}; use ax_sync::spin::SpinNoIrq; @@ -8,14 +28,21 @@ const ETH_ZLEN: usize = 60; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum NetDeviceError { + /// Operation should be retried later. Again, + /// Device is not in a state that can perform the operation. BadState, + /// Caller supplied an invalid size or argument. InvalidParam, + /// Driver or transport I/O failed. Io, + /// Driver could not allocate required resources. NoMemory, + /// Operation is not supported by this device. Unsupported, } +/// Bitmask of network interrupt events reported by a driver. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct NetIrqEvents(u32); @@ -54,33 +81,57 @@ impl core::ops::BitOrAssign for NetIrqEvents { pub type NetDeviceResult = Result; +/// Receive buffer returned by a low-level driver. pub trait NetRxBuffer: Send { + /// Returns the packet bytes received from the device. fn packet(&self) -> &[u8]; + /// Returns the packet length. fn packet_len(&self) -> usize { self.packet().len() } } +/// Transmit buffer allocated by a low-level driver. pub trait NetTxBuffer: Send { + /// Returns the current packet contents. fn packet(&self) -> &[u8]; + /// Returns writable packet storage. fn packet_mut(&mut self) -> &mut [u8]; + /// Returns the packet length requested at allocation time. fn packet_len(&self) -> usize; } +/// Minimal Ethernet driver contract consumed by [`EthernetDevice`]. +/// +/// Drivers may own DMA rings, MMIO state, or virtual queues internally. ax-net +/// only depends on packet buffers, a transmit/receive entry point, and an IRQ +/// summary so the protocol core stays detached from platform details. pub trait EthernetDriver: Send + Sync { + /// Stable human-readable device name. fn device_name(&self) -> &str; + /// Platform IRQ number, if the device uses the shared Ethernet IRQ path. fn irq_num(&self) -> Option; + /// Enables device IRQ delivery. fn enable_irq(&mut self); + /// Disables device IRQ delivery. fn disable_irq(&mut self); + /// Returns the device MAC address. fn mac_address(&self) -> [u8; 6]; + /// Allocates a TX buffer large enough for one Ethernet frame. fn alloc_tx_buffer(&mut self, size: usize) -> NetDeviceResult>; + /// Reclaims completed TX buffers owned by the driver. fn recycle_tx_buffers(&mut self) -> NetDeviceResult; + /// Submits one filled TX buffer. fn transmit(&mut self, tx_buf: &mut dyn NetTxBuffer) -> NetDeviceResult; + /// Receives one packet, or returns [`NetDeviceError::Again`] when idle. fn receive(&mut self) -> NetDeviceResult>; + /// Returns an RX buffer to the driver. fn recycle_rx_buffer(&mut self, rx_buf: &mut dyn NetRxBuffer) -> NetDeviceResult; + /// Handles a device interrupt and reports wake-relevant events. fn handle_irq(&mut self) -> NetIrqEvents; } +/// List of Ethernet drivers handed to network initialization. pub type EthernetDeviceList = Vec>; struct VecTxBuffer { @@ -134,6 +185,7 @@ pub struct RdNetDriver { } impl RdNetDriver { + /// Wraps an `rd_net` endpoint as an Ethernet driver. pub fn new(name: impl Into, mut net: Net, irq: Option) -> NetDeviceResult { let mac = net.mac_address(); let tx_queue = net.create_tx_queue().map_err(map_net_error)?; diff --git a/net/ax-net/src/device/ethernet.rs b/net/ax-net/src/device/ethernet.rs index d86d432d08..deff4f4ce5 100644 --- a/net/ax-net/src/device/ethernet.rs +++ b/net/ax-net/src/device/ethernet.rs @@ -1,3 +1,25 @@ +//! Ethernet device adapter. +//! +//! The adapter translates between the generic ax-net device contract and +//! Ethernet NIC drivers. It owns neighbor discovery state, emits Ethernet/ARP +//! frames, feeds IP packets into the router RX buffer, and exposes readiness +//! through IRQ or out-of-band wakeups. +//! +//! # Responsibilities +//! +//! - Wrap complete IP packets in Ethernet frames for TX. +//! - Parse inbound Ethernet frames, update ARP state, and deliver IP payloads +//! to the router's RX packet buffer. +//! - Buffer a bounded number of packets while ARP resolution for a next hop is +//! pending. +//! - Bridge platform IRQ registration into device-worker wakeups. +//! +//! # Non-Responsibilities +//! +//! The adapter does not decide which interface should be used for a destination +//! and does not inspect TCP/UDP socket state. Route selection is performed by +//! the router before Ethernet sees the packet. + use alloc::{boxed::Box, string::String, sync::Arc, vec, vec::Vec}; use core::{ptr::NonNull, task::Waker}; @@ -23,6 +45,7 @@ const EMPTY_MAC: EthernetAddress = EthernetAddress([0; 6]); pub trait EthernetIrqRegistration: Send + Sync {} +/// Opaque action installed into a platform IRQ registrar. #[derive(Clone, Copy)] pub struct EthernetIrqAction { data: NonNull<()>, @@ -54,10 +77,13 @@ unsafe impl Sync for EthernetIrqAction {} #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum EthernetIrqOutcome { + /// IRQ was handled and no network worker wakeup is needed. Handled, + /// IRQ indicates network progress; wake the poll path. Wake, } +/// Platform hook used by Ethernet devices that expose a shared IRQ line. pub trait EthernetIrqRegistrar: Send + Sync { fn register_shared( &self, @@ -69,9 +95,13 @@ pub trait EthernetIrqRegistrar: Send + Sync { #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum EthernetIrqRegistrationError { + /// The IRQ number is invalid for this platform. InvalidIrq, + /// The IRQ line cannot be shared or is already occupied. Busy, + /// IRQ registration is not supported on this platform. Unsupported, + /// Other platform-specific registration failure. Other, } @@ -139,6 +169,7 @@ impl EthernetDevice { const NEIGHBOR_TTL: Duration = Duration::from_secs(300); const ARP_REQUEST_RETRY: Duration = Duration::from_secs(1); + /// Creates an Ethernet adapter driven by the shared IRQ/poll path. pub fn new(name: String, inner: Box, ip: Option) -> Self { Self::new_inner(name, inner, ip, false) } diff --git a/net/ax-net/src/device/loopback.rs b/net/ax-net/src/device/loopback.rs index ad9a8c87d3..598bde6954 100644 --- a/net/ax-net/src/device/loopback.rs +++ b/net/ax-net/src/device/loopback.rs @@ -1,3 +1,15 @@ +//! Loopback device marker. +//! +//! Loopback traffic is handled by the router fast path rather than by device +//! workers. This device still exists so the control plane can expose `lo` as a +//! normal interface and route local packets through the same route table. +//! +//! # Fast Path +//! +//! `Router::dispatch()` copies loopback packets directly from the smoltcp TX +//! buffer into the smoltcp-facing RX buffer. That avoids an extra queue hop and +//! avoids spawning RX/TX workers for a device that has no hardware latency. + use core::task::Waker; use smoltcp::{time::Instant, wire::IpAddress}; @@ -34,8 +46,7 @@ impl Device for LoopbackDevice { } fn send(&mut self, _next_hop: IpAddress, _packet: &[u8], _timestamp: Instant) -> bool { - // Fast path: loopback packets are injected directly in Router::dispatch() - // See Router::dispatch_loopback() + // Fast path: loopback packets are injected directly in Router::dispatch(). true } diff --git a/net/ax-net/src/device/mod.rs b/net/ax-net/src/device/mod.rs index bec4023080..369d68c87e 100644 --- a/net/ax-net/src/device/mod.rs +++ b/net/ax-net/src/device/mod.rs @@ -1,3 +1,23 @@ +//! Logical network device abstraction. +//! +//! Device implementations hide physical transport details from the single +//! protocol core. The router polls devices through this trait, while concrete +//! adapters such as Ethernet and loopback decide how packets enter or leave the +//! underlying hardware or virtual link. +//! +//! # Contract +//! +//! `recv()` moves complete IP packets into the caller-provided packet buffer; +//! `send()` accepts complete IP packets plus the already selected next hop. +//! Devices should not perform socket lookup, TCP/UDP processing, or route +//! selection. Those belong above this trait in `service` and `router`. +//! +//! # Readiness +//! +//! A device may use platform IRQs, polling, or out-of-band notifications. The +//! router only requires that `register_waker()` and `wake_rx()` make blocked +//! device workers observable without exposing driver-specific details. + use alloc::{string::String, vec::Vec}; use core::task::Waker; @@ -23,16 +43,27 @@ pub use vsock::*; #[derive(Clone, Debug, Eq, PartialEq)] pub struct ArpEntry { + /// IPv4 address in network byte order. pub ip_addr: [u8; 4], + /// ARP hardware type. pub hw_type: u16, + /// ARP entry flags exposed to userspace. pub flags: u16, + /// Link-layer address. pub hw_addr: [u8; 6], + /// Interface name that owns this neighbor entry. pub device: String, } +/// Packet I/O endpoint behind the multi-device router. pub trait Device: Send + Sync { + /// Human-readable device name used in logs and userspace queries. fn name(&self) -> &str; + /// Moves packets from the device into the shared IP RX buffer. + /// + /// Returns `true` when at least one packet was delivered and the protocol + /// core should be polled again. fn recv( &mut self, interface_id: InterfaceId, @@ -47,8 +78,10 @@ pub trait Device: Send + Sync { /// up packet processing. fn send(&mut self, next_hop: IpAddress, packet: &[u8], timestamp: Instant) -> bool; + /// Updates the IPv4 address used by device-local protocol helpers. fn set_ipv4_addr(&mut self, _addr: Option) {} + /// Returns device-local ARP/neighbor entries for userspace queries. fn arp_entries(&self, _timestamp: Instant) -> Vec { Vec::new() } @@ -60,5 +93,6 @@ pub trait Device: Send + Sync { /// the device after `notify_oob_rx`. Default is a no-op. fn wake_rx(&self) {} + /// Registers a waker for RX readiness notifications. fn register_waker(&self, waker: &Waker); } diff --git a/net/ax-net/src/device/vsock.rs b/net/ax-net/src/device/vsock.rs index 186c9f99ce..f047ad71a4 100644 --- a/net/ax-net/src/device/vsock.rs +++ b/net/ax-net/src/device/vsock.rs @@ -1,3 +1,21 @@ +//! Vsock device polling glue. +//! +//! Vsock is driven outside the smoltcp IP path. This module owns the single +//! registered vsock interface, adapts its event stream into the connection +//! manager, and starts an adaptive poll task while vsock connections exist. +//! +//! # Polling Model +//! +//! The vsock device exposes connection and credit events rather than IP +//! packets. A reference-counted poll task runs only while stream transports are +//! active, backs off when no events are observed, and pushes data into the +//! vsock connection manager's byte rings. +//! +//! # Isolation From IP Stack +//! +//! This code must not acquire smoltcp service/socket locks. Vsock readiness is +//! handled through its own connection manager and socket transport layer. + use alloc::{collections::VecDeque, string::ToString}; use core::{ sync::atomic::{AtomicBool, AtomicU64, Ordering}, @@ -20,7 +38,7 @@ static PENDING_EVENTS: Mutex> = Mutex::new(VecDeque::new()) const VSOCK_RX_TMPBUF_SIZE: usize = 0x1000; // 4KiB buffer for vsock receive -/// Registers a vsock device. Only one vsock device can be registered. +/// Registers the single vsock device used by the system. pub fn register_vsock_device(dev: VsockDevice) -> AxResult { let mut guard = VSOCK_DEVICE.lock(); if guard.is_some() { @@ -88,6 +106,7 @@ pub fn start_vsock_poll() { } } +/// Drops one active-user reference to the adaptive vsock poll task. pub fn stop_vsock_poll() { let mut count = POLL_REF_COUNT.lock(); if *count == 0 { diff --git a/net/ax-net/src/dhcp_server.rs b/net/ax-net/src/dhcp_server.rs index 05489d7a84..9d87ae245d 100644 --- a/net/ax-net/src/dhcp_server.rs +++ b/net/ax-net/src/dhcp_server.rs @@ -1,8 +1,18 @@ -//! 最简 IPv4 DHCP 服务器(用于 SoftAP 模式给单个客户端分配地址)。 +//! Minimal IPv4 DHCP server for SoftAP-style deployments. //! -//! 不依赖 smoltcp 的 DHCP socket,与现有 DHCP 客户端一样手工解析/封装 -//! `DhcpRepr` → `UdpRepr` → `Ipv4Repr`。处理 Discover→Offer、Request→Ack。 -//! 仅支持单客户端、单地址租约,够 AP 验证 ping/ssh 用。 +//! The server is intentionally small: it supports one interface, one client +//! lease, and the Discover/Offer plus Request/Ack exchange needed to bring up a +//! directly attached peer. It does not create a smoltcp UDP socket; instead it +//! parses and emits `DhcpRepr`, `UdpRepr`, and `Ipv4Repr` directly in the +//! service data path. +//! +//! # Scope +//! +//! This is a control-plane helper, not a general-purpose DHCP daemon. It keeps +//! no lease database, performs no conflict detection, and only replies to DHCP +//! packets arriving on the configured interface. That makes it suitable for +//! embedded AP validation such as ping or ssh, while keeping the normal socket +//! path free of DHCP server-specific state. use alloc::{vec, vec::Vec}; @@ -16,26 +26,27 @@ use smoltcp::{ use crate::config::InterfaceId; -/// 租约时长(秒) +/// Lease duration advertised in Offer/Ack replies, in seconds. const LEASE_SECS: u32 = 86400; -/// DHCP 服务器配置/状态。 +/// Minimal DHCP server configuration and one-client lease state. pub struct DhcpServer { - /// 服务器自身 IP(同时作为 gateway / server identifier) + /// Server address, also advertised as router and server identifier. pub server_ip: Ipv4Address, - /// 分配给客户端的 IP + /// Single IPv4 address offered to the client. pub client_ip: Ipv4Address, - /// 子网掩码 + /// Subnet mask advertised to the client. pub subnet_mask: Ipv4Address, - /// 设备索引(回复从该设备广播出去) + /// Router device index used when the service broadcasts replies. pub dev: usize, - /// DHCP 请求必须来自这个接口。 + /// Interface that is allowed to feed requests into this server. interface_id: InterfaceId, - /// 已分配给哪个 MAC(简单单客户端记录) + /// MAC address that accepted the single lease, if any. leased_to: Option, } impl DhcpServer { + /// Creates a DHCP helper bound to one router device and interface. pub fn new( dev: usize, interface_id: InterfaceId, @@ -53,8 +64,10 @@ impl DhcpServer { } } - /// 解析一个入站以太网负载(IPv4 包)。若是发给本服务器的 DHCP - /// Discover/Request,返回要广播回去的完整 IPv4 应答包字节。 + /// Processes one inbound IPv4 packet and returns a broadcast DHCP reply. + /// + /// Non-DHCP traffic, unsupported DHCP message types, or packets from other + /// interfaces are ignored by returning `None`. pub fn process_packet(&mut self, interface_id: InterfaceId, packet: &[u8]) -> Option> { if interface_id != self.interface_id { return None; @@ -74,7 +87,7 @@ impl DhcpServer { &ChecksumCapabilities::default(), ) .ok()?; - // 客户端 → 服务器:src=68, dst=67 + // Client -> server uses UDP src=68, dst=67. if udp_repr.src_port != DHCP_CLIENT_PORT || udp_repr.dst_port != DHCP_SERVER_PORT { return None; } @@ -107,7 +120,7 @@ impl DhcpServer { Some(self.build_reply(client_mac, xid, reply_type)) } - /// 构造 Offer/Ack 应答(完整 IPv4 包,广播)。 + /// Builds a complete IPv4 packet containing a DHCP Offer/Ack reply. fn build_reply( &self, client_mac: EthernetAddress, diff --git a/net/ax-net/src/general.rs b/net/ax-net/src/general.rs index cbecc4a9dc..63307a93a7 100644 --- a/net/ax-net/src/general.rs +++ b/net/ax-net/src/general.rs @@ -1,3 +1,19 @@ +//! Shared socket options and blocking helpers. +//! +//! Protocol-specific sockets embed `GeneralOptions` for common POSIX socket +//! state such as nonblocking mode, reuse-address, timeouts, socket identity, and +//! device binding. Keeping these fields here avoids duplicating subtly +//! different getsockopt/setsockopt behavior in TCP, UDP, raw, Unix, and vsock +//! transports. +//! +//! # Blocking Semantics +//! +//! The helpers in this module bridge poll-based readiness with synchronous +//! socket operations. They should only wait on protocol-specific pollers and +//! must not drive the smoltcp interface directly. Progress is requested through +//! the net-poll worker so application threads do not become temporary protocol +//! stack owners. + use core::{ sync::atomic::{AtomicBool, AtomicI32, AtomicU32, AtomicU64, Ordering}, task::Waker, @@ -21,9 +37,12 @@ pub(crate) struct GeneralOptions { /// Whether the socket should reuse the address. reuse_address: AtomicBool, + /// Per-socket send timeout in nanoseconds; zero means no timeout. send_timeout_nanos: AtomicU64, + /// Per-socket receive timeout in nanoseconds; zero means no timeout. recv_timeout_nanos: AtomicU64, + /// Bound interface id encoded as zero for "not bound". bound_if: AtomicU32, /// Socket type: SOCK_STREAM (1), SOCK_DGRAM (2), SOCK_RAW (3). @@ -54,24 +73,29 @@ impl GeneralOptions { } } + /// Returns whether this socket is in non-blocking mode. pub fn nonblocking(&self) -> bool { self.nonblock.load(Ordering::Relaxed) } + /// Returns whether SO_REUSEADDR-style bind reuse is enabled. pub fn reuse_address(&self) -> bool { self.reuse_address.load(Ordering::Relaxed) } + /// Returns the configured send timeout, or `None` for blocking forever. pub fn send_timeout(&self) -> Option { let nanos = self.send_timeout_nanos.load(Ordering::Relaxed); (nanos > 0).then(|| Duration::from_nanos(nanos)) } + /// Returns the configured receive timeout, or `None` for blocking forever. pub fn recv_timeout(&self) -> Option { let nanos = self.recv_timeout_nanos.load(Ordering::Relaxed); (nanos > 0).then(|| Duration::from_nanos(nanos)) } + /// Updates the interface binding used by route selection. pub fn set_device_binding(&self, binding: DeviceBinding) { self.bound_if.store( binding.bound_if.map_or(0, InterfaceId::get), @@ -79,6 +103,7 @@ impl GeneralOptions { ); } + /// Returns the current interface binding. pub fn device_binding(&self) -> DeviceBinding { let raw = self.bound_if.load(Ordering::Acquire); DeviceBinding { @@ -86,10 +111,12 @@ impl GeneralOptions { } } + /// Registers a waker with the service/device path for the bound interface. pub fn register_waker(&self, waker: &Waker) { get_service().register_waker(self.device_binding(), waker); } + /// Runs a send operation through the standard blocking/nonblocking poller. pub fn send_poller AxResult, T>( &self, pollable: &P, @@ -98,6 +125,7 @@ impl GeneralOptions { self.send_poller_with(pollable, false, f) } + /// Runs a receive operation through the standard blocking/nonblocking poller. pub fn recv_poller AxResult, T>( &self, pollable: &P, diff --git a/net/ax-net/src/lib.rs b/net/ax-net/src/lib.rs index 984d6d6118..1a8b283209 100644 --- a/net/ax-net/src/lib.rs +++ b/net/ax-net/src/lib.rs @@ -1,17 +1,33 @@ //! Unified network stack for TGOSKits systems. //! -//! It provides TCP, UDP, raw IPv4/ICMP, Unix domain socket, optional vsock, -//! DNS, DHCP, and readiness primitives on top of [smoltcp] and shared device -//! interfaces. +//! ax-net provides the socket-facing API used by kernels and syscall layers, +//! while delegating TCP/IP protocol mechanics to smoltcp. The crate exposes +//! TCP, UDP, raw IPv4/IPv6 sockets, Unix domain sockets, optional vsock, DNS, +//! DHCP helpers, readiness polling, and interface/control-plane queries. //! -//! # Organization +//! # Architecture //! -//! - [`tcp::TcpSocket`]: TCP socket implementation. -//! - [`udp::UdpSocket`]: UDP socket implementation. -//! - [`raw`]: raw socket support. -//! - [`unix`]: Unix domain socket support. +//! The stack intentionally uses one smoltcp `Interface` and one global +//! `SocketSet`. Multiple physical or virtual devices are aggregated below that +//! protocol core by `router::Router`, which acts as a multi-device smoltcp +//! `Device`. This keeps socket ownership, port tables, listen queues, and +//! routing decisions centralized instead of duplicating socket state per NIC. //! -//! [smoltcp]: https://github.com/smoltcp-rs/smoltcp +//! # Polling Model +//! +//! Protocol progress is driven by the dedicated net-poll worker. Socket methods +//! request progress with `request_poll()` and then rely on poll/waker readiness; +//! they must not synchronously drive the whole protocol stack from application +//! hot paths. This preserves the single-owner smoltcp model and avoids lock +//! re-entry between socket operations and interface polling. +//! +//! # Main Modules +//! +//! - `service`: owns the smoltcp interface, net-poll flow, and control plane. +//! - `router`: aggregates devices, route lookup, loopback, and packet queues. +//! - `socket`, `tcp`, `udp`, `raw`: POSIX-like IP socket surface. +//! - `listen_table`, `orphan`, `wrapper`: side tables around smoltcp sockets. +//! - `unix` and `vsock`: local transports outside the smoltcp IP path. #![no_std] @@ -420,26 +436,32 @@ pub fn request_poll() { NET_POLL_WAKE.notify_one(true); } +/// Returns ARP/neighbor entries collected from all devices. pub fn arp_entries() -> Vec { get_service().arp_entries() } +/// Returns a snapshot of all configured network interfaces. pub fn interfaces() -> Vec { get_control().interfaces() } +/// Looks up an interface snapshot by name. pub fn interface_by_name(name: &str) -> Option { get_control().interface_by_name(name) } +/// Looks up an interface snapshot by stable interface id. pub fn interface_by_id(id: InterfaceId) -> Option { get_control().interface_by_id(id) } +/// Returns the IPv4 configuration for an interface by name. pub fn ipv4_config(name: &str) -> Option { get_control().ipv4_config(name) } +/// Returns public snapshots of configured IPv4 default routes. pub fn default_routes() -> Vec { get_control().default_routes() } @@ -449,10 +471,15 @@ pub fn default_routes() -> Vec { /// This is used by drivers that appear after the normal device-probe phase, /// for example Wi-Fi AP mode devices. pub struct NetConfig { + /// Name assigned to the dynamically registered interface. pub name: String, + /// Static IPv4 address. pub ip: [u8; 4], + /// CIDR prefix length. pub prefix_len: u8, + /// If set, enables the built-in one-client DHCP server with this client IP. pub dhcp_server_client_ip: Option<[u8; 4]>, + /// Whether this device is woken through the out-of-band poll task. pub dedicated_poll: bool, } @@ -495,6 +522,7 @@ pub fn notify_oob_rx() { OOB_RX_SIGNAL.wake(); } +/// Convenience helper for retrieving `eth0` IPv4 configuration. pub fn eth0_ipv4_config() -> Option { get_service().eth0_ipv4_config() } @@ -577,10 +605,12 @@ pub fn dns_servers() -> Vec { const DNS_DEFAULT_TIMEOUT: Duration = Duration::from_secs(5); +/// Resolves an A record using the default DNS timeout. pub fn dns_query(name: &str) -> AxResult> { dns_query_timeout(name, DNS_DEFAULT_TIMEOUT) } +/// Resolves an A record using the configured DNS servers and timeout. pub fn dns_query_timeout(name: &str, timeout: Duration) -> AxResult> { let servers = dns_servers(); if servers.is_empty() { diff --git a/net/ax-net/src/listen_table.rs b/net/ax-net/src/listen_table.rs index 1d326b2a21..62f144d397 100644 --- a/net/ax-net/src/listen_table.rs +++ b/net/ax-net/src/listen_table.rs @@ -1,3 +1,30 @@ +//! TCP listen table and accept backlog management. +//! +//! smoltcp sockets are connection endpoints, so ax-net keeps a listener table +//! that maps bound listen endpoints to pending child sockets. Incoming TCP +//! packets create per-connection smoltcp sockets, queue them for accept(), and +//! wake the listener when a child becomes ready. +//! +//! # Why Not One smoltcp Listener Socket +//! +//! The public TCP listen socket is a stable userspace object, but smoltcp needs +//! an actual TCP socket to advance each handshake. This table bridges that +//! mismatch: the listener owns an accept queue, while each pending flow owns a +//! child smoltcp socket that can move through SYN-RECEIVED to ESTABLISHED. +//! +//! # Address Semantics +//! +//! Bind conflicts follow Linux-style wildcard behavior. A wildcard listener +//! conflicts with every specific address on the same port, while two distinct +//! specific addresses may share a port. Incoming packets are matched by port and +//! local destination address before a child is created. +//! +//! # Lock Ordering +//! +//! Callers that inspect child socket state pass a locked `SocketSet` into this +//! module. The required order is `SOCKET_SET -> listen-table bucket`; this file +//! must never acquire the outer service lock. + use alloc::{boxed::Box, collections::VecDeque, sync::Arc, vec, vec::Vec}; use core::task::Waker; @@ -18,16 +45,24 @@ use crate::{ const PORT_NUM: usize = 65536; struct ListenTableEntryInner { + /// Local endpoint accepted by this listener. listen_endpoint: IpListenEndpoint, + /// Maximum pending child sockets. backlog: usize, + /// Pending smoltcp child sockets waiting for accept(). syn_queue: VecDeque, + /// Wakes accept/poll waiters when child readiness changes. accept_poll: Arc, } +/// Child TCP socket returned by accept(). #[derive(Clone, Copy)] pub(crate) struct AcceptedTcp { + /// smoltcp child socket handle. pub(crate) handle: SocketHandle, + /// Local endpoint observed for this connection. pub(crate) local_endpoint: IpEndpoint, + /// Remote endpoint observed for this connection. pub(crate) remote_endpoint: IpEndpoint, } @@ -37,6 +72,7 @@ struct PendingTcp { } impl ListenTableEntryInner { + /// Creates a listener entry and clamps backlog to the global limit. pub fn new(listen_endpoint: IpListenEndpoint, backlog: usize) -> Self { let backlog = backlog.clamp(1, LISTEN_QUEUE_SIZE); Self { @@ -47,6 +83,7 @@ impl ListenTableEntryInner { } } + /// Returns whether an incoming packet's destination matches this listener. fn can_accept_endpoint(&self, dst: IpEndpoint) -> bool { if self.listen_endpoint.port != dst.port { return false; @@ -57,6 +94,7 @@ impl ListenTableEntryInner { } } + /// Consumes the entry and returns all queued child handles for cleanup. fn into_handles(self) -> Vec { self.syn_queue .into_iter() @@ -64,6 +102,7 @@ impl ListenTableEntryInner { .collect() } + /// Returns whether a child socket for this endpoint pair already exists. fn has_pending(&self, src: IpEndpoint, dst: IpEndpoint) -> bool { self.syn_queue.iter().any(|pending| { pending.accepted.local_endpoint == dst && pending.accepted.remote_endpoint == src @@ -73,11 +112,13 @@ impl ListenTableEntryInner { type ListenTableEntry = Arc>>; +/// Per-port table of active TCP listeners. pub struct ListenTable { tcp: Box<[ListenTableEntry]>, } impl ListenTable { + /// Creates an empty listen table indexed by TCP port. pub fn new() -> Self { let tcp = unsafe { let mut buf = Box::new_uninit_slice(PORT_NUM); @@ -89,6 +130,7 @@ impl ListenTable { Self { tcp } } + /// Checks whether a listen endpoint can be registered. pub fn can_listen(&self, listen_endpoint: IpListenEndpoint) -> bool { self.tcp[listen_endpoint.port as usize] .lock() @@ -96,6 +138,7 @@ impl ListenTable { .all(|entry| !listen_addrs_conflict(entry.listen_endpoint.addr, listen_endpoint.addr)) } + /// Registers a listening endpoint and backlog. pub fn listen(&self, listen_endpoint: IpListenEndpoint, backlog: usize) -> AxResult { let port = listen_endpoint.port; assert_ne!(port, 0); @@ -111,6 +154,7 @@ impl ListenTable { Ok(()) } + /// Removes a listener and destroys any unaccepted child sockets. pub fn unlisten(&self, listen_endpoint: IpListenEndpoint) { debug!("TCP socket unlisten on {}", listen_endpoint); let handles = { @@ -134,6 +178,7 @@ impl ListenTable { // Callers pass the locked SocketSet to keep the global order: // SERVICE -> SOCKET_SET -> listen entry. + /// Returns whether accept() can return a ready child socket. pub fn can_accept( &self, listen_endpoint: IpListenEndpoint, @@ -156,6 +201,7 @@ impl ListenTable { } } + /// Removes and returns one acceptable child socket from the listen queue. pub fn accept( &self, listen_endpoint: IpListenEndpoint, @@ -195,6 +241,7 @@ impl ListenTable { Err(AxError::WouldBlock) } + /// Registers a waker for listener readiness and queued child progress. pub fn register_accept_waker( &self, listen_endpoint: IpListenEndpoint, @@ -217,6 +264,7 @@ impl ListenTable { } } + /// Snoop hook called before smoltcp processes a potential passive open. pub fn incoming_tcp_packet( &self, src: IpEndpoint, @@ -238,6 +286,9 @@ impl ListenTable { return; } + // The listening socket remains a userspace-facing object. Each new + // flow gets a child smoltcp socket so the protocol core can advance + // the handshake independently before accept() returns it. let mut socket = smoltcp::socket::tcp::Socket::new( SocketBuffer::new(vec![0; TCP_RX_BUF_LEN]), SocketBuffer::new(vec![0; TCP_TX_BUF_LEN]), diff --git a/net/ax-net/src/options.rs b/net/ax-net/src/options.rs index 43ae421823..3c581e4e4d 100644 --- a/net/ax-net/src/options.rs +++ b/net/ax-net/src/options.rs @@ -1,3 +1,17 @@ +//! Socket option data structures and dispatch traits. +//! +//! This module defines Linux-compatible option payloads plus the `Configurable` +//! trait used by each socket implementation to handle getsockopt/setsockopt. +//! The goal is to keep the syscall layer protocol-neutral: it builds one option +//! request enum, then the concrete socket decides which values it supports. +//! +//! # Compatibility Boundary +//! +//! Option structs model Linux-visible ABI state, but not every field maps to a +//! smoltcp feature. Unsupported or synthetic fields should be filled +//! conservatively in the socket implementation, with defaults documented near +//! the protocol that reports them. + use alloc::boxed::Box; use core::time::Duration; diff --git a/net/ax-net/src/orphan.rs b/net/ax-net/src/orphan.rs index fcfb7f76e9..d58a17955f 100644 --- a/net/ax-net/src/orphan.rs +++ b/net/ax-net/src/orphan.rs @@ -1,12 +1,26 @@ //! Orphan socket management for TCP connections. //! -//! When a user closes a TCP socket (via Drop), the socket enters the orphan state: -//! - Unbound from the user-facing API (handle not accessible to new operations) -//! - smoltcp socket handle remains active in the protocol stack -//! - Connection teardown (FIN handshake, TIME_WAIT) continues in background -//! - Removed after smoltcp reaches Closed, or after TIME_WAIT/max linger expires +//! When a user closes a TCP socket, the userspace object is dropped immediately, +//! but the underlying smoltcp socket may still need to finish FIN exchange or +//! TIME-WAIT. This module keeps those sockets in a small orphan pool so the +//! dedicated net-poll worker can continue protocol teardown after the file +//! descriptor is gone. //! -//! This ensures RFC 793 compliance and prevents premature connection termination. +//! # Lifecycle +//! +//! - `TcpSocket::drop()` unregisters public bind/listen state and moves the +//! smoltcp handle into the orphan pool. +//! - `reap_orphans()` runs from the poll path while `SocketSet` is already +//! locked. +//! - Closed sockets are removed immediately; TIME-WAIT and FIN states are kept +//! until smoltcp finishes or the maximum linger time expires. +//! +//! # Overflow Policy +//! +//! The pool has a hard capacity guard, but it does not blindly kill active +//! TIME-WAIT/FIN sockets just because the pool is full. Closed or expired +//! entries are reaped first; still-tearing-down entries are preserved so normal +//! TCP semantics win over aggressive cleanup. use alloc::vec::Vec; @@ -20,7 +34,9 @@ use spin::LazyLock; /// Orphaned TCP socket awaiting final cleanup. struct OrphanSocket { + /// smoltcp socket handle kept alive after the public socket was dropped. handle: SocketHandle, + /// Timestamp when the socket entered the orphan pool. orphaned_at: Instant, } @@ -36,7 +52,9 @@ impl OrphanSocket { #[derive(Clone, Copy)] enum ReapReason { + /// smoltcp reached the Closed state. Closed, + /// The orphan exceeded the maximum linger time. Expired, } diff --git a/net/ax-net/src/raw.rs b/net/ax-net/src/raw.rs index 945753a32b..6aa6c9e5be 100644 --- a/net/ax-net/src/raw.rs +++ b/net/ax-net/src/raw.rs @@ -3,6 +3,25 @@ // See LICENSES for license details. //! Raw IP socket implementation for ICMP-style traffic. +//! +//! Raw sockets expose packet-oriented access above IP and below TCP/UDP. They +//! are primarily used by ICMP/ICMPv6 tests and tools, but still share the same +//! global smoltcp `SocketSet`, route selection, device binding, and readiness +//! model as UDP/TCP sockets. +//! +//! # Packet Format +//! +//! smoltcp raw sockets receive complete IP packets. The public raw socket API +//! returns protocol payloads for normal IPv4/IPv6 raw sockets while preserving +//! enough packet context for peer filtering and `MSG_PEEK`. Deferred packets +//! must therefore be stored in a consistent wire-packet form until delivery is +//! decided. +//! +//! # Loopback And Peer Filtering +//! +//! Loopback ICMP-style traffic may be delivered through a local fast path. For +//! connected raw sockets, packets from other peers can be skipped or deferred +//! without corrupting the smoltcp receive queue format. use alloc::vec; use core::{ @@ -34,6 +53,7 @@ use crate::{ request_poll, }; +/// Allocates a smoltcp raw socket for one IP version and protocol. pub(crate) fn new_raw_socket( ip_version: IpVersion, ip_protocol: IpProtocol, @@ -48,15 +68,25 @@ pub(crate) fn new_raw_socket( /// A raw IP socket used for ICMP and ICMPv6 traffic. pub struct RawSocket { + /// Handle into the global smoltcp socket set. handle: SocketHandle, + /// IP version accepted by this socket. ip_version: IpVersion, + /// Optional local address filter. local_addr: RwLock>, + /// Optional connected peer filter. peer_addr: RwLock>, + /// Locally generated loopback packet waiting to be received. loopback_rx: Mutex)>>, + /// Non-peer packet held after filtering without corrupting wire format. deferred_rx: Mutex)>>, + /// Optional outgoing TTL/hop-limit override. ttl: RwLock>, + /// Public read-half closed state. rx_closed: AtomicBool, + /// Public write-half closed state. tx_closed: AtomicBool, + /// Shared socket options and blocking helpers. general: GeneralOptions, } @@ -80,6 +110,7 @@ impl RawSocket { } } + /// Restricts this socket to one interface for route selection. pub fn bind_device(&self, interface_id: InterfaceId) -> AxResult { if interface_by_id(interface_id).is_none() { return Err(AxError::NoSuchDevice); @@ -90,10 +121,12 @@ impl RawSocket { Ok(()) } + /// Borrows the underlying smoltcp raw socket by handle. fn with_smol_socket(&self, f: impl FnOnce(&mut smol::Socket) -> R) -> R { SOCKET_SET.with_socket_mut::(self.handle, f) } + /// Validates that an address belongs to this socket's IP version. fn check_ip_version(&self, addr: IpAddress) -> AxResult { match (self.ip_version, addr) { (IpVersion::Ipv4, IpAddress::Ipv4(_)) | (IpVersion::Ipv6, IpAddress::Ipv6(_)) => { @@ -103,6 +136,7 @@ impl RawSocket { } } + /// Resolves the per-call or connected remote address. fn remote_address(&self, options: &SendOptions) -> AxResult { match &options.to { Some(addr) => { @@ -113,6 +147,7 @@ impl RawSocket { } } + /// Selects the local source address used for an outgoing raw packet. fn local_address_for(&self, remote: IpAddress) -> AxResult { if let Some(local) = *self.local_addr.read() { return Ok(local); @@ -125,6 +160,7 @@ impl RawSocket { .source) } + /// Parses a complete IP packet and returns its source plus deliverable bytes. fn parse_ip_packet<'a>(&self, packet: &'a [u8]) -> AxResult<(IpAddress, &'a [u8])> { match self.ip_version { IpVersion::Ipv4 => { @@ -140,10 +176,12 @@ impl RawSocket { } } + /// Returns whether a received source passes the connected-peer filter. fn source_matches_peer(&self, source: IpAddress) -> bool { self.peer_addr.read().is_none_or(|peer| source == peer) } + /// Delivers one parsed raw packet to the caller's receive buffer. fn deliver_packet( &self, source: IpAddress, diff --git a/net/ax-net/src/router.rs b/net/ax-net/src/router.rs index 44551e33f2..97efd5d8d5 100644 --- a/net/ax-net/src/router.rs +++ b/net/ax-net/src/router.rs @@ -1,3 +1,37 @@ +//! Multi-device router used as the single smoltcp device. +//! +//! ax-net exposes one smoltcp `Interface` and one global `SocketSet`, then +//! places this router underneath as a virtual device that aggregates all +//! physical and virtual links. From smoltcp's perspective this module is a +//! single `Device`; internally it performs route lookup, source-address +//! selection, loopback delivery, and handoff to per-device workers. +//! +//! # Why This Exists +//! +//! smoltcp sockets are owned by one interface. Creating one interface per NIC +//! would split socket handle spaces, make wildcard listen sockets hard to keep +//! coherent, and push routing decisions up into applications. This router keeps +//! the protocol core single-owner while still allowing multiple interfaces and +//! route metrics. +//! +//! # Data Paths +//! +//! - RX workers poll real devices and enqueue `RxPacket`s into a bounded shared +//! RX queue. `Router::poll()` drains that queue into the smoltcp-facing packet +//! buffer. +//! - smoltcp TX writes into `tx_buffer`. `Router::dispatch()` parses the IP +//! destination, selects a route, and enqueues the packet to the chosen device +//! worker. +//! - Loopback bypasses workers and the shared RX queue: dispatch copies directly +//! from TX buffer to RX buffer and asks the protocol core to poll again. +//! +//! # Concurrency Rules +//! +//! Device workers only touch their `DeviceHandle` queues and concrete device +//! locks. Route lookup uses the shared route table read lock. Socket and service +//! locks are owned by the poll path, so worker threads must not call back into +//! socket operations. + use alloc::{ boxed::Box, collections::VecDeque, @@ -37,16 +71,24 @@ use crate::{ #[derive(Debug)] pub struct Rule { + /// Destination prefix matched by this route. pub filter: IpCidr, + /// Optional gateway. `None` means the destination is directly reachable. pub via: Option, + /// Index into `Router::devices`. pub dev: usize, + /// Stable public interface id. pub interface_id: InterfaceId, + /// Source address selected when this route is used. pub src: IpAddress, + /// Route metric; lower values win for equal prefix lengths. pub metric: u32, + /// Insertion order used as a stable tie-breaker. pub order: u64, } impl Rule { + /// Creates a route rule before insertion order is assigned. pub fn new( filter: IpCidr, via: Option, @@ -82,6 +124,7 @@ type PacketBuffer = smoltcp::storage::PacketBuffer<'static, InterfaceId>; // egress interface from the packet destination and route table. const TX_INTERFACE_PLACEHOLDER: InterfaceId = InterfaceId::new(0); +/// Bounded FIFO used between the protocol core and per-device workers. struct BoundedPacketQueue { inner: Mutex>, capacity: usize, @@ -120,12 +163,16 @@ impl BoundedPacketQueue { } struct TxPacket { + /// Next-hop IP selected by the route table. next_hop: IpAddress, + /// Complete IP packet to transmit. bytes: QueuedPacket, } struct RxPacket { + /// Interface that received the packet. interface_id: InterfaceId, + /// Complete IP packet received from a device. bytes: QueuedPacket, } @@ -157,17 +204,27 @@ impl QueuedPacket { } struct RouterQueues { + /// Shared RX queue filled by device workers and drained by `Router::poll`. rx: Arc>, } +/// Runtime handle for one physical or virtual device. struct DeviceHandle { + /// Stable interface id exposed to the control plane. interface_id: InterfaceId, + /// Device name used for logs and userspace queries. name: String, + /// Concrete device implementation. inner: Arc>>, + /// Shared router RX queue. rx_queue: Arc>, + /// Per-device TX queue. tx_queue: Arc>, + /// Wait queue used by the RX worker. rx_wake: Arc, + /// Wait queue used by the TX worker. tx_wake: Arc, + /// Waker registered into the concrete device. rx_waker: Waker, } @@ -237,18 +294,25 @@ fn now() -> Instant { #[derive(Debug, Clone, Copy)] pub struct RouteDecision { + /// Selected router device index. pub dev: usize, + /// Selected public interface id. pub interface_id: InterfaceId, + /// Source address that should be used for this route. pub source: IpAddress, + /// Next hop to pass to the device. pub next_hop: IpAddress, + /// Metric of the selected route. pub metric: u32, } +/// Route table sorted by longest prefix, then metric, then insertion order. pub struct RouteTable { rules: Vec, next_order: u64, } impl RouteTable { + /// Creates an empty route table. pub fn new() -> Self { Self { rules: Vec::new(), @@ -256,6 +320,7 @@ impl RouteTable { } } + /// Adds one route and re-sorts according to lookup priority. pub fn add_rule(&mut self, mut rule: Rule) { rule.order = self.next_order; self.next_order = self.next_order.saturating_add(1); @@ -273,6 +338,7 @@ impl RouteTable { }); } + /// Selects the best route to `dst` whose interface passes `is_usable`. pub fn select_route_if( &self, dst: &IpAddress, @@ -290,6 +356,7 @@ impl RouteTable { }) } + /// Selects the best route to `dst` that preserves an already chosen source. pub fn select_route_for_source( &self, dst: &IpAddress, @@ -307,6 +374,7 @@ impl RouteTable { }) } + /// Returns public snapshots of IPv4 default routes. pub fn default_routes(&self) -> Vec { self.rules .iter() @@ -320,6 +388,7 @@ impl RouteTable { .collect() } + /// Removes IPv4 routes owned by one interface. pub fn remove_ipv4_rules_for_interface(&mut self, interface_id: InterfaceId) { self.rules.retain(|rule| { !matches!( @@ -329,6 +398,7 @@ impl RouteTable { }); } + /// Atomically replaces IPv4 routes owned by one interface. pub fn replace_ipv4_rules_for_interface( &mut self, interface_id: InterfaceId, @@ -346,6 +416,7 @@ impl RouteTable { pub(crate) type SharedRouteTable = Arc>; +/// Virtual smoltcp device that multiplexes all concrete devices. pub struct Router { rx_buffer: PacketBuffer, tx_buffer: PacketBuffer, @@ -354,6 +425,7 @@ pub struct Router { table: SharedRouteTable, } impl Router { + /// Creates the virtual multi-device endpoint used by smoltcp. pub fn new(table: SharedRouteTable) -> Self { let rx_buffer = PacketBuffer::new( vec![PacketMetadata::EMPTY; SOCKET_BUFFER_SIZE], @@ -375,20 +447,24 @@ impl Router { } } + /// Adds a route to the shared route table. pub fn add_rule(&mut self, rule: Rule) { self.table.write().add_rule(rule); } + /// Registers a concrete device and returns its router device index. pub fn add_device(&mut self, interface_id: InterfaceId, device: Box) -> usize { self.devices .push(DeviceHandle::new(interface_id, device, &self.queues)); self.devices.len() - 1 } + /// Returns the public interface id for a router device index. pub fn interface_id_for_dev(&self, dev: usize) -> Option { self.devices.get(dev).map(|device| device.interface_id) } + /// Returns names of all registered devices. pub fn device_names(&self) -> Vec { self.devices .iter() @@ -396,18 +472,21 @@ impl Router { .collect() } + /// Starts TX workers for all non-loopback devices. pub fn start_tx_workers(&self) { for dev in 0..self.devices.len() { self.start_device_tx_worker(dev); } } + /// Starts RX workers for all non-loopback devices. pub fn start_rx_workers(&self) { for dev in 0..self.devices.len() { self.start_device_rx_worker(dev); } } + /// Starts RX/TX workers for one dynamically registered device. pub fn start_device_workers(&self, dev: usize) { self.start_device_rx_worker(dev); self.start_device_tx_worker(dev); @@ -439,6 +518,7 @@ impl Router { ax_task::spawn_with_name(move || device_rx_worker(device), name); } + /// Applies an IPv4 address/gateway update to one device and its routes. pub fn set_ipv4_config( &mut self, dev: usize, @@ -453,6 +533,7 @@ impl Router { .replace_ipv4_rules_for_interface(interface_id, new_rules); } + /// Builds the connected and default IPv4 route rules for one interface. pub(crate) fn ipv4_rules( &mut self, dev: usize, @@ -487,12 +568,15 @@ impl Router { rules } + /// Moves device-produced packets into the smoltcp RX buffer. pub fn poll( &mut self, _timestamp: Instant, sockets: &mut SocketSet<'_>, mut snoop: impl FnMut(InterfaceId, &[u8]), ) { + // Drain worker-produced packets into the smoltcp-facing RX buffer. + // smoltcp later consumes this buffer through Device::receive(). while !self.rx_buffer.is_full() { let Some(packet) = self.queues.rx.pop() else { break; @@ -508,6 +592,7 @@ impl Router { } } + /// Sends a control-plane packet on a specific device. pub fn send_on_device( &mut self, dev: usize, @@ -522,6 +607,7 @@ impl Router { device.enqueue_tx(next_hop, packet) } + /// Collects ARP/neighbor entries from all devices. pub fn arp_entries(&self, timestamp: Instant) -> Vec { let mut entries = Vec::new(); for device in &self.devices { @@ -530,6 +616,7 @@ impl Router { entries } + /// Registers a global device-readiness waker for all devices. pub fn register_device_waker(&self, waker: &core::task::Waker) { for device in &self.devices { device.inner.lock().register_waker(&device.rx_waker); @@ -537,6 +624,7 @@ impl Router { } } + /// Forces all device RX workers to re-check their devices. pub fn wake_all_devices(&self) { for device in &self.devices { device.inner.lock().wake_rx(); @@ -544,6 +632,7 @@ impl Router { } } + /// Registers a waker for devices allowed by a socket's binding. pub fn register_waker(&self, binding: DeviceBinding, waker: &core::task::Waker) { for device in &self.devices { if binding.bound_if.is_none_or(|id| id == device.interface_id) { @@ -553,6 +642,7 @@ impl Router { } } + /// Routes smoltcp-emitted TX packets to loopback or device workers. pub fn dispatch(&mut self, _timestamp: Instant, sockets: &mut SocketSet<'_>) -> bool { let mut poll_next = false; while let Ok((_, packet)) = self.tx_buffer.dequeue() { @@ -583,6 +673,10 @@ impl Router { let dev = &self.devices[route.dev]; if dev.interface_id == InterfaceId::LOOPBACK { + // Loopback packets are copied directly from the TX + // buffer into the RX buffer. This avoids the + // per-device worker and shared RX queue used by + // real devices. poll_next |= inject_loopback_rx_direct( &mut self.rx_buffer, dst_addr, @@ -637,6 +731,7 @@ impl Router { } } +/// Injects a loopback packet directly into the smoltcp-facing RX buffer. fn inject_loopback_rx_direct( rx_buffer: &mut PacketBuffer, dst_addr: IpAddress, @@ -680,6 +775,7 @@ fn inject_loopback_rx( true } +/// Dedicated worker that drains one device's TX queue. fn device_tx_worker(device: Arc) { loop { if let Some(packet) = device.tx_queue.pop() { @@ -697,6 +793,7 @@ fn device_tx_worker(device: Arc) { } } +/// Dedicated worker that polls one device and forwards packets to router RX. fn device_rx_worker(device: Arc) { let mut rx_buffer = PacketBuffer::new(vec![PacketMetadata::EMPTY; 1], vec![0u8; STANDARD_MTU]); @@ -742,6 +839,7 @@ fn device_rx_worker(device: Arc) { } } +/// smoltcp TX token backed by the router's temporary TX buffer. pub struct TxToken<'a>(&'a mut PacketBuffer); impl smoltcp::phy::TxToken for TxToken<'_> { @@ -758,6 +856,7 @@ impl smoltcp::phy::TxToken for TxToken<'_> { } } +/// Detects passive TCP opens before smoltcp consumes the incoming packet. fn snoop_tcp_packet(buf: &[u8], sockets: &mut SocketSet<'_>) { let (protocol, src_addr, dst_addr, payload) = match IpVersion::of_packet(buf).unwrap() { IpVersion::Ipv4 => { @@ -790,6 +889,7 @@ fn snoop_tcp_packet(buf: &[u8], sockets: &mut SocketSet<'_>) { } } +/// smoltcp RX token for one packet queued by the router. pub struct RxToken<'a> { interface_id: InterfaceId, packet: &'a [u8], diff --git a/net/ax-net/src/socket.rs b/net/ax-net/src/socket.rs index 8bfbdc1bfc..3edb7f0b28 100644 --- a/net/ax-net/src/socket.rs +++ b/net/ax-net/src/socket.rs @@ -1,3 +1,23 @@ +//! Public socket facade. +//! +//! This module defines the protocol-independent socket API used by syscall +//! layers: common send/recv flags, extended address families, shutdown modes, +//! and the `SocketOps` trait implemented by TCP, UDP, raw, Unix, and vsock +//! transports. +//! +//! # Compatibility Boundary +//! +//! The syscall layer should not need to know whether a socket is backed by +//! smoltcp, an in-kernel Unix transport, or a vsock connection manager. It +//! passes `SocketAddrEx`, `SendOptions`, and `RecvOptions` into this facade, and +//! each concrete transport maps them onto its own semantics. +//! +//! # Design Rule +//! +//! This module contains dispatch and common ABI shapes only. Protocol behavior +//! such as TCP accept queues, UDP corking, raw packet format, or Unix ancillary +//! data delivery belongs in the corresponding transport module. + use alloc::{boxed::Box, vec::Vec}; use core::{ any::Any, diff --git a/net/ax-net/src/state.rs b/net/ax-net/src/state.rs index 8b099a17a7..ec4f3abe18 100644 --- a/net/ax-net/src/state.rs +++ b/net/ax-net/src/state.rs @@ -1,3 +1,18 @@ +//! Lightweight socket state gate. +//! +//! Socket methods use this atomic guard to serialize high-level state +//! transitions without holding the global smoltcp socket-set lock across an +//! entire POSIX operation. It is intentionally smaller than a mutex: one method +//! can move a socket into `Busy`, perform the protocol operation, then commit or +//! roll back the public state. +//! +//! # Relationship To smoltcp State +//! +//! This state is the user-visible control state, not a replacement for +//! smoltcp's TCP state machine. For example, a TCP socket may be publicly +//! `Connecting` while the smoltcp socket is `SynSent`. Socket code must update +//! both sides at clear transition points. + use core::sync::atomic::{AtomicU8, Ordering}; use ax_errno::AxResult; @@ -31,10 +46,12 @@ impl TryFrom for State { pub struct StateLock(AtomicU8); impl StateLock { + /// Creates a state gate initialized to `state`. pub fn new(state: State) -> Self { Self(AtomicU8::new(state as u8)) } + /// Loads the current public socket state. pub fn get(&self) -> State { self.0 .load(Ordering::Acquire) @@ -42,10 +59,12 @@ impl StateLock { .expect("invalid state") } + /// Stores a new public socket state. pub fn set(&self, state: State) { self.0.store(state as u8, Ordering::Release); } + /// Moves from `expect` to `Busy`, returning the observed state on failure. pub fn lock(&self, expect: State) -> Result, State> { match self.0.compare_exchange( expect as u8, @@ -60,8 +79,10 @@ impl StateLock { } #[must_use] +/// Guard for a pending state transition. pub struct StateGuard<'a>(&'a StateLock, u8); impl StateGuard<'_> { + /// Runs a transition body and commits the new state only on success. pub fn transit(self, new: State, f: impl FnOnce() -> AxResult) -> AxResult { match f() { Ok(result) => { diff --git a/net/ax-net/src/tcp.rs b/net/ax-net/src/tcp.rs index 7b082f9ad7..c31c9a3099 100644 --- a/net/ax-net/src/tcp.rs +++ b/net/ax-net/src/tcp.rs @@ -1,3 +1,30 @@ +//! TCP socket implementation. +//! +//! TCP sockets wrap smoltcp stream sockets with POSIX-like behavior: bind and +//! listen bookkeeping, accept queues, nonblocking readiness, keepalive and +//! TCP_INFO options, orphan cleanup, and route-aware device binding. +//! +//! # smoltcp Boundary +//! +//! The actual TCP state machine, retransmission timers, and stream buffers live +//! in smoltcp. This module owns the public socket state around that core: +//! ephemeral port allocation, wildcard/specific bind registration, listener +//! setup, accepted child socket construction, shutdown semantics, and +//! Linux-compatible error reporting. +//! +//! # Polling Model +//! +//! Socket methods never synchronously drive the full interface poll loop. +//! Instead they mutate the smoltcp socket, call `request_poll()`, register +//! wakers through `PollSet`, and let the dedicated net-poll worker advance +//! timers, handshakes, retransmission, and close states. +//! +//! # Related Side Tables +//! +//! - `TCP_BOUND_PORTS` records public bind ownership. +//! - `LISTEN_TABLE` owns passive-open child sockets and accept wakeups. +//! - `orphan` keeps dropped sockets alive long enough for FIN/TIME-WAIT cleanup. + use alloc::{sync::Arc, vec, vec::Vec}; use core::{ net::{Ipv4Addr, SocketAddr}, @@ -31,6 +58,7 @@ use crate::{ state::*, }; +/// Allocates a smoltcp TCP socket with ax-net's default buffers. pub(crate) fn new_tcp_socket() -> smol::Socket<'static> { smol::Socket::new( smol::SocketBuffer::new(vec![0; TCP_RX_BUF_LEN]), @@ -52,21 +80,36 @@ const TCP_INFO_DEFAULT_REORDERING: u32 = 3; /// A TCP socket that provides POSIX-like APIs. pub struct TcpSocket { + /// Public high-level socket state gate. state: StateLock, + /// Handle into the global smoltcp socket set. handle: SocketHandle, + /// Bound listen endpoint, or an empty endpoint before bind/connect. bound_endpoint: Mutex, + /// Connected peer endpoint once established. peer_endpoint: Mutex>, + /// Whether `bound_endpoint` is registered in `TCP_BOUND_PORTS`. bound_registered: AtomicBool, + /// Shared socket options and blocking helpers. general: GeneralOptions, + /// Pending Linux errno-style connection error. pending_error: AtomicI32, + /// TCP_KEEPIDLE value in seconds. keep_idle_secs: AtomicU32, + /// TCP_KEEPINTVL value in seconds. keep_interval_secs: AtomicU32, + /// TCP_KEEPCNT value. keep_count: AtomicU32, + /// TCP_USER_TIMEOUT value in milliseconds. user_timeout_millis: AtomicU32, + /// Whether the read half was shut down from the public API. rx_closed: AtomicBool, + /// Shared RX readiness poll set. poll_rx: Arc, + /// Shared TX readiness poll set. poll_tx: Arc, + /// Wakes waiters when the receive side becomes closed. poll_rx_closed: PollSet, } @@ -95,6 +138,7 @@ impl TcpSocket { } } + /// Restricts this socket to one interface for route selection. pub fn bind_device(&self, interface_id: InterfaceId) -> AxResult { if interface_by_id(interface_id).is_none() { return Err(AxError::NoSuchDevice); @@ -771,6 +815,7 @@ const fn empty_endpoint() -> IpListenEndpoint { } impl TcpSocket { + /// Starts an active open and leaves completion to the net-poll worker. fn start_connect(&self, remote_addr: SocketAddr) -> AxResult { self.state .lock(State::Idle) @@ -857,6 +902,7 @@ impl TcpSocket { }) } + /// Registers the public TCP bind side table if not already registered. fn register_bound_endpoint(&self, endpoint: IpListenEndpoint) -> AxResult { if !self.bound_registered.load(Ordering::Acquire) { register_tcp_bound(endpoint)?; @@ -865,6 +911,7 @@ impl TcpSocket { Ok(()) } + /// Removes the public TCP bind side-table entry, if present. fn unregister_bound_endpoint(&self) { if self.bound_registered.swap(false, Ordering::AcqRel) { unregister_tcp_bound(*self.bound_endpoint.lock()); @@ -875,6 +922,7 @@ impl TcpSocket { static TCP_BOUND_PORTS: LazyLock>>>> = LazyLock::new(|| Mutex::new(HashMap::new())); +/// Registers TCP bind ownership with wildcard/specific address conflicts. fn register_tcp_bound(endpoint: IpListenEndpoint) -> AxResult { if endpoint.port == 0 { return Ok(()); @@ -892,6 +940,7 @@ fn register_tcp_bound(endpoint: IpListenEndpoint) -> AxResult { Ok(()) } +/// Removes one TCP bind registration. fn unregister_tcp_bound(endpoint: IpListenEndpoint) { if endpoint.port != 0 { let mut bound_ports = TCP_BOUND_PORTS.lock(); @@ -906,6 +955,7 @@ fn unregister_tcp_bound(endpoint: IpListenEndpoint) { } } +/// Returns whether a port is safe for ephemeral TCP allocation. fn tcp_port_available(port: u16) -> bool { // Ephemeral ports are selected conservatively: avoid any port that has a // listener or bound socket on any local address. @@ -913,6 +963,7 @@ fn tcp_port_available(port: u16) -> bool { && !TCP_BOUND_PORTS.lock().contains_key(&port) } +/// Returns whether two listen/bind addresses conflict on the same port. fn listen_addrs_conflict( a: Option, b: Option, diff --git a/net/ax-net/src/udp.rs b/net/ax-net/src/udp.rs index b8170eed04..6d88d392f1 100644 --- a/net/ax-net/src/udp.rs +++ b/net/ax-net/src/udp.rs @@ -1,3 +1,29 @@ +//! UDP socket implementation. +//! +//! UDP sockets wrap smoltcp datagram sockets with POSIX-style bind/connect, +//! per-address port ownership, connected-peer filtering, route-aware source +//! selection, and MSG_MORE corking for datagram coalescing. +//! +//! # Bind And Routing Semantics +//! +//! Public binds are checked through `SocketSetWrapper` so wildcard and specific +//! address conflicts match Linux expectations. When a socket is connected or +//! sends to a destination, the control plane selects the source address and +//! device binding from the route table unless the socket was explicitly bound +//! to a concrete local address/interface. +//! +//! # Datagram Semantics +//! +//! smoltcp stores UDP payload plus metadata, while the POSIX surface exposes +//! per-call source addresses, `MSG_TRUNC`, `MSG_PEEK`, `MSG_DONTWAIT`, and +//! `MSG_MORE`. This module is responsible for preserving message boundaries and +//! for filtering connected sockets to their expected peer. +//! +//! # Polling +//! +//! UDP send/recv operations request the shared net-poll worker after socket +//! state changes. They do not run the interface poll loop directly. + use alloc::{vec, vec::Vec}; use core::{ net::{IpAddr, Ipv4Addr, SocketAddr}, @@ -37,6 +63,7 @@ struct CorkState { source: IpAddress, } +/// Allocates a smoltcp UDP socket with ax-net's default packet buffers. pub(crate) fn new_udp_socket() -> smol::Socket<'static> { // TODO(mivik): buffer size smol::Socket::new( @@ -47,10 +74,14 @@ pub(crate) fn new_udp_socket() -> smol::Socket<'static> { /// A UDP socket that provides POSIX-like APIs. pub struct UdpSocket { + /// Handle into the global smoltcp socket set. handle: SocketHandle, + /// Bound local endpoint as exposed by POSIX socket calls. local_addr: RwLock>, + /// Connected remote endpoint plus selected source address. peer_addr: RwLock>, + /// Shared socket options and blocking helpers. general: GeneralOptions, /// MSG_MORE corking state: captures endpoint at first MSG_MORE /// so the merged datagram always goes to the correct peer. @@ -74,6 +105,7 @@ impl UdpSocket { } } + /// Restricts this socket to one interface for route selection. pub fn bind_device(&self, interface_id: InterfaceId) -> AxResult { if interface_by_id(interface_id).is_none() { return Err(AxError::NoSuchDevice); @@ -84,10 +116,12 @@ impl UdpSocket { Ok(()) } + /// Borrows the underlying smoltcp UDP socket by handle. fn with_smol_socket(&self, f: impl FnOnce(&mut smol::Socket) -> R) -> R { SOCKET_SET.with_socket_mut::(self.handle, f) } + /// Returns the connected peer and cached source address. fn remote_endpoint(&self) -> AxResult<(IpEndpoint, IpAddress)> { match self.peer_addr.try_read() { Some(addr) => addr.ok_or(AxError::NotConnected), @@ -95,6 +129,7 @@ impl UdpSocket { } } + /// Selects the source address used to reach `remote`. fn source_for_remote(&self, remote: &IpAddress) -> AxResult { Ok(get_control() .select_route_with_binding(remote, self.general.device_binding())? @@ -144,6 +179,7 @@ impl Configurable for UdpSocket { } } impl SocketOps for UdpSocket { + /// Binds the UDP socket and records public port ownership. fn bind(&self, local_addr: SocketAddrEx) -> AxResult { let mut local_addr = local_addr.into_ip()?; let mut guard = self.local_addr.write(); @@ -184,6 +220,7 @@ impl SocketOps for UdpSocket { Ok(()) } + /// Stores a default peer and source address for connected UDP semantics. fn connect(&self, remote_addr: SocketAddrEx) -> AxResult { let remote_addr = remote_addr.into_ip()?; let mut guard = self.peer_addr.write(); @@ -225,6 +262,7 @@ impl SocketOps for UdpSocket { Ok(()) } + /// Sends one datagram, or appends to/flushed a MSG_MORE corked datagram. fn send(&self, mut src: impl Read + IoBuf, options: SendOptions) -> AxResult { // MSG_OOB is only valid on stream sockets (SOCK_STREAM), not DGRAM. if options.flags.contains(SendFlags::OOB) { @@ -404,6 +442,7 @@ impl SocketOps for UdpSocket { }) } + /// Receives one datagram while honoring peer filters and recv flags. fn recv(&self, mut dst: impl Write, mut options: RecvOptions) -> AxResult { enum ExpectedRemote<'a> { Any(&'a mut SocketAddrEx), diff --git a/net/ax-net/src/unix/dgram.rs b/net/ax-net/src/unix/dgram.rs index f77ff2b44d..550fd832ea 100644 --- a/net/ax-net/src/unix/dgram.rs +++ b/net/ax-net/src/unix/dgram.rs @@ -1,3 +1,23 @@ +//! Unix datagram transport. +//! +//! Datagram sockets use async channels to preserve message boundaries and pass +//! ancillary data together with each packet. Bound endpoints publish a sender in +//! the Unix namespace, while connected socket pairs keep direct peer channels +//! for fast local delivery. +//! +//! # Delivery Semantics +//! +//! Each send builds one `Packet` containing payload, cmsg data, and sender +//! address. A receiver consumes exactly one packet per recv call, which keeps +//! Unix datagram behavior separate from the byte-stream logic in +//! `stream.rs`. +//! +//! # Readiness +//! +//! Bound sockets and socketpairs both carry a `PollSet`. Senders wake the +//! receiver after enqueueing a packet; poll registration never touches the +//! global smoltcp socket set. + use alloc::{boxed::Box, sync::Arc, vec::Vec}; use core::task::Context; @@ -17,18 +37,25 @@ use crate::{ }; struct Packet { + /// Datagram payload. data: Vec, + /// Ancillary messages carried with this datagram. cmsg: Vec, + /// Sender address reported by recvmsg. sender: UnixSocketAddr, } struct Channel { + /// Sender side of the peer's datagram queue. data_tx: async_channel::Sender, + /// Poll set woken when data is queued. poll_update: Arc, } pub struct Bind { + /// Sender published in the Unix namespace for this bound address. data_tx: async_channel::Sender, + /// Poll set associated with the receiver bound at this address. poll_update: Arc, } impl Bind { @@ -43,11 +70,17 @@ impl Bind { /// Datagram transport for Unix domain sockets. pub struct DgramTransport { + /// Receiver installed when the socket is bound or paired. data_rx: Mutex, Arc)>>, + /// Direct peer channel for connected datagram sockets. connected: RwLock>, + /// Address reported as sender on outgoing datagrams. local_addr: RwLock, + /// Poll set for local state changes. poll_state: Arc, + /// Shared socket options. general: GeneralOptions, + /// Creator pid used for SO_PEERCRED-style reporting. pid: u32, } impl DgramTransport { diff --git a/net/ax-net/src/unix/mod.rs b/net/ax-net/src/unix/mod.rs index e8645897ee..8aca936b4d 100644 --- a/net/ax-net/src/unix/mod.rs +++ b/net/ax-net/src/unix/mod.rs @@ -1,3 +1,22 @@ +//! Unix domain socket facade. +//! +//! This module provides the shared address namespace and transport dispatch for +//! Unix stream and datagram sockets. The concrete transports live in +//! `stream.rs` and `dgram.rs`; this layer handles bind/connect/accept plumbing +//! and exposes them through the common socket API. +//! +//! # Namespace Model +//! +//! Abstract names are stored in an in-memory map owned by ax-net. Path names are +//! delegated to an optional filesystem namespace provider so the socket layer +//! does not depend on a concrete VFS implementation. +//! +//! # Transport Split +//! +//! `UnixSocket` owns local/remote address state and a protocol-erased +//! `Transport`. Stream and datagram transports implement the actual byte-stream +//! or message semantics, including cmsg handling and poll readiness. + pub(crate) mod dgram; pub mod namespace; pub(crate) mod stream; @@ -92,13 +111,16 @@ impl Pollable for Transport { /// Holds binding state for stream and datagram transports at a Unix address. #[derive(Default)] pub struct BindSlot { + /// Stream listener bound at this address. stream: Mutex>, + /// Datagram endpoint bound at this address. dgram: Mutex>, } static ABSTRACT_BINDS: LazyLock, BindSlot>>> = LazyLock::new(|| Mutex::new(HashMap::new())); +/// Resolves an existing bind slot and runs `f` with it. pub(crate) fn with_slot( addr: &UnixSocketAddr, f: impl FnOnce(&BindSlot) -> AxResult, @@ -119,6 +141,7 @@ pub(crate) fn with_slot( }), } } +/// Resolves or creates a bind slot and runs `f` with it. fn with_slot_or_insert( addr: &UnixSocketAddr, f: impl FnOnce(&BindSlot) -> AxResult, @@ -138,8 +161,11 @@ fn with_slot_or_insert( /// A Unix domain socket. pub struct UnixSocket { + /// Concrete stream or datagram transport. transport: Transport, + /// Public local Unix address. local_addr: Mutex, + /// Public remote Unix address. remote_addr: Mutex, } impl UnixSocket { diff --git a/net/ax-net/src/unix/namespace.rs b/net/ax-net/src/unix/namespace.rs index 7a012570a3..f6a0ae2711 100644 --- a/net/ax-net/src/unix/namespace.rs +++ b/net/ax-net/src/unix/namespace.rs @@ -1,3 +1,16 @@ +//! Filesystem-backed Unix socket namespace hook. +//! +//! Abstract Unix socket names are managed inside ax-net. Path-based Unix socket +//! names are delegated to an optional filesystem provider through this trait. +//! +//! # Integration Boundary +//! +//! The network crate only needs to resolve, create, and remove bind slots for a +//! path. It does not own dentries, permissions, mount namespaces, or lifecycle +//! rules beyond unbinding the slot when the Unix socket transport is dropped. +//! Kernels that do not enable filesystem support can leave this provider +//! unregistered and still use unnamed or abstract Unix sockets. + use alloc::{boxed::Box, sync::Arc}; use ax_errno::{AxResult, ax_err_type}; diff --git a/net/ax-net/src/unix/stream.rs b/net/ax-net/src/unix/stream.rs index 2c31a2f44c..1b0d4b69ad 100644 --- a/net/ax-net/src/unix/stream.rs +++ b/net/ax-net/src/unix/stream.rs @@ -1,3 +1,23 @@ +//! Unix stream transport. +//! +//! Stream sockets are implemented as paired byte rings with explicit close +//! flags and a small cmsg side channel. Listening sockets enqueue connection +//! requests in the Unix namespace, and accepted sockets receive one half of a +//! connected channel pair. +//! +//! # Channel Layout +//! +//! A connected pair is two unidirectional byte rings plus shared close flags. +//! Each endpoint writes into one ring and reads from the other. This mirrors the +//! full-duplex behavior of Unix stream sockets without involving smoltcp. +//! +//! # Ancillary Data +//! +//! cmsg data is attached to byte ranges rather than individual bytes. The +//! receiver delivers a cmsg when it reaches the first byte of the send call that +//! carried it, and recv may stop at a cmsg boundary so the next recvmsg starts +//! with the next message's ancillary data. + use alloc::{boxed::Box, collections::VecDeque, sync::Arc, vec::Vec}; use core::{ sync::atomic::{AtomicBool, Ordering}, @@ -110,6 +130,7 @@ pub struct Bind { /// New connections are sent to this channel. conn_tx: async_channel::Sender, poll_new_conn: Arc, + /// PID of the process that created the listening transport. pid: u32, } impl Bind { @@ -130,19 +151,29 @@ impl Bind { } struct ConnRequest { + /// Server-side channel half created for accept(). channel: Channel, + /// Client address reported to accept(). addr: UnixSocketAddr, + /// Client pid used for peer credentials. pid: u32, } /// Stream transport for Unix domain sockets. pub struct StreamTransport { + /// Connected channel, if this endpoint is connected or accepted. channel: Mutex>, + /// Listener receive queue installed by bind/listen. conn_rx: Mutex, Arc)>>, + /// Poll set for local stream state. poll_state: PollSet, + /// Shared socket options. general: GeneralOptions, + /// Creator pid used for credentials. pid: u32, + /// Public receive-half shutdown flag. rx_closed: AtomicBool, + /// Public transmit-half shutdown flag. tx_closed: AtomicBool, } impl StreamTransport { diff --git a/net/ax-net/src/vsock/connection_manager.rs b/net/ax-net/src/vsock/connection_manager.rs index ca14fd3752..c7f21c6d1d 100644 --- a/net/ax-net/src/vsock/connection_manager.rs +++ b/net/ax-net/src/vsock/connection_manager.rs @@ -1,3 +1,22 @@ +//! Vsock connection registry. +//! +//! The manager tracks listening, connecting, and established vsock stream +//! connections, owns their byte rings, and provides wakeups used by the vsock +//! transport and device polling glue. +//! +//! # Event Flow +//! +//! Device polling turns host events into manager calls such as connection +//! request, connected, received data, credit update, and disconnect. Socket +//! transports then observe manager state through connection handles and poll +//! sets. +//! +//! # Buffering +//! +//! Each connection owns an RX byte ring. When the ring is full, the device event +//! path keeps the event pending rather than dropping data, so backpressure is +//! expressed through poll readiness and later receive calls. + use alloc::{collections::BTreeMap, sync::Arc}; use ax_errno::{AxError, AxResult, ax_bail}; @@ -12,41 +31,54 @@ use crate::device::{start_vsock_poll, stop_vsock_poll}; pub const VSOCK_RX_BUFFER_SIZE: usize = 64 * 1024; // 64KB receive buffer const VSOCK_ACCEPT_QUEUE_SIZE: usize = 128; // accept queue size -/// connection states +/// Public state of a vsock connection tracked by the manager. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ConnectionState { + /// Allocated but not listening/connecting. Idle, + /// Registered as a listener. Listening, + /// Outgoing connection request in progress. Connecting, + /// Connected and usable for I/O. Connected, + /// Disconnected or closed. Closed, } -/// Connection +/// Per-connection state shared by vsock device events and stream transports. pub struct Connection { + /// Manager-level connection state. state: ConnectionState, + /// Local vsock address. local_addr: VsockAddr, + /// Peer address, if known. peer_addr: Option, - /// recv buffer read from driver + /// Producer side filled by device receive events. rx_producer: HeapProd, + /// Consumer side drained by socket recv. rx_consumer: HeapCons, - /// wait queues for tx due to InsufficientBufferSpaceInPeer + /// Wait queue for TX blocked by peer credit/buffer pressure. tx_wait_queue: WaitQueue, - /// Waker lists + /// RX readiness waiters. rx_wakers: PollSet, + /// Connect/listen state waiters. connect_wakers: PollSet, - /// closed flags + /// Whether the receive half is closed. rx_closed: bool, + /// Whether the transmit half is closed. tx_closed: bool, - /// statistics - rx_bytes: usize, // received bytes count - tx_bytes: usize, // sent bytes count - dropped_bytes: usize, // dropped bytes count + /// Received byte count. + rx_bytes: usize, + /// Transmitted byte count. + tx_bytes: usize, + /// Dropped byte count. + dropped_bytes: usize, } impl Connection { diff --git a/net/ax-net/src/vsock/mod.rs b/net/ax-net/src/vsock/mod.rs index aad26a686e..6241ce9469 100644 --- a/net/ax-net/src/vsock/mod.rs +++ b/net/ax-net/src/vsock/mod.rs @@ -1,3 +1,16 @@ +//! Vsock socket facade. +//! +//! This module exposes vsock transports through the common socket API. Stream +//! transport is implemented today; the transport enum leaves room for future +//! datagram support without changing the public socket wrapper. +//! +//! # Stack Boundary +//! +//! Vsock is not an IP protocol and is not driven through smoltcp. The facade +//! shares the same `SocketOps`, `Pollable`, and socket option plumbing as IP +//! sockets, but actual connection state lives in `connection_manager` and the +//! device event loop in `device::vsock`. + // pub(crate) mod dgram; todo pub(crate) mod connection_manager; @@ -66,6 +79,7 @@ impl Pollable for VsockTransport { /// A network socket using the vsock protocol. pub struct VsockSocket { + /// Concrete vsock transport. transport: VsockTransport, } diff --git a/net/ax-net/src/vsock/stream.rs b/net/ax-net/src/vsock/stream.rs index da9db79f0e..2ab6b15881 100644 --- a/net/ax-net/src/vsock/stream.rs +++ b/net/ax-net/src/vsock/stream.rs @@ -1,3 +1,22 @@ +//! Vsock stream transport. +//! +//! Stream sockets are backed by entries in the vsock connection manager and are +//! driven by the adaptive vsock device poll task rather than the smoltcp IP +//! poller. +//! +//! # Public State +//! +//! The transport uses `StateLock` for POSIX-facing socket transitions while the +//! connection manager tracks host-visible vsock connection state. Operations +//! must keep those two views synchronized at bind, listen, connect, accept, and +//! shutdown boundaries. +//! +//! # Readiness +//! +//! Poll readiness comes from connection-manager wait queues and poll sets. The +//! stream transport must not acquire smoltcp service/socket locks because vsock +//! is independent from the IP protocol core. + use alloc::sync::Arc; use core::task::Context; @@ -18,9 +37,13 @@ use crate::{ /// Stream transport for vsock sockets. pub struct VsockStreamTransport { + /// Connection id registered with the vsock manager. conn_id: Mutex>, + /// Shared connection state once bound, connecting, or connected. connection: Mutex>>>, + /// Public POSIX-facing stream state. state: StateLock, + /// Shared socket options. general: GeneralOptions, } @@ -35,6 +58,7 @@ impl VsockStreamTransport { } } + /// Returns the manager connection associated with this stream. fn get_connection(&self) -> AxResult>> { self.connection.lock().clone().ok_or(AxError::NotConnected) } diff --git a/net/ax-net/src/wrapper.rs b/net/ax-net/src/wrapper.rs index 8a6a4ff604..9d892cc625 100644 --- a/net/ax-net/src/wrapper.rs +++ b/net/ax-net/src/wrapper.rs @@ -1,3 +1,28 @@ +//! Shared smoltcp socket-set wrapper. +//! +//! ax-net keeps one global smoltcp `SocketSet` behind this wrapper. The extra +//! UDP bind table fills the per-address bind semantics that smoltcp itself does +//! not track for all POSIX cases. +//! +//! # Ownership +//! +//! All TCP, UDP, and raw smoltcp socket handles live in the same handle space. +//! This is what allows the service poller, router snooping path, listen table, +//! and orphan reaper to coordinate without per-interface socket duplication. +//! +//! # UDP Side Table +//! +//! smoltcp validates whether a UDP socket can bind, but ax-net needs +//! Linux-style wildcard/specific-address conflict checks across sockets. The +//! `udp_binds` table records only successful public binds and is cleaned when a +//! socket is removed. +//! +//! # Lock Boundary +//! +//! The wrapper lock protects smoltcp socket state. Callers should keep the lock +//! scoped to direct socket access and avoid waking tasks or acquiring the outer +//! service lock while it is held. + use alloc::vec; use ax_errno::{AxError, AxResult}; @@ -11,16 +36,21 @@ use smoltcp::{ #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] struct UdpBindKey { + /// `None` represents a wildcard bind. addr: Option, port: u16, } +/// Global socket container plus protocol-specific side tables. pub(crate) struct SocketSetWrapper<'a> { + /// The shared smoltcp socket set. pub inner: Mutex>, + /// UDP bind ownership tracked with Linux-style wildcard conflicts. udp_binds: Mutex>, } impl<'a> SocketSetWrapper<'a> { + /// Creates an empty wrapper around smoltcp's socket set. pub fn new() -> Self { Self { inner: Mutex::new(SocketSet::new(vec![])), @@ -28,12 +58,14 @@ impl<'a> SocketSetWrapper<'a> { } } + /// Adds a smoltcp socket and returns its global handle. pub fn add>(&self, socket: T) -> SocketHandle { let handle = self.inner.lock().add(socket); debug!("socket {}: created", handle); handle } + /// Runs a closure with mutable access to one smoltcp socket. pub fn with_socket_mut, R, F>(&self, handle: SocketHandle, f: F) -> R where F: FnOnce(&mut T) -> R, @@ -43,6 +75,7 @@ impl<'a> SocketSetWrapper<'a> { f(socket) } + /// Records a successful public UDP bind after checking address conflicts. pub fn udp_bind(&self, handle: SocketHandle, addr: IpAddress, port: u16) -> AxResult { if port == 0 { return Ok(()); @@ -60,6 +93,7 @@ impl<'a> SocketSetWrapper<'a> { Ok(()) } + /// Returns whether a UDP port can be used for an ephemeral bind. pub fn udp_port_available(&self, addr: IpAddress, port: u16) -> bool { if port == 0 { return true; @@ -71,12 +105,14 @@ impl<'a> SocketSetWrapper<'a> { udp_bind_available(&self.udp_binds.lock(), key) } + /// Removes any UDP bind table entries owned by `handle`. pub fn udp_unbind(&self, handle: SocketHandle) { self.udp_binds .lock() .retain(|_, bound_handle| *bound_handle != handle); } + /// Removes a socket and all wrapper-maintained side-table state. pub fn remove(&self, handle: SocketHandle) { self.udp_unbind(handle); self.inner.lock().remove(handle); @@ -84,6 +120,7 @@ impl<'a> SocketSetWrapper<'a> { } } +/// Implements UDP wildcard/specific-address bind conflict rules. fn udp_bind_available(binds: &HashMap, key: UdpBindKey) -> bool { let wildcard = UdpBindKey { addr: None, From 3983f219a5a5607225dff584a3c8dfe34febfddb Mon Sep 17 00:00:00 2001 From: ZCShou <72115@163.com> Date: Tue, 16 Jun 2026 09:33:37 +0800 Subject: [PATCH 25/31] docs(net): remove line-number references, clarify route binding, poller, and IPv6/ARP semantics --- docs/docs/architecture/net/api.md | 66 ++++----- docs/docs/architecture/net/architecture.md | 2 +- docs/docs/architecture/net/configuration.md | 44 +++--- docs/docs/architecture/net/control.md | 15 +- docs/docs/architecture/net/devices.md | 7 +- docs/docs/architecture/net/flows.md | 144 +++++++++++--------- docs/docs/architecture/net/integration.md | 8 +- docs/docs/architecture/net/overview.md | 10 +- docs/docs/architecture/net/sockets.md | 36 ++--- docs/docs/architecture/net/testing.md | 11 +- 10 files changed, 184 insertions(+), 159 deletions(-) diff --git a/docs/docs/architecture/net/api.md b/docs/docs/architecture/net/api.md index 3daff3790d..8f5e26f5d0 100644 --- a/docs/docs/architecture/net/api.md +++ b/docs/docs/architecture/net/api.md @@ -12,37 +12,37 @@ sidebar_label: "API 参考" 定义在 [net/ax-net/src/lib.rs](net/ax-net/src/lib.rs): ```rust -pub fn init_network(net_devs: EthernetDeviceList, config: NetworkConfig); // L125 -pub fn poll_interfaces(); // L401 -pub fn request_poll(); // L408 +pub fn init_network(net_devs: EthernetDeviceList, config: NetworkConfig); +pub fn poll_interfaces(); +pub fn request_poll(); #[cfg(feature = "vsock")] -pub fn init_vsock(vsock_devs: VsockDeviceList); // L360 +pub fn init_vsock(vsock_devs: VsockDeviceList); ``` 说明: -- `init_network()` 由 `ax-runtime` 调用,只能调用一次(重复调用 panic,见 [lib.rs#L130-L133](net/ax-net/src/lib.rs#L130-L133))。 +- `init_network()` 由 `ax-runtime` 调用,只能调用一次(重复调用 panic,见 [lib.rs](net/ax-net/src/lib.rs))。 - `request_poll()` 是轻量 poll 请求入口,socket 和设备路径应调用它。 -- `poll_interfaces()` 保留为 public trigger/debug 入口,内部仅转调 `request_poll()`([lib.rs#L401-L403](net/ax-net/src/lib.rs#L401-L403))。 +- `poll_interfaces()` 保留为 public trigger/debug 入口,内部仅转调 `request_poll()`([lib.rs](net/ax-net/src/lib.rs)),不会在调用者线程里同步执行 `Service::poll()`。 - `init_vsock()` 仅在 `vsock` feature 下存在。 ## 接口查询 API ```rust -pub fn interfaces() -> Vec; // L417 -pub fn interface_by_name(name: &str) -> Option; // L421 -pub fn interface_by_id(id: InterfaceId) -> Option; // L425 -pub fn ipv4_config(name: &str) -> Option; // L429 -pub fn default_routes() -> Vec; // L433 -pub fn arp_entries() -> Vec; // L413 +pub fn interfaces() -> Vec; +pub fn interface_by_name(name: &str) -> Option; +pub fn interface_by_id(id: InterfaceId) -> Option; +pub fn ipv4_config(name: &str) -> Option; +pub fn default_routes() -> Vec; +pub fn arp_entries() -> Vec; ``` 这些 API 返回只读快照。调用方不应保存快照后假设其永久有效;DHCP 更新或后续接口状态变化可能改变地址、路由和 DNS。 ## 接口类型 -`InterfaceId` 与 `InterfaceFlags` 定义在 [config.rs#L8-L55](net/ax-net/src/config.rs#L8-L55): +`InterfaceId` 与 `InterfaceFlags` 定义在 [config.rs](net/ax-net/src/config.rs): ```rust pub struct InterfaceId(u32); @@ -63,7 +63,7 @@ bitflags::bitflags! { } ``` -`InterfaceInfo` 见 [config.rs#L58-L70](net/ax-net/src/config.rs#L58-L70)。`InterfaceId` 同时作为 StarryOS Linux ifindex 数值来源: +`InterfaceInfo` 见 [config.rs](net/ax-net/src/config.rs)。`InterfaceId` 同时作为 StarryOS Linux ifindex 数值来源: ```rust let linux_ifindex = info.id.to_linux_ifindex(); @@ -72,7 +72,7 @@ let id = InterfaceId::from_linux_ifindex(linux_ifindex).unwrap(); ## Socket API -主要 socket 类型(见 [lib.rs#L38-L46](net/ax-net/src/lib.rs#L38-L46)): +主要 socket 类型(见 [lib.rs](net/ax-net/src/lib.rs)): ```rust pub mod tcp; @@ -84,7 +84,7 @@ pub mod unix; pub mod vsock; ``` -统一 socket trait `SocketOps`([net/ax-net/src/socket.rs#L177-L202](net/ax-net/src/socket.rs#L177-L202)): +统一 socket trait `SocketOps`([net/ax-net/src/socket.rs](net/ax-net/src/socket.rs)): ```rust pub trait SocketOps: Configurable { @@ -101,7 +101,7 @@ pub trait SocketOps: Configurable { } ``` -`Socket` 枚举([socket.rs#L253-L265](net/ax-net/src/socket.rs#L253-L265))统一各 backend,并对 `SocketOps` / `Configurable` 做透传分派: +`Socket` 枚举([socket.rs](net/ax-net/src/socket.rs))统一各 backend,并对 `SocketOps` / `Configurable` 做透传分派: ```rust pub enum Socket { @@ -114,7 +114,7 @@ pub enum Socket { } ``` -`SocketAddrEx`([socket.rs#L26-L35](net/ax-net/src/socket.rs#L26-L35))支持: +`SocketAddrEx`([socket.rs](net/ax-net/src/socket.rs))支持: - IP socket address - Unix domain socket address @@ -131,7 +131,7 @@ pub enum SocketAddrEx { ## Socket options -[options.rs](net/ax-net/src/options.rs) 通过 `define_options!` 宏([options.rs#L148](net/ax-net/src/options.rs#L148))一次性生成 `GetSocketOption<'a>` 和 `SetSocketOption<'a>` 两个枚举,覆盖 SO_\* / TCP_\* / IP_\* 三层选项: +[options.rs](net/ax-net/src/options.rs) 通过 `define_options!` 宏([options.rs](net/ax-net/src/options.rs))一次性生成 `GetSocketOption<'a>` 和 `SetSocketOption<'a>` 两个枚举,覆盖 SO_\* / TCP_\* / IP_\* 三层选项: ```rust define_options! { @@ -170,7 +170,7 @@ define_options! { } ``` -`Configurable` trait([options.rs#L185-L208](net/ax-net/src/options.rs#L185-L208))提供 `get_option` / `set_option`,未支持的选项返回 `ENOPROTOOPT`: +`Configurable` trait([options.rs](net/ax-net/src/options.rs))提供 `get_option` / `set_option`,未支持的选项返回 `ENOPROTOOPT`: ```rust #[enum_dispatch] @@ -182,9 +182,9 @@ pub trait Configurable { } ``` -通用选项(非阻塞、超时、`SO_BINDTODEVICE` 等)由 `GeneralOptions`([general.rs#L18-L31](net/ax-net/src/general.rs#L18-L31))实现,各 socket backend 的 `get_option_inner` / `set_option_inner` 先尝试 `GeneralOptions`,再处理自身特有选项。`TcpInfo`([options.rs#L58-L101](net/ax-net/src/options.rs#L58-L101))提供 transport-independent 的 TCP_INFO 快照。 +通用选项(非阻塞、超时、`SO_BINDTODEVICE` 等)由 `GeneralOptions`([general.rs](net/ax-net/src/general.rs))实现,各 socket backend 的 `get_option_inner` / `set_option_inner` 先尝试 `GeneralOptions`,再处理自身特有选项。`TcpInfo`([options.rs](net/ax-net/src/options.rs))提供 transport-independent 的 TCP_INFO 快照。 -`SO_BINDTODEVICE` 在 `ax-net` 内表达为 `DeviceBinding`([config.rs#L142-L146](net/ax-net/src/config.rs#L142-L146)): +`SO_BINDTODEVICE` 在 `ax-net` 内表达为 `DeviceBinding`([config.rs](net/ax-net/src/config.rs)): ```rust #[derive(Debug, Clone, Copy, Default, Eq, PartialEq)] @@ -201,9 +201,9 @@ pub struct DeviceBinding { ## DNS API ```rust -pub fn dns_servers() -> Vec; // L490 -pub fn dns_query(name: &str) -> AxResult>; // L496 -pub fn dns_query_timeout(name: &str, timeout: Duration) -> AxResult>; // L500 +pub fn dns_servers() -> Vec; +pub fn dns_query(name: &str) -> AxResult>; +pub fn dns_query_timeout(name: &str, timeout: Duration) -> AxResult>; ``` 说明: @@ -215,10 +215,10 @@ pub fn dns_query_timeout(name: &str, timeout: Duration) -> AxResult> ## ARP API ```rust -pub fn arp_entries() -> Vec; // L413 +pub fn arp_entries() -> Vec; ``` -返回所有 Ethernet 设备上已解析的 ARP 条目快照([ArpEntry](net/ax-net/src/device/mod.rs#L24-L31)): +返回所有 Ethernet 设备上已解析的 ARP 条目快照([ArpEntry](net/ax-net/src/device/mod.rs)): ```rust pub struct ArpEntry { @@ -234,7 +234,7 @@ pub struct ArpEntry { ### EthernetDriver(能力边界 trait) -定义在 [device/driver.rs#L70-L82](net/ax-net/src/device/driver.rs#L70-L82): +定义在 [device/driver.rs](net/ax-net/src/device/driver.rs): ```rust pub trait EthernetDriver: Send + Sync { @@ -275,15 +275,17 @@ pub trait EthernetIrqRegistrar: Send + Sync { ```rust #[cfg(feature = "vsock")] -pub fn init_vsock(vsock_devs: VsockDeviceList); // L360 +pub fn init_vsock(vsock_devs: VsockDeviceList); // types: pub struct VsockAddr; pub struct VsockConnId; -pub type VsockDevice = Box; +pub type VsockDevice = Box; pub type VsockDeviceList = Vec; ``` +`VsockDevice` 是 `rdif_vsock::Interface` trait object,表示 vsock 设备事件/收发接口。它不等同于 `ax-net` 内部的 IP `Device` trait,也不进入 smoltcp `SocketSet`。 + ## bind_device 方法 每个 socket 类型提供独立的接口绑定方法(不通过 socket option): @@ -338,14 +340,14 @@ Unix stream transport 通过 `StreamTransport::new_pair(pid)` 创建 socketpair ## Unix Namespace API ```rust -pub fn register_unix_namespace(ns: Arc); +pub fn register_unix_namespace(ns: impl UnixNamespace + 'static); ``` StarryOS 通过此 API 注入文件系统 namespace(路径解析和 inode 管理)。`UnixNamespace` trait 定义在 [unix/namespace.rs](net/ax-net/src/unix/namespace.rs)。 ## TCP / UDP / raw 接口绑定 -TCP、UDP 和 raw socket 提供 `bind_device(InterfaceId)`,例如 `TcpSocket::bind_device()`([tcp.rs#L94-L102](net/ax-net/src/tcp.rs#L94-L102)): +TCP、UDP 和 raw socket 提供 `bind_device(InterfaceId)`,例如 `TcpSocket::bind_device()`([tcp.rs](net/ax-net/src/tcp.rs)): ```rust pub fn bind_device(&self, interface_id: InterfaceId) -> AxResult { diff --git a/docs/docs/architecture/net/architecture.md b/docs/docs/architecture/net/architecture.md index ddc0f8bf3a..b739a804b5 100644 --- a/docs/docs/architecture/net/architecture.md +++ b/docs/docs/architecture/net/architecture.md @@ -37,7 +37,7 @@ sidebar_label: "总体架构" flowchart TB subgraph Api["Public API"] Init["init_network() / init_vsock()"] - Query["interfaces() / routes() / arp_entries()"] + Query["interfaces() / default_routes() / arp_entries()"] DnsApi["dns_servers() / dns_query()"] Sockets["TcpSocket / UdpSocket / RawSocket / UnixSocket / VsockSocket"] PollApi["request_poll() / poll_interfaces()"] diff --git a/docs/docs/architecture/net/configuration.md b/docs/docs/architecture/net/configuration.md index a187ea4aba..40341b68c7 100644 --- a/docs/docs/architecture/net/configuration.md +++ b/docs/docs/architecture/net/configuration.md @@ -9,7 +9,7 @@ sidebar_label: "配置参考" ## Cargo Feature -`ax-net` 的 feature 定义在 [net/ax-net/Cargo.toml#L10-L12](net/ax-net/Cargo.toml#L10-L12): +`ax-net` 的 feature 定义在 [net/ax-net/Cargo.toml](net/ax-net/Cargo.toml): ```toml [features] @@ -20,9 +20,9 @@ vsock = ["dep:rdif-vsock"] | --- | --- | | `vsock` | 启用 `rdif-vsock` 依赖和 vsock socket / device 支持 | -启用后,`lib.rs` 中通过 `#[cfg(feature = "vsock")]` 条件编译导出 `init_vsock()`([lib.rs#L360](net/ax-net/src/lib.rs#L360))、`vsock` 模块([lib.rs#L45](net/ax-net/src/lib.rs#L45))以及 `Socket::Vsock` 变体([socket.rs#L263-L264](net/ax-net/src/socket.rs#L263-L264))。基础 TCP、UDP、raw、Unix domain socket、DNS、DHCP 和 Ethernet 能力不需要额外 feature。 +启用后,`lib.rs` 中通过 `#[cfg(feature = "vsock")]` 条件编译导出 `init_vsock()`([lib.rs](net/ax-net/src/lib.rs))、`vsock` 模块([lib.rs](net/ax-net/src/lib.rs))以及 `Socket::Vsock` 变体([socket.rs](net/ax-net/src/socket.rs))。基础 TCP、UDP、raw、Unix domain socket、DNS、DHCP 和 Ethernet 能力不需要额外 feature。 -smoltcp 在 [Cargo.toml#L34-L52](net/ax-net/Cargo.toml#L34-L52) 中固定启用以下能力: +smoltcp 在 [Cargo.toml](net/ax-net/Cargo.toml) 中固定启用以下能力: - `alloc` - `log` @@ -56,11 +56,11 @@ pub const ETHERNET_MAX_PENDING_PACKETS: usize = 128; pub const DEVICE_TX_QUEUE_SIZE: usize = 128; ``` -`ETHERNET_MAX_PENDING_PACKETS` 取 128 而非 32,是为了容纳应用启动时多个并发 TCP 连接的首个 SYN 突发,以及长连接在 `NEIGHBOR_TTL` 过期后重新进入 ARP-pending 队列的 burst(见源码注释 [consts.rs#L14-L27](net/ax-net/src/consts.rs#L14-L27))。 +`ETHERNET_MAX_PENDING_PACKETS` 取 128 而非 32,是为了容纳应用启动时多个并发 TCP 连接的首个 SYN 突发,以及长连接在 `NEIGHBOR_TTL` 过期后重新进入 ARP-pending 队列的 burst(见源码注释 [consts.rs](net/ax-net/src/consts.rs))。 ## NetworkConfig -定义在 [net/ax-net/src/config.rs#L80-L97](net/ax-net/src/config.rs#L80-L97): +定义在 [net/ax-net/src/config.rs](net/ax-net/src/config.rs): ```rust #[derive(Debug, Clone, Default)] @@ -86,7 +86,7 @@ pub struct InterfaceConfig { ## InterfaceMatcher -显式接口配置通过 `InterfaceMatcher` 匹配真实设备,定义在 [config.rs#L72-L76](net/ax-net/src/config.rs#L72-L76): +显式接口配置通过 `InterfaceMatcher` 匹配真实设备,定义在 [config.rs](net/ax-net/src/config.rs): ```rust #[derive(Debug, Clone)] @@ -105,11 +105,11 @@ pub enum InterfaceMatcher { - 显式配置必须匹配唯一设备。 - 未显式配置的 Ethernet 设备默认启用 DHCP。 -匹配逻辑在 `find_interface_config()`([lib.rs#L328-L355](net/ax-net/src/lib.rs#L328-L355)),多配置匹配同一设备会 panic。 +匹配逻辑在 `find_interface_config()`([lib.rs](net/ax-net/src/lib.rs)),多配置匹配同一设备会 panic。 ## 静态 IPv4 -`StaticIpConfig` 定义在 [config.rs#L100-L104](net/ax-net/src/config.rs#L100-L104): +`StaticIpConfig` 定义在 [config.rs](net/ax-net/src/config.rs): ```rust #[derive(Debug, Clone)] @@ -138,7 +138,7 @@ pub struct StaticIpConfig { - 显式配置没有匹配任何设备。 - 接口名冲突。 -这些校验集中在 `init_network()` 开头([lib.rs#L127-L170](net/ax-net/src/lib.rs#L127-L170))。 +这些校验集中在 `init_network()` 开头([lib.rs](net/ax-net/src/lib.rs))。 ## DHCP @@ -213,7 +213,7 @@ let config = NetworkConfig { | InterfaceId | 接口 | 说明 | | --- | --- | --- | -| 0 | `TX_INTERFACE_PLACEHOLDER` | 内部占位符([router.rs](net/ax-net/src/router.rs#L76)),不出现在任何 public API 中 | +| 0 | `TX_INTERFACE_PLACEHOLDER` | 内部占位符([router.rs](net/ax-net/src/router.rs)),不出现在任何 public API 中 | | 1 | `lo` | Loopback,固定 ID | | 2+ | `eth0`, `eth1`, ... | Ethernet 设备,按发现顺序(`net_devs.drain(..)`)递增 | @@ -229,7 +229,7 @@ let config = NetworkConfig { ## TCP Keep-Alive 默认值 -TCP keep-alive 相关常量定义在 [tcp.rs](net/ax-net/src/tcp.rs#L45-L52): +TCP keep-alive 相关常量定义在 [tcp.rs](net/ax-net/src/tcp.rs): ```rust const TCP_KEEPIDLE_DEFAULT_SECS: u32 = 7200; // 2小时空闲后开始探测 @@ -250,7 +250,7 @@ const TCP_KEEPCNT_MAX: u32 = 127; ## TCP_INFO 默认值 -`TcpInfo` 快照中的默认/估计值([tcp.rs](net/ax-net/src/tcp.rs#L48-L52)): +`TcpInfo` 快照中的默认/估计值([tcp.rs](net/ax-net/src/tcp.rs)): ```rust const TCP_INFO_DEFAULT_MSS: u32 = 1460; // 1500 - 20(IP) - 20(TCP) @@ -267,23 +267,23 @@ const TCP_INFO_DEFAULT_REORDERING: u32 = 3; | 常量 | 位置 | 值 | 说明 | | --- | --- | --- | --- | -| `DNS_DEFAULT_TIMEOUT` | [lib.rs#L488](net/ax-net/src/lib.rs#L488) | 5s | DNS 查询超时 | -| `DHCP_BOOTSTRAP_ATTEMPTS` | [lib.rs#L107](net/ax-net/src/lib.rs#L107) | 200 | DHCP bootstrap 最大重试次数 | -| `DHCP_BOOTSTRAP_POLL_INTERVAL` | [lib.rs#L108](net/ax-net/src/lib.rs#L108) | 10ms | DHCP bootstrap poll 间隔 | -| `DHCP_MAX_RETRY_SHIFT` | [service.rs#L283](net/ax-net/src/service.rs#L283) | 4 | DHCP 指数退避最大位移(最大 16s) | -| `NEIGHBOR_TTL` | [ethernet.rs#L118](net/ax-net/src/device/ethernet.rs#L118) | 300s | ARP neighbor 缓存 TTL | -| `ARP_REQUEST_RETRY` | [ethernet.rs#L119](net/ax-net/src/device/ethernet.rs#L119) | 1s | ARP 请求重试间隔 | -| Idle poll interval | [lib.rs#L438](net/ax-net/src/lib.rs#L438) | 100ms | net-poll worker 空闲轮询间隔 | +| `DNS_DEFAULT_TIMEOUT` | [lib.rs](net/ax-net/src/lib.rs) | 5s | DNS 查询超时 | +| `DHCP_BOOTSTRAP_ATTEMPTS` | [lib.rs](net/ax-net/src/lib.rs) | 200 | DHCP bootstrap 最大重试次数 | +| `DHCP_BOOTSTRAP_POLL_INTERVAL` | [lib.rs](net/ax-net/src/lib.rs) | 10ms | DHCP bootstrap poll 间隔 | +| `DHCP_MAX_RETRY_SHIFT` | [service.rs](net/ax-net/src/service.rs) | 4 | DHCP 指数退避最大位移(最大 16s) | +| `NEIGHBOR_TTL` | [ethernet.rs](net/ax-net/src/device/ethernet.rs) | 300s | ARP neighbor 缓存 TTL | +| `ARP_REQUEST_RETRY` | [ethernet.rs](net/ax-net/src/device/ethernet.rs) | 1s | ARP 请求重试间隔 | +| Idle poll interval | [lib.rs](net/ax-net/src/lib.rs) | 100ms | net-poll worker 空闲轮询间隔 | ## Router 缓冲区配置 | 名称 | 位置 | 容量 | | --- | --- | --- | -| `Router::rx_buffer` | [router.rs#L326](net/ax-net/src/router.rs#L326) | `SOCKET_BUFFER_SIZE` 个 MTU 槽位 × 1500 字节 | -| `Router::tx_buffer` | [router.rs#L330](net/ax-net/src/router.rs#L330) | 同上 | +| `Router::rx_buffer` | [router.rs](net/ax-net/src/router.rs) | `SOCKET_BUFFER_SIZE` 个 MTU 槽位 × 1500 字节 | +| `Router::tx_buffer` | [router.rs](net/ax-net/src/router.rs) | 同上 | | `RouterQueues::rx` | [router.rs](net/ax-net/src/router.rs) | `SOCKET_BUFFER_SIZE` 个 inline `RxPacket`(每包最多 MTU) | | `DeviceHandle::tx_queue` | [router.rs](net/ax-net/src/router.rs) | `DEVICE_TX_QUEUE_SIZE` 个 inline `TxPacket`(每包最多 MTU) | -| `EthernetDevice::pending_packets` | [ethernet.rs#L123](net/ax-net/src/device/ethernet.rs#L123) | `ETHERNET_MAX_PENDING_PACKETS`(128)个 IP 包 | +| `EthernetDevice::pending_packets` | [ethernet.rs](net/ax-net/src/device/ethernet.rs) | `ETHERNET_MAX_PENDING_PACKETS`(128)个 IP 包 | `LoopbackDevice` 不维护独立 buffer。普通 smoltcp loopback TX 在 `Router::dispatch()` 中直接注入 `Router::rx_buffer`;`send_on_device()` 的 loopback 特殊发包才进入共享 `RouterQueues::rx`,且同样使用 inline `QueuedPacket`。 diff --git a/docs/docs/architecture/net/control.md b/docs/docs/architecture/net/control.md index aab0b5081e..7dccef6d3f 100644 --- a/docs/docs/architecture/net/control.md +++ b/docs/docs/architecture/net/control.md @@ -110,14 +110,25 @@ pub fn interface_by_id(&self, id: InterfaceId) -> Option { ### 路由决策 -`select_route()` 按目的地址查路由,`is_usable` 闭包跳过未 `UP` 的接口: +`select_route()` 按目的地址查路由,默认不带接口绑定约束;`select_route_with_binding()` 会额外应用 `DeviceBinding`,用于 `SO_BINDTODEVICE`、显式 `bind_device()` 和由本地地址推导出的接口约束。两者最终都通过 `select_route_if()` 查询路由表,`is_usable` 闭包会跳过未 `UP` 的接口: ```rust // service.rs pub fn select_route(&self, dst_addr: &IpAddress) -> AxResult { + self.select_route_with_binding(dst_addr, DeviceBinding::default()) +} + +pub fn select_route_with_binding( + &self, + dst_addr: &IpAddress, + binding: DeviceBinding, +) -> AxResult { let state = self.state.read(); let routes = self.routes.read(); let route = routes.select_route_if(dst_addr, |interface_id| { + if binding.bound_if.is_some_and(|bound_if| bound_if != interface_id) { + return false; + } state.interfaces.iter() .find(|i| i.id == interface_id) .is_some_and(|i| i.flags.contains(InterfaceFlags::UP)) @@ -299,7 +310,7 @@ pub struct DeviceBinding { ``` - `None`:不绑定接口,route decision 选择出接口。 -- `Some(id)`:只允许对应接口参与 route/waker/device 选择。 +- `Some(id)`:只允许对应接口参与 route、waker 注册和设备可用性过滤。 设置/读取通过 `set_device_binding()` / `device_binding()` 操作 `AtomicU32`。 diff --git a/docs/docs/architecture/net/devices.md b/docs/docs/architecture/net/devices.md index 58acbf5560..42d5b0b0e9 100644 --- a/docs/docs/architecture/net/devices.md +++ b/docs/docs/architecture/net/devices.md @@ -62,8 +62,7 @@ RX 队列是所有设备共享的(`RouterQueues::rx`),因为 smoltcp 从 - **IPv4 广播**(dst=255.255.255.255):复制到所有非 loopback Ethernet 设备。 - **IPv4 单播**:按 `select_route_for_source()` 选路由(要求源地址匹配)。如果出接口是 loopback,走快速路径 `inject_loopback_rx_direct()`(snoop TCP + 直接写入 `rx_buffer`);否则推入对应设备 TX 队列。 -- **IPv6 多播**:复制到所有非 loopback Ethernet 设备。 -- **IPv6 单播**:按 `select_route_for_source()` 选路由,同样有 loopback 快速路径。 +- **IPv6 多播/单播**:`Router::dispatch()` 有对应分支并会按 route/loopback 规则分发;但当前 `EthernetDevice` 的链路层发送和接收主要实现 IPv4/ARP,完整 Ethernet IPv6/NDP 不在当前完成范围。这里的 IPv6 dispatch 描述仅表示 Router 层有分支,不表示物理 Ethernet IPv6 已完整可用。 ### RX 数据流 @@ -113,7 +112,7 @@ pub struct EthernetDevice { ## Loopback 快速路径 -`LoopbackDevice`([loopback.rs](net/ax-net/src/device/loopback.rs))是零状态占位类型(`pub struct LoopbackDevice;`),`Device` trait 全为 no-op。真正的 loopback 数据路径在 `Router` 中以快速路径实现: +`LoopbackDevice`([loopback.rs](net/ax-net/src/device/loopback.rs))是零状态占位类型(`pub struct LoopbackDevice;`)。它提供接口名 `lo`,`recv()` 与 waker 注册不做实际工作,`send()` 只返回成功以满足内部 `Device` trait;真正的 loopback 数据路径在 `Router` 中以快速路径实现: - **TX 方向**(`Router::dispatch()`):路由选中 loopback 时,`inject_loopback_rx_direct()` 直接将 IP 包写入 `Router.rx_buffer`,并在写入前执行 `snoop_tcp_packet()` 预创建 TCP listen socket。回环包在**同一个 `Service::poll()` 周期内**被 smoltcp 消费。 - **DHCP/特殊发包**(`Router::send_on_device()`):对 loopback 调用 `inject_loopback_rx()` 写入共享 `RouterQueues::rx`,由下一轮 `Router::poll()` 消费。 @@ -222,5 +221,5 @@ pub trait Device: Send + Sync { 两个实现: -- `LoopbackDevice`:零状态占位类型,`Device` trait 全为 no-op。真正的回环在 `Router::dispatch()` 的快速路径中完成。 +- `LoopbackDevice`:零状态占位类型,提供 `lo` 接口标识;`recv()`/waker 注册无实际工作,真实回环在 `Router::dispatch()` 的快速路径中完成。 - `EthernetDevice`:维护 ARP 邻居表(`NEIGHBOR_TTL = 300s`)、pending packet 队列、Ethernet 帧封装/解析,并对接 IRQ 适配。支持 IRQ 驱动和 OOB 驱动两种 RX 模式。 diff --git a/docs/docs/architecture/net/flows.md b/docs/docs/architecture/net/flows.md index 62da27d7fe..073d18acb5 100644 --- a/docs/docs/architecture/net/flows.md +++ b/docs/docs/architecture/net/flows.md @@ -11,7 +11,7 @@ sidebar_label: "运行时流程" ## 1. 初始化流程 -入口 `init_network()` 在 [net/ax-net/src/lib.rs#L125](net/ax-net/src/lib.rs#L125)。 +入口 `init_network()` 在 [net/ax-net/src/lib.rs](net/ax-net/src/lib.rs)。 ```mermaid sequenceDiagram @@ -58,7 +58,7 @@ sequenceDiagram ### 步骤详解 -**步骤 1 — 配置校验**([lib.rs#L132-L168](net/ax-net/src/lib.rs#L132-L168)) +**步骤 1 — 配置校验**([lib.rs](net/ax-net/src/lib.rs)) 对每个 `InterfaceConfig`: @@ -71,7 +71,7 @@ sequenceDiagram 不满足则 `panic!`(启动阶段配置错误应 fail-fast)。 -**步骤 2 — 创建 loopback**([lib.rs#L184-L201](net/ax-net/src/lib.rs#L184-L201)) +**步骤 2 — 创建 loopback**([lib.rs](net/ax-net/src/lib.rs)) ```rust let lo_id = InterfaceId::LOOPBACK; // InterfaceId(1) @@ -82,30 +82,30 @@ router.add_rule(Rule::new(lo_ip.into(), None, lo_dev, lo_id, lo_ip.address().int Loopback 设备注册后,立即安装直连路由 `127.0.0.0/8 → lo_dev`,源地址 `127.0.0.1`,metric 0。`LoopbackDevice` 是零状态占位类型,真正的回环数据路径在 `Router::dispatch()` 中以快速路径实现(见 [loopback 快速路径](architecture.md#loopback-快速路径))。 -**步骤 3 — 注册 Ethernet 设备**([lib.rs#L205-L292](net/ax-net/src/lib.rs#L205-L292)) +**步骤 3 — 注册 Ethernet 设备**([lib.rs](net/ax-net/src/lib.rs)) 对每个 driver 提供的 `EthernetDevice`(顺序由 `net_devs.drain(..)` 决定): 1. 计算 `default_name = "eth{order}"`。 -2. 用 `find_interface_config()` 匹配 `InterfaceConfig`。匹配方式由 `InterfaceMatcher` 决定:`ByOrder`、`ByMac` 或 `ByDriverName`([lib.rs#L328-L356](net/ax-net/src/lib.rs#L328-L356))。如果有多个 config 匹配同一设备则 panic。 +2. 用 `find_interface_config()` 匹配 `InterfaceConfig`。匹配方式由 `InterfaceMatcher` 决定:`ByOrder`、`ByMac` 或 `ByDriverName`([lib.rs](net/ax-net/src/lib.rs))。如果有多个 config 匹配同一设备则 panic。 3. 检查接口名冲突。 4. 分配 `InterfaceId`:`order + 2`(0 保留给 placeholder,1 是 loopback)。 5. 调用 `router.add_device()` 将 `EthernetDevice` 包装为 `DeviceHandle`,注册到 `Router::devices` 列表。 6. **静态接口**:调用 `router.set_ipv4_config()` 安装直连路由和默认路由(如果有 gateway)。 7. **DHCP 接口**:推入 `dhcp_ifaces` 向量,稍后在 `Service` 中启用。 -匹配完成后,检查 `used_configs`:如果某个配置没有匹配到任何设备,则 panic([lib.rs#L294-L301](net/ax-net/src/lib.rs#L294-L301))。 +匹配完成后,检查 `used_configs`:如果某个配置没有匹配到任何设备,则 panic([lib.rs](net/ax-net/src/lib.rs))。 -**步骤 4 — 启动 worker 线程**([lib.rs#L305-L306](net/ax-net/src/lib.rs#L305-L306)) +**步骤 4 — 启动 worker 线程**([lib.rs](net/ax-net/src/lib.rs)) ```rust router.start_rx_workers(); // 每个 Ethernet DeviceHandle 一个 RX worker router.start_tx_workers(); // 每个 Ethernet DeviceHandle 一个 TX worker ``` -RX worker 入口 `device_rx_worker`([router.rs#L596-L626](net/ax-net/src/router.rs#L596-L626)),TX worker 入口 `device_tx_worker`([router.rs#L584-L596](net/ax-net/src/router.rs#L584-L596))。Loopback 不需要 worker。 +RX worker 入口 `device_rx_worker`([router.rs](net/ax-net/src/router.rs)),TX worker 入口 `device_tx_worker`([router.rs](net/ax-net/src/router.rs))。Loopback 不需要 worker。 -**步骤 5 — 构建 Service 和控制面**([lib.rs#L308-L323](net/ax-net/src/lib.rs#L308-L323)) +**步骤 5 — 构建 Service 和控制面**([lib.rs](net/ax-net/src/lib.rs)) ```rust let control = Arc::new(NetControl::new(interfaces, routes, dns)); @@ -120,7 +120,7 @@ service.iface.update_ip_addrs(|ip_addrs| { DHCP 接口的 IP 在 ACK 后由 `commit_network_state()` → `set_interface_ipv4()` 动态注入。 -**步骤 6 — 注册全局单例并启动 poll worker**([lib.rs#L325-L332](net/ax-net/src/lib.rs#L325-L332)) +**步骤 6 — 注册全局单例并启动 poll worker**([lib.rs](net/ax-net/src/lib.rs)) ```rust NET_CONTROL.call_once(|| control); @@ -129,17 +129,17 @@ get_service().register_device_waker(&NET_POLL_DEVICE_WAKER); ax_task::spawn_with_name(net_poll_worker, "net-poll".to_owned()); ``` -`NET_POLL_DEVICE_WAKER` 是一个 `NetPollWake` 实例,其 `wake()` 直接调用 `request_poll()`([lib.rs#L455-L464](net/ax-net/src/lib.rs#L455-L464))。这样当设备 driver 有新数据可读时,能直接唤醒 net-poll worker。 +`NET_POLL_DEVICE_WAKER` 是一个 `NetPollWake` 实例,其 `wake()` 直接调用 `request_poll()`([lib.rs](net/ax-net/src/lib.rs))。这样当设备 driver 有新数据可读时,能直接唤醒 net-poll worker。 -**步骤 7 — DHCP bootstrap 等待**([lib.rs#L333-L335](net/ax-net/src/lib.rs#L333-L335)) +**步骤 7 — DHCP bootstrap 等待**([lib.rs](net/ax-net/src/lib.rs)) ```rust if dhcp_enabled { wait_for_dhcp_bootstrap(); } ``` -`wait_for_dhcp_bootstrap()`([lib.rs#L613-L624](net/ax-net/src/lib.rs#L613-L624))循环最多 `DHCP_BOOTSTRAP_ATTEMPTS` 次,每次 `request_poll()` + `sleep(DHCP_BOOTSTRAP_POLL_INTERVAL)`,检查 `dhcp_configured()` 是否为 true。 +`wait_for_dhcp_bootstrap()`([lib.rs](net/ax-net/src/lib.rs))循环最多 `DHCP_BOOTSTRAP_ATTEMPTS` 次,每次 `request_poll()` + `sleep(DHCP_BOOTSTRAP_POLL_INTERVAL)`,检查 `dhcp_configured()` 是否为 true。 -关键策略:**只要任一 DHCP 接口配置成功就返回**([service.rs#L490-L496](net/ax-net/src/service.rs#L490-L496))。这避免了一个断网的 DHCP 网卡阻塞整个系统启动。 +关键策略:**只要任一 DHCP 接口配置成功就返回**([service.rs](net/ax-net/src/service.rs))。这避免了一个断网的 DHCP 网卡阻塞整个系统启动。 超时后只打印 warning,不 panic。未配置的 DHCP 接口后续仍可在运行时异步完成配置。 @@ -186,12 +186,12 @@ sequenceDiagram Smol->>Router: RxToken::consume(payload) Smol->>Smol: TCP/UDP/ICMP state machine Smol-->>PollW: readiness changes - PollW->>PollW: wake socket waiters (outside locks) + Smol-->>Sock: wake registered socket wakers / PollSet waiters ``` ### 步骤详解 -**步骤 1 — RX worker 唤醒**([router.rs#L596-L626](net/ax-net/src/router.rs#L596-L626)) +**步骤 1 — RX worker 唤醒**([router.rs](net/ax-net/src/router.rs)) RX worker 在 `device.rx_wake.wait()` 上阻塞。设备 driver 收到 IRQ 或被其他路径唤醒后,调用 `rx_wake.notify_one()` 唤醒 worker。 @@ -214,7 +214,7 @@ while rx_buffer.is_empty() // 释放设备锁 ``` -`EthernetDevice::recv()`([device/ethernet.rs](net/ax-net/src/device/ethernet.rs#L230))内部流程: +`EthernetDevice::recv()`([device/ethernet.rs](net/ax-net/src/device/ethernet.rs))内部流程: 1. 从 driver 的 RX ring 取一帧。 2. 用 `EthernetFrame::new_unchecked()` 解析帧头。 @@ -245,7 +245,7 @@ while let Ok((interface_id, packet)) = rx_buffer.dequeue() { **步骤 4 — Router drain 共享 RX 队列** -`net-poll` worker 被唤醒后进入 `poll_until_idle()` → `Service::poll()`。`Service::poll()` 首先调用 `Router::poll()`([router.rs#L434-L455](net/ax-net/src/router.rs#L434-L455)): +`net-poll` worker 被唤醒后进入 `poll_until_idle()` → `Service::poll()`。`Service::poll()` 首先调用 `Router::poll()`([router.rs](net/ax-net/src/router.rs)): ```rust while !self.rx_buffer.is_full() { @@ -261,15 +261,15 @@ while !self.rx_buffer.is_full() { } ``` -`Router.rx_buffer` 是 smoltcp 的 `phy::Device` RX token 来源。smoltcp 在 `Interface::poll()` 中通过 `Router::receive()` 从 `rx_buffer` 取出一个包构造 `RxToken`([router.rs#L680-L700](net/ax-net/src/router.rs#L680-L700))。 +`Router.rx_buffer` 是 smoltcp 的 `phy::Device` RX token 来源。smoltcp 在 `Interface::poll()` 中通过 `Router::receive()` 从 `rx_buffer` 取出一个包构造 `RxToken`([router.rs](net/ax-net/src/router.rs))。 **步骤 5 — TCP SYN 预探测** -`snoop_tcp_packet()`([router.rs#L640-L678](net/ax-net/src/router.rs#L640-L678))在交付 smoltcp 前解析 IP 头和 TCP 头: +`snoop_tcp_packet()`([router.rs](net/ax-net/src/router.rs))在交付 smoltcp 前解析 IP 头和 TCP 头: - 如果是 `SYN && !ACK`(连接首包),调用 `LISTEN_TABLE.incoming_tcp_packet(src, dst, sockets)`。 - `LISTEN_TABLE` 检查是否有 socket 在 `(src_ip, dst_port)` 上 listen。 -- 如果是,在 `SocketSet` 中预创建一个 `smoltcp::socket::tcp::Socket`,使其处于 `SYN-RECEIVED` 状态。 +- 如果是,在 `SocketSet` 中预创建一个处于 listen 状态的 `smoltcp::socket::tcp::Socket` 并放入 `ListenTable` 的 `syn_queue`。随后 smoltcp 在 `Interface::poll()` 中消费这个 SYN,才把该 child socket 推进到 `SynReceived`。 - 这样当 smoltcp 正式处理这个 SYN 时,已经有 socket 接收它,避免 SYN 被丢弃。 **步骤 6 — smoltcp 协议处理** @@ -335,7 +335,7 @@ sequenceDiagram **步骤 1 — Socket 层写入** -TCP socket 的 `send()`([tcp.rs#L489-L514](net/ax-net/src/tcp.rs#L489-L514)): +TCP socket 的 `send()`([tcp.rs](net/ax-net/src/tcp.rs)): ```rust fn send(&self, mut src: impl Read, options: SendOptions) -> AxResult { @@ -360,7 +360,7 @@ fn send(&self, mut src: impl Read, options: SendOptions) -> AxResult { } ``` -`send_poller_with` 是一个通用 poller:在闭包返回 `WouldBlock` 时注册 waker 并让出任务,直到有 `IoEvents::OUT` 事件再次唤醒。 +`send_poller_with` 是通用 poller 入口:它把 socket 操作闭包交给 `poll_io()`。闭包返回 `WouldBlock` 时,由具体 socket 的 `Pollable::register()` 注册 waker,然后任务挂起,直到 `IoEvents::OUT` readiness、timeout 或其他错误使任务重新运行。 数据写入 smoltcp socket 的 TX buffer 后,**socket 层不直接发送**。发送发生在 smoltcp `Interface::poll()` 时。 @@ -368,7 +368,7 @@ fn send(&self, mut src: impl Read, options: SendOptions) -> AxResult { 在 `Service::poll()` 中,`self.iface.poll()` 驱动 TCP/UDP 状态机。当 socket 有待发送数据且发送窗口允许时,smoltcp 构造 IP 包并通过 `phy::Device` 的 `TxToken::consume()` 输出。 -`Router` 实现了 smoltcp 的 `phy::Device` trait,`transmit()` 返回 `TxToken`([router.rs#L660-L678](net/ax-net/src/router.rs#L660-L678)): +`Router` 实现了 smoltcp 的 `phy::Device` trait,`transmit()` 返回 `TxToken`([router.rs](net/ax-net/src/router.rs)): ```rust fn transmit(&mut self, _timestamp: Instant) -> Option> { @@ -392,9 +392,10 @@ while let Ok((_, packet)) = self.tx_buffer.dequeue() { let dst_addr = IpAddress::Ipv4(packet.dst_addr()); if packet.dst_addr().is_broadcast() { // 广播:发往所有 Ethernet 设备(不含 loopback) + let buf = packet.into_inner(); for dev in &self.devices { if dev.interface_id != InterfaceId::LOOPBACK { - poll_next |= dev.enqueue_tx(dst_addr, packet.into_inner()); + poll_next |= dev.enqueue_tx(dst_addr, buf); } } } else { @@ -428,9 +429,9 @@ while let Ok((_, packet)) = self.tx_buffer.dequeue() { **步骤 4 — 推入 per-device TX 队列** -`DeviceHandle::enqueue_tx()` 将 `TxPacket { next_hop, bytes }` 推入 `tx_queue`(同样是 `BoundedPacketQueue`)。如果队列满,返回 `true` 表示需要重新 poll(`poll_next`)。 +`DeviceHandle::enqueue_tx()` 将 `TxPacket { next_hop, bytes }` 推入 `tx_queue`(同样是 `BoundedPacketQueue`)。成功入队后唤醒该设备的 TX worker 并返回 `true`,表示 dispatch 后可以继续 poll;如果包超过 MTU 或队列已满,则打印 warning、丢包并返回 `false`。 -**步骤 5 — TX worker 发送**([router.rs#L584-L596](net/ax-net/src/router.rs#L584-L596)) +**步骤 5 — TX worker 发送**([router.rs](net/ax-net/src/router.rs)) ```rust fn device_tx_worker(device: Arc) { @@ -451,7 +452,7 @@ fn device_tx_worker(device: Arc) { 2. 如果找到:封装 Ethernet 帧(`src=自身MAC, dst=邻居MAC, ethertype=IPv4`),调用 `driver.send()`。 3. 如果没找到:请求 ARP(见 ARP 流程),包暂存在 `pending_packets` 队列。 -**步骤 6 — ARP 完成后发送暂存包**([device/ethernet.rs#L330](net/ax-net/src/device/ethernet.rs#L330)) +**步骤 6 — ARP 完成后发送暂存包**([device/ethernet.rs](net/ax-net/src/device/ethernet.rs)) ARP reply 到达后,`process_arp()` 查到邻居 MAC,drain `pending_packets` 队列: @@ -489,16 +490,14 @@ stateDiagram-v2 [*] --> NoEntry: 包待发, 无邻居 NoEntry --> Pending: request_arp() Pending --> Resolved: 收到 ARP Reply - Pending --> Pending: 超时重发 (retry < MAX) - Pending --> Expired: 超过最大重试 + Pending --> Pending: 超时后下一次发送重发 ARP Resolved --> Resolved: TTL 内正常收发 Resolved --> NoEntry: TTL 过期 (300s) - Expired --> [*] ``` ### 步骤详解 -**ARP 请求**([device/ethernet.rs#L278-L310](net/ax-net/src/device/ethernet.rs#L278-L310)) +**ARP 请求**([device/ethernet.rs](net/ax-net/src/device/ethernet.rs)) 当 `EthernetDevice::send()` 发现下一跳 IP 不在 `neighbors` 表中时,调用 `request_arp()`: @@ -507,7 +506,9 @@ stateDiagram-v2 3. 将 `(target_ip, PendingNeighbor { requested_at: now })` 插入 `pending_neighbors`。 4. 将待发送的 IP 包存入 `pending_packets` 有界队列。 -**ARP 响应处理**([device/ethernet.rs#L312-L405](net/ax-net/src/device/ethernet.rs#L312-L405)) +当前实现没有最大 ARP 重试计数,也没有独立 `Expired` 状态。只要后续仍有包要发往该 next hop,`EthernetDevice::send()` 会在 `ARP_REQUEST_RETRY=1s` 到期后再次发送 ARP request。 + +**ARP 响应处理**([device/ethernet.rs](net/ax-net/src/device/ethernet.rs)) 收到 ARP 帧时 `process_arp()`: @@ -545,13 +546,12 @@ sequenceDiagram Sock->>Sock: state: Idle → Connecting Sock->>SockSet: smoltcp socket.connect(remote, local) Sock->>Poll: request_poll() - loop poll_connect() - Sock->>SockSet: socket.state() == Connected? - alt Connected - Sock-->>App: Ok - else still connecting - Sock->>Poll: register waker + request_poll() → yield - end + Sock->>SockSet: poll_connect(): socket.state() == Connected? + alt Connected + Sock-->>App: Ok + else still connecting + Sock->>Poll: Pollable::register() + request_poll() → yield + Note over Sock: poll_io() resumes and retries closure after readiness/timeout end ``` @@ -559,7 +559,7 @@ sequenceDiagram 1. `start_connect()` 查路由获取源地址,在 smoltcp socket 上调用 `connect()`。 2. `request_poll()` 唤醒 net-poll worker 处理 SYN 发送和 SYN-ACK 接收。 -3. 在 `send_poller` 闭包中循环 `poll_connect()`,每次 `request_poll()` 触发协议栈推进。 +3. `send_poller` 把一次 `poll_connect()` 检查交给 `poll_io()`;如果返回 `WouldBlock`,由 `Pollable::register()` 注册 waker,任务挂起,ready/timeout 后重新执行闭包。 4. 连接成功返回 `Ok(())`,连接失败返回对应错误(`ConnectionRefused` 等)。 ### 5.2 Listen + Accept @@ -587,11 +587,11 @@ fn accept(&self) -> AxResult { } ``` -Accept 依赖 RX 流程中的 `snoop_tcp_packet()` 预创建 socket。当 SYN 到达时,listen table 已在 SocketSet 中创建了 SYN-RECEIVED socket。三次握手完成后,`LISTEN_TABLE.accept()` 将该 socket 从 listen pool 移出并返回。 +Accept 依赖 RX 流程中的 `snoop_tcp_packet()` 预创建 child socket。当 SYN 到达时,listen table 先在 `SocketSet` 中创建一个 listen 状态的 child socket 并推入 `syn_queue`;随后 smoltcp 消费 SYN,把 child socket 推进到 `SynReceived` 并发送 SYN-ACK。三次握手完成后,`LISTEN_TABLE.accept()` 将该 socket 从 listen pool 移出并返回。 ### 5.3 Send / Recv -见 TX 流程中的 TCP send 部分。TCP `recv()`([tcp.rs#L516-L561](net/ax-net/src/tcp.rs#L516-L561))在 `recv_poller_with` 中循环读取 smoltcp socket 的 RX buffer,直到用户 buffer 填满或 RX 队列空。 +见 TX 流程中的 TCP send 部分。TCP `recv()`([tcp.rs](net/ax-net/src/tcp.rs))在 `recv_poller_with` 中循环读取 smoltcp socket 的 RX buffer,直到用户 buffer 填满或 RX 队列空。 --- @@ -599,7 +599,7 @@ Accept 依赖 RX 流程中的 `snoop_tcp_packet()` 预创建 socket。当 SYN ### 6.1 Send -UDP `send()`([udp.rs#L236-L353](net/ax-net/src/udp.rs#L236-L353))比 TCP 多了几个分支: +UDP `send()`([udp.rs](net/ax-net/src/udp.rs))比 TCP 多了几个分支: 1. **MSG_MORE corking**:如果 `SendFlags::MORE` 被设置,数据缓存在 `self.cork` 中而非立即发送。Cork buffer 上限 `UDP_TX_BUF_LEN`。后续不带 MORE 的 send 会 flush cork buffer。 2. **目标地址解析**:connected socket 用 `remote_endpoint()`,unconnected socket 用 `options.to`。 @@ -610,7 +610,7 @@ UDP `send()`([udp.rs#L236-L353](net/ax-net/src/udp.rs#L236-L353))比 TCP 多 ### 6.2 Recv -UDP `recv()`([udp.rs#L364-L425](net/ax-net/src/udp.rs#L364-L425)): +UDP `recv()`([udp.rs](net/ax-net/src/udp.rs)): ```rust fn recv(&self, mut dst: impl Write, mut options: RecvOptions) -> AxResult { @@ -634,7 +634,12 @@ fn recv(&self, mut dst: impl Write, mut options: RecvOptions) -> AxResult } ``` -Connected UDP socket 只接受来自 peer 的包,其他包返回 `WouldBlock`。 +Connected UDP socket 只接受来自 peer 的包,但 `MSG_PEEK` 与普通 `recv()` 的消费语义不同: + +- `MSG_PEEK`:如果队首包来自非 peer,返回 `WouldBlock`,不消费该包。 +- 普通 `recv()`:先从 smoltcp UDP 队列取出一个 datagram,再检查 peer;如果该包来自非 peer,则丢弃该包并返回 `WouldBlock`,不会继续扫描队列后面的 datagram。 + +这是当前实现的保守语义,避免为 UDP 维护额外 deferred queue。需要“跳过非 peer 并继续扫描”时,应在 UDP 层显式设计一个有界暂存策略,不能复用 raw socket 的 wire-packet deferred 格式。 --- @@ -675,7 +680,7 @@ sequenceDiagram ### 步骤详解 -**DhcpState 结构**([service.rs#L242-L275](net/ax-net/src/service.rs#L242-L275)) +**DhcpState 结构**([service.rs](net/ax-net/src/service.rs)) ```rust struct DhcpState { @@ -695,7 +700,7 @@ struct DhcpState { } ``` -**状态机转换**([service.rs#L313-L402](net/ax-net/src/service.rs#L313-L402)) +**状态机转换**([service.rs](net/ax-net/src/service.rs)) `process_packet()` 按 `(phase, message_type)` 驱动: @@ -708,13 +713,13 @@ struct DhcpState { **DHCP 包构造** -`poll_packet()`([service.rs#L434-L460](net/ax-net/src/service.rs#L434-L460))在 retry 到期时构造 DHCP Discover 或 Request: +`poll_packet()`([service.rs](net/ax-net/src/service.rs))在 retry 到期时构造 DHCP Discover 或 Request: -- 使用 `dhcp_transaction_id()`(由 MAC 派生,[service.rs#L694](net/ax-net/src/service.rs#L694))。 +- 使用 `dhcp_transaction_id()`(由 MAC 派生,[service.rs](net/ax-net/src/service.rs))。 - `DHCP_PARAMETER_REQUEST_LIST = [1, 3, 6, 42]`(subnet mask, router, DNS, NTP)。 - 指数退避:`retry` 最大位移 `DHCP_MAX_RETRY_SHIFT = 4`。 -**ACK 后状态提交**([service.rs#L534-L612](net/ax-net/src/service.rs#L534-L612)) +**ACK 后状态提交**([service.rs](net/ax-net/src/service.rs)) `handle_dhcp_event(DhcpEvent::Configured)` → `commit_network_state()`: @@ -724,7 +729,7 @@ struct DhcpState { **Bootstrap 等待策略** -`dhcp_configured()`([service.rs#L490-L496](net/ax-net/src/service.rs#L490-L496))只需**任一** DHCP 接口配置成功即返回 true。这确保一个断网的网卡不会阻塞启动。 +`dhcp_configured()`([service.rs](net/ax-net/src/service.rs))只需**任一** DHCP 接口配置成功即返回 true。这确保一个断网的网卡不会阻塞启动。 --- @@ -762,7 +767,7 @@ sequenceDiagram ### 步骤详解 -`dns_query_timeout()`([lib.rs#L494-L510](net/ax-net/src/lib.rs#L494-L510)): +`dns_query_timeout()`([lib.rs](net/ax-net/src/lib.rs)): 1. 获取 DNS server 列表(DHCP + 静态 + fallback,去重排序)。 2. 用 `select_route()` 过滤掉不可路由的 server。 @@ -771,7 +776,7 @@ sequenceDiagram 5. 循环 `request_poll()` + `get_query_result()`,超时由 monotonic clock 判断。 6. `DnsSocketGuard` 的 `Drop` 自动移除 socket,避免泄漏。 -超时常量:`DNS_DEFAULT_TIMEOUT = 5s`([lib.rs#L488](net/ax-net/src/lib.rs#L488))。 +超时常量:`DNS_DEFAULT_TIMEOUT = 5s`([lib.rs](net/ax-net/src/lib.rs))。 --- @@ -779,7 +784,7 @@ sequenceDiagram ### request_poll() 轻量唤醒 -所有热路径(socket send/recv/connect/drop、设备 RX/TX、timer)只调用 `request_poll()`([lib.rs#L408-L411](net/ax-net/src/lib.rs#L408-L411)): +所有热路径(socket send/recv/connect/drop、设备 RX/TX、timer)只调用 `request_poll()`([lib.rs](net/ax-net/src/lib.rs)): ```rust pub fn request_poll() { @@ -807,7 +812,7 @@ fn net_poll_worker() { } ``` -1. `next_poll_delay()`([lib.rs#L437-L460](net/ax-net/src/lib.rs#L437-L460))向 smoltcp 询问下一次 poll 截止时间(TCP retransmit、DHCP retry、keep-alive 等),结合 monotonic clock 计算睡眠时长。空闲时回退到 100ms。 +1. `next_poll_delay()`([lib.rs](net/ax-net/src/lib.rs))向 smoltcp 询问下一次 poll 截止时间(TCP retransmit、DHCP retry、keep-alive 等),结合 monotonic clock 计算睡眠时长。空闲时回退到 100ms。 2. `wait_timeout_until()` 等待 wake 或超时。 3. 被 wake 唤醒(非超时)时清除 `NET_POLL_REQUESTED` 标志。 4. 进入 `poll_until_idle()`。 @@ -848,6 +853,8 @@ fn poll_once() -> bool { 1. Service lock (get_service()) 2. SocketSet lock (SOCKET_SET.inner.lock()) ├─ Router::poll() — drain RX queue → rx_buffer,snoop DHCP client/server + ├─ handle_dhcp_event() — DHCP ACK/NAK 后提交接口、路由、DNS 更新 + ├─ DHCP server replies — SoftAP DHCP server 通过 Router::send_on_device() 发回复 ├─ smoltcp Interface::poll(timestamp, router, sockets) │ ├─ Router::receive() / Router::transmit() (phy::Device trait) │ └─ TCP/UDP/ICMP state machine @@ -856,7 +863,7 @@ fn poll_once() -> bool { └─ Router::dispatch(timestamp, sockets) — tx_buffer → 设备/loopback 快速路径 3. 释放 SocketSet lock 4. 释放 Service lock -5. wake socket waiters (在锁外部) +5. waker 的实际唤醒由 PollSet/smoltcp socket waker 路径触发;socket poller 被唤醒后会重新执行原闭包 ``` **严格禁止**: @@ -867,19 +874,23 @@ fn poll_once() -> bool { ### Waker 注册路径 -Socket 的 `register()` 将 task waker 注册到 `general.waker` 中。当 smoltcp poll 改变 socket readiness 后,`Service::poll()` 返回,poll worker 在释放锁后调用各 socket 的 waker: +Socket readiness 由各 backend 的 `Pollable::register()` 注册。`GeneralOptions` 不保存全局 `general.waker`,它只提供 `register_waker()` 辅助,把同一个 task waker 注册到符合 `DeviceBinding` 的设备唤醒路径。 + +典型路径: ``` socket.send/recv/connect returns WouldBlock - → general.recv_poller / send_poller registers waker + → poll_io() 调用 socket Pollable::register() + → TCP/UDP/raw 将 waker 注册到 smoltcp socket recv/send waker 或本地 PollSet + → GeneralOptions::register_waker() 将 waker 注册到匹配 DeviceBinding 的设备路径 → task yields → net-poll worker calls poll_until_idle() → smoltcp updates socket readiness - → poll worker wakes registered wakers + → smoltcp/PollSet/device waker 唤醒等待任务 → task resumes, retries send/recv ``` -`NetPollWake` 实现了 `core::task::Wake` trait([lib.rs#L455-L464](net/ax-net/src/lib.rs#L455-L464)),使设备 driver 可以通过标准 waker 接口唤醒 poll worker。 +`NetPollWake` 实现了 `core::task::Wake` trait([lib.rs](net/ax-net/src/lib.rs)),使设备 driver 可以通过标准 waker 接口唤醒 poll worker。 --- @@ -1015,7 +1026,7 @@ Vsock stream 的 send/recv 直接从 `Connection` 的 ring buffer 读写数据 ## 12. ICMP Loopback Echo 流程 -`RawSocket::send()` 对 loopback 地址有特殊处理([raw.rs](net/ax-net/src/raw.rs#L188-L203)): +`RawSocket::send()` 对 loopback 地址有特殊处理([raw.rs](net/ax-net/src/raw.rs)): 1. 检查目的地址是否是 loopback(`127.0.0.1` 或 `::1`)。 2. 如果是,**不经过 smoltcp 发送**。而是解析 ICMP echo request(type=8, code=0)。 @@ -1025,6 +1036,13 @@ Vsock stream 的 send/recv 直接从 `Connection` 的 ring buffer 读写数据 这允许 `ping 127.0.0.1` 在不需要完整 IP stack 回环的情况下正确工作。 +Raw socket 还有一个容易误读的 receive 细节:smoltcp raw socket 队列中保存的是完整 IP packet,而 public raw API 通常交付协议 payload。为了同时支持 connected peer 过滤和 `MSG_PEEK`,`raw.rs` 使用两个本地暂存点: + +- `loopback_rx`:保存本地生成的 loopback reply,`recv()` 优先消费。 +- `deferred_rx`:保存被 peer 过滤暂存下来的**完整 wire packet**,而不是已经剥离后的 payload。 + +因此 raw deferred packet 在再次交付前会重新经过 IP 解析,再调用统一的 `deliver_packet()`。这个约束很重要:如果 deferred queue 存 payload 而不是完整 IP packet,后续 `MSG_PEEK`/peer filtering 会把 packet 格式解释错。 + --- ## 13. TCP Accept 完整流程图 @@ -1049,7 +1067,7 @@ sequenceDiagram ListenTbl->>ListenTbl: syn_queue.push_back(PendingTcp) Router->>Smol: Interface::poll() - Smol->>Smol: process SYN → SYN-RECEIVED state + Smol->>Smol: process SYN → SynReceived state Smol->>Smol: send SYN-ACK (via TX path) Remote->>Eth: TCP ACK (三次握手完成) diff --git a/docs/docs/architecture/net/integration.md b/docs/docs/architecture/net/integration.md index a9503229d7..fe74f0d564 100644 --- a/docs/docs/architecture/net/integration.md +++ b/docs/docs/architecture/net/integration.md @@ -12,11 +12,11 @@ sidebar_label: "系统集成" `ax-runtime` 是网络栈初始化的主要入口。它负责: - 在平台设备 probe 完成后收集 Ethernet 设备。 -- 将 `rd-net` 设备包装为 `ax-net` 的 `EthernetDriver`(由 `RdNetDriver` 实现,[driver.rs#L128](net/ax-net/src/device/driver.rs#L128))。 -- 注册网络 IRQ adapter(`set_ethernet_irq_registrar()`,[ethernet.rs#L80-L82](net/ax-net/src/device/ethernet.rs#L80-L82))。 +- 将 `rd-net` 设备包装为 `ax-net` 的 `EthernetDriver`(由 `RdNetDriver` 实现,[driver.rs](net/ax-net/src/device/driver.rs))。 +- 注册网络 IRQ adapter(`set_ethernet_irq_registrar()`,[ethernet.rs](net/ax-net/src/device/ethernet.rs))。 - 构造结构化 `NetworkConfig`。 -- 调用 `ax_net::init_network(net_devs, config)`([lib.rs#L125](net/ax-net/src/lib.rs#L125))。 -- 在 `vsock` feature 下调用 `ax_net::init_vsock(vsock_devs)`([lib.rs#L367](net/ax-net/src/lib.rs#L367))。 +- 调用 `ax_net::init_network(net_devs, config)`([lib.rs](net/ax-net/src/lib.rs))。 +- 在 `vsock` feature 下调用 `ax_net::init_vsock(vsock_devs)`([lib.rs](net/ax-net/src/lib.rs))。 `ax-runtime` 不负责: diff --git a/docs/docs/architecture/net/overview.md b/docs/docs/architecture/net/overview.md index 79a6d5d752..2eac0315a8 100644 --- a/docs/docs/architecture/net/overview.md +++ b/docs/docs/architecture/net/overview.md @@ -4,7 +4,7 @@ sidebar_label: "概览" --- # 网络栈概览 -TGOSKits 的网络能力收敛在 `net/ax-net`。它是 ArceOS、StarryOS 和 Axvisor 共享的统一网络栈,向上提供 TCP、UDP、raw socket、Unix domain socket、可选 vsock、DNS、DHCP、ARP、接口查询和 readiness/poll 能力,向下通过 `rd-net` / `rdif-eth` 消费 Ethernet 设备。 +TGOSKits 的网络能力收敛在 `net/ax-net`。它是 ArceOS、StarryOS 和 Axvisor 共享的统一网络栈,向上提供 TCP、UDP、raw socket、Unix domain socket、可选 vsock、DNS、DHCP、ARP、接口查询和 readiness/poll 能力,向下通过 `EthernetDriver` 能力边界适配真实网卡;当前标准实现是基于 `rd-net` 的 `RdNetDriver`。 ## 源码 源码位于 [net/ax-net/src/](net/ax-net/src/),入口 [lib.rs](net/ax-net/src/lib.rs)。Socket backend 包括 IP 类([tcp.rs](net/ax-net/src/tcp.rs)、[udp.rs](net/ax-net/src/udp.rs)、[raw.rs](net/ax-net/src/raw.rs),基于 smoltcp)、[unix/](net/ax-net/src/unix/)(自包含 stream/dgram,不经 smoltcp)和可选的 [vsock/](net/ax-net/src/vsock/)(基于 `rdif-vsock` 驱动,含 connection manager 与 ring buffer)。 @@ -64,7 +64,7 @@ TGOSKits 的网络能力收敛在 `net/ax-net`。它是 ArceOS、StarryOS 和 Ax | 线程 | 职责 | 阻塞点 | | --- | --- | --- | -| `net-poll` worker | 驱动 smoltcp poll、DHCP 状态机、ARP、DNS、TX dispatch | `NET_POLL_WAKE.wait_timeout_until()` | +| `net-poll` worker | 驱动 smoltcp poll、DHCP 状态机、DNS socket 和 TX dispatch;ARP 解析由发送路径中的 Ethernet 设备完成 | `NET_POLL_WAKE.wait_timeout_until()` | | `{ifname}-rx` worker | 每网卡一个,从 driver 收包压入 `RouterQueues::rx` 有界队列 | `device.rx_wake.wait()` | | `{ifname}-tx` worker | 每网卡一个,从 `DeviceHandle::tx_queue` 取包调用 driver send | `device.tx_wake.wait_until()` | | 调用者线程 | 应用/内核线程调用 socket API | `StateLock::lock()`、`block_on(poll_io())` | @@ -78,8 +78,8 @@ TGOSKits 的网络能力收敛在 `net/ax-net`。它是 ArceOS、StarryOS 和 Ax ``` SERVICE (Mutex) → SOCKET_SET.inner (Mutex) - → LISTEN_TABLE.tcp[port] (Mutex) - → DeviceHandle.inner (Mutex) + → TCP_BOUND_PORTS (Mutex>) + → LISTEN_TABLE.tcp[port] (Mutex) → NET_CONTROL.state (RwLock) ``` @@ -87,7 +87,7 @@ SERVICE (Mutex) - `SERVICE` mutex 保护 smoltcp `Interface` 和 DHCP 状态机,poll 期间独占。 - `NET_CONTROL.state` 是独立 RwLock,接口查询(只读)可以在不持有 `SERVICE` 的情况下进行。 - `ListenTable` 条目锁在 `SOCKET_SET` 锁内获取,保证 accept/snoop 的一致性。 -- 设备锁(`DeviceHandle.inner`)在 poll 路径的最内层获取。 +- 设备锁(`DeviceHandle.inner`)主要由 `{ifname}-rx` / `{ifname}-tx` worker 独立获取。worker 不应在持有设备锁时反向进入 `SERVICE` 或 `SOCKET_SET`,避免设备路径与协议核心互相阻塞。 ## 核心方案 diff --git a/docs/docs/architecture/net/sockets.md b/docs/docs/architecture/net/sockets.md index 9e6c56d7d1..afaba2dc1f 100644 --- a/docs/docs/architecture/net/sockets.md +++ b/docs/docs/architecture/net/sockets.md @@ -62,7 +62,7 @@ pub trait SocketOps: Configurable { } ``` -TCP/UDP 实现完整接口;Raw socket 不实现 `listen`/`accept`;Unix/vsock 有各自独立的 transport trait。 +TCP 实现完整 stream 接口(含 `listen`/`accept`);UDP 实现 datagram 的 bind/connect/send/recv/poll,不支持 `listen`/`accept`;Raw socket 不实现 `listen`/`accept`;Unix/vsock 有各自独立的 transport trait。 ## SocketSetWrapper @@ -86,7 +86,7 @@ pub(crate) struct SocketSetWrapper<'a> { ### UDP 端口仲裁 -`udp_bind_available()` 实现 Linux `SO_REUSEADDR` 语义: +`udp_bind_available()` 实现默认 UDP bind 冲突仲裁。`SO_REUSEADDR` 不在该函数内部处理,而是在 `UdpSocket::bind()` 中通过 `general.reuse_address()` 决定是否跳过 `SOCKET_SET.udp_bind()` 这张 side table: ```rust // wrapper.rs @@ -105,7 +105,7 @@ fn udp_bind_available(binds: &HashMap, key: UdpBindKey | --- | --- | --- | | 精确 bind | `192.168.1.1:53` | 同地址同端口已存在,或 wildcard `0.0.0.0:53` 已存在 | | Wildcard bind | `0.0.0.0:53` | 任何地址已占用同一端口 | -| `SO_REUSEADDR` | — | 跳过所有仲裁 | +| `SO_REUSEADDR` | — | `UdpSocket::bind()` 跳过 wrapper 的 UDP bind side table;smoltcp 自身仍会执行 socket 状态/地址可用性检查 | `udp_port_available()` 暴露给 ephemeral port 分配器使用。 @@ -321,32 +321,26 @@ pub(crate) struct GeneralOptions { ### 核心 Poller -`send_poller_with()` 和 `recv_poller_with()` 是通用的 poll 循环: +`send_poller_with()` 和 `recv_poller_with()` 是通用的阻塞/非阻塞等待入口。当前源码不手写循环,而是把一次 socket 操作闭包交给 `ax_task::future::poll_io()`,再套上 send/recv timeout: ```rust // general.rs pub fn send_poller_with AxResult, T>( - &self, pollable: &P, extra_nb: bool, mut f: F) -> AxResult + &self, pollable: &P, extra_nb: bool, f: F) -> AxResult { - loop { - // 尝试执行 f(),如果成功返回结果 - match f() { - Ok(result) => return Ok(result), - Err(AxError::WouldBlock) => { - // 检查 nonblock 模式 -> 直接返回 WouldBlock - if self.nonblock.load(Relaxed) || extra_nb { return Err(AxError::WouldBlock); } - } - Err(e) => return Err(e), - } - // 注册 waker 到对应设备 - self.register_waker(context.waker()); - // block_on 等待 I/O 事件或超时 - // ... - } + block_on(timeout( + self.send_timeout(), + poll_io( + pollable, + IoEvents::OUT, + self.nonblocking() || extra_nb, + f, + ), + ))? } ``` -流程:`f()` 返回 `WouldBlock` → `register_waker()` 注册到 `Service` → `block_on(poll_io())` 等待 → 超时返回 `TimedOut` → 重新循环。 +流程:`poll_io()` 先调用 `f()`;如果返回 `WouldBlock` 且当前调用不是 nonblocking,则通过 `pollable.register(context, IoEvents::OUT/IN)` 注册 waker,挂起当前任务,等待 readiness 或 timeout 后重试。`GeneralOptions::register_waker()` 只是提供给具体 socket 的 `Pollable::register()` 使用,用于把同一个 waker 注册到符合 `DeviceBinding` 的设备唤醒路径。 `register_waker()` 根据 `DeviceBinding` 选择性注册到匹配的设备: diff --git a/docs/docs/architecture/net/testing.md b/docs/docs/architecture/net/testing.md index 8ca8ef5fdf..565eca8e01 100644 --- a/docs/docs/architecture/net/testing.md +++ b/docs/docs/architecture/net/testing.md @@ -23,7 +23,7 @@ sidebar_label: "测试与限制" - loopback dispatch 直接注入 `Router.rx_buffer`,并在注入前触发 TCP SYN snoop。 - DHCP bootstrap 和 per-interface 状态。 - DHCP server Discover/Request 回复。 -- TCP orphan 在 Closed/Expired/overflow 场景下的回收。 +- TCP orphan 在 Closed、TIME_WAIT/FIN teardown 超时和 overflow 场景下的回收。 - TCP/UDP bind 到具体本地地址后保留接口绑定。 - 未绑定 TCP/UDP connect 按 route decision 选择接口。 @@ -41,17 +41,17 @@ cargo test -p ax-net | --- | --- | --- | | `RouteTable` | 添加/删除/替换规则、排序验证、默认路由查询、`select_route_if` with mock filter | 高 | | `ListenTable` | port 冲突、per-address listen、wildcard 冲突、backlog 上限、`can_listen`/`listen`/`unlisten`、SYN 队列溢出、`can_accept`/`accept` | 高 | -| `SocketSetWrapper` | UDP bind 冲突仲裁(精确/wildcard/SO_REUSEADDR)、add/remove | 中 | +| `SocketSetWrapper` | UDP bind 冲突仲裁(精确/wildcard)、add/remove;`SO_REUSEADDR` 跳过 side table 的行为由 `UdpSocket::bind()` 覆盖 | 中 | | `StateLock` | Idle→Busy CAS、transit 成功/失败回退、状态读取一致性 | 中 | | `GeneralOptions` | nonblock/send_timeout/recv_timeout、device_binding 读写、Atomic 一致性 | 中 | | `TcpSocket` | `connect()` 成功/失败、`bind_device()` 无效接口、`poll_connect()` 状态转换、`tcp_info_snapshot()` 字段 | 高 | | `UdpSocket` | `send()` with MSG_MORE corking、connected/disconnected send、`recv()` truncation | 高 | -| `RawSocket` | `send()` ICMP echo→loopback reply、TTL 读写、IP version 校验 | 中 | +| `RawSocket` | `send()` ICMP echo→loopback reply、TTL 读写、IP version 校验、`MSG_PEEK` 与 `deferred_rx` wire-packet 格式 | 中 | | `UnixSocket` | stream pair send/recv、cmsg 传递、datagram pair、abstract namespace bind/connect | 中 | | `VsockSocket` | bind/listen/connect/accept 状态转换、send/recv | 低(需 `vsock` feature) | | `DhcpState` | Discovering→Offer→Requesting→ACK→Bound 状态机、NAK→reset、`process_packet` 校验 | 中 | | `DhcpServer` | Discover→Offer、Request→Ack、错误 xid/mac/interface 过滤、单客户端租约覆盖 | 中 | -| `orphan` | Closed 立即回收、teardown 超时回收、overflow 只回收 Closed 且保留正在 teardown 的 socket | 高 | +| `orphan` | Closed 立即回收、teardown 超时回收、overflow 只 warn 且保留仍在 teardown 的 socket | 高 | | DNS | `dns_query_timeout` 超时、不可路由 server 过滤、`DnsSocketGuard` drop | 中 | ## 接口路由测试 @@ -134,7 +134,8 @@ Test: bind_device TcpSocket::new().bind_device(valid_id) → Ok Test: SO_REUSEADDR - 两个 TcpSocket 都设 SO_REUSEADDR → 都可以 bind 同一端口 + UDP: 设置 SO_REUSEADDR 后 UdpSocket::bind() 跳过 SocketSetWrapper 的 UDP side table + TCP: 当前 TCP_BOUND_PORTS 仍做 wildcard/specific 地址冲突检查,不能简单断言两个 TCP socket 设 SO_REUSEADDR 后一定可 bind 同一端口 Test: per-address listen listen(127.0.0.1:8080) 与 listen(10.0.2.15:8080) 可以共存 From dbd617de1f0af091df994b57907537bc18254d79 Mon Sep 17 00:00:00 2001 From: ZCShou <72115@163.com> Date: Tue, 16 Jun 2026 12:14:34 +0800 Subject: [PATCH 26/31] docs(net): rewrite architecture docs with design boundaries, source mappings, and expanded test matrix --- docs/docs/architecture/net/api.md | 623 ++++++--- docs/docs/architecture/net/configuration.md | 428 +++--- docs/docs/architecture/net/control.md | 892 ++++++++++--- docs/docs/architecture/net/devices.md | 710 ++++++++-- docs/docs/architecture/net/flows.md | 1286 ++++++------------- docs/docs/architecture/net/integration.md | 345 ++++- docs/docs/architecture/net/sockets.md | 752 ++++++++--- docs/docs/architecture/net/testing.md | 537 +++++--- p.md | 552 ++++++++ 9 files changed, 4128 insertions(+), 1997 deletions(-) create mode 100644 p.md diff --git a/docs/docs/architecture/net/api.md b/docs/docs/architecture/net/api.md index 8f5e26f5d0..00f328cf68 100644 --- a/docs/docs/architecture/net/api.md +++ b/docs/docs/architecture/net/api.md @@ -1,107 +1,340 @@ --- sidebar_position: 6 -sidebar_label: "API 参考" +sidebar_label: "对外接口" --- -# 网络栈 Public API +# 对外接口 -本文汇总 `ax-net` 的正式 public API,并说明哪些 API 属于系统初始化、接口查询、socket、设备适配和 DNS。 +`ax-net` 的 public API 面向三类调用方:启动阶段的 runtime、系统 ABI/socket 层,以及设备驱动适配层。API 设计保持一个原则:外部通过稳定的接口 ID、快照和 trait object 访问网络栈,不直接接触 `Service`、`Router`、smoltcp `SocketSet` 等内部对象。 -## 初始化 API +核心 re-export 定义在 [lib.rs](net/ax-net/src/lib.rs): -定义在 [net/ax-net/src/lib.rs](net/ax-net/src/lib.rs): +```rust +pub use self::{ + config::{ + DeviceBinding, InterfaceConfig, InterfaceFlags, InterfaceId, InterfaceInfo, + InterfaceKind, InterfaceMatcher, Ipv4InterfaceConfig, NetworkConfig, + RouteInfo, StaticIpConfig, + }, + device::{ + ArpEntry, EthernetDeviceList, EthernetDriver, EthernetIrqAction, + EthernetIrqOutcome, EthernetIrqRegistrar, EthernetIrqRegistration, + EthernetIrqRegistrationError, NetDeviceError, NetDeviceResult, + NetIrqEvents, NetRxBuffer, NetTxBuffer, RdNetDriver, + set_ethernet_irq_registrar, + }, + socket::{ + CMsgData, RecvFlags, RecvOptions, SendFlags, SendOptions, + Shutdown, Socket, SocketAddrEx, SocketOps, + }, +}; +``` + +## API 分层 + +public API 按生命周期和调用方分为: + +- 初始化与配置 API:由 `ax-runtime` 或平台初始化代码调用。 +- 运行时查询 API:由系统 ABI、诊断接口、`/proc`、ioctl 等读取网络状态。 +- Socket facade API:由 syscall/socket 层创建并操作具体 socket。 +- Socket option API:由 getsockopt/setsockopt 层转发。 +- 设备驱动 API:由 NIC driver、IRQ registrar、运行期设备注册路径使用。 +- DNS/ARP 辅助 API:由 resolver 和 Linux 兼容层使用。 + +API 边界如下: + +```mermaid +flowchart TB + Runtime["ax-runtime / platform init"] --> Init["init_network() / init_vsock()"] + Drivers["NIC drivers"] --> DriverApi["EthernetDriver / IRQ registrar"] + Syscall["socket syscalls"] --> SocketApi["SocketOps / Socket enum"] + Abi["ioctl / proc / diagnostics"] --> QueryApi["interfaces() / routes / arp"] + Resolver["resolver"] --> DnsApi["dns_query() / dns_servers()"] + + Init --> Internal["Service + NetControl + Router"] + DriverApi --> Internal + SocketApi --> Internal + QueryApi --> Internal + DnsApi --> Internal +``` + +## 初始化与配置 + +初始化 API 构造全局网络栈。它们是全局单例初始化入口,不返回 `Service` 或 `Router` 的可变引用。 + +### NetworkConfig + +`NetworkConfig` 描述启动时的接口配置: + +```rust +pub struct NetworkConfig { + pub interfaces: Vec, + pub default_dns_servers: Vec, +} + +pub struct InterfaceConfig { + pub name: String, + pub match_by: InterfaceMatcher, + pub static_ip: Option, + pub dhcp: bool, + pub metric: u32, + pub dns_servers: Vec, +} +``` + +`InterfaceMatcher` 支持按探测顺序、MAC 或 driver name 匹配设备: + +```rust +pub enum InterfaceMatcher { + ByOrder(usize), + ByMac(EthernetAddress), + ByDriverName(String), +} +``` + +静态 IPv4 配置使用: + +```rust +pub struct StaticIpConfig { + pub ip: Ipv4Addr, + pub prefix_len: u8, + pub gateway: Ipv4Addr, +} +``` + +配置语义: + +- `lo` 由 `ax-net` 固定创建,不通过 `NetworkConfig` 覆盖。 +- 未显式匹配的 Ethernet 设备按默认策略加入接口 registry。 +- `static_ip` 和 `dhcp` 表达互斥配置。 +- `metric` 同时影响路由选择和 DNS server 排序。 +- `default_dns_servers` 是接口级 DNS 不可用时的 fallback 来源。 + +### init_network ```rust pub fn init_network(net_devs: EthernetDeviceList, config: NetworkConfig); -pub fn poll_interfaces(); +``` + +调用方传入已发现的 Ethernet driver 列表和结构化配置。初始化会完成: + +- 创建 loopback。 +- 为每个 Ethernet 设备分配 `InterfaceId` 和接口名。 +- 创建 `Router`、`NetControl`、smoltcp `Interface` 和全局 `SocketSet`。 +- 安装静态地址、DHCP client 状态、DNS entries 和 route rules。 +- 启动非 loopback 设备的 RX/TX worker。 +- 启动专用 net-poll worker。 + +`init_network()` 是一次性初始化入口,重复初始化会触发全局单例保护。 + +### Poll Trigger + +```rust pub fn request_poll(); +pub fn poll_interfaces(); +``` + +`request_poll()` 是 socket、设备和控制路径使用的轻量进度请求入口: + +```rust +pub fn request_poll() { + NET_POLL_REQUESTED.store(true, Ordering::Release); + NET_POLL_WAKE.notify_one(true); +} +``` +`poll_interfaces()` 保留为 public trigger/debug API,内部只转调 `request_poll()`,不会在调用者线程中同步执行完整 `Service::poll()`。 + +### Vsock 初始化 + +```rust #[cfg(feature = "vsock")] pub fn init_vsock(vsock_devs: VsockDeviceList); + +#[cfg(feature = "vsock")] +pub type VsockDevice = Box; +#[cfg(feature = "vsock")] +pub type VsockDeviceList = Vec; ``` -说明: +vsock 不进入 smoltcp `SocketSet`,也不实现 `ax-net` 内部 IP `Device` trait。它通过 `rdif_vsock::Interface` 和 vsock connection manager 进入 AF_VSOCK socket backend。 -- `init_network()` 由 `ax-runtime` 调用,只能调用一次(重复调用 panic,见 [lib.rs](net/ax-net/src/lib.rs))。 -- `request_poll()` 是轻量 poll 请求入口,socket 和设备路径应调用它。 -- `poll_interfaces()` 保留为 public trigger/debug 入口,内部仅转调 `request_poll()`([lib.rs](net/ax-net/src/lib.rs)),不会在调用者线程里同步执行 `Service::poll()`。 -- `init_vsock()` 仅在 `vsock` feature 下存在。 +## 运行时查询 -## 接口查询 API +查询 API 返回只读快照。调用方不应持有快照并假设其永久有效;DHCP、运行期设备注册或后续 link state 更新都可能改变接口、路由和 DNS 状态。 + +### 接口快照 ```rust pub fn interfaces() -> Vec; pub fn interface_by_name(name: &str) -> Option; pub fn interface_by_id(id: InterfaceId) -> Option; pub fn ipv4_config(name: &str) -> Option; -pub fn default_routes() -> Vec; -pub fn arp_entries() -> Vec; ``` -这些 API 返回只读快照。调用方不应保存快照后假设其永久有效;DHCP 更新或后续接口状态变化可能改变地址、路由和 DNS。 - -## 接口类型 - -`InterfaceId` 与 `InterfaceFlags` 定义在 [config.rs](net/ax-net/src/config.rs): +`InterfaceId` 是稳定接口 ID,同时作为 StarryOS/Linux ifindex 来源: ```rust pub struct InterfaceId(u32); -pub enum InterfaceKind { - Loopback, - Ethernet, +impl InterfaceId { + pub const LOOPBACK: Self = Self(1); + pub const fn new(raw: u32) -> Self; + pub const fn get(self) -> u32; + pub const fn to_linux_ifindex(self) -> i32; + pub const fn from_linux_ifindex(ifindex: i32) -> Option; } +``` -bitflags::bitflags! { - pub struct InterfaceFlags: u32 { - const UP = 1 << 0; - const RUNNING = 1 << 1; - const LOOPBACK = 1 << 2; - const BROADCAST = 1 << 3; - const MULTICAST = 1 << 4; - } +`InterfaceInfo` 是 public snapshot: + +```rust +pub struct InterfaceInfo { + pub id: InterfaceId, + pub name: String, + pub kind: InterfaceKind, + pub mac: Option, + pub ipv4: Option, + pub mtu: usize, + pub flags: InterfaceFlags, + pub metric: u32, } ``` -`InterfaceInfo` 见 [config.rs](net/ax-net/src/config.rs)。`InterfaceId` 同时作为 StarryOS Linux ifindex 数值来源: +系统 ABI 映射应使用 `InterfaceId`,而不是假设 `eth0`: ```rust +let info = ax_net::interface_by_name("eth1").ok_or(AxError::NoSuchDevice)?; let linux_ifindex = info.id.to_linux_ifindex(); let id = InterfaceId::from_linux_ifindex(linux_ifindex).unwrap(); ``` -## Socket API +### 路由快照 + +```rust +pub fn default_routes() -> Vec; +``` + +`RouteInfo` 是对外 route snapshot,不暴露 Router 内部 device index: + +```rust +pub struct RouteInfo { + pub filter: IpCidr, + pub via: Option, + pub interface_id: InterfaceId, + pub source: IpAddress, + pub metric: u32, +} +``` + +调用方可用它实现 route 诊断、默认网关展示或 Linux 兼容查询;socket 发送路径不应自行遍历 `RouteInfo`,而应通过 socket backend 调用控制面的 route decision。 -主要 socket 类型(见 [lib.rs](net/ax-net/src/lib.rs)): +### ARP 快照 ```rust -pub mod tcp; -pub mod udp; -pub mod raw; -pub mod unix; +pub fn arp_entries() -> Vec; +``` -#[cfg(feature = "vsock")] -pub mod vsock; +`ArpEntry` 用于 `/proc/net/arp` 等兼容层: + +```rust +pub struct ArpEntry { + pub ip_addr: [u8; 4], + pub hw_type: u16, + pub flags: u16, + pub hw_addr: [u8; 6], + pub device: String, +} +``` + +`device` 字段是真实接口名。loopback 不产生 ARP entry。 + +### 兼容 helper + +```rust +pub fn eth0_ipv4_config() -> Option; +``` + +该函数是旧调用方的 convenience helper。新代码应优先使用 `ipv4_config(name)` 或接口 registry API,避免重新引入固定 `eth0` 假设。 + +## Socket Facade + +socket API 统一 AF_INET、AF_UNIX 和 AF_VSOCK 的公共操作形状。协议细节由具体 backend 负责。 + +### SocketAddrEx + +```rust +pub enum SocketAddrEx { + Ip(SocketAddr), + Unix(UnixSocketAddr), + #[cfg(feature = "vsock")] + Vsock(VsockAddr), +} +``` + +地址族不匹配时,`into_ip()`、`into_unix()`、`into_vsock()` 返回对应错误,调用方不需要手工 match 所有 backend。 + +### Send/Recv Options + +```rust +pub type CMsgData = Box; + +pub struct SendOptions { + pub to: Option, + pub flags: SendFlags, + pub cmsg: Vec, +} + +pub struct RecvOptions<'a> { + pub from: Option<&'a mut SocketAddrEx>, + pub flags: RecvFlags, + pub cmsg: Option<&'a mut Vec>, + pub truncated: Option<&'a mut bool>, +} +``` + +`SendFlags` 和 `RecvFlags` 使用 Linux `MSG_*` 数值,便于 syscall 层直接转换: + +```rust +bitflags! { + pub struct SendFlags: u32 { + const OOB = 0x01; + const DONTROUTE = 0x04; + const DONTWAIT = 0x40; + const EOR = 0x80; + const CONFIRM = 0x800; + const NOSIGNAL = 0x4000; + const MORE = 0x8000; + } + + pub struct RecvFlags: u32 { + const PEEK = 0x01; + const TRUNCATE = 0x02; + const DONTWAIT = 0x40; + } +} ``` -统一 socket trait `SocketOps`([net/ax-net/src/socket.rs](net/ax-net/src/socket.rs)): +`Shutdown::{Read, Write, Both}` 表达 `shutdown(2)` 的半关闭方向。 + +### SocketOps ```rust pub trait SocketOps: Configurable { fn bind(&self, local_addr: SocketAddrEx) -> AxResult; fn connect(&self, remote_addr: SocketAddrEx) -> AxResult; - fn listen(&self, _backlog: usize) -> AxResult { Err(AxError::OperationNotSupported) } - fn accept(&self) -> AxResult { Err(AxError::OperationNotSupported) } + fn listen(&self, _backlog: usize) -> AxResult; + fn accept(&self) -> AxResult; fn send(&self, src: impl Read + IoBuf, options: SendOptions) -> AxResult; fn recv(&self, dst: impl Write + IoBufMut, options: RecvOptions<'_>) -> AxResult; - fn recv_available(&self) -> AxResult { Err(AxError::OperationNotSupported) } + fn recv_available(&self) -> AxResult; fn local_addr(&self) -> AxResult; fn peer_addr(&self) -> AxResult; fn shutdown(&self, how: Shutdown) -> AxResult; } ``` -`Socket` 枚举([socket.rs](net/ax-net/src/socket.rs))统一各 backend,并对 `SocketOps` / `Configurable` 做透传分派: +`Socket` 枚举把统一 API 分发给各 backend: ```rust pub enum Socket { @@ -114,28 +347,65 @@ pub enum Socket { } ``` -`SocketAddrEx`([socket.rs](net/ax-net/src/socket.rs))支持: +支持的 backend: -- IP socket address -- Unix domain socket address -- vsock address(`vsock` feature) +| Backend | 构造入口 | 地址族/类型 | +| --- | --- | --- | +| `tcp::TcpSocket` | `TcpSocket::new()` | AF_INET / SOCK_STREAM | +| `udp::UdpSocket` | `UdpSocket::new()` | AF_INET / SOCK_DGRAM | +| `raw::RawSocket` | `RawSocket::new(ip_version, ip_protocol)` | AF_INET / SOCK_RAW | +| `unix::UnixSocket` | `UnixSocket::new(Transport)` | AF_UNIX / stream,dgram | +| `vsock::VsockSocket` | `VsockSocket::new(VsockTransport)` | AF_VSOCK / stream | + +### 设备绑定 + +TCP、UDP 和 raw socket 提供直接绑定接口: ```rust -pub enum SocketAddrEx { - Ip(SocketAddr), - Unix(UnixSocketAddr), - #[cfg(feature = "vsock")] - Vsock(VsockAddr), +impl TcpSocket { + pub fn bind_device(&self, interface_id: InterfaceId) -> AxResult; +} +impl UdpSocket { + pub fn bind_device(&self, interface_id: InterfaceId) -> AxResult; +} +impl RawSocket { + pub fn bind_device(&self, interface_id: InterfaceId) -> AxResult; +} +``` + +不存在的接口返回 `AxError::NoSuchDevice`。成功后内部设置: + +```rust +pub struct DeviceBinding { + pub bound_if: Option, +} +``` + +绑定具体本地地址时,TCP/UDP/raw backend 也会通过控制面反查该地址所属接口,并把 route/waker 选择限制到对应接口。未绑定接口的 connect/sendto 由 route decision 自动选择源地址和出接口。 + +## Socket Options + +socket option API 使用一个 get enum 和一个 set enum 表达 SO_*、TCP_* 和 IP_*。 + +### Configurable + +```rust +#[enum_dispatch] +pub trait Configurable { + fn get_option_inner(&self, opt: &mut GetSocketOption) -> AxResult; + fn set_option_inner(&self, opt: SetSocketOption) -> AxResult; + + fn get_option(&self, mut opt: GetSocketOption) -> AxResult { /* dispatch */ } + fn set_option(&self, opt: SetSocketOption) -> AxResult { /* dispatch */ } } ``` -## Socket options +`get_option()` / `set_option()` 在 backend 返回 `supported = false` 时映射为 `ENOPROTOOPT`,调用方不需要自己区分“协议不支持”和“选项值非法”。 -[options.rs](net/ax-net/src/options.rs) 通过 `define_options!` 宏([options.rs](net/ax-net/src/options.rs))一次性生成 `GetSocketOption<'a>` 和 `SetSocketOption<'a>` 两个枚举,覆盖 SO_\* / TCP_\* / IP_\* 三层选项: +### Option 集合 ```rust define_options! { - // ---- Socket level options (SO_*) ---- ReuseAddress(bool), Error(i32), DontRoute(bool), @@ -152,7 +422,6 @@ define_options! { SocketDomain(i32), BindToDevice(Option), - // --- TCP level options (TCP_*) ---- NoDelay(bool), MaxSegment(usize), TcpKeepIdle(u32), @@ -161,44 +430,35 @@ define_options! { TcpUserTimeout(u32), TcpInfo(TcpInfo), - // ---- IP level options (IP_*) ---- Ttl(u8), RecvErr(bool), - // ---- Extra options ---- NonBlocking(bool), } ``` -`Configurable` trait([options.rs](net/ax-net/src/options.rs))提供 `get_option` / `set_option`,未支持的选项返回 `ENOPROTOOPT`: - -```rust -#[enum_dispatch] -pub trait Configurable { - fn get_option_inner(&self, opt: &mut GetSocketOption) -> AxResult; - fn set_option_inner(&self, opt: SetSocketOption) -> AxResult; - fn get_option(&self, mut opt: GetSocketOption) -> AxResult { /* ... */ } - fn set_option(&self, opt: SetSocketOption) -> AxResult { /* ... */ } -} -``` - -通用选项(非阻塞、超时、`SO_BINDTODEVICE` 等)由 `GeneralOptions`([general.rs](net/ax-net/src/general.rs))实现,各 socket backend 的 `get_option_inner` / `set_option_inner` 先尝试 `GeneralOptions`,再处理自身特有选项。`TcpInfo`([options.rs](net/ax-net/src/options.rs))提供 transport-independent 的 TCP_INFO 快照。 - -`SO_BINDTODEVICE` 在 `ax-net` 内表达为 `DeviceBinding`([config.rs](net/ax-net/src/config.rs)): +通用选项由 `GeneralOptions` 实现,协议特有选项由具体 socket backend 继续处理。`TcpInfo` 是 transport-independent TCP_INFO snapshot: ```rust -#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)] -pub struct DeviceBinding { - pub bound_if: Option, +pub struct TcpInfo { + pub state: TcpState, + pub options: TcpInfoOptions, + pub rto_micros: u32, + pub snd_mss: u32, + pub rcv_mss: u32, + pub notsent_bytes: u32, + pub pmtu: u32, + pub snd_cwnd: u32, + pub rcv_space: u32, + pub snd_wnd: u32, + pub rcv_wnd: u32, + // 省略其他 Linux TCP_INFO 兼容字段 } ``` -语义: - -- `None`:未绑定接口,按 route decision 选择出接口。 -- `Some(id)`:只允许对应接口参与 route/waker/device 选择。 +## DNS 与名称解析 -## DNS API +DNS API 位于 [lib.rs](net/ax-net/src/lib.rs),使用控制面的 DNS registry 和 route decision。 ```rust pub fn dns_servers() -> Vec; @@ -206,37 +466,23 @@ pub fn dns_query(name: &str) -> AxResult>; pub fn dns_query_timeout(name: &str, timeout: Duration) -> AxResult>; ``` -说明: +语义: -- `dns_servers()` 返回按 (metric, interface_id, server_ip) 排序去重的 DNS server 列表。来源包括 DHCP、静态配置和 fallback。 -- `dns_query()` 使用 5 秒默认超时(`DNS_DEFAULT_TIMEOUT`)。内部过滤不可路由的 DNS server,创建临时 `dns::Socket`,循环 poll 直到解析完成或超时。 -- 查询结束后 `DnsSocketGuard` 自动从 `SOCKET_SET` 移除临时 socket。 +- `dns_servers()` 返回按 `(metric, interface_id, server_ip)` 排序并去重的 IPv4 DNS server。 +- DNS server 来源包括 DHCP、接口静态配置和 global fallback。 +- `dns_query()` 使用默认 5 秒超时。 +- `dns_query_timeout()` 会跳过不可路由的 DNS server。 +- 查询期间临时创建 smoltcp DNS socket,结束后由 guard 从 `SOCKET_SET` 移除。 -## ARP API +## 设备驱动 API -```rust -pub fn arp_entries() -> Vec; -``` +设备驱动 API 是 low-level NIC 和 `ax-net` 之间的能力边界。驱动提供 buffer 和 IRQ 语义,协议栈不依赖具体 DMA ring 或虚拟队列实现。 -返回所有 Ethernet 设备上已解析的 ARP 条目快照([ArpEntry](net/ax-net/src/device/mod.rs)): +### EthernetDriver ```rust -pub struct ArpEntry { - pub ip_addr: [u8; 4], - pub hw_type: u16, - pub flags: u16, - pub hw_addr: [u8; 6], - pub device: String, // 真实接口名 -} -``` +pub type EthernetDeviceList = Vec>; -## 设备 Driver API - -### EthernetDriver(能力边界 trait) - -定义在 [device/driver.rs](net/ax-net/src/device/driver.rs): - -```rust pub trait EthernetDriver: Send + Sync { fn device_name(&self) -> &str; fn irq_num(&self) -> Option; @@ -252,7 +498,24 @@ pub trait EthernetDriver: Send + Sync { } ``` -`RdNetDriver` 是该 trait 的标准实现(基于 `rd-net`),`EthernetDeviceList` 即 `Vec>`。 +RX/TX buffer trait: + +```rust +pub trait NetRxBuffer: Send { + fn packet(&self) -> &[u8]; + fn packet_len(&self) -> usize { + self.packet().len() + } +} + +pub trait NetTxBuffer: Send { + fn packet(&self) -> &[u8]; + fn packet_mut(&mut self) -> &mut [u8]; + fn packet_len(&self) -> usize; +} +``` + +`RdNetDriver` 是基于 `rd-net` 的标准适配实现。 ### IRQ 注册 @@ -269,131 +532,77 @@ pub trait EthernetIrqRegistrar: Send + Sync { } ``` -`EthernetIrqAction` 封装 `(data: NonNull<()>, handler: unsafe fn(NonNull<()>) -> EthernetIrqOutcome)` 对,由 `ax-runtime` 注册。IRQ 触发时 `handle_ethernet_irq()` → `driver.handle_irq()` → 返回 `RX_READY` 时 wake RX worker。 - -## Vsock API(`vsock` feature) - -```rust -#[cfg(feature = "vsock")] -pub fn init_vsock(vsock_devs: VsockDeviceList); - -// types: -pub struct VsockAddr; -pub struct VsockConnId; -pub type VsockDevice = Box; -pub type VsockDeviceList = Vec; -``` - -`VsockDevice` 是 `rdif_vsock::Interface` trait object,表示 vsock 设备事件/收发接口。它不等同于 `ax-net` 内部的 IP `Device` trait,也不进入 smoltcp `SocketSet`。 - -## bind_device 方法 - -每个 socket 类型提供独立的接口绑定方法(不通过 socket option): - -```rust -impl TcpSocket { - pub fn bind_device(&self, interface_id: InterfaceId) -> AxResult; -} -impl UdpSocket { - pub fn bind_device(&self, interface_id: InterfaceId) -> AxResult; -} -impl RawSocket { - pub fn bind_device(&self, interface_id: InterfaceId) -> AxResult; -} -``` - -校验现有接口,不存在返回 `AxError::NoSuchDevice`。内部设置 `DeviceBinding { bound_if: Some(id) }`。 +`EthernetIrqAction` 封装平台 IRQ 回调。IRQ handler 返回 `EthernetIrqOutcome::Wake` 时,Ethernet adapter 会唤醒设备 RX worker 和 net-poll 路径。 -## Socket 构造 API +### 动态设备注册 ```rust -impl TcpSocket { - pub fn new() -> Self; // 创建 Idle 状态 socket - fn new_connected(handle, local_ep, remote_ep) -> Self; // accept 内部使用 -} - -impl UdpSocket { - pub fn new() -> Self; -} - -impl RawSocket { - pub fn new(ip_version: IpVersion, ip_protocol: IpProtocol) -> Self; -} - -impl UnixSocket { - pub fn new(transport: impl Into) -> Self; - // 其中 Transport::Stream(StreamTransport) 或 Transport::Dgram(DgramTransport) +pub struct NetConfig { + pub name: String, + pub ip: [u8; 4], + pub prefix_len: u8, + pub dhcp_server_client_ip: Option<[u8; 4]>, + pub dedicated_poll: bool, } -impl VsockSocket { - pub fn new(transport: impl Into) -> Self; - // VsockTransport::Stream(VsockStreamTransport) -} +pub fn register_device_with_config(dev: Box, config: NetConfig); +pub fn notify_oob_rx(); ``` -Unix stream transport 通过 `StreamTransport::new_pair(pid)` 创建 socketpair,Unix datagram transport 通过 `DgramTransport::new_pair(pid)` 创建 pair。 +`register_device_with_config()` 用于运行期加入静态 IPv4 Ethernet 设备,例如 Wi-Fi AP 模式设备。它会: -## Ephemeral Port 分配 +- 创建 `EthernetDevice` 或 OOB RX `EthernetDevice`。 +- 分配新的 `InterfaceId`。 +- 更新 smoltcp address list、接口 registry 和 route table。 +- 启动该设备的 RX/TX worker。 +- 可选启用内置单客户端 DHCP server。 -未在 public API 中暴露,但 TCP bind 和 UDP bind 在 `port == 0` 时调用内部函数分配临时端口(从 `49152` 开始,即 IANA 动态端口范围的下界)。 +`dedicated_poll = true` 时,设备 RX readiness 不走 Ethernet IRQ registrar,而由外部驱动线程调用 `notify_oob_rx()` 唤醒 OOB poll task。 ## Unix Namespace API +Unix path socket 需要外部文件系统 namespace provider: + ```rust pub fn register_unix_namespace(ns: impl UnixNamespace + 'static); ``` -StarryOS 通过此 API 注入文件系统 namespace(路径解析和 inode 管理)。`UnixNamespace` trait 定义在 [unix/namespace.rs](net/ax-net/src/unix/namespace.rs)。 - -## TCP / UDP / raw 接口绑定 - -TCP、UDP 和 raw socket 提供 `bind_device(InterfaceId)`,例如 `TcpSocket::bind_device()`([tcp.rs](net/ax-net/src/tcp.rs)): +abstract Unix socket 使用 `ax-net` 内部内存 namespace;path socket 通过注册的 `UnixNamespace` 完成路径绑定和解析。 -```rust -pub fn bind_device(&self, interface_id: InterfaceId) -> AxResult { - if interface_by_id(interface_id).is_none() { - return Err(AxError::NoSuchDevice); - } - self.general.set_device_binding(DeviceBinding { - bound_if: Some(interface_id), - }); - Ok(()) -} -``` +## 兼容语义 -```rust -let eth1 = ax_net::interface_by_name("eth1").ok_or(AxError::NoSuchDevice)?; +这些 API 语义影响 syscall 层和测试用例,应作为稳定约定维护。 -let udp = ax_net::udp::UdpSocket::new(); -udp.bind_device(eth1.id)?; -``` +### Per-address Bind/Listen -绑定具体本地地址时,`ax-net` 会根据地址所属接口设置 `DeviceBinding`。例如 `UdpSocket::bind()` 调用 `get_control().local_binding_for(&endpoint)?`([udp.rs](net/ax-net/src/udp.rs))从监听地址反查接口。未绑定接口的 connect/sendto 会按 route decision 自动选择源地址和出接口。 +TCP listen 和 UDP bind 支持 Linux 风格 wildcard/specific-address 冲突: -## 动态设备注册与 OOB RX +- wildcard 地址与同端口所有具体地址冲突。 +- 两个具体地址只有地址相同时冲突。 +- TCP 由 `TCP_BOUND_PORTS` 和 `ListenTable` 共同维护。 +- UDP 由 `SocketSetWrapper` 的 `udp_binds` side table 维护。 +- `SO_REUSEADDR` 会影响 UDP wrapper side table 注册,但不绕过 smoltcp 自身状态检查。 -用于运行时添加 Ethernet 设备(如 Wi-Fi AP 模式启动后),以及 SDIO Wi-Fi 等 out-of-band RX 设备: +### Ephemeral Port -```rust -pub struct NetConfig { - pub name: String, - pub ip: [u8; 4], - pub prefix_len: u8, - pub dhcp_server_client_ip: Option<[u8; 4]>, - pub dedicated_poll: bool, // true = 使用 notify_oob_rx() 驱动 RX -} +TCP/UDP 在 bind port 为 `0` 时分配临时端口。临时端口范围从 `49152` 开始,符合 IANA dynamic/private port 下界。该分配器不是 public API,但影响 `bind(0)` 和自动 bind 行为。 -pub fn register_device_with_config(dev: Box, config: NetConfig); -pub fn notify_oob_rx(); // SDIO Wi-Fi 收到数据后调用 -pub fn eth0_ipv4_config() -> Option; -``` +### Error Mapping -`register_device_with_config()` 在 `Service` 中调用 `register_static_device()` 添加设备和路由,可选启用 DHCP server(`enable_dhcp_server()`)。`dedicated_poll = true` 时设备用 `EthernetDevice::new_oob_rx()` 创建,RX 就绪由驱动线程调用 `notify_oob_rx()` 唤醒独立 poll task(`{ifname}-oob-poll`),不经过 Ethernet IRQ 框架。 +常见错误约定: -## Per-address Listen 支持 +| 场景 | 错误 | +| --- | --- | +| 绑定不存在接口 | `AxError::NoSuchDevice` | +| 绑定本机不存在地址 | `AxError::NoSuchDeviceOrAddress` | +| 地址/端口冲突 | `AxError::AddrInUse` | +| 操作不支持 | `AxError::OperationNotSupported` | +| nonblocking 或 `MSG_DONTWAIT` 下 would block | `AxError::WouldBlock` | +| 不支持的 socket option | Linux `ENOPROTOOPT` 映射 | -`ListenTable` 和 `SocketSetWrapper` 支持 Linux 语义的同端口不同地址 listen/bind: +### API 使用建议 -- `LISTEN_TABLE.listen(endpoint, backlog)`:`ListenTableEntry` 是 `Vec`,每个 entry 存储完整的 `IpListenEndpoint`。 -- `listen_addrs_conflict(a, b)`:wildcard(`None`)与所有地址冲突;两个 `Some(addr)` 仅当相等时冲突。 -- `SocketSetWrapper::udp_bind_available()`:UDP 同端口不同地址允许共存,wildcard 与所有冲突。 +- 新代码使用 `interfaces()`、`interface_by_name()`、`interface_by_id()` 和 `ipv4_config(name)`,不要依赖 `eth0_ipv4_config()`。 +- socket 发送路径不要直接使用 `default_routes()` 自行选路,应交给 TCP/UDP/raw backend。 +- 设备驱动只实现 `EthernetDriver`,不要直接接触 `Router` 或 `SocketSet`。 +- 需要唤醒协议栈进度时调用 `request_poll()`,不要从调用者上下文同步 poll smoltcp。 diff --git a/docs/docs/architecture/net/configuration.md b/docs/docs/architecture/net/configuration.md index 40341b68c7..84fe3bf7f5 100644 --- a/docs/docs/architecture/net/configuration.md +++ b/docs/docs/architecture/net/configuration.md @@ -5,11 +5,25 @@ sidebar_label: "配置参考" # 配置参考 -`ax-net` 的配置目标是用结构化数据表达接口意图,而不是依赖旧的全局单网口环境变量。每个 Ethernet 接口可以独立使用静态 IPv4 或 DHCP,并携带 metric 和 DNS 配置。 +`ax-net` 的配置由结构化 `NetworkConfig`、Cargo feature、运行时设备注册参数和一组集中常量组成。配置目标是明确表达每个接口的意图,避免旧式单网口全局变量和隐式 `eth0` 假设。 -## Cargo Feature +核心源码: -`ax-net` 的 feature 定义在 [net/ax-net/Cargo.toml](net/ax-net/Cargo.toml): +| 配置域 | 源码 | +| --- | --- | +| feature | [Cargo.toml](net/ax-net/Cargo.toml) | +| 接口配置模型 | [config.rs](net/ax-net/src/config.rs) | +| 初始化解析与校验 | [lib.rs](net/ax-net/src/lib.rs) `init_network()` | +| 缓冲区/队列常量 | [consts.rs](net/ax-net/src/consts.rs) | +| TCP keepalive / TCP_INFO 默认值 | [tcp.rs](net/ax-net/src/tcp.rs) | +| DHCP/DNS 默认值 | [lib.rs](net/ax-net/src/lib.rs), [service.rs](net/ax-net/src/service.rs) | +| Ethernet ARP 默认值 | [device/ethernet.rs](net/ax-net/src/device/ethernet.rs) | + +## 构建配置 + +构建配置决定是否启用可选协议族。基础 TCP、UDP、raw、Unix domain socket、DNS、DHCP 和 Ethernet 能力不需要额外 feature。 + +### Cargo Feature ```toml [features] @@ -18,11 +32,18 @@ vsock = ["dep:rdif-vsock"] | feature | 作用 | | --- | --- | -| `vsock` | 启用 `rdif-vsock` 依赖和 vsock socket / device 支持 | +| `vsock` | 启用 `rdif-vsock` 依赖、AF_VSOCK socket backend 和 vsock device 初始化 | + +启用 `vsock` 后导出: -启用后,`lib.rs` 中通过 `#[cfg(feature = "vsock")]` 条件编译导出 `init_vsock()`([lib.rs](net/ax-net/src/lib.rs))、`vsock` 模块([lib.rs](net/ax-net/src/lib.rs))以及 `Socket::Vsock` 变体([socket.rs](net/ax-net/src/socket.rs))。基础 TCP、UDP、raw、Unix domain socket、DNS、DHCP 和 Ethernet 能力不需要额外 feature。 +- `init_vsock(vsock_devs)`。 +- `vsock` 模块。 +- `Socket::Vsock` 变体。 +- `VsockDevice` / `VsockDeviceList` 类型别名。 -smoltcp 在 [Cargo.toml](net/ax-net/Cargo.toml) 中固定启用以下能力: +### smoltcp 能力 + +`ax-net` 固定启用的 smoltcp 能力包括: - `alloc` - `log` @@ -38,39 +59,32 @@ smoltcp 在 [Cargo.toml](net/ax-net/Cargo.toml) 中固定启用以下能力: - `socket-dhcpv4` - `socket-dns` -## 缓冲区常量 - -socket 与设备缓冲区大小集中定义在 [net/ax-net/src/consts.rs](net/ax-net/src/consts.rs): - -```rust -pub const STANDARD_MTU: usize = 1500; -pub const TCP_RX_BUF_LEN: usize = 64 * 1024; -pub const TCP_TX_BUF_LEN: usize = 64 * 1024; -pub const UDP_RX_BUF_LEN: usize = 64 * 1024; -pub const UDP_TX_BUF_LEN: usize = 64 * 1024; -pub const RAW_RX_BUF_LEN: usize = 64 * 1024; -pub const RAW_TX_BUF_LEN: usize = 64 * 1024; -pub const LISTEN_QUEUE_SIZE: usize = 512; -pub const SOCKET_BUFFER_SIZE: usize = 64; -pub const ETHERNET_MAX_PENDING_PACKETS: usize = 128; -pub const DEVICE_TX_QUEUE_SIZE: usize = 128; -``` +Router 对 smoltcp 暴露 `Medium::Ip`,Ethernet frame 处理在 `EthernetDevice` 中完成。 -`ETHERNET_MAX_PENDING_PACKETS` 取 128 而非 32,是为了容纳应用启动时多个并发 TCP 连接的首个 SYN 突发,以及长连接在 `NEIGHBOR_TTL` 过期后重新进入 ARP-pending 队列的 burst(见源码注释 [consts.rs](net/ax-net/src/consts.rs))。 +## 启动配置模型 -## NetworkConfig +启动配置通过 `NetworkConfig` 传入 `init_network()`。它描述“哪些设备应该成为哪些接口,以及接口如何获得 IPv4/DNS/metric”。 -定义在 [net/ax-net/src/config.rs](net/ax-net/src/config.rs): +### NetworkConfig ```rust #[derive(Debug, Clone, Default)] pub struct NetworkConfig { - /// Per-interface configuration. pub interfaces: Vec, - /// DNS servers used when no interface-level DNS server is available. pub default_dns_servers: Vec, } +``` + +语义: + +- `interfaces` 是显式接口配置列表。 +- 未显式匹配的 Ethernet 设备按默认策略注册。 +- `default_dns_servers` 是 fallback DNS 来源,metric 为 `u32::MAX`。 +- `lo` 固定由 `ax-net` 创建,不出现在 `NetworkConfig` 中。 +### InterfaceConfig + +```rust #[derive(Debug, Clone)] pub struct InterfaceConfig { pub name: String, @@ -82,11 +96,18 @@ pub struct InterfaceConfig { } ``` -`NetworkConfig` 由 `ax-runtime` 构造并传入 `ax_net::init_network()`。`ax-net` 不再读取旧的全局 `AX_IP`、`AX_GW`、`AX_PREFIX_LEN`、`AX_DNS` 单网口语义。 +字段语义: -## InterfaceMatcher +| 字段 | 语义 | +| --- | --- | +| `name` | 对外接口名,例如 `eth0`、`uplink0` | +| `match_by` | 将配置绑定到某个探测到的 Ethernet driver | +| `static_ip` | 静态 IPv4 配置;与 `dhcp` 互斥 | +| `dhcp` | 是否启用 DHCP client | +| `metric` | 接口路由和接口级 DNS 优先级,值越小越优先 | +| `dns_servers` | 绑定到该接口的静态 DNS server | -显式接口配置通过 `InterfaceMatcher` 匹配真实设备,定义在 [config.rs](net/ax-net/src/config.rs): +### InterfaceMatcher ```rust #[derive(Debug, Clone)] @@ -99,17 +120,13 @@ pub enum InterfaceMatcher { 匹配规则: -- `ByOrder(0)` 匹配第一个发现的 Ethernet 设备,通常命名为 `eth0`。 -- `ByMac(mac)` 用 MAC 地址匹配设备。 -- `ByDriverName(name)` 用 driver 暴露的设备名匹配。 -- 显式配置必须匹配唯一设备。 -- 未显式配置的 Ethernet 设备默认启用 DHCP。 - -匹配逻辑在 `find_interface_config()`([lib.rs](net/ax-net/src/lib.rs)),多配置匹配同一设备会 panic。 - -## 静态 IPv4 +- `ByOrder(0)` 匹配第一个发现的 Ethernet device。 +- `ByMac(mac)` 按 MAC 地址匹配。 +- `ByDriverName(name)` 按 driver 暴露的设备名匹配。 +- 同一设备不能被多个配置匹配。 +- 每个显式配置必须匹配到一个设备。 -`StaticIpConfig` 定义在 [config.rs](net/ax-net/src/config.rs): +### StaticIpConfig ```rust #[derive(Debug, Clone)] @@ -120,54 +137,221 @@ pub struct StaticIpConfig { } ``` -静态接口初始化时会: +静态接口初始化会: -- 设置接口 IPv4 地址。 +- 将 `ip/prefix_len` 加入 smoltcp `Interface` address list。 - 安装直连路由。 -- 如果 gateway 有效,安装默认路由。 -- 将接口级 DNS server 记录为静态 DNS 来源。 +- 如果 `gateway != 0.0.0.0`,安装默认路由。 +- 将 `dns_servers` 记录为 `DnsSource::Static`。 -配置错误会在初始化时 panic,包括: +`gateway = 0.0.0.0` 表示不安装默认路由。 -- 接口名为保留名 `lo`。 -- `dhcp = true` 且 `static_ip.is_some()`。 -- 静态 IP 为 unspecified。 -- prefix 大于 32。 -- gateway 可以为 unspecified,表示不安装默认路由。 -- DNS server 为 unspecified。 -- 显式配置没有匹配任何设备。 -- 接口名冲突。 +## 初始化校验 -这些校验集中在 `init_network()` 开头([lib.rs](net/ax-net/src/lib.rs))。 +`init_network()` 对配置执行 fail-fast 校验。启动阶段配置错误直接 panic,避免系统在半初始化网络状态下运行。 -## DHCP +### 校验规则 -未显式配置的 Ethernet 接口默认 DHCP。显式配置也可以设置 `dhcp = true`。 +| 配置项 | 规则 | +| --- | --- | +| 接口名 | 不能是 `lo`,不能重复 | +| `static_ip` + `dhcp` | 不能同时启用 | +| 静态 IP | 不能是 `0.0.0.0` | +| prefix | 不能大于 32 | +| gateway | 可以是 `0.0.0.0`,表示无默认路由 | +| DNS server | 不能是 `0.0.0.0` | +| matcher | 每个显式配置必须匹配唯一设备 | + +### 默认策略 + +未显式配置的 Ethernet 设备: -DHCP 行为: +- 名称为 `eth{order}`。 +- `InterfaceId = order + 2`。 +- metric 为 `100`。 +- 默认启用 DHCP。 +- 无静态接口级 DNS。 -- 每个 DHCP 接口独立 `DhcpState`。 -- DHCP packet 按 ingress `InterfaceId` 分发。 -- DHCP ACK 更新对应接口 IPv4、gateway、路由和 DNS。 -- 启动等待只要求任一 DHCP 接口成功配置,避免一个断开的接口阻塞系统启动。 -- DHCP 租约续期、过期回收和地址冲突检测仍属于后续增强。 +loopback: + +- 名称为 `lo`。 +- `InterfaceId::LOOPBACK == 1`。 +- 地址为 `127.0.0.1/8`。 +- metric 为 `0`。 +- flags 包含 `UP | RUNNING | LOOPBACK`。 ## DNS 配置 -DNS server 来源分三类: +DNS server 来源分三类,并按 metric 排序后去重。 + +| 来源 | 创建时机 | metric | interface_id | +| --- | --- | --- | --- | +| DHCP | DHCP ACK | 对应接口 metric | 对应接口 | +| Static | `InterfaceConfig::dns_servers` | 对应接口 metric | 对应接口 | +| Fallback | `NetworkConfig::default_dns_servers` | `u32::MAX` | loopback | + +对外 `dns_servers()` 只返回地址列表。DNS 查询时还会过滤不可路由 server: + +```text +dns_servers() + -> sort by (metric, interface_id, server_ip) + -> dedup + -> dns_query_timeout() + -> select_route(server) must succeed +``` + +## 路由与 Metric + +路由表排序策略: + +1. 最长前缀匹配。 +2. 低 metric 优先。 +3. 同 metric 按插入顺序稳定选择。 + +每个静态或 DHCP IPv4 接口会生成: + +- 直连路由:`interface_cidr -> dev`。 +- 默认路由:`0.0.0.0/0 -> gateway`,仅 gateway 存在时安装。 + +多网口场景下,metric 用于选择默认路由和 DNS server 优先级;socket 已绑定接口时,route lookup 还会叠加 `DeviceBinding` 过滤。 + +## 运行时设备配置 + +运行期可以注册静态 IPv4 Ethernet 设备,主要用于 Wi-Fi AP 等晚于启动阶段出现的设备。 + +### NetConfig + +```rust +pub struct NetConfig { + pub name: String, + pub ip: [u8; 4], + pub prefix_len: u8, + pub dhcp_server_client_ip: Option<[u8; 4]>, + pub dedicated_poll: bool, +} +``` + +### register_device_with_config + +```rust +pub fn register_device_with_config(dev: Box, config: NetConfig); +pub fn notify_oob_rx(); +``` + +注册过程: + +- 根据 `dedicated_poll` 创建普通或 OOB RX `EthernetDevice`。 +- 分配新的 `InterfaceId`。 +- 将静态 IPv4 加入 smoltcp address list。 +- 添加接口 registry、route table 和 worker。 +- `dhcp_server_client_ip` 存在时启用内置单客户端 DHCP server。 +- 调用 `request_poll()` 让 net-poll worker 看到新状态。 + +`dedicated_poll = true` 时,驱动侧收到 out-of-band RX 事件后调用 `notify_oob_rx()`。 + +## 资源预算 + +缓冲区和队列常量集中定义在 [consts.rs](net/ax-net/src/consts.rs)。这些值共同决定嵌入式目标上的默认内存占用。 + +### Socket Buffer + +```rust +pub const TCP_RX_BUF_LEN: usize = 64 * 1024; +pub const TCP_TX_BUF_LEN: usize = 64 * 1024; +pub const UDP_RX_BUF_LEN: usize = 64 * 1024; +pub const UDP_TX_BUF_LEN: usize = 64 * 1024; +pub const RAW_RX_BUF_LEN: usize = 64 * 1024; +pub const RAW_TX_BUF_LEN: usize = 64 * 1024; +``` + +这些是每个 socket 的默认协议缓冲区大小。 + +### Router / Device Queue + +```rust +pub const STANDARD_MTU: usize = 1500; +pub const SOCKET_BUFFER_SIZE: usize = 64; +pub const DEVICE_TX_QUEUE_SIZE: usize = 128; +pub const ETHERNET_MAX_PENDING_PACKETS: usize = 128; +pub const LISTEN_QUEUE_SIZE: usize = 512; +``` -| 来源 | 说明 | +| 常量 | 含义 | | --- | --- | -| DHCP | DHCP ACK 下发,绑定来源接口和 metric | -| Static | `InterfaceConfig::dns_servers` | -| Fallback | `NetworkConfig::default_dns_servers` | +| `STANDARD_MTU` | Router 和 Ethernet 默认 MTU | +| `SOCKET_BUFFER_SIZE` | Router RX/TX smoltcp-facing packet buffer 槽位数 | +| `DEVICE_TX_QUEUE_SIZE` | 每设备 TX queue 槽位数 | +| `ETHERNET_MAX_PENDING_PACKETS` | ARP resolution pending packet 上限 | +| `LISTEN_QUEUE_SIZE` | TCP listen backlog clamp 上限 | + +Router queue 中的 packet 使用 inline `[u8; STANDARD_MTU] + len`,不为每个 queued packet 分配 `Box<[u8]>`。 + +### Unix Stream Buffer + +Unix stream transport 使用 `ringbuf::HeapRb`: + +```rust +const BUF_SIZE: usize = 64 * 1024; +``` + +socketpair 两个方向各 64 KiB,总计约 128 KiB 数据缓冲区。 + +## 协议默认值 + +协议默认值集中在对应模块中,影响兼容行为和超时策略。 + +### TCP Keepalive + +```rust +const TCP_KEEPIDLE_DEFAULT_SECS: u32 = 7200; +const TCP_KEEPINTVL_DEFAULT_SECS: u32 = 75; +const TCP_KEEPCNT_DEFAULT: u32 = 9; +const TCP_USER_TIMEOUT_DEFAULT_MS: u32 = 0; + +const TCP_KEEPIDLE_MAX_SECS: u32 = 32767; +const TCP_KEEPINTVL_MAX_SECS: u32 = 32767; +const TCP_KEEPCNT_MAX: u32 = 127; +``` + +`TCP_USER_TIMEOUT_DEFAULT_MS = 0` 表示使用协议栈默认策略。 + +### TCP_INFO + +```rust +const TCP_INFO_DEFAULT_MSS: u32 = 1460; +const TCP_INFO_DEFAULT_PMTU: u32 = 1500; +const TCP_INFO_INITIAL_RTO_MICROS: u32 = 1_000_000; +const TCP_INFO_DEFAULT_REORDERING: u32 = 3; +``` -内部记录为 `DnsServerEntry`,对外 `dns_servers()` 返回按 metric 和接口 ID 排序后的去重地址列表。 +这些值用于填充 `TcpInfo` 中无法直接从 smoltcp 获得或需要 Linux 兼容默认值的字段。 -DNS 查询时会先检查 DNS server 是否可路由,不可路由则尝试下一个 server。 +### DHCP / DNS / ARP + +| 常量 | 值 | 含义 | +| --- | --- | --- | +| `DNS_DEFAULT_TIMEOUT` | 5s | `dns_query()` 默认超时 | +| `DHCP_BOOTSTRAP_ATTEMPTS` | 200 | DHCP bootstrap 最大轮数 | +| `DHCP_BOOTSTRAP_POLL_INTERVAL` | 10ms | DHCP bootstrap 每轮 sleep | +| `DHCP_MAX_RETRY_SHIFT` | 4 | DHCP 指数退避上限,最大 16s | +| `NEIGHBOR_TTL` | 300s | ARP neighbor cache TTL | +| `ARP_REQUEST_RETRY` | 1s | ARP request 重试间隔 | + +### Ephemeral Port + +TCP 和 UDP 的 `bind(0)` 从 IANA dynamic/private port 下界开始分配: + +```rust +const PORT_START: u16 = 0xc000; // 49152 +const PORT_END: u16 = 0xffff; +``` + +TCP 分配会避开任何已 listen 或已 bind 的同端口;UDP 分配使用 UDP bind side table 检查 wildcard/specific-address 冲突。 ## 配置示例 +### 双静态网口 + ```rust use alloc::{string::ToString, vec}; use core::net::Ipv4Addr; @@ -205,94 +389,26 @@ let config = NetworkConfig { }; ``` -## 接口命名与 ID 分配 - -> 以下为运行时常量与默认值。 - -`InterfaceId` 数值的分配规则: - -| InterfaceId | 接口 | 说明 | -| --- | --- | --- | -| 0 | `TX_INTERFACE_PLACEHOLDER` | 内部占位符([router.rs](net/ax-net/src/router.rs)),不出现在任何 public API 中 | -| 1 | `lo` | Loopback,固定 ID | -| 2+ | `eth0`, `eth1`, ... | Ethernet 设备,按发现顺序(`net_devs.drain(..)`)递增 | - -默认命名 `"eth{order}"`,但配置中的 `name` 字段会覆盖默认名称。接口名不允许冲突,且 `"lo"` 为保留名。 - -## 路由 Metric 行为 - -- 直连路由的 metric 与接口配置相同。 -- 默认路由的 metric 与接口配置相同。 -- 路由表排序:最长前缀匹配 > 低 metric 优先 > 同 metric 稳定插入顺序。 -- 未显式配置的 Ethernet 接口默认启用 DHCP,metric 默认为 100;fallback DNS 来源使用 `u32::MAX`,因此会排在接口级 DNS 之后。 -- 同一目的地址存在多个匹配路由时,优先选择 metric 较低且接口 `UP` 的路由。 - -## TCP Keep-Alive 默认值 - -TCP keep-alive 相关常量定义在 [tcp.rs](net/ax-net/src/tcp.rs): +### DHCP 主接口 + fallback DNS ```rust -const TCP_KEEPIDLE_DEFAULT_SECS: u32 = 7200; // 2小时空闲后开始探测 -const TCP_KEEPINTVL_DEFAULT_SECS: u32 = 75; // 探测间隔 75s -const TCP_KEEPCNT_DEFAULT: u32 = 9; // 最多 9 次探测 -const TCP_USER_TIMEOUT_DEFAULT_MS: u32 = 0; // 默认不限制 -``` - -上限约束: - -```rust -const TCP_KEEPIDLE_MAX_SECS: u32 = 32767; // ~9.1 小时 -const TCP_KEEPINTVL_MAX_SECS: u32 = 32767; -const TCP_KEEPCNT_MAX: u32 = 127; -``` - -`TCP_USER_TIMEOUT_DEFAULT_MS = 0` 表示使用 smoltcp 内置默认超时。 - -## TCP_INFO 默认值 - -`TcpInfo` 快照中的默认/估计值([tcp.rs](net/ax-net/src/tcp.rs)): - -```rust -const TCP_INFO_DEFAULT_MSS: u32 = 1460; // 1500 - 20(IP) - 20(TCP) -const TCP_INFO_DEFAULT_PMTU: u32 = 1500; -const TCP_INFO_INITIAL_RTO_MICROS: u32 = 1_000_000; // 1s 初始 RTO -const TCP_INFO_DEFAULT_REORDERING: u32 = 3; +let config = NetworkConfig { + interfaces: vec![InterfaceConfig { + name: "eth0".to_string(), + match_by: InterfaceMatcher::ByOrder(0), + static_ip: None, + dhcp: true, + metric: 100, + dns_servers: vec![], + }], + default_dns_servers: vec![Ipv4Addr::new(8, 8, 8, 8)], +}; ``` -## Ephemeral Port 范围 - -自动端口分配(TCP/UDP bind 时 `port == 0`)从 `49152`(IANA 动态端口范围下界,`0xC000`)开始,逐个尝试直到找到未占用端口。 - -## 超时与 Poll 间隔 - -| 常量 | 位置 | 值 | 说明 | -| --- | --- | --- | --- | -| `DNS_DEFAULT_TIMEOUT` | [lib.rs](net/ax-net/src/lib.rs) | 5s | DNS 查询超时 | -| `DHCP_BOOTSTRAP_ATTEMPTS` | [lib.rs](net/ax-net/src/lib.rs) | 200 | DHCP bootstrap 最大重试次数 | -| `DHCP_BOOTSTRAP_POLL_INTERVAL` | [lib.rs](net/ax-net/src/lib.rs) | 10ms | DHCP bootstrap poll 间隔 | -| `DHCP_MAX_RETRY_SHIFT` | [service.rs](net/ax-net/src/service.rs) | 4 | DHCP 指数退避最大位移(最大 16s) | -| `NEIGHBOR_TTL` | [ethernet.rs](net/ax-net/src/device/ethernet.rs) | 300s | ARP neighbor 缓存 TTL | -| `ARP_REQUEST_RETRY` | [ethernet.rs](net/ax-net/src/device/ethernet.rs) | 1s | ARP 请求重试间隔 | -| Idle poll interval | [lib.rs](net/ax-net/src/lib.rs) | 100ms | net-poll worker 空闲轮询间隔 | - -## Router 缓冲区配置 - -| 名称 | 位置 | 容量 | -| --- | --- | --- | -| `Router::rx_buffer` | [router.rs](net/ax-net/src/router.rs) | `SOCKET_BUFFER_SIZE` 个 MTU 槽位 × 1500 字节 | -| `Router::tx_buffer` | [router.rs](net/ax-net/src/router.rs) | 同上 | -| `RouterQueues::rx` | [router.rs](net/ax-net/src/router.rs) | `SOCKET_BUFFER_SIZE` 个 inline `RxPacket`(每包最多 MTU) | -| `DeviceHandle::tx_queue` | [router.rs](net/ax-net/src/router.rs) | `DEVICE_TX_QUEUE_SIZE` 个 inline `TxPacket`(每包最多 MTU) | -| `EthernetDevice::pending_packets` | [ethernet.rs](net/ax-net/src/device/ethernet.rs) | `ETHERNET_MAX_PENDING_PACKETS`(128)个 IP 包 | - -`LoopbackDevice` 不维护独立 buffer。普通 smoltcp loopback TX 在 `Router::dispatch()` 中直接注入 `Router::rx_buffer`;`send_on_device()` 的 loopback 特殊发包才进入共享 `RouterQueues::rx`,且同样使用 inline `QueuedPacket`。 - -## Unix Stream 缓冲区 - -Unix stream transport 使用 `ringbuf::HeapRb` 作为双向通道: - -```rust -const BUF_SIZE: usize = 64 * 1024; // 64 KiB 每方向 -``` +## 配置建议 -一对 socketpair 的总内存开销为 `2 × 64 KiB = 128 KiB`。 +- 多网口默认路由通过 metric 控制,主出口使用较小 metric。 +- `gateway = 0.0.0.0` 用于只有直连路由的静态接口。 +- 需要稳定接口名时优先使用 `ByMac` 或 `ByDriverName`,避免依赖探测顺序。 +- 新代码通过 `ipv4_config(name)` 查询地址,不依赖 `eth0_ipv4_config()`。 +- 提高队列常量时应按“每 socket”或“每设备”的乘数估算内存,而不是只看单个 buffer。 diff --git a/docs/docs/architecture/net/control.md b/docs/docs/architecture/net/control.md index 7dccef6d3f..b8f56bb1c1 100644 --- a/docs/docs/architecture/net/control.md +++ b/docs/docs/architecture/net/control.md @@ -5,65 +5,209 @@ sidebar_label: "控制面" # 控制面 -控制面是 `ax-net` 的接口管理和路由决策层,与数据面完全分离。所有接口查询、路由查找和 DNS 查询通过 `NetControl` 进行,只读操作仅持 `RwLock` 读锁,不进入设备锁也不推进 smoltcp poll。 +控制面维护 `ax-net` 的接口、地址、路由、DNS 和 socket 设备绑定状态,为协议核心和系统 ABI 提供查询与决策入口。对应到 Linux,相关职责分布在 netdevice、地址管理、FIB/route table、resolver 配置和 socket bind 状态中;对应到 lwIP/smoltcp,则是 netif 配置、地址管理和路由选择逻辑。 + +核心源码: + +| 源码 | 职责 | +| --- | --- | +| [config.rs](net/ax-net/src/config.rs) | 控制面公开数据模型:`InterfaceId`、`InterfaceInfo`、`NetworkConfig`、`RouteInfo`、`DeviceBinding` | +| [service.rs](net/ax-net/src/service.rs) | `NetControl`、接口 registry、DNS registry、DHCP commit、route 查询入口 | +| [router.rs](net/ax-net/src/router.rs) | `RouteTable`、`Rule`、`RouteDecision`、TX dispatch route lookup | +| [general.rs](net/ax-net/src/general.rs) | socket 通用选项中的 `SO_BINDTODEVICE` / `DeviceBinding` 存取 | +| [tcp.rs](net/ax-net/src/tcp.rs)、[udp.rs](net/ax-net/src/udp.rs)、[raw.rs](net/ax-net/src/raw.rs) | connect/send/bind 时使用控制面做地址、路由和设备绑定决策 | + +## 设计边界 + +控制面是 `NetControl` 持有的只读状态层,通过 `spin::RwLock` 保护接口 registry、DNS registry 和共享路由表。它的查询接口(`interfaces()`、`select_route()`、`dns_servers()` 等)只持读锁、返回快照,不进入 `Service` 或 `SocketSet` 锁,也不接触设备收发队列。协议状态机推进、包收发和 socket payload 读写全部由数据面的 `Service::poll()` 和设备 worker 在独立的锁层级中完成。 + +```mermaid +flowchart TB + Config["NetworkConfig / InterfaceConfig"] --> Init["init_network()"] + Dhcp["DHCP ACK / NAK"] --> Commit["commit_network_state()"] + Bind["bind(addr) / SO_BINDTODEVICE"] --> Binding["DeviceBinding"] + + Init --> Control["NetControl"] + Commit --> Control + + subgraph ControlState["Control Plane State"] + Ifaces["interfaces: Vec"] + Dns["dns: Vec"] + Routes["SharedRouteTable"] + end + + Control --> Ifaces + Control --> Dns + Control --> Routes + + Ifaces --> Query["interfaces()/ipv4_config()"] + Routes --> RouteLookup["select_route_with_binding()"] + Dns --> DnsQuery["dns_servers()/dns_query_timeout()"] + Binding --> RouteLookup + RouteLookup --> Sockets["TCP/UDP/raw connect/send"] + Routes --> Dispatch["Router::dispatch()"] +``` + +控制面状态的写入路径只有两个:`init_network()` 构造初始状态,`commit_interface_update()` 在 DHCP ACK/NAK 后原子替换某接口的地址、DNS 和路由规则。两条路径都在 `Service` 锁内执行,确保 smoltcp IP address list 与控制面状态一致更新。 + +`SharedRouteTable`(`Arc>`)同时被 `NetControl`(查询侧)和 `Router`(TX dispatch 侧)持有,两者指向同一实例。控制面通过 `select_route_with_binding()` 提供 socket 级别的路由查询;`Router::dispatch()` 通过 `select_route_for_source()` 做实际发包时的出接口选择。两者共享同一套路由规则,但查询时机和过滤条件不同。 + +## 初始化流程 + +`init_network()` 是控制面状态的唯一构造入口。它按固定顺序构建 loopback、Ethernet 接口、静态地址、DNS registry 和共享路由表,然后把所有状态提交给 `NetControl` 和 `Service`。 + +```mermaid +sequenceDiagram + participant Runtime as ax-runtime + participant Lib as init_network() + participant Router as Router + participant Control as NetControl + participant Service as Service + + Runtime->>Lib: init_network(net_devs, NetworkConfig) + + Note over Lib: 1. 创建 loopback + Lib->>Router: add_device(LOOPBACK, LoopbackDevice) + Lib->>Router: add_rule(127.0.0.0/8 → lo) + + Note over Lib: 2. 遍历 Ethernet 设备 + loop 每个 net_dev + Lib->>Lib: find_interface_config(order, mac, driver_name) + alt 静态 IP + Lib->>Router: set_ipv4_config(dev, cidr, gw) + Lib->>Lib: dns.extend(static_servers) + else DHCP + Lib->>Lib: dhcp_ifaces.push(...) + end + end + + Note over Lib: 3. 构建 control + service + Lib->>Control: NetControl::new(interfaces, routes, dns) + Lib->>Service: Service::new(router, control.clone()) + Lib->>Service: iface.update_ip_addrs(lo_ip + static_ips) + + Note over Lib: 4. 注册全局单例 + Lib->>Lib: NET_CONTROL.call_once(control) + Lib->>Lib: SERVICE.call_once(Mutex(service)) + + Note over Lib: 5. DHCP bootstrap + opt DHCP enabled + loop 每个 dhcp_iface + Lib->>Service: enable_dhcp(id, dev, mac, metric) + end + Lib->>Lib: wait_for_dhcp_bootstrap() + end +``` + +关键点: -核心类型定义在 [config.rs](net/ax-net/src/config.rs),逻辑在 [service.rs](net/ax-net/src/service.rs) 和 [router.rs](net/ax-net/src/router.rs)。 +- `routes` 是 `Arc>`,`Router` 和 `NetControl` 共享同一实例。 +- `NetControl` 先于 `SERVICE` 初始化,确保 `get_control()` 在 poll worker 启动前可用。 +- DHCP bootstrap 只要求任一 DHCP 接口配置成功即返回,避免断网卡阻塞启动。 -## 接口标识 +## 数据模型 -`InterfaceId(u32)` 贯穿整个网络栈,也是 StarryOS Linux ABI 的 `ifindex` 来源: +控制面状态分为三类:对外稳定的接口标识、可快照查询的接口/DNS 状态,以及被 Router 和 `NetControl` 共享的路由表。接口 ID 用于跨模块引用同一接口,接口快照用于系统 ABI 和诊断接口,`NetControl` 负责把这些状态组织成可查询的控制面视图。 + +### 接口标识 + +`InterfaceId(u32)` 是 `ax-net` 内部和对外统一的接口标识,也是 StarryOS Linux ABI 的 ifindex 来源。 ```rust -// net/ax-net/src/config.rs +// config.rs #[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct InterfaceId(u32); impl InterfaceId { pub const LOOPBACK: Self = Self(1); - pub const fn new(raw: u32) -> Self { Self(raw) } - pub const fn get(self) -> u32 { self.0 } - pub const fn to_linux_ifindex(self) -> i32 { self.0 as i32 } + + pub const fn new(raw: u32) -> Self { + Self(raw) + } + + pub const fn get(self) -> u32 { + self.0 + } + + pub const fn to_linux_ifindex(self) -> i32 { + self.0 as i32 + } + pub const fn from_linux_ifindex(ifindex: i32) -> Option { - if ifindex > 0 { Some(Self(ifindex as u32)) } else { None } + if ifindex > 0 { + Some(Self(ifindex as u32)) + } else { + None + } } } ``` +约定: + - `InterfaceId::LOOPBACK == 1`,固定对应 `lo`。 -- Ethernet 接口从 `2` 开始,`InterfaceId::new((order as u32) + 2)` 递增。 -- `InterfaceId(0)` 是内部占位符 `TX_INTERFACE_PLACEHOLDER`,不对外暴露。 +- Ethernet 接口从 `2` 开始分配,默认命名为 `eth0`、`eth1`。 +- `InterfaceId(0)` 是内部 TX 占位符,不对外暴露。 +- StarryOS 的 `SIOCGIFINDEX`、AF_PACKET `sockaddr_ll.sll_ifindex` 都应通过 `InterfaceId` 映射。 + +### 接口快照 -`InterfaceInfo` 是对外只读快照,由内部 `NetInterface` 通过 `to_info()` 转换: +对外接口信息使用 `InterfaceInfo`,它是快照,不是内部对象引用: ```rust // config.rs pub struct InterfaceInfo { pub id: InterfaceId, pub name: String, - pub kind: InterfaceKind, // Loopback | Ethernet + pub kind: InterfaceKind, pub mac: Option, pub ipv4: Option, pub mtu: usize, - pub flags: InterfaceFlags, // UP | RUNNING | LOOPBACK | BROADCAST | MULTICAST + pub flags: InterfaceFlags, pub metric: u32, } +``` + +内部状态是 `NetInterface`: -// service.rs (crate-internal) +```rust +// service.rs pub(crate) struct NetInterface { pub id: InterfaceId, pub name: String, pub kind: InterfaceKind, pub mac: Option, - pub ipv4: Option, // smoltcp native type + pub ipv4: Option, pub gateway: Option, - pub mtu: usize, pub metric: u32, + pub mtu: usize, + pub metric: u32, pub flags: InterfaceFlags, } + +impl NetInterface { + fn to_info(&self) -> InterfaceInfo { + InterfaceInfo { + id: self.id, + name: self.name.clone(), + kind: self.kind, + mac: self.mac, + ipv4: self.ipv4.map(|address| Ipv4InterfaceConfig { + address, + gateway: self.gateway, + }), + mtu: self.mtu, + flags: self.flags, + metric: self.metric, + } + } +} ``` -## NetControl +这里特意返回快照,是为了让查询方不持有内部锁,也不依赖接口状态长期不变。DHCP 更新、动态设备注册或后续 link state 更新都可能改变快照内容。 -`NetControl` 在 `init_network()` 中构建,早于 `SERVICE` 初始化,通过 `Arc` 共享: +### NetControl + +`NetControl` 是控制面的核心对象。它在 `init_network()` 中创建,并早于 `SERVICE` 注册到全局 `NET_CONTROL`。 ```rust // service.rs @@ -74,24 +218,85 @@ struct ControlState { pub struct NetControl { state: RwLock, - pub(crate) routes: SharedRouteTable, // = Arc> + pub(crate) routes: SharedRouteTable, } -// init_network() 中的构造顺序: +impl NetControl { + pub(crate) fn new( + interfaces: Vec, + routes: SharedRouteTable, + dns: Vec, + ) -> Self { + Self { + state: RwLock::new(ControlState { interfaces, dns }), + routes, + } + } +} +``` + +初始化时,`lib.rs` 构造 loopback、Ethernet 接口、静态 DNS 和共享路由表,然后把同一份 `routes` 同时交给 `Router` 和 `NetControl`: + +```rust +// lib.rs, 简化示意 +let routes: SharedRouteTable = Arc::new(spin::RwLock::new(RouteTable::new())); +let mut router = Router::new(routes.clone()); + +let lo_id = InterfaceId::LOOPBACK; +let lo_dev = router.add_device(lo_id, Box::new(LoopbackDevice::new())); +router.add_rule(Rule::new( + lo_ip.into(), + None, + lo_dev, + lo_id, + lo_ip.address().into(), + 0, +)); + +// 遍历 net_devs,为每个 Ethernet 分配 InterfaceId、name、metric、 +// 静态地址或 DHCP 状态,并写入 interfaces / routes / dns。 + let control = Arc::new(NetControl::new(interfaces, routes, dns)); let mut service = Service::new(router, control.clone()); + NET_CONTROL.call_once(|| control); SERVICE.call_once(|| Mutex::new(service)); ``` -`state` 使用 `spin::RwLock`,查询只持读锁。`routes` 与 `Service` 共享同一 `SharedRouteTable`。 +这个共享关系很关键:控制面查询看到的是 `NetControl.routes`,数据面 TX dispatch 使用的是 `Router.table`,两者实际指向同一个 `SharedRouteTable`。 + +### DNS 注册表 + +DNS server 不是简单地址列表,而是带来源和 metric 的 registry: + +```rust +pub enum DnsSource { Dhcp, Static, Fallback } + +pub(crate) struct DnsServerEntry { + pub server: Ipv4Address, + pub interface_id: InterfaceId, + pub metric: u32, + pub source: DnsSource, +} +``` + +| 来源 | 创建时机 | metric | +| --- | --- | --- | +| DHCP | DHCP ACK 后 `commit_interface_update()` | 对应接口 metric | +| Static | `init_network()` 从 `InterfaceConfig::dns_servers` | 对应接口 metric | +| Fallback | `init_network()` 从 `NetworkConfig::default_dns_servers` | `u32::MAX` | + +`dns_servers()` 排序去重后返回纯地址列表。`dns_query_timeout()` 还会通过 route decision 过滤不可达 server。 + +## 查询与决策 + +查询入口只返回快照或 route decision,不把内部锁、Router 设备索引以外的可变对象暴露给调用方。公共 API 通过 `lib.rs` facade 进入 `NetControl`,socket 实现则直接使用 crate 内部查询函数完成 bind/connect/send 前的决策。 ### 接口查询 -返回只读快照,调用者不应假设快照永久有效(DHCP 可能更新地址/路由/DNS): +只读查询都走 `NetControl` 的读锁: ```rust -// service.rs pub fn interfaces(&self) -> Vec { let state = self.state.read(); state.interfaces.iter().map(NetInterface::to_info).collect() @@ -99,25 +304,139 @@ pub fn interfaces(&self) -> Vec { pub fn interface_by_name(&self, name: &str) -> Option { let state = self.state.read(); - state.interfaces.iter().find(|i| i.name == name).map(NetInterface::to_info) + state + .interfaces + .iter() + .find(|interface| interface.name == name) + .map(NetInterface::to_info) } pub fn interface_by_id(&self, id: InterfaceId) -> Option { let state = self.state.read(); - state.interfaces.iter().find(|i| i.id == id).map(NetInterface::to_info) + state + .interfaces + .iter() + .find(|interface| interface.id == id) + .map(NetInterface::to_info) +} + +pub fn ipv4_config(&self, name: &str) -> Option { + let state = self.state.read(); + state + .interfaces + .iter() + .find(|interface| interface.name == name) + .and_then(|interface| interface.ipv4.map(|address| (interface, address))) + .map(|(interface, address)| Ipv4InterfaceConfig { + address, + gateway: interface.gateway, + }) +} +``` + +public facade 直接转发到 `NetControl`: + +```rust +// lib.rs +pub fn interfaces() -> Vec { + get_control().interfaces() +} + +pub fn interface_by_name(name: &str) -> Option { + get_control().interface_by_name(name) +} + +pub fn interface_by_id(id: InterfaceId) -> Option { + get_control().interface_by_id(id) +} + +pub fn ipv4_config(name: &str) -> Option { + get_control().ipv4_config(name) } ``` -### 路由决策 +### RouteTable -`select_route()` 按目的地址查路由,默认不带接口绑定约束;`select_route_with_binding()` 会额外应用 `DeviceBinding`,用于 `SO_BINDTODEVICE`、显式 `bind_device()` 和由本地地址推导出的接口约束。两者最终都通过 `select_route_if()` 查询路由表,`is_usable` 闭包会跳过未 `UP` 的接口: +`RouteTable` 存在于 [router.rs](net/ax-net/src/router.rs),被 `Arc>` 包装为 `SharedRouteTable`。 ```rust -// service.rs -pub fn select_route(&self, dst_addr: &IpAddress) -> AxResult { - self.select_route_with_binding(dst_addr, DeviceBinding::default()) +pub type SharedRouteTable = Arc>; + +#[derive(Debug)] +pub struct Rule { + pub filter: IpCidr, + pub via: Option, + pub dev: usize, + pub interface_id: InterfaceId, + pub src: IpAddress, + pub metric: u32, + pub order: u64, +} + +pub struct RouteTable { + rules: Vec, + next_order: u64, } +``` + +每条规则同时保存两类索引: + +- `dev`:`Router.devices` 的内部索引,用于 TX dispatch 找到真实设备。 +- `interface_id`:对外稳定接口 ID,用于查询、绑定和 Linux ifindex 映射。 + +这两个值不能混用。`dev` 是 Router 内部位置,`interface_id` 是公共语义。 + +#### 排序策略 + +路由规则在 add/replace 后排序: + +```rust +fn sort_rules(&mut self) { + self.rules.sort_by(|a, b| { + b.filter + .prefix_len() + .cmp(&a.filter.prefix_len()) + .then_with(|| a.metric.cmp(&b.metric)) + .then_with(|| a.order.cmp(&b.order)) + }); +} +``` + +优先级: +1. 最长前缀匹配。 +2. 低 metric 优先。 +3. 插入顺序稳定。 + +#### 查询策略 + +普通路由查询使用 `select_route_if()`: + +```rust +pub fn select_route_if( + &self, + dst: &IpAddress, + mut is_usable: impl FnMut(InterfaceId) -> bool, +) -> Option { + self.rules + .iter() + .find(|rule| rule.filter.contains_addr(dst) && is_usable(rule.interface_id)) + .map(|rule| RouteDecision { + dev: rule.dev, + interface_id: rule.interface_id, + source: rule.src, + next_hop: rule.via.unwrap_or(*dst), + metric: rule.metric, + }) +} +``` + +`NetControl::select_route_with_binding()` 在这个闭包里应用两个过滤条件: + +- 如果 socket 绑定了接口,只允许该接口。 +- 只允许 `InterfaceFlags::UP` 的接口。 + +```rust pub fn select_route_with_binding( &self, dst_addr: &IpAddress, @@ -125,181 +444,286 @@ pub fn select_route_with_binding( ) -> AxResult { let state = self.state.read(); let routes = self.routes.read(); - let route = routes.select_route_if(dst_addr, |interface_id| { - if binding.bound_if.is_some_and(|bound_if| bound_if != interface_id) { - return false; - } - state.interfaces.iter() - .find(|i| i.id == interface_id) - .is_some_and(|i| i.flags.contains(InterfaceFlags::UP)) - }).ok_or_else(|| ax_err_type!(NoSuchDeviceOrAddress, - format!("no route to destination {dst_addr}")))?; + let route = routes + .select_route_if(dst_addr, |interface_id| { + if binding + .bound_if + .is_some_and(|bound_if| bound_if != interface_id) + { + return false; + } + state + .interfaces + .iter() + .find(|interface| interface.id == interface_id) + .is_some_and(|interface| interface.flags.contains(InterfaceFlags::UP)) + }) + .ok_or_else(|| { + ax_err_type!( + NoSuchDeviceOrAddress, + format!("no route to destination {dst_addr}") + ) + })?; Ok(route) } ``` -`RouteDecision` 返回选中的设备索引、源地址、下一跳和 metric: +TX dispatch 使用 `select_route_for_source()`: ```rust -// router.rs -#[derive(Debug, Clone, Copy)] -pub struct RouteDecision { - pub dev: usize, // Router::devices 索引 - pub interface_id: InterfaceId, - pub source: IpAddress, // smoltcp 注入 IP 包头的 src_ip - pub next_hop: IpAddress, // 直连时 = dst,有 gateway 时 = gateway - pub metric: u32, +pub fn select_route_for_source( + &self, + dst: &IpAddress, + source: &IpAddress, +) -> Option { + self.rules + .iter() + .find(|rule| rule.filter.contains_addr(dst) && &rule.src == source) + .map(|rule| RouteDecision { + dev: rule.dev, + interface_id: rule.interface_id, + source: rule.src, + next_hop: rule.via.unwrap_or(*dst), + metric: rule.metric, + }) } ``` -### 接口-地址绑定 +这个函数服务于多宿主场景:smoltcp 已经生成 IP 包并选择了源地址,Router 不能只按目的地址选路由,否则可能从 `eth1` 发出源地址属于 `eth0` 的包。 + +## 状态更新流程 + +动态状态更新主要来自 DHCP 和运行期设备注册。更新必须同时覆盖 smoltcp `Interface` 地址、控制面接口快照、DNS registry 和 route table,避免外部查询和数据面发送路径看到不一致的网络状态。 + +### 路由规则更新 -`local_binding_for()` 遍历接口,将监听地址映射为 `DeviceBinding`: +静态接口初始化或 DHCP ACK 后都会生成一组 IPv4 规则: ```rust -// service.rs -pub fn local_binding_for(&self, endpoint: &IpListenEndpoint) -> AxResult { - match endpoint.addr { - Some(addr) => { - let state = self.state.read(); - let bound_if = state.interfaces.iter().find_map(|i| { - (i.ipv4.is_some_and(|ip| IpAddress::Ipv4(ip.address()) == addr)) - .then_some(i.id) - }); - bound_if.map(|id| DeviceBinding { bound_if: Some(id) }) - .ok_or_else(|| ax_err_type!(NoSuchDeviceOrAddress)) +// router.rs +pub(crate) fn ipv4_rules( + &mut self, + dev: usize, + interface_id: InterfaceId, + metric: u32, + address: Option, + gateway: Option, +) -> Vec { + self.devices[dev].inner.lock().set_ipv4_addr(address); + + let mut rules = Vec::new(); + if let Some(address) = address { + rules.push(Rule::new( + address.into(), + None, + dev, + interface_id, + address.address().into(), + metric, + )); + if let Some(gateway) = gateway { + rules.push(Rule::new( + Ipv4Cidr::new(Ipv4Address::UNSPECIFIED, 0).into(), + Some(gateway), + dev, + interface_id, + address.address().into(), + metric, + )); } - None => Ok(DeviceBinding::default()), // wildcard → 不绑定接口 } + rules } ``` -### 运行期更新 - -`commit_interface_update()` 在 DHCP ACK 后以事务方式更新接口状态、DNS 和路由: +替换某接口 IPv4 规则时使用 `replace_ipv4_rules_for_interface()`: ```rust -// service.rs -fn commit_interface_update(&self, update: &NetworkStateUpdate, - routes: Vec) { - let mut state = self.state.write(); - // 1. 更新接口 IPv4/gateway - if let Some(i) = state.interfaces.iter_mut() - .find(|i| i.id == update.interface_id) - { - i.ipv4 = update.ipv4; - i.gateway = update.gateway; +pub fn replace_ipv4_rules_for_interface( + &mut self, + interface_id: InterfaceId, + mut new_rules: Vec, +) { + self.remove_ipv4_rules_for_interface(interface_id); + for rule in &mut new_rules { + rule.order = self.next_order; + self.next_order = self.next_order.saturating_add(1); } - // 2. 替换该接口同来源的 DNS 条目 - state.dns.retain(|e| e.interface_id != update.interface_id - || e.source != update.dns_source); - state.dns.extend(update.dns_servers.iter().copied().map(|s| DnsServerEntry { - server: s, interface_id: update.interface_id, - metric: update.metric, source: update.dns_source, - })); - // 3. 原子替换路由规则 - self.routes.write().replace_ipv4_rules_for_interface(update.interface_id, routes); + self.rules.extend(new_rules); + self.sort_rules(); } ``` -三步在同一写锁内完成,外部不会看到半更新状态。 +这保证 DHCP 更新不会留下旧地址或旧默认路由。 -## RouteTable +### DHCP 事务更新 -`RouteTable` 在 [router.rs](net/ax-net/src/router.rs) 中实现,由 `Arc>`(`SharedRouteTable`)包装,被 `NetControl` 和 `Service` 共享。 +DHCP 更新跨越三类状态: + +- smoltcp `Interface` 的 IP address list。 +- `NetControl.state.interfaces` 中的 IPv4/gateway。 +- DNS entries 和 route table。 + +更新入口是 `Service::handle_dhcp_event()`: ```rust -// router.rs -#[derive(Debug)] -pub struct Rule { - pub filter: IpCidr, // 目标网段 - pub via: Option, // gateway(None = 直连) - pub dev: usize, // Router::devices 索引 - pub interface_id: InterfaceId, - pub src: IpAddress, // 源地址 - pub metric: u32, - pub order: u64, // 插入顺序(next_order 自增) +fn handle_dhcp_event(&mut self, event: DhcpEvent) { + let update = match event { + DhcpEvent::Configured { + interface_id, + dev, + metric, + address, + router, + dns_servers, + .. + } => { + let old_ipv4 = { + let Some(state) = self + .dhcp + .iter_mut() + .find(|state| state.interface_id == interface_id) + else { + return; + }; + let old_ipv4 = state.address; + state.address = Some(address); + state.dns_servers = dns_servers.clone(); + old_ipv4 + }; + NetworkStateUpdate { + interface_id, + dev, + metric, + old_ipv4, + ipv4: Some(address), + gateway: router, + dns_source: DnsSource::Dhcp, + dns_servers, + } + } + DhcpEvent::Deconfigured { /* 同接口清空 DHCP 状态 */ } => { + /* 生成 ipv4=None / gateway=None / dns_servers=[] 的 update */ + } + }; + self.commit_network_state(update); } +``` -pub struct RouteTable { - rules: Vec, - next_order: u64, +真正提交在 `commit_network_state()`: + +```rust +fn commit_network_state(&mut self, update: NetworkStateUpdate) { + Self::set_interface_ipv4(&mut self.iface, update.old_ipv4, update.ipv4); + let routes = self.router.ipv4_rules( + update.dev, + update.interface_id, + update.metric, + update.ipv4, + update.gateway.map(IpAddress::Ipv4), + ); + self.control.commit_interface_update(&update, routes); } ``` -### 排序策略 - -`sort_rules()` 在每次 add/replace 后调用: +`NetControl::commit_interface_update()` 在一个控制面写锁内替换接口状态、DNS 和路由: ```rust -fn sort_rules(&mut self) { - self.rules.sort_by(|a, b| { - b.filter.prefix_len().cmp(&a.filter.prefix_len()) // 1. 最长前缀优先 - .then_with(|| a.metric.cmp(&b.metric)) // 2. 低 metric 优先 - .then_with(|| a.order.cmp(&b.order)) // 3. 插入顺序稳定 +fn commit_interface_update( + &self, + update: &NetworkStateUpdate, + routes: Vec, +) { + let mut state = self.state.write(); + if let Some(interface) = state + .interfaces + .iter_mut() + .find(|interface| interface.id == update.interface_id) + { + interface.ipv4 = update.ipv4; + interface.gateway = update.gateway; + } + state.dns.retain(|entry| { + entry.interface_id != update.interface_id || entry.source != update.dns_source }); + state.dns.extend(update.dns_servers.iter().copied().map(|server| { + DnsServerEntry { + server, + interface_id: update.interface_id, + metric: update.metric, + source: update.dns_source, + } + })); + self.routes + .write() + .replace_ipv4_rules_for_interface(update.interface_id, routes); } ``` -### 查询 +这个过程保证外部查询不会看到“接口地址已更新但 DNS/路由仍旧”的半更新状态。需要注意的是,smoltcp IP address list 的更新发生在 `Service` 内,因为它属于协议核心;`NetControl` 只维护对外查询和 route decision 所需状态。 -`select_route_if` 带 `is_usable` 闭包,`find()` 返回首个匹配项即优先级最高: +## Socket 绑定 -```rust -pub fn select_route_if(&self, dst: &IpAddress, - mut is_usable: impl FnMut(InterfaceId) -> bool) - -> Option -{ - self.rules.iter() - .find(|rule| rule.filter.contains_addr(dst) && is_usable(rule.interface_id)) - .map(|rule| RouteDecision { - dev: rule.dev, interface_id: rule.interface_id, - source: rule.src, next_hop: rule.via.unwrap_or(*dst), metric: rule.metric, - }) -} -``` +socket 层通过控制面把本地地址、`SO_BINDTODEVICE` 和 DNS server 可达性统一到接口语义上。绑定结果不直接保存设备索引,而是保存稳定的 `InterfaceId`,后续 route lookup 和 waker 注册再根据它过滤可用接口。 + +### 本地地址推导 -`select_route_for_source` 在 TX dispatch 时额外要求源地址匹配(多宿主场景): +`bind(具体本地地址)` 会推导接口绑定。核心函数是 `local_binding_for()`: ```rust -pub fn select_route_for_source(&self, dst: &IpAddress, source: &IpAddress) - -> Option -{ - self.rules.iter() - .find(|rule| rule.filter.contains_addr(dst) && &rule.src == source) - .map(|rule| RouteDecision { /* ... */ }) +pub fn local_binding_for(&self, endpoint: &IpListenEndpoint) -> AxResult { + match endpoint.addr { + Some(addr) => { + let state = self.state.read(); + let bound_if = state.interfaces.iter().find_map(|interface| { + (interface + .ipv4 + .is_some_and(|ipv4| IpAddress::Ipv4(ipv4.address()) == addr)) + .then_some(interface.id) + }); + bound_if + .map(|interface_id| DeviceBinding { + bound_if: Some(interface_id), + }) + .ok_or_else(|| { + ax_err_type!( + NoSuchDeviceOrAddress, + format!("local address {addr} is not assigned to any interface") + ) + }) + } + None => Ok(DeviceBinding::default()), + } } ``` -### 规则管理 +语义: -```rust -pub fn add_rule(&mut self, mut rule: Rule) { - rule.order = self.next_order; self.next_order = self.next_order.saturating_add(1); - self.rules.push(rule); self.sort_rules(); -} +- 绑定具体地址:必须是某个接口已经拥有的 IPv4 地址,并推导出 `DeviceBinding { bound_if: Some(id) }`。 +- wildcard bind:返回默认绑定,不限制接口。 +- 该绑定会影响后续 route lookup 和 waker 注册。 -pub fn default_routes(&self) -> Vec { - self.rules.iter() - .filter(|rule| matches!(rule.filter, - IpCidr::Ipv4(cidr) if cidr.address() == Ipv4Address::UNSPECIFIED - && cidr.prefix_len() == 0)) - .map(Rule::to_info).collect() -} +TCP/UDP bind 会使用这个结果。例如 UDP bind 的设计是: -pub fn replace_ipv4_rules_for_interface(&mut self, interface_id: InterfaceId, - mut new_rules: Vec) { - self.remove_ipv4_rules_for_interface(interface_id); - for rule in &mut new_rules { - rule.order = self.next_order; self.next_order = self.next_order.saturating_add(1); - } - self.rules.extend(new_rules); self.sort_rules(); +```rust +let endpoint = IpListenEndpoint { + addr: if local_addr.ip().is_unspecified() { + None + } else { + Some(local_addr.ip().into()) + }, + port: local_addr.port(), +}; + +let binding = get_control().local_binding_for(&endpoint)?; +if binding.bound_if.is_some() { + self.general.set_device_binding(binding); } ``` -## DeviceBinding +### DeviceBinding -对应 Linux `SO_BINDTODEVICE`,存储在 `GeneralOptions::bound_if: AtomicU32`: +`DeviceBinding` 对应 Linux `SO_BINDTODEVICE` 和本地地址推导出的接口约束: ```rust // config.rs @@ -309,41 +733,151 @@ pub struct DeviceBinding { } ``` -- `None`:不绑定接口,route decision 选择出接口。 -- `Some(id)`:只允许对应接口参与 route、waker 注册和设备可用性过滤。 +`GeneralOptions` 用 `AtomicU32` 保存它: + +```rust +pub(crate) struct GeneralOptions { + bound_if: AtomicU32, + // ... +} + +pub fn set_device_binding(&self, binding: DeviceBinding) { + self.bound_if.store( + binding.bound_if.map_or(0, InterfaceId::get), + Ordering::Release, + ); +} + +pub fn device_binding(&self) -> DeviceBinding { + let raw = self.bound_if.load(Ordering::Acquire); + DeviceBinding { + bound_if: (raw != 0).then_some(InterfaceId::new(raw)), + } +} +``` + +影响范围: + +- `select_route_with_binding()` 只允许匹配接口的 route。 +- `register_waker(binding, waker)` 只向匹配接口的设备注册 waker。 +- `SO_BINDTODEVICE` 设置后,socket 不应被无关设备 readiness 唤醒。 + +## 运行期设备注册 + +启动时注册的 NIC 和运行期新增的静态设备都通过同一套接口 registry、smoltcp address list 和 route table 更新路径进入协议栈。控制面因此不是只读配置表,而是网络状态变化的提交点。 -设置/读取通过 `set_device_binding()` / `device_binding()` 操作 `AtomicU32`。 -## DNS 注册 -DNS server 分三类来源,记录为 `DnsServerEntry`: +控制面也服务于运行时静态设备注册,例如 Wi-Fi SoftAP: ```rust -// config.rs -pub(crate) struct DnsServerEntry { - pub server: Ipv4Address, - pub interface_id: InterfaceId, - pub metric: u32, - pub source: DnsSource, // Dhcp | Static | Fallback +pub fn register_device_with_config(dev: Box, config: NetConfig) { + let mac = EthernetAddress(dev.mac_address()); + let server_ip = Ipv4Address::new(config.ip[0], config.ip[1], config.ip[2], config.ip[3]); + let cidr = Ipv4Cidr::new(server_ip, config.prefix_len); + let eth_dev = if config.dedicated_poll { + EthernetDevice::new_oob_rx(config.name.clone(), dev, Some(cidr)) + } else { + EthernetDevice::new(config.name.clone(), dev, Some(cidr)) + }; + let dev_idx = get_service().register_static_device(config.name.clone(), eth_dev, mac, cidr); + // 可选启用 DHCP server... + request_poll(); } ``` -| 来源 | 创建时机 | metric | -| --- | --- | --- | -| DHCP | DHCP ACK 后 `commit_interface_update` | 对应接口 metric | -| Static | `init_network()` 从 `InterfaceConfig::dns_servers` | 对应接口 metric | -| Fallback | `init_network()` 从 `NetworkConfig::default_dns_servers` | `u32::MAX` | - -`dns_servers()` 排序去重返回纯地址列表: +`Service::register_static_device()` 会: ```rust -// service.rs -pub fn dns_servers(&self) -> Vec { - let state = self.state.read(); - let mut entries = state.dns.clone(); - entries.sort_by_key(|e| (e.metric, e.interface_id.get(), e.server.octets())); - let mut servers = Vec::new(); - for e in entries { if !servers.contains(&e.server) { servers.push(e.server); } } - servers +pub fn register_static_device( + &mut self, + name: String, + dev: EthernetDevice, + mac: EthernetAddress, + cidr: Ipv4Cidr, +) -> usize { + let interface_id = self.control.allocate_interface_id(); + let metric = 100; + let dev = self.router.add_device(interface_id, Box::new(dev)); + let routes = self + .router + .ipv4_rules(dev, interface_id, metric, Some(cidr), None); + Self::set_interface_ipv4(&mut self.iface, None, Some(cidr)); + self.control.add_interface(/* NetInterface */, routes); + self.router.start_device_workers(dev); + dev } ``` + +这条路径说明控制面不是仅启动时静态表;它也能接收运行期新增接口,并把新接口加入 registry、route table 和 smoltcp address list。 + +## 并发与锁 + +控制面锁只保护接口、DNS 和路由状态,不保护设备收发队列,也不推进 smoltcp poll。数据面 worker、socket 热路径和 DHCP commit 通过固定锁顺序进入控制面,避免设备锁与协议核心锁互相反向嵌套。 + +### 锁边界 + +控制面锁边界应遵循: + +- 只读查询只持 `NetControl.state.read()`,返回快照后释放锁。 +- 路由查询同时读取 `state` 和 `routes`,不进入设备锁。 +- DHCP commit 在 `Service` 锁内更新 smoltcp IP list,然后进入 `NetControl` 写锁提交接口/DNS/route 状态。 +- 设备 worker 持有设备锁时不得反向进入 `Service` 或 `SocketSet`。 + +典型路径可以分开理解: + +```text +poll path: + SERVICE -> SOCKET_SET -> smoltcp Interface/SocketSet + +TCP bind/listen path: + SOCKET_SET -> TCP_BOUND_PORTS -> LISTEN_TABLE + +DHCP commit path: + SERVICE -> SOCKET_SET -> NET_CONTROL.state -> RouteTable + +control query path: + NET_CONTROL.state -> RouteTable +``` + +控制面查询路径通常不持有 `SERVICE`,因此 `interfaces()`、`default_routes()`、`dns_servers()` 不会阻塞在 smoltcp poll 上;运行期 commit 则由 `Service` 协调,确保 smoltcp 地址和控制面状态一致。 + +## 与数据面的交互 + +控制面状态不是被动配置表——它在 socket 操作的每个关键路径上被主动查询。以下是 TCP/UDP socket 典型生命周期中控制面的参与点: + +```mermaid +flowchart LR + subgraph Socket["Socket 操作"] + Bind["bind(addr)"] + Connect["connect(dst)"] + Send["send(data)"] + end + + subgraph Control["控制面查询"] + LB["local_binding_for()"] + SR["select_route_with_binding()"] + RW["register_waker(binding)"] + end + + subgraph Data["数据面"] + Dispatch["Router::dispatch()"] + Snoop["snoop_tcp_packet()"] + Poll["smoltcp Interface::poll()"] + end + + Bind -->|"推导接口"| LB + LB -->|"DeviceBinding"| RW + Connect -->|"查路由"| SR + SR -->|"RouteDecision"| Send + Send -->|"写入 smoltcp TX buffer"| Poll + Poll -->|"TX IP 包"| Dispatch + Dispatch -->|"select_route_for_source()"| Control +``` + +- **bind**:`local_binding_for()` 从监听地址推导出 `DeviceBinding`,写入 `GeneralOptions::bound_if`。 +- **connect**:`select_route_with_binding()` 按目的地址 + 绑定约束选出接口和源地址,smoltcp 用此源地址构造 SYN。 +- **send**:socket 只写入 smoltcp TX buffer 并 `request_poll()`,真正的出接口选择在 `Router::dispatch()` 中由 `select_route_for_source()` 完成。 +- **poll**:smoltcp 消费 RX 包后改变 socket readiness,通过 `register_waker()` 注册的 waker 唤醒等待的 socket 操作。 + +这种设计确保控制面查询与数据面发送在时间上解耦:bind/connect 时做一次路由决策确定源地址,实际发包时再由 dispatch 根据完整 IP 包头选择出接口。 diff --git a/docs/docs/architecture/net/devices.md b/docs/docs/architecture/net/devices.md index 42d5b0b0e9..05f1d12adc 100644 --- a/docs/docs/architecture/net/devices.md +++ b/docs/docs/architecture/net/devices.md @@ -5,221 +5,679 @@ sidebar_label: "多设备实现" # 多设备实现 -本文详述 `ax-net` 的 Router 多设备适配、设备驱动层、数据面 poll 流程和协议处理实现。架构总览见[总体架构](architecture.md),Socket 层见 [Socket 系统](sockets.md)。 +`ax-net` 使用 **single smoltcp Interface + Router as Device** 的数据面结构。smoltcp 只看到一个 `phy::Device`,这个虚拟设备在内部聚合 loopback、Ethernet 和运行期注册的静态设备,并通过共享路由表把 TX packet 分发到真实出接口。 + +核心源码: + +| 源码 | 职责 | +| --- | --- | +| [router.rs](net/ax-net/src/router.rs) | `Router` 虚拟设备、route dispatch、bounded queue、loopback 快速路径、RX/TX worker | +| [device/mod.rs](net/ax-net/src/device/mod.rs) | 内部 `Device` trait、ARP entry 对外模型 | +| [device/ethernet.rs](net/ax-net/src/device/ethernet.rs) | Ethernet 帧封装/解析、ARP、IRQ/OOB readiness | +| [device/loopback.rs](net/ax-net/src/device/loopback.rs) | `lo` 接口占位设备,真实回环由 Router 快速路径完成 | +| [device/driver.rs](net/ax-net/src/device/driver.rs) | `rd-net` 到 `EthernetDriver` 的适配 | +| [service.rs](net/ax-net/src/service.rs) | `Service::poll()` 调度 Router、smoltcp、DHCP、orphan | +| [lib.rs](net/ax-net/src/lib.rs) | net-poll worker、`request_poll()`、设备注册入口 | + +## 设计边界 + +多设备层的核心是 `Router`——它实现 smoltcp 的 `phy::Device` trait,对协议核心暴露 `Medium::Ip` 层的单一虚拟设备,内部聚合 loopback 和多个 Ethernet 设备。smoltcp 只通过 `Router::receive()`/`transmit()` 读写 IP packet,不感知真实网卡数量。每个 packet 携带 ingress `InterfaceId` 元数据,用于 TCP SYN snoop、DHCP 分发和诊断。 + +TX 方向由 `Router::dispatch()` 在每次 `Service::poll()` 周期中执行:解析 smoltcp 输出的 IP 包头,通过共享 `RouteTable` 的 `select_route_for_source()` 选择出接口和 next hop。Loopback 目的地址走直接注入快速路径(`inject_loopback_rx_direct()`),在同一 poll 周期内完成 TX→RX 回环;Ethernet 设备的 packet 推入 per-device 有界 TX queue,由专用 TX worker 调用 `Device::send()` 发出。 + +设备 worker(`device_rx_worker`/`device_tx_worker`)只和有界队列交互,不进入 `Service` 或 `SocketSet` 锁。RX worker 从硬件读取 packet 后推入共享 `RouterQueues::rx`,并调用 `request_poll()` 唤醒 net-poll worker;TX worker 从 per-device TX queue 取出 packet 调用设备发送。这种隔离确保硬件收发延迟不阻塞协议核心,协议核心锁也不阻塞设备收发。 + +典型关系如下: + +```mermaid +flowchart TB + Service["Service::poll()"] --> Router["Router as smoltcp Device"] + Service --> Smol["smoltcp Interface + SocketSet"] + + subgraph RouterState["Router state"] + RxBuf["rx_buffer"] + TxBuf["tx_buffer"] + Routes["SharedRouteTable"] + Devices["Vec"] + RxQueue["shared bounded RX queue"] + end + + Router --> RxBuf + Router --> TxBuf + Router --> Routes + Router --> Devices + Devices --> RxQueue + + DevRx["per-device RX worker"] --> RxQueue + TxBuf --> Dispatch["Router::dispatch()"] + Dispatch --> Loopback["loopback direct injection"] + Dispatch --> DevTx["per-device TX queue"] + DevTx --> TxWorker["per-device TX worker"] + TxWorker --> Device["EthernetDevice / driver"] + Device --> DevRx +``` + +## 设备抽象层 -## Router 内部结构 +设备抽象层把硬件细节限制在 `device/*`,Router 只处理完整 IP packet 和 next-hop IP。这样 Ethernet、loopback、OOB Wi-Fi 等设备可以共享同一个 smoltcp 协议核心。 -`Router`([router.rs](net/ax-net/src/router.rs))是 smoltcp 与多设备之间的适配层。它的职责更接近 "MultiDevice adapter":对 smoltcp 暴露一个 `phy::Device`,内部聚合 loopback 和多个 Ethernet 设备,并在 TX 路径根据路由选择真实出接口。 +### Device Trait + +内部 `Device` trait 是 Router 与具体设备之间的能力边界: ```rust -pub struct Router { - rx_buffer: PacketBuffer, // smoltcp Device RX buffer - tx_buffer: PacketBuffer, // smoltcp Device TX buffer - queues: Arc, // 全局 RX 队列(所有设备共享) - devices: Vec>, // 设备列表(按 add_device() 顺序) - table: SharedRouteTable, // 路由表 +pub trait Device: Send + Sync { + fn name(&self) -> &str; + + fn recv( + &mut self, + interface_id: InterfaceId, + buffer: &mut PacketBuffer, + timestamp: Instant, + snoop: &mut dyn FnMut(&[u8]), + ) -> bool; + + fn send(&mut self, next_hop: IpAddress, packet: &[u8], timestamp: Instant) -> bool; + + fn set_ipv4_addr(&mut self, _addr: Option) {} + + fn arp_entries(&self, _timestamp: Instant) -> Vec { + Vec::new() + } + + fn wake_rx(&self) {} + + fn register_waker(&self, waker: &Waker); } ``` -### BoundedPacketQueue +约束: + +- `recv()` 输出完整 IP packet,不输出 Ethernet frame。 +- `send()` 输入完整 IP packet 和已选好的 `next_hop`。 +- route lookup、source address selection、TCP/UDP/raw 分发都在设备层之上完成。 +- 设备只负责链路层封装、邻居解析、硬件 RX/TX 和 readiness。 -`BoundedPacketQueue` 是有界 MPSC 队列,容量默认 `SOCKET_BUFFER_SIZE=64`: +### LoopbackDevice + +`LoopbackDevice` 是 `lo` 的控制面占位设备: ```rust -struct BoundedPacketQueue { - inner: Mutex>, - capacity: usize, - len: AtomicUsize, // 无锁长度读取,用于 is_empty() 检查 +pub struct LoopbackDevice; + +impl Device for LoopbackDevice { + fn name(&self) -> &str { + "lo" + } + + fn recv(...) -> bool { + false + } + + fn send(&mut self, _next_hop: IpAddress, _packet: &[u8], _timestamp: Instant) -> bool { + true + } + + fn register_waker(&self, _waker: &Waker) {} +} +``` + +真实 loopback 数据路径不走 `LoopbackDevice::send()/recv()`,而是在 `Router::dispatch()` 中直接把 smoltcp TX buffer 的 packet 注入 `Router.rx_buffer`。保留这个设备对象是为了让控制面、路由表和 Linux ifindex 能把 `lo` 作为普通接口处理。 + +### EthernetDevice + +`EthernetDevice` 是主要真实设备实现: + +```rust +pub struct EthernetDevice { + name: String, + inner: Arc, + neighbors: HashMap, + pending_neighbors: HashMap, + ip: Option, + pending_packets: PacketBuffer<'static, IpAddress>, +} +``` + +职责: + +- 从 `EthernetDriver::receive()` 读取 Ethernet frame。 +- 解析 ARP 和 IPv4。 +- 把 IPv4 payload 上交为完整 IP packet。 +- 根据 next hop 做 ARP/neighbor lookup。 +- 封装 Ethernet frame 并通过 driver 发送。 +- 导出 `/proc/net/arp` 所需的 ARP entry。 + +### RdNetDriver + +`RdNetDriver` 是 `rd-net` 设备到 `EthernetDriver` trait 的适配层。它持有 `rd_net::TxQueue` 和 `rd_net::RxQueue`,并通过少量 RX 预取降低驱动锁竞争。Router 不直接依赖 `rd-net` 类型,只依赖内部 `Device` trait。 + +## Router as MultiDevice + +`Router` 是 smoltcp `phy::Device` 的实现,也是单协议核心和多设备数据面之间的适配器。它不是传统意义上只维护 route table 的 router,而是一个 MultiDevice adapter。 + +### 核心结构 + +```rust +pub struct Router { + rx_buffer: PacketBuffer, + tx_buffer: PacketBuffer, + queues: Arc, + devices: Vec>, + table: SharedRouteTable, } ``` -- `push()`:加锁,满则返回 `Err(T)`(上层丢弃并打 warning)。 -- `pop()`:加锁,空返回 `None`。 -- `is_empty()`:原子读 `len`,无锁。 +字段语义: + +- `rx_buffer`:smoltcp-facing RX packet buffer,由 `Router::receive()` 消费。 +- `tx_buffer`:smoltcp-facing TX packet buffer,由 `TxToken::consume()` 写入。 +- `queues.rx`:所有非 loopback 设备 worker 共享的有界 RX 队列。 +- `devices`:Router 内部设备索引空间,和公开 `InterfaceId` 分离。 +- `table`:与控制面共享的 route table。 ### DeviceHandle -每设备一个 `DeviceHandle`,持有设备锁、收发队列和唤醒机制: +每个真实设备对应一个 `DeviceHandle`: ```rust struct DeviceHandle { interface_id: InterfaceId, name: String, inner: Arc>>, - rx_queue: Arc>, // 指向 RouterQueues::rx(共享) - tx_queue: Arc>, // 独立 TX 队列 + rx_queue: Arc>, + tx_queue: Arc>, rx_wake: Arc, tx_wake: Arc, rx_waker: Waker, } ``` -RX 队列是所有设备共享的(`RouterQueues::rx`),因为 smoltcp 从同一个 `Router::rx_buffer` 消费;TX 队列每设备独立,由 `dispatch()` 按路由决策分发。 +RX queue 是所有设备共享的,因为 smoltcp 只能从一个 `Router.rx_buffer` 获取 packet;TX queue 是每设备独立的,因为 dispatch 已经决定了出接口。 -### TX Dispatch +### smoltcp Device 实现 -`Router::dispatch()` 接收 `&mut SocketSet`,从 `tx_buffer` 取出 smoltcp 发出的 IP 包: +Router 对 smoltcp 暴露 `Medium::Ip`,即 smoltcp 看到的是 IP packet 设备,而不是 Ethernet frame 设备: -- **IPv4 广播**(dst=255.255.255.255):复制到所有非 loopback Ethernet 设备。 -- **IPv4 单播**:按 `select_route_for_source()` 选路由(要求源地址匹配)。如果出接口是 loopback,走快速路径 `inject_loopback_rx_direct()`(snoop TCP + 直接写入 `rx_buffer`);否则推入对应设备 TX 队列。 -- **IPv6 多播/单播**:`Router::dispatch()` 有对应分支并会按 route/loopback 规则分发;但当前 `EthernetDevice` 的链路层发送和接收主要实现 IPv4/ARP,完整 Ethernet IPv6/NDP 不在当前完成范围。这里的 IPv6 dispatch 描述仅表示 Router 层有分支,不表示物理 Ethernet IPv6 已完整可用。 +```rust +impl smoltcp::phy::Device for Router { + type RxToken<'a> = RxToken<'a>; + type TxToken<'a> = TxToken<'a>; + + fn receive(&mut self, _timestamp: Instant) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> { + if self.rx_buffer.is_empty() || self.tx_buffer.is_full() { + None + } else { + let (interface_id, packet) = self.rx_buffer.dequeue().unwrap(); + Some((RxToken { interface_id, packet }, TxToken(&mut self.tx_buffer))) + } + } -### RX 数据流 + fn transmit(&mut self, _timestamp: Instant) -> Option> { + if self.tx_buffer.is_full() { + None + } else { + Some(TxToken(&mut self.tx_buffer)) + } + } -`Router::poll()` 从共享 RX 队列消费包到 smoltcp `rx_buffer`,每个包先调 `snoop_tcp_packet()` 检查 TCP SYN 预创建,再调 snoop 回调(DHCP 分发)。 + fn capabilities(&self) -> DeviceCapabilities { + let mut caps = DeviceCapabilities::default(); + caps.medium = Medium::Ip; + caps.max_transmission_unit = STANDARD_MTU; + caps.max_burst_size = Some(SOCKET_BUFFER_SIZE); + caps + } +} +``` -## Ethernet 设备实现 +ingress `InterfaceId` 在 `Router::poll()` 阶段用于 TCP SYN snoop、DHCP 分发和后续诊断;进入 smoltcp `RxToken` 后只作为 Router 内部元数据保留,smoltcp 本身仍只消费 IP packet。`TxToken` 写入时先使用内部占位接口 ID,真实出接口由 `Router::dispatch()` 解析 IP header 后按 route table 决定。 -`EthernetDevice`([ethernet.rs](net/ax-net/src/device/ethernet.rs))是 `Device` trait 的主要实现: +## 队列与 Buffer + +队列层的目标是有界、低分配和清晰所有权:设备 worker 不持有 `Router` 本体,Router 不直接阻塞在硬件收发上。 + +### BoundedPacketQueue + +`BoundedPacketQueue` 是 Router 和设备 worker 之间的有界 FIFO: ```rust -pub struct EthernetDevice { - name: String, - inner: Arc, - neighbors: HashMap, - pending_neighbors: HashMap, - ip: Option, - pending_packets: PacketBuffer, +struct BoundedPacketQueue { + inner: Mutex>, + capacity: usize, + len: AtomicUsize, } ``` -### ARP 处理 +语义: -`EthernetDevice::recv()` 处理入站 Ethernet 帧: +- `push()` 满时返回 `Err(packet)`,调用方丢包并记录 warning。 +- `pop()` 空时返回 `None`。 +- `is_empty()` 只读原子 `len`,用于 worker wait predicate。 +- 队列容量由 `SOCKET_BUFFER_SIZE` 和 `DEVICE_TX_QUEUE_SIZE` 控制。 -1. 调用 `EthernetDriver::receive()` 从硬件获取一帧。 -2. 解析 `EthernetFrame`,按 `EthernetProtocol` 分派: - - **ARP**:更新 neighbor 表(reply 和 gratuitous request),从 `pending_packets` 释放等待的包。 - - **IPv4**:encapsulation 检查,将 payload 写入 smoltcp `PacketBuffer`。 +### QueuedPacket -`send()` 路径: +队列中保存的是固定大小 packet buffer,而不是每包堆分配: -1. 查 `neighbors` 表,有则直接发送。 -2. 无则检查 `pending_neighbors`:如果已发送 ARP request 未超时,将包写入 `pending_packets`。 -3. 否则发送 ARP request,记录到 `pending_neighbors`。 -4. Neighbor TTL = 300s(与 Linux 一致),ARP retry = 1s。 +```rust +struct QueuedPacket { + bytes: [u8; STANDARD_MTU], + len: usize, +} +``` -### IRQ 模型 +`QueuedPacket::new(packet)` 会拒绝超过 `STANDARD_MTU` 的 packet。这个设计牺牲了端到端 zero-copy,但给出了明确内存上限,并避免早期 loopback 队列路径中的 `to_vec()` 分配。 -`EthernetIrqState` 管理 IRQ 注册和驱动锁。支持两种 RX 就绪模式: +### RX/TX Packet -- **IRQ 驱动**(`new()`):通过 `EthernetIrqRegistrar` 注册硬件 IRQ,IRQ 触发时 `handle_ethernet_irq()` → `poll_ready.wake()` 唤醒 RX worker。 -- **OOB 驱动**(`new_oob_rx()`):用于 SDIO Wi-Fi 等自带中断线程的设备。RX 就绪由驱动线程调用 `notify_oob_rx()` 唤醒独立 poll task(`{ifname}-oob-poll`),不经过 Ethernet IRQ 框架。 +```rust +struct RxPacket { + interface_id: InterfaceId, + bytes: QueuedPacket, +} -### RdNetDriver +struct TxPacket { + next_hop: IpAddress, + bytes: QueuedPacket, +} +``` -`RdNetDriver`([driver.rs](net/ax-net/src/device/driver.rs))是 `EthernetDriver` trait 的 `rd-net` 适配实现。内部持有 `rd_net::TxQueue` 和 `rd_net::RxQueue`,通过预取 `RX_PREFETCH_TARGET=1` 个包到 `pending_rx: VecDeque` 减少锁竞争。 +RX 需要保存 ingress `InterfaceId`,用于 DHCP 分发、诊断和后续扩展;TX 保存的是 route table 已经选择好的 next hop。 -## Loopback 快速路径 +## 数据路径 -`LoopbackDevice`([loopback.rs](net/ax-net/src/device/loopback.rs))是零状态占位类型(`pub struct LoopbackDevice;`)。它提供接口名 `lo`,`recv()` 与 waker 注册不做实际工作,`send()` 只返回成功以满足内部 `Device` trait;真正的 loopback 数据路径在 `Router` 中以快速路径实现: +数据路径分为设备 RX、smoltcp poll、TX dispatch 和 loopback 快速路径。所有路径都围绕 `Service::poll()` 批量推进。 -- **TX 方向**(`Router::dispatch()`):路由选中 loopback 时,`inject_loopback_rx_direct()` 直接将 IP 包写入 `Router.rx_buffer`,并在写入前执行 `snoop_tcp_packet()` 预创建 TCP listen socket。回环包在**同一个 `Service::poll()` 周期内**被 smoltcp 消费。 -- **DHCP/特殊发包**(`Router::send_on_device()`):对 loopback 调用 `inject_loopback_rx()` 写入共享 `RouterQueues::rx`,由下一轮 `Router::poll()` 消费。 -- **Worker 跳过**:`start_tx_workers()` 和 `start_rx_workers()` 显式跳过 `InterfaceId::LOOPBACK`。 +### RX Path -## 数据面 poll 流程 +RX worker 从真实设备获取 packet,写入共享 RX queue: -`Service::poll()` 在每个 poll 周期执行: +```text +EthernetDriver RX + -> EthernetDevice::recv() + -> device_rx_worker local PacketBuffer + -> shared RouterQueues.rx + -> request_poll() +``` + +`Router::poll()` 在协议核心线程中把共享 RX queue drain 到 smoltcp-facing `rx_buffer`: ```rust -pub fn poll(&mut self, sockets: &mut SocketSet) -> bool { - let timestamp = now(); - // 1. Router::poll():drain RX queue → rx_buffer,snoop DHCP client/server - // 2. 处理 DHCP client 事件(更新地址、路由、DNS) - // 3. DHCP server 回复通过 send_on_device() 广播 - // 4. smoltcp Interface::poll():协议状态机推进 - // 5. DHCP client 发包定时器 - // 6. 回收已完成 teardown 的 orphan TCP socket - // 7. TX dispatch:路由到设备(含 loopback 快速路径) +pub fn poll( + &mut self, + _timestamp: Instant, + sockets: &mut SocketSet<'_>, + mut snoop: impl FnMut(InterfaceId, &[u8]), +) { + while !self.rx_buffer.is_full() { + let Some(packet) = self.queues.rx.pop() else { + break; + }; + let bytes = packet.bytes.as_slice(); + snoop_tcp_packet(bytes, sockets); + snoop(packet.interface_id, bytes); + let Ok(dst) = self.rx_buffer.enqueue(bytes.len(), packet.interface_id) else { + break; + }; + dst.copy_from_slice(bytes); + } } ``` -关键:`dispatch()` 接收 `&mut SocketSet`,使 loopback 快速路径能在注入 RX buffer 前执行 `snoop_tcp_packet()`。`reap_orphans()` 在持有 SocketSet 锁时执行,删除 smoltcp socket 的动作放在 orphan 锁外执行。 +`snoop_tcp_packet()` 在 smoltcp 消费 packet 前识别 TCP SYN,为 listen socket 预创建 child;`snoop(interface_id, bytes)` 用于 DHCP client/server 等按 ingress 接口分发的控制协议。 + +### TX Path + +smoltcp 发送 packet 时只写入 `tx_buffer`,随后由 Router dispatch: + +```text +smoltcp socket + -> TxToken::consume() + -> Router.tx_buffer + -> Router::dispatch() + -> route lookup by dst + source + -> loopback direct RX or per-device TX queue +``` + +dispatch 规则: -## net-poll Worker +- IPv4 limited broadcast:复制到所有非 loopback 设备。 +- IPv4/IPv6 单播:使用 `select_route_for_source(dst, src)`,确保源地址和出接口一致。 +- IPv6 multicast:Router 层会发往非 loopback 设备;完整 Ethernet IPv6/NDP 不在当前设备层完成范围。 +- 无 route:记录 warning 并丢弃该 packet。 -`net_poll_worker` 是协议核心的唯一驱动线程: +普通设备 TX 进入对应设备的 TX queue: ```rust -fn net_poll_worker() { +fn enqueue_tx(&self, next_hop: IpAddress, packet: &[u8]) -> bool { + let Some(bytes) = QueuedPacket::new(packet) else { + return false; + }; + if self.tx_queue.push(TxPacket { next_hop, bytes }).is_err() { + return false; + } + self.tx_wake.notify_one(true); + true +} +``` + +TX worker 再调用具体设备: + +```rust +fn device_tx_worker(device: Arc) { loop { - let delay = next_poll_delay(); - let timed_out = NET_POLL_WAKE.wait_timeout_until(delay, - || NET_POLL_REQUESTED.load(Acquire)); - if !timed_out { NET_POLL_REQUESTED.store(false, Release); } - poll_until_idle(); + if let Some(packet) = device.tx_queue.pop() { + let poll_next = + device.inner.lock().send(packet.next_hop, packet.bytes.as_slice(), now()); + if poll_next { + crate::request_poll(); + } + } else { + device.tx_wake.wait_until(|| !device.tx_queue.is_empty()); + } } } ``` -`poll_until_idle()` 使用 `POLLING_INTERFACES` CAS 锁防止重入,循环内**不 yield**——net-poll worker 是专用线程,在有工作时持续批量处理以最大化吞吐。 +### Loopback Fast Path + +loopback TX 不经过设备 worker,也不进入共享 RX queue。dispatch 选中 `InterfaceId::LOOPBACK` 后直接注入 smoltcp-facing RX buffer: + +```rust +fn inject_loopback_rx_direct( + rx_buffer: &mut PacketBuffer, + dst_addr: IpAddress, + packet: &[u8], + sockets: &mut SocketSet<'_>, +) -> bool { + snoop_tcp_packet(packet, sockets); + let Ok(dst) = rx_buffer.enqueue(packet.len(), InterfaceId::LOOPBACK) else { + warn!("Loopback: RX buffer full, dropping packet to {}", dst_addr); + return false; + }; + dst.copy_from_slice(packet); + true +} +``` + +这个路径减少了一次队列 hop 和一次 packet 临时分配,并允许 loopback TCP SYN 在同一个 `Service::poll()` 周期内触发 child socket 预创建。 -## DHCP 状态机 +`send_on_device()` 的 loopback 分支仍使用 `inject_loopback_rx()` 写入共享 RX queue,主要用于指定设备发送的控制面 packet;普通 socket TX loopback 走 direct injection。 -`DhcpState`([service.rs](net/ax-net/src/service.rs))维护 per-interface DHCP 客户端: +## Worker 与唤醒 +设备 worker 是多设备层和硬件之间的异步边界。worker 不访问 `SocketSet`,也不进入 `Service`。 + +### Worker 启动 + +Router 为每个非 loopback 设备启动 RX/TX worker: + +```rust +pub fn start_tx_workers(&self) { + for dev in 0..self.devices.len() { + self.start_device_tx_worker(dev); + } +} + +fn start_device_tx_worker(&self, dev: usize) { + let Some(device) = self.devices.get(dev) else { + return; + }; + if device.interface_id == InterfaceId::LOOPBACK { + return; + } + ax_task::spawn_with_name(move || device_tx_worker(device), name); +} +``` + +运行期新增静态设备时,`register_static_device()` 会调用 `router.start_device_workers(dev)`,走同一套 worker 模型。 + +### RX Worker + +RX worker 持有设备锁调用 `Device::recv()`,然后把 packet 转成 `RxPacket` 推入共享 RX queue: + +```rust +fn device_rx_worker(device: Arc) { + let mut rx_buffer = PacketBuffer::new(vec![PacketMetadata::EMPTY; 1], vec![0u8; STANDARD_MTU]); + + loop { + let mut received = false; + { + let mut device_inner = device.inner.lock(); + let mut snoop = |_packet: &[u8]| {}; + while rx_buffer.is_empty() + && device_inner.recv(device.interface_id, &mut rx_buffer, now(), &mut snoop) + { + received = true; + } + } + + while let Ok((interface_id, packet)) = rx_buffer.dequeue() { + let Some(bytes) = QueuedPacket::new(packet) else { + continue; + }; + if device.rx_queue.push(RxPacket { interface_id, bytes }).is_err() { + break; + } + crate::request_poll(); + received = true; + } + + if !received { + device.inner.lock().register_waker(&device.rx_waker); + device.rx_wake.wait(); + } + } +} ``` -Discovering ──Offer──→ Requesting ──ACK──→ Bound - │ │ │ - └──timeout→retry── └──timeout→retry── └──NAK→Discovering (reset) + +### Waker 注册 + +Router 提供两类 waker 注册: + +```rust +pub fn register_device_waker(&self, waker: &Waker) { + for device in &self.devices { + device.inner.lock().register_waker(&device.rx_waker); + device.inner.lock().register_waker(waker); + } +} + +pub fn register_waker(&self, binding: DeviceBinding, waker: &Waker) { + for device in &self.devices { + if binding.bound_if.is_none_or(|id| id == device.interface_id) { + device.inner.lock().register_waker(&device.rx_waker); + device.inner.lock().register_waker(waker); + } + } +} ``` -- **Discovering**:广播 DHCPDISCOVER,指数退避重试(1,2,4,8... 最多 16 秒间隔)。 -- **Requesting**:收到 DHCPOFFER 后广播 DHCPREQUEST。 -- **Bound**:收到 DHCPACK,状态转为 Bound。NAK 触发 reset → Discovering。 +`register_device_waker()` 用于 net-poll worker 的全局设备 readiness;`register_waker(binding, waker)` 用于 socket readiness,只向 `SO_BINDTODEVICE` 或本地地址绑定允许的接口注册。 + +## Ethernet 链路层 + +Ethernet 设备在 IP packet 与真实 Ethernet frame 之间转换,并维护 ARP/neighbor 状态。 + +### RX 处理 -`process_packet()` 按 ingress `InterfaceId` 匹配,校验 `transaction_id` 和 `client_hardware_address` 防止误收。 +`EthernetDevice::recv()` 的入站流程: -## TCP Orphan Socket 回收 +1. 从 `EthernetDriver::receive()` 读取一帧。 +2. 解析 `EthernetFrame`。 +3. ARP frame:更新 neighbor 表、处理 gratuitous request/reply、释放 pending packet。 +4. IPv4 frame:校验链路层目标,取出 IP payload,写入 Router 提供的 packet buffer。 +5. 其他协议:忽略或记录。 -当用户关闭 TCP socket(`TcpSocket::drop()`)时,如果连接仍处于活跃 teardown 状态或有未发送数据,socket 转为 orphan 状态([orphan.rs](net/ax-net/src/orphan.rs)): +`recv()` 输出给 Router 的始终是 IP packet,而不是 Ethernet frame。 -- **目的**:保证 RFC 793 合规的 FIN 四次挥手和 TIME_WAIT,避免对端连接悬挂。 -- **回收**:`reap_orphans()` 在每个 `Service::poll()` 周期中调用: - - `Closed` → 立即移除 - - `TimeWait/FinWait*/LastAck/Closing` → 最长 60 秒后强制移除 - - 其他意外状态 → 60 秒后强制移除并 warn -- **溢出保护**:`ORPHAN_MAX_SOCKETS = 1024`,超限时只 warn 不强制清除正在 teardown 的连接。 +### TX 处理 -## DHCP 服务器(SoftAP 模式) +`EthernetDevice::send(next_hop, packet)` 的出站流程: -`DhcpServer`([dhcp_server.rs](net/ax-net/src/dhcp_server.rs))是最简 IPv4 DHCP 服务器,用于 Wi-Fi SoftAP 模式: +1. 查询 `neighbors`。 +2. 命中:封装 Ethernet frame 并发送。 +3. 未命中但已有 pending ARP:把 packet 放入 `pending_packets`。 +4. 未命中且需要重试:发送 ARP request,记录 `pending_neighbors`。 -- 单客户端、单地址租约,处理 Discover→Offer、Request→Ack。 -- 不依赖 smoltcp 的 DHCP socket,手工解析/封装 `DhcpRepr → UdpRepr → Ipv4Repr`。 -- 入站包在 `Router::poll()` 的 snoop 回调中分发,与 DHCP client 路径完全独立。 +关键参数: -## DNS 解析流程 +- neighbor TTL:300 秒。 +- ARP retry:1 秒。 +- pending packet buffer:有界。 -`dns_query()`([lib.rs](net/ax-net/src/lib.rs)): +### IRQ 与 OOB RX -1. `dns_servers()` 获取按 (metric, interface_id, server_ip) 排序的去重 DNS server 列表。 -2. 过滤不可路由的 server(通过 `select_route()` 检查可达性)。 -3. 在 `SOCKET_SET` 中创建 `dns::Socket`,发起 A 记录查询。 -4. 循环 `request_poll()` + `yield_now()`,检查 `get_query_result()`。 -5. 默认 5 秒超时,超时返回 `ETIMEDOUT`。 -6. `DnsSocketGuard` 在 drop 时从 `SOCKET_SET` 移除 DNS socket。 +Ethernet 支持两种 RX readiness 模式: -## 设备能力边界(Device trait) +- IRQ 模式:`EthernetIrqRegistrar` 注册硬件 IRQ,IRQ 到来后 `handle_ethernet_irq()` 唤醒 `poll_ready`。 +- OOB RX 模式:用于 SDIO Wi-Fi 等设备,RX 就绪由设备外部线程调用 `notify_oob_rx()`,再唤醒 `{ifname}-oob-poll` 和设备 worker。 -`ax-net` 不直接依赖硬件驱动框架。所有设备实现统一的内部 `Device` trait([device/mod.rs](net/ax-net/src/device/mod.rs)): +`register_waker()` 只在存在 IRQ registration 或 OOB RX wake source 时注册: ```rust -pub trait Device: Send + Sync { - fn name(&self) -> &str; - fn recv(&mut self, interface_id: InterfaceId, - buffer: &mut PacketBuffer, timestamp: Instant, - snoop: &mut dyn FnMut(&[u8])) -> bool; - fn send(&mut self, next_hop: IpAddress, packet: &[u8], timestamp: Instant) -> bool; - fn set_ipv4_addr(&mut self, _addr: Option) {} - fn arp_entries(&self, _timestamp: Instant) -> Vec { Vec::new() } - fn register_waker(&self, waker: &Waker); - fn wake_rx(&self) {} +fn register_waker(&self, waker: &Waker) { + if self.inner.irq_registration.get().is_some() || self.inner.oob_rx { + self.inner.poll_ready.register(waker); + } } ``` -两个实现: +纯 polling 设备如果没有 wake source,不能把 waker 挂在永远不会被唤醒的 `poll_ready` 上。 + +## Service Poll 集成 + +`Service::poll()` 是 Router、smoltcp 和网络控制协议的汇合点。设备 worker 只负责把 packet 放入队列,真正协议推进由 net-poll worker 调用 `Service::poll()` 完成。 + +### Poll 顺序 + +```rust +pub fn poll(&mut self, sockets: &mut SocketSet) -> bool { + let timestamp = now(); + // 1. router.poll(): drain device RX queue into smoltcp-facing rx_buffer + // 2. process DHCP client/server snoop events + // 3. iface.poll(timestamp, &mut router, sockets) + // 4. DHCP client timers and sends + // 5. orphan TCP reaping + // 6. router.dispatch(): route smoltcp TX packets to devices or loopback +} +``` + +关键顺序: + +- `router.poll()` 必须在 `iface.poll()` 前执行,让 smoltcp 能消费新 RX packet。 +- DHCP client/server snoop 发生在 packet 进入 smoltcp 前,保留 ingress `InterfaceId`。 +- orphan reaper 在持有 `SocketSet` 的 poll 上下文中运行,但删除列表在 orphan 锁外执行。 +- `router.dispatch()` 在 smoltcp poll 后执行,把本轮产生的 TX packet 交给真实设备。 + +### net-poll Worker + +socket 和设备路径都只调用轻量 `request_poll()`: + +```rust +pub fn request_poll() { + NET_POLL_REQUESTED.store(true, Ordering::Release); + NET_POLL_WAKE.notify_one(true); +} +``` + +专用 net-poll worker 独占调用 `poll_until_idle()`: + +```rust +fn poll_until_idle() { + POLL_AGAIN.store(true, Ordering::Release); + loop { + if POLLING_INTERFACES + .compare_exchange(false, true, Ordering::Acquire, Ordering::Acquire) + .is_err() + { + return; + } + + while POLL_AGAIN.swap(false, Ordering::AcqRel) { + while poll_once() {} + } + POLLING_INTERFACES.store(false, Ordering::Release); + if !POLL_AGAIN.load(Ordering::Acquire) { + return; + } + } +} +``` + +这个模型保持应用线程、设备 worker 和协议核心驱动线程分离。socket 热路径不会同步执行完整 smoltcp poll,设备 worker 也不会进入 socket set。 + +## 与控制协议的交界 + +设备文档只描述控制协议进入数据面的交界,协议状态机细节放在对应文档中。 + +### DHCP Client/Server + +DHCP client 和 DHCP server 都依赖 Router RX snoop 拿到 ingress `InterfaceId`: + +```text +device RX + -> Router::poll() + -> snoop(interface_id, packet) + -> DHCP client/server packet classifier +``` + +DHCP client ACK 会通过 `NetworkStateUpdate` 更新 smoltcp address list、控制面接口快照、DNS 和 route table。DHCP server 的 Offer/Ack 使用 `Router::send_on_device(dev, next_hop, packet, timestamp)` 从指定接口发出。 + +### ARP Entries + +`arp_entries()` 从所有设备收集邻居表快照: + +```rust +pub fn arp_entries(&self, timestamp: Instant) -> Vec { + let mut entries = Vec::new(); + for device in &self.devices { + entries.extend(device.inner.lock().arp_entries(timestamp)); + } + entries +} +``` + +Ethernet 设备返回 ARP entry,loopback 返回空列表。 + +## 并发边界 + +多设备层的并发边界以“worker 不进入协议核心,协议核心不阻塞硬件”为原则。 + +### 锁顺序 + +典型路径: + +```text +net-poll path: + SERVICE -> SOCKET_SET -> Router -> RouteTable/device queues + +device RX worker: + DeviceHandle.inner -> Device::recv -> shared RX queue -> request_poll() + +device TX worker: + per-device TX queue -> DeviceHandle.inner -> Device::send + +socket readiness: + GeneralOptions -> Service::register_waker -> Router::register_waker -> Device::register_waker +``` + +禁止的反向路径: + +- 设备 worker 持设备锁进入 `Service` 或 `SocketSet`。 +- socket 热路径直接调用完整 interface poll。 +- Router dispatch 持 route table 锁时执行阻塞设备发送。 +- loopback 普通 TX 重新绕到设备队列。 + +### 性能边界 + +该设计优先保证简洁和有界资源: + +- 单 smoltcp `Interface` 保持 socket handle、wildcard listen 和动态 route 的一致性。 +- per-device worker 解耦硬件收发和协议核心。 +- 有界队列防止网络热路径无界增长。 +- `QueuedPacket` 避免每包堆分配。 +- loopback direct injection 避免额外 queue hop。 -- `LoopbackDevice`:零状态占位类型,提供 `lo` 接口标识;`recv()`/waker 注册无实际工作,真实回环在 `Router::dispatch()` 的快速路径中完成。 -- `EthernetDevice`:维护 ARP 邻居表(`NEIGHBOR_TTL = 300s`)、pending packet 队列、Ethernet 帧封装/解析,并对接 IRQ 适配。支持 IRQ 驱动和 OOB 驱动两种 RX 模式。 +不承诺端到端 zero-copy。若后续要继续降低复制,需要同时调整 `rd-net` buffer ownership、smoltcp token 和 Router queue 的 packet 生命周期。 diff --git a/docs/docs/architecture/net/flows.md b/docs/docs/architecture/net/flows.md index 073d18acb5..bbd9bcc3af 100644 --- a/docs/docs/architecture/net/flows.md +++ b/docs/docs/architecture/net/flows.md @@ -5,1087 +5,519 @@ sidebar_label: "运行时流程" # 运行时流程 -本章详述 `ax-net` 的初始化、收包、发包、ARP、TCP/UDP socket 操作、DHCP、DNS 和 poll 循环的完整运行路径。每个流程给出精确的锁获取顺序、数据结构转换和错误处理分支。 +本文描述 `ax-net` 从初始化到运行期收发包、socket 阻塞等待、DHCP/DNS 和本地 transport 的关键流程。流程以源码中的真实边界为准:应用线程只修改 socket 状态并请求 poll,设备 worker 只搬运 packet,专用 net-poll worker 独占推进 smoltcp `Interface` 和全局 `SocketSet`。 ---- - -## 1. 初始化流程 - -入口 `init_network()` 在 [net/ax-net/src/lib.rs](net/ax-net/src/lib.rs)。 - -```mermaid -sequenceDiagram - participant Runtime as ax-runtime - participant AxNet as init_network() - participant Router as Router - participant Control as NetControl - participant Service as Service - participant Worker as device / net-poll workers - - Runtime->>AxNet: init_network(net_devs, NetworkConfig) - Note over AxNet: 1. 校验配置(IP/前缀/网关/DNS) - AxNet->>Router: Router::new(routes) - AxNet->>Router: add_device(LOOPBACK, LoopbackDevice) - AxNet->>Router: add_rule(lo 直连路由 127.0.0.0/8) - Note over AxNet: 2. 遍历 net_devs - loop 每个 Ethernet 设备 - AxNet->>AxNet: find_interface_config(order, mac, driver_name) - AxNet->>Router: add_device(id, EthernetDevice) - alt 静态 IP - AxNet->>Router: set_ipv4_config(ip, gw) - else DHCP - AxNet->>AxNet: dhcp_ifaces.push(...) - end - end - AxNet->>Router: start_rx_workers() - AxNet->>Router: start_tx_workers() - AxNet->>Control: NetControl::new(interfaces, routes, dns) - AxNet->>Service: Service::new(router, control) - Note over Service: 安装 lo_ip + 静态 eth_ips 到 smoltcp ip_addrs - opt DHCP enabled - loop 每个 dhcp_ifaces - AxNet->>Service: enable_dhcp(id, dev, mac, metric) - end - end - AxNet->>AxNet: NET_CONTROL.call_once(control) - AxNet->>AxNet: SERVICE.call_once(Mutex(service)) - AxNet->>Service: register_device_waker(&NET_POLL_DEVICE_WAKER) - AxNet->>Worker: spawn net_poll_worker - opt DHCP enabled - AxNet->>AxNet: wait_for_dhcp_bootstrap() - end -``` - -### 步骤详解 - -**步骤 1 — 配置校验**([lib.rs](net/ax-net/src/lib.rs)) - -对每个 `InterfaceConfig`: - -- 名字不能是 `"lo"`(保留)。 -- `dhcp` 和 `static_ip` 不能同时配置。 -- 静态 IP 不能是 `0.0.0.0`(unspecified)。 -- 静态前缀长度不能超过 32。 -- 静态网关不能是 `0.0.0.0`。 -- 所有 DNS server 都不能是 unspecified。 - -不满足则 `panic!`(启动阶段配置错误应 fail-fast)。 - -**步骤 2 — 创建 loopback**([lib.rs](net/ax-net/src/lib.rs)) - -```rust -let lo_id = InterfaceId::LOOPBACK; // InterfaceId(1) -let lo_dev = router.add_device(lo_id, Box::new(LoopbackDevice::new())); -let lo_ip = Ipv4Cidr::new(Ipv4Address::new(127,0,0,1), 8); -router.add_rule(Rule::new(lo_ip.into(), None, lo_dev, lo_id, lo_ip.address().into(), 0)); -``` - -Loopback 设备注册后,立即安装直连路由 `127.0.0.0/8 → lo_dev`,源地址 `127.0.0.1`,metric 0。`LoopbackDevice` 是零状态占位类型,真正的回环数据路径在 `Router::dispatch()` 中以快速路径实现(见 [loopback 快速路径](architecture.md#loopback-快速路径))。 - -**步骤 3 — 注册 Ethernet 设备**([lib.rs](net/ax-net/src/lib.rs)) - -对每个 driver 提供的 `EthernetDevice`(顺序由 `net_devs.drain(..)` 决定): - -1. 计算 `default_name = "eth{order}"`。 -2. 用 `find_interface_config()` 匹配 `InterfaceConfig`。匹配方式由 `InterfaceMatcher` 决定:`ByOrder`、`ByMac` 或 `ByDriverName`([lib.rs](net/ax-net/src/lib.rs))。如果有多个 config 匹配同一设备则 panic。 -3. 检查接口名冲突。 -4. 分配 `InterfaceId`:`order + 2`(0 保留给 placeholder,1 是 loopback)。 -5. 调用 `router.add_device()` 将 `EthernetDevice` 包装为 `DeviceHandle`,注册到 `Router::devices` 列表。 -6. **静态接口**:调用 `router.set_ipv4_config()` 安装直连路由和默认路由(如果有 gateway)。 -7. **DHCP 接口**:推入 `dhcp_ifaces` 向量,稍后在 `Service` 中启用。 - -匹配完成后,检查 `used_configs`:如果某个配置没有匹配到任何设备,则 panic([lib.rs](net/ax-net/src/lib.rs))。 - -**步骤 4 — 启动 worker 线程**([lib.rs](net/ax-net/src/lib.rs)) - -```rust -router.start_rx_workers(); // 每个 Ethernet DeviceHandle 一个 RX worker -router.start_tx_workers(); // 每个 Ethernet DeviceHandle 一个 TX worker -``` - -RX worker 入口 `device_rx_worker`([router.rs](net/ax-net/src/router.rs)),TX worker 入口 `device_tx_worker`([router.rs](net/ax-net/src/router.rs))。Loopback 不需要 worker。 +核心流程涉及: -**步骤 5 — 构建 Service 和控制面**([lib.rs](net/ax-net/src/lib.rs)) +| 流程 | 关键源码 | +| --- | --- | +| 初始化 | [lib.rs](net/ax-net/src/lib.rs) `init_network()` | +| poll 主循环 | [lib.rs](net/ax-net/src/lib.rs) `net_poll_worker()` / `poll_until_idle()` | +| 协议推进 | [service.rs](net/ax-net/src/service.rs) `Service::poll()` | +| RX/TX dispatch | [router.rs](net/ax-net/src/router.rs) `Router::poll()` / `Router::dispatch()` | +| socket wait | [general.rs](net/ax-net/src/general.rs) `send_poller_with()` / `recv_poller_with()` | +| TCP passive open | [listen_table.rs](net/ax-net/src/listen_table.rs) / [router.rs](net/ax-net/src/router.rs) `snoop_tcp_packet()` | +| DHCP/DNS | [service.rs](net/ax-net/src/service.rs), [lib.rs](net/ax-net/src/lib.rs) | -```rust -let control = Arc::new(NetControl::new(interfaces, routes, dns)); -let mut service = Service::new(router, control.clone()); -service.iface.update_ip_addrs(|ip_addrs| { - ip_addrs.push(lo_ip.into()).unwrap(); - for ip in eth_ips { ip_addrs.push(ip.into()).unwrap(); } -}); -``` - -`NetControl` 持有接口 registry、路由表和 DNS 来源的可读快照。`Service` 持有 `smoltcp::Interface`、`Router` 和 `NetControl` 的引用。smoltcp 的 `ip_addrs` 在此阶段注入 loopback 和所有静态接口的 IP。 +## 总体时序 -DHCP 接口的 IP 在 ACK 后由 `commit_network_state()` → `set_interface_ipv4()` 动态注入。 +运行期只有一个协议核心 owner:net-poll worker。socket 调用者和设备 worker 都通过轻量唤醒把工作交给它。 -**步骤 6 — 注册全局单例并启动 poll worker**([lib.rs](net/ax-net/src/lib.rs)) +```mermaid +flowchart TB + App["socket send/recv/connect"] --> Req1["request_poll()"] + Dev["device RX/TX worker"] --> Req2["request_poll()"] + Timer["smoltcp/DHCP timer"] --> Worker["net-poll worker"] + Req1 --> Worker + Req2 --> Worker -```rust -NET_CONTROL.call_once(|| control); -SERVICE.call_once(|| Mutex::new(service)); -get_service().register_device_waker(&NET_POLL_DEVICE_WAKER); -ax_task::spawn_with_name(net_poll_worker, "net-poll".to_owned()); + Worker --> Poll["poll_until_idle()"] + Poll --> Service["Service::poll()"] + Service --> RouterPoll["Router::poll(): RX queue -> rx_buffer"] + Service --> Iface["smoltcp Interface::poll()"] + Service --> Dhcp["DHCP client/server"] + Service --> Orphan["TCP orphan reaper"] + Service --> Dispatch["Router::dispatch(): tx_buffer -> device"] ``` -`NET_POLL_DEVICE_WAKER` 是一个 `NetPollWake` 实例,其 `wake()` 直接调用 `request_poll()`([lib.rs](net/ax-net/src/lib.rs))。这样当设备 driver 有新数据可读时,能直接唤醒 net-poll worker。 +## 初始化阶段 -**步骤 7 — DHCP bootstrap 等待**([lib.rs](net/ax-net/src/lib.rs)) +初始化阶段构造控制面、单协议核心和多设备数据面。`init_network()` 是一次性入口,重复调用会 panic。 -```rust -if dhcp_enabled { wait_for_dhcp_bootstrap(); } -``` +### 配置校验 -`wait_for_dhcp_bootstrap()`([lib.rs](net/ax-net/src/lib.rs))循环最多 `DHCP_BOOTSTRAP_ATTEMPTS` 次,每次 `request_poll()` + `sleep(DHCP_BOOTSTRAP_POLL_INTERVAL)`,检查 `dhcp_configured()` 是否为 true。 +`init_network()` 首先校验 `NetworkConfig`: -关键策略:**只要任一 DHCP 接口配置成功就返回**([service.rs](net/ax-net/src/service.rs))。这避免了一个断网的 DHCP 网卡阻塞整个系统启动。 +- 接口名不能是保留名 `lo`。 +- `dhcp = true` 不能同时配置 `static_ip`。 +- 静态 IP 不能是 unspecified。 +- 静态 prefix 不能大于 32。 +- DNS server 不能是 unspecified。 +- 每个显式 `InterfaceConfig` 必须匹配一个设备。 +- 一个设备不能被多个 config 同时匹配。 +- 接口名不能冲突。 -超时后只打印 warning,不 panic。未配置的 DHCP 接口后续仍可在运行时异步完成配置。 - ---- +网关 `0.0.0.0` 是有效配置,表示不安装默认路由。 -## 2. 接收(RX)流程 +### 设备与控制面构建 ```mermaid sequenceDiagram - participant Dev as Ethernet driver - participant RxW as device_rx_worker - participant DevLock as DeviceHandle.inner - participant RxQ as DeviceHandle.rx_queue
(BoundedPacketQueue) - participant PollW as net-poll worker - participant Router as Router - participant RouterRx as Router.rx_buffer
(PacketBuffer) - participant Smol as smoltcp Interface - participant Sock as SocketSet - - Dev-->>RxW: IRQ → rx_wake.wait() returns - RxW->>DevLock: lock() - loop rx_buffer is empty - RxW->>Dev: device_inner.recv(interface_id, &mut rx_buffer) - Note over Dev: EthernetDevice::handle_frame:
parse → snoop → enqueue payload - end - DevLock-->>RxW: unlock - loop rx_buffer.dequeue() - RxW->>RxQ: push(RxPacket{interface_id, QueuedPacket}) - alt 队列满 - RxW->>RxW: warn + drop packet + participant Runtime + participant Init as init_network() + participant Router + participant Control as NetControl + participant Service + participant Worker + + Runtime->>Init: EthernetDeviceList + NetworkConfig + Init->>Router: Router::new(routes) + Init->>Router: add_device(LOOPBACK, LoopbackDevice) + Init->>Router: add_rule(127.0.0.0/8 -> lo) + loop each Ethernet driver + Init->>Init: match InterfaceConfig + Init->>Router: add_device(interface_id, EthernetDevice) + alt static IPv4 + Init->>Router: set_ipv4_config() + else DHCP enabled + Init->>Init: dhcp_ifaces.push() end - RxW->>PollW: request_poll() - end - RxW->>Dev: rx_wake.wait() (if no data) - Note over PollW: === poll_until_idle() === - PollW->>Router: Service::poll() - Router->>Router: wake_rx_workers() - loop Router.rx_buffer not full - Router->>RxQ: pop() - Router->>Sock: snoop_tcp_packet() → LISTEN_TABLE - Router->>RouterRx: enqueue(payload, interface_id) + Init->>Init: collect static DNS / interface snapshot end - Router->>Smol: Interface::poll(timestamp, router, sockets) - Smol->>Router: RxToken::consume(payload) - Smol->>Smol: TCP/UDP/ICMP state machine - Smol-->>PollW: readiness changes - Smol-->>Sock: wake registered socket wakers / PollSet waiters + Init->>Router: start_rx_workers() / start_tx_workers() + Init->>Control: NetControl::new(interfaces, routes, dns) + Init->>Service: Service::new(router, control) + Init->>Service: install lo/static ip_addrs + Init->>Service: enable_dhcp() for DHCP interfaces + Init->>Worker: spawn net-poll ``` -### 步骤详解 +接口 ID 约定: -**步骤 1 — RX worker 唤醒**([router.rs](net/ax-net/src/router.rs)) +- `InterfaceId(1)` 固定为 `lo`。 +- Ethernet 接口从 `InterfaceId(2)` 开始按设备发现顺序分配。 +- `InterfaceId(0)` 是 Router TX 内部占位符,不出现在 public API。 -RX worker 在 `device.rx_wake.wait()` 上阻塞。设备 driver 收到 IRQ 或被其他路径唤醒后,调用 `rx_wake.notify_one()` 唤醒 worker。 +### DHCP Bootstrap -Worker 使用一个本地 `PacketBuffer`(容量 1,大小 `STANDARD_MTU`)作为从 driver 读取的临时 buffer: +如果存在 DHCP 接口,初始化末尾调用 `wait_for_dhcp_bootstrap()`: ```rust -let mut rx_buffer = PacketBuffer::new(vec![PacketMetadata::EMPTY; 1], vec![0u8; STANDARD_MTU]); -``` - -**步骤 2 — 从设备读取**(持设备锁) - -```rust -let mut device_inner = device.inner.lock(); // 获取设备写锁 -let mut snoop = |_packet: &[u8]| {}; // 空 snoop(已在 Router::poll 中做 TCP snoop) -while rx_buffer.is_empty() - && device_inner.recv(device.interface_id, &mut rx_buffer, now(), &mut snoop) -{ - received = true; -} -// 释放设备锁 -``` - -`EthernetDevice::recv()`([device/ethernet.rs](net/ax-net/src/device/ethernet.rs))内部流程: - -1. 从 driver 的 RX ring 取一帧。 -2. 用 `EthernetFrame::new_unchecked()` 解析帧头。 -3. 检查目的 MAC:广播/空 MAC/自身 MAC 才接受,否则丢弃。 -4. `EthernetProtocol::Ipv4` → `snoop(payload)` + `buffer.enqueue(len, interface_id)` + `copy_from_slice`。 -5. `EthernetProtocol::Arp` → `process_arp()`(见 ARP 流程)。 -6. 其他 ethertype → 丢弃。 - -**步骤 3 — 推入有界 RX 队列**(释放设备锁后) - -```rust -while let Ok((interface_id, packet)) = rx_buffer.dequeue() { - let Some(bytes) = QueuedPacket::new(packet) else { - warn!("{}: RX packet exceeds MTU, dropping packet", device.name); - continue; - }; - let rx = RxPacket { interface_id, bytes }; - if device.rx_queue.push(rx).is_err() { - warn!("{}: RX queue is full, dropping packet", device.name); - break; +fn wait_for_dhcp_bootstrap() { + for _ in 0..DHCP_BOOTSTRAP_ATTEMPTS { + request_poll(); + if get_service().dhcp_configured() { + return; + } + ax_task::sleep(DHCP_BOOTSTRAP_POLL_INTERVAL); } - crate::request_poll(); // 每包唤醒 net-poll worker - received = true; -} -``` - -`rx_queue` 是 `BoundedPacketQueue`,容量 `SOCKET_BUFFER_SIZE = 64`。`RxPacket` 内部使用 `[u8; STANDARD_MTU] + len` 的 inline `QueuedPacket`,不为每个包分配 `Box<[u8]>`。队列满时丢弃剩余包并 break,避免在队列已满时继续从 driver 拷贝。 - -**步骤 4 — Router drain 共享 RX 队列** - -`net-poll` worker 被唤醒后进入 `poll_until_idle()` → `Service::poll()`。`Service::poll()` 首先调用 `Router::poll()`([router.rs](net/ax-net/src/router.rs)): - -```rust -while !self.rx_buffer.is_full() { - let Some(packet) = self.queues.rx.pop() else { break; }; - let bytes = packet.bytes.as_slice(); - snoop_tcp_packet(bytes, sockets); // 预创建 listen socket - snoop(packet.interface_id, bytes); // DHCP snoop 回调 - let Ok(dst) = self.rx_buffer.enqueue(bytes.len(), packet.interface_id) else { - warn!("Router RX buffer is full, dropping packet"); - break; - }; - dst.copy_from_slice(bytes); + warn!("DHCP bootstrap timed out"); } ``` -`Router.rx_buffer` 是 smoltcp 的 `phy::Device` RX token 来源。smoltcp 在 `Interface::poll()` 中通过 `Router::receive()` 从 `rx_buffer` 取出一个包构造 `RxToken`([router.rs](net/ax-net/src/router.rs))。 - -**步骤 5 — TCP SYN 预探测** +bootstrap 等待只要求任一 DHCP 接口成功获得地址。这样一个断开的 DHCP 网口不会阻塞整个系统启动;未配置完成的接口后续仍由 net-poll worker 继续推进。 -`snoop_tcp_packet()`([router.rs](net/ax-net/src/router.rs))在交付 smoltcp 前解析 IP 头和 TCP 头: +## Poll 主循环 -- 如果是 `SYN && !ACK`(连接首包),调用 `LISTEN_TABLE.incoming_tcp_packet(src, dst, sockets)`。 -- `LISTEN_TABLE` 检查是否有 socket 在 `(src_ip, dst_port)` 上 listen。 -- 如果是,在 `SocketSet` 中预创建一个处于 listen 状态的 `smoltcp::socket::tcp::Socket` 并放入 `ListenTable` 的 `syn_queue`。随后 smoltcp 在 `Interface::poll()` 中消费这个 SYN,才把该 child socket 推进到 `SynReceived`。 -- 这样当 smoltcp 正式处理这个 SYN 时,已经有 socket 接收它,避免 SYN 被丢弃。 +poll 主循环由 `net-poll` 线程执行。它合并 socket、设备和 timer 唤醒,并通过 CAS 防止重入。 -**步骤 6 — smoltcp 协议处理** - -`self.iface.poll(timestamp, &mut self.router, sockets)` 在持有 `Service` 锁和 `SocketSet` 锁时执行。smoltcp 从 `Router`(作为 `phy::Device`)获取 RxToken,调用 `RxToken::consume()` 将 payload 交给 TCP/UDP/ICMP 状态机处理。 - -处理结果反映在 `SocketSet` 中各 socket 的 readiness 上。poll 返回后,等待者在外部(不持锁)被唤醒。 - -### RX 数据结构转换链 - -``` -driver RX ring - → [copy] PacketBuffer (local, capacity=1, MTU=1500) - → [copy] BoundedPacketQueue (shared RX queue, capacity=64, no heap allocation per packet) - → [copy] Router.rx_buffer (PacketBuffer, shared, capacity from smoltcp config) - → [zero-copy ref] RxToken → smoltcp Interface::poll -``` - -每次 hop 最多一次内存拷贝。最坏路径有 3 次拷贝(driver → local buffer → queue → router buffer),但通过有界 inline queue 控制了背压并避免每包堆分配。 - ---- - -## 3. 发送(TX)流程 - -```mermaid -sequenceDiagram - participant App as socket send()/connect() - participant Smol as smoltcp Interface - participant Router as Router (phy::Device) - participant RouterTx as Router.tx_buffer
(PacketBuffer) - participant TxQ as DeviceHandle.tx_queue
(BoundedPacketQueue) - participant TxW as device_tx_worker - participant Dev as Ethernet driver - - App->>Smol: socket.send(data) → smoltcp TCP/UDP state machine - Smol->>Router: TxToken::consume(len) → write IP packet - Note over RouterTx: IP packet 进入 Router.tx_buffer - App->>App: request_poll() → 唤醒 net-poll - Note over net-poll: === poll_until_idle() → dispatch() === - Router->>Router: dispatch(timestamp, sockets) - loop tx_buffer.dequeue() - Router->>Router: parse IP version - alt IPv4 unicast - Router->>Router: select_route_for_source(dst, src) - Router->>TxQ: enqueue_tx(next_hop, packet) - else IPv4 broadcast - loop 每个 Ethernet 设备 - Router->>TxQ: enqueue_tx(dst, packet) - end - end - end - TxQ-->>TxW: tx_wake.notify() - TxW->>TxQ: pop() - TxW->>Dev: device.inner.lock().send(next_hop, bytes) - Note over Dev: EthernetDevice::send:
查 ARP → 封装帧 → driver.send() - Dev-->>TxW: poll_next (true if needs re-poll) - opt poll_next - TxW->>App: request_poll() - end -``` - -### 步骤详解 - -**步骤 1 — Socket 层写入** - -TCP socket 的 `send()`([tcp.rs](net/ax-net/src/tcp.rs)): +### request_poll ```rust -fn send(&self, mut src: impl Read, options: SendOptions) -> AxResult { - let extra_nb = options.flags.contains(crate::SendFlags::DONTWAIT); - let result = self.general.send_poller_with(self, extra_nb, || { - request_poll(); // 请求 poll 驱动协议栈 - self.with_smol_socket(|socket| { - if !socket.is_active() { Err(AxError::NotConnected) } - else if !socket.can_send() { Err(AxError::WouldBlock) } - else { - let len = socket.send(|buffer| { - let result = src.read(buffer); - let len = result.unwrap_or(0); - (len, result) - }).map_err(|_| ax_err_type!(NotConnected))??; - Ok(len) - } - }) - }); - if result.is_ok() { request_poll(); } // 写入后再请求一次 poll 触发发送 - result +pub fn request_poll() { + NET_POLL_REQUESTED.store(true, Ordering::Release); + NET_POLL_WAKE.notify_one(true); } ``` -`send_poller_with` 是通用 poller 入口:它把 socket 操作闭包交给 `poll_io()`。闭包返回 `WouldBlock` 时,由具体 socket 的 `Pollable::register()` 注册 waker,然后任务挂起,直到 `IoEvents::OUT` readiness、timeout 或其他错误使任务重新运行。 - -数据写入 smoltcp socket 的 TX buffer 后,**socket 层不直接发送**。发送发生在 smoltcp `Interface::poll()` 时。 - -**步骤 2 — smoltcp 生成 IP 包** +`request_poll()` 不执行协议栈,只设置标志并唤醒 worker。socket 热路径、设备 RX/TX worker、DHCP/DNS 查询都使用这个入口。 -在 `Service::poll()` 中,`self.iface.poll()` 驱动 TCP/UDP 状态机。当 socket 有待发送数据且发送窗口允许时,smoltcp 构造 IP 包并通过 `phy::Device` 的 `TxToken::consume()` 输出。 - -`Router` 实现了 smoltcp 的 `phy::Device` trait,`transmit()` 返回 `TxToken`([router.rs](net/ax-net/src/router.rs)): +### poll_until_idle ```rust -fn transmit(&mut self, _timestamp: Instant) -> Option> { - if self.tx_buffer.is_full() { None } - else { Some(TxToken(&mut self.tx_buffer)) } -} -``` - -`TxToken::consume()` 将 smoltcp 构造的 IP 包写入 `Router.tx_buffer`,metadata 设为 `TX_INTERFACE_PLACEHOLDER`(占位符,出接口由 `dispatch()` 决定)。 - -**步骤 3 — Router::dispatch() 选择出接口**([router.rs](net/ax-net/src/router.rs)) - -`dispatch()` 接收 `&mut SocketSet`,在 smoltcp `Interface::poll()` 返回后执行,从 `tx_buffer` 取出 IP 包: +fn poll_until_idle() { + POLL_AGAIN.store(true, Ordering::Release); + loop { + if POLLING_INTERFACES + .compare_exchange(false, true, Ordering::Acquire, Ordering::Acquire) + .is_err() + { + return; + } -```rust -while let Ok((_, packet)) = self.tx_buffer.dequeue() { - match IpVersion::of_packet(packet).expect("got invalid IP packet") { - IpVersion::Ipv4 => { - let packet = Ipv4Packet::new_checked(packet).expect("got invalid IPv4 packet"); - let src_addr = IpAddress::Ipv4(packet.src_addr()); - let dst_addr = IpAddress::Ipv4(packet.dst_addr()); - if packet.dst_addr().is_broadcast() { - // 广播:发往所有 Ethernet 设备(不含 loopback) - let buf = packet.into_inner(); - for dev in &self.devices { - if dev.interface_id != InterfaceId::LOOPBACK { - poll_next |= dev.enqueue_tx(dst_addr, buf); - } - } - } else { - // 单播:按 (源地址, 目的地址) 查路由 - let routes = self.table.read(); - let Some(route) = routes.select_route_for_source(&dst_addr, &src_addr) else { - warn!("No route found for source {} destination {}", src_addr, dst_addr); - continue; - }; - let dev = &self.devices[route.dev]; - if dev.interface_id == InterfaceId::LOOPBACK { - // Loopback 快速路径:直接注入 rx_buffer + snoop TCP - poll_next |= inject_loopback_rx_direct( - &mut self.rx_buffer, dst_addr, packet.into_inner(), sockets); - } else { - poll_next |= dev.enqueue_tx(route.next_hop, packet.into_inner()); - } - } + while POLL_AGAIN.swap(false, Ordering::AcqRel) { + while poll_once() {} + } + POLLING_INTERFACES.store(false, Ordering::Release); + if !POLL_AGAIN.load(Ordering::Acquire) { + return; } - IpVersion::Ipv6 => { /* 类似逻辑 */ } } } ``` -关键点: - -- **出接口不由 socket binding 决定**,而是由 IP 包的 `(源地址, 目的地址)` 在路由表中查 `select_route_for_source()` 决定。 -- **Loopback 快速路径**:当出接口是 loopback 时,`inject_loopback_rx_direct()` 在写入 `rx_buffer` 前执行 `snoop_tcp_packet()`,使回环 TCP SYN 在**同一个 `Service::poll()` 周期内**被 smoltcp 消费,不经过设备 worker。 -- 广播包发往所有 Ethernet 设备。 -- 查不到路由时 `warn!` + `continue`,不 panic。 +`poll_once()` 的锁顺序是: -**步骤 4 — 推入 per-device TX 队列** - -`DeviceHandle::enqueue_tx()` 将 `TxPacket { next_hop, bytes }` 推入 `tx_queue`(同样是 `BoundedPacketQueue`)。成功入队后唤醒该设备的 TX worker 并返回 `true`,表示 dispatch 后可以继续 poll;如果包超过 MTU 或队列已满,则打印 warning、丢包并返回 `false`。 - -**步骤 5 — TX worker 发送**([router.rs](net/ax-net/src/router.rs)) - -```rust -fn device_tx_worker(device: Arc) { - loop { - if let Some(packet) = device.tx_queue.pop() { - let poll_next = device.inner.lock().send(packet.next_hop, packet.bytes.as_slice(), now()); - if poll_next { crate::request_poll(); } - } else { - device.tx_wake.wait_until(|| !device.tx_queue.is_empty()); - } - } -} +```text +SERVICE -> SOCKET_SET.inner -> Service::poll() ``` -`EthernetDevice::send()` 内部: +`poll_until_idle()` 在仍有工作时批量推进,不主动 yield。它是专用 worker,不需要在每个 packet 后让出给应用线程。 -1. 查 `neighbors` 表获取下一跳的 MAC 地址。 -2. 如果找到:封装 Ethernet 帧(`src=自身MAC, dst=邻居MAC, ethertype=IPv4`),调用 `driver.send()`。 -3. 如果没找到:请求 ARP(见 ARP 流程),包暂存在 `pending_packets` 队列。 +### Service::poll -**步骤 6 — ARP 完成后发送暂存包**([device/ethernet.rs](net/ax-net/src/device/ethernet.rs)) - -ARP reply 到达后,`process_arp()` 查到邻居 MAC,drain `pending_packets` 队列: +`Service::poll()` 是协议核心的单轮调度。下面是保留关键顺序的简化示意: ```rust -let mut kept: Vec<(IpAddress, Vec)> = Vec::with_capacity(ETHERNET_MAX_PENDING_PACKETS); -for _ in 0..ETHERNET_MAX_PENDING_PACKETS { - let Ok((&next_hop, buf)) = self.pending_packets.peek() else { break; }; - let action = match self.neighbors.get(&next_hop) { - Some(neighbor) => Action::Send(neighbor.hardware_address, buf), - None => Action::Keep(buf), // 保留不匹配的包,避免头阻塞 - }; - // ... -} -``` - -重要策略:drain 遍历所有暂存包,**不从头阻塞**。如果一个不可达 IP 的包在队列头部,不会阻止后面可达 IP 的包发送。 +pub fn poll(&mut self, sockets: &mut SocketSet) -> bool { + let timestamp = now(); + let mut dhcp_events = Vec::new(); + let mut dhcp_server_replies = Vec::new(); -### TX 数据结构转换链 - -``` -socket.send(data) - → [copy] smoltcp socket TX buffer - → [smoltcp 内部] IP packet 构造 - → [TxToken::consume → copy] Router.tx_buffer - → [dispatch → copy] DeviceHandle.tx_queue (QueuedPacket inline, no heap allocation per packet) - → [EthernetDevice::send → copy] driver TX buffer -``` + self.router.poll(timestamp, sockets, |interface_id, packet| { + // DHCP client/server snoop + }); ---- + for event in dhcp_events { + self.handle_dhcp_event(event); + } + let mut dhcp_server_sent = false; + for (dev, reply) in dhcp_server_replies { + dhcp_server_sent |= self.router.send_on_device( + dev, + IpAddress::Ipv4(Ipv4Address::BROADCAST), + &reply, + timestamp, + ); + } -## 4. ARP / 邻居发现流程 + let socket_state_changed = + self.iface.poll(timestamp, &mut self.router, sockets) == PollResult::SocketStateChanged; + let dhcp_poll_next = self.poll_dhcp(timestamp); + crate::orphan::reap_orphans(timestamp, sockets); -```mermaid -stateDiagram-v2 - [*] --> NoEntry: 包待发, 无邻居 - NoEntry --> Pending: request_arp() - Pending --> Resolved: 收到 ARP Reply - Pending --> Pending: 超时后下一次发送重发 ARP - Resolved --> Resolved: TTL 内正常收发 - Resolved --> NoEntry: TTL 过期 (300s) + self.router.dispatch(timestamp, sockets) + || dhcp_poll_next + || dhcp_server_sent + || socket_state_changed +} ``` -### 步骤详解 - -**ARP 请求**([device/ethernet.rs](net/ax-net/src/device/ethernet.rs)) - -当 `EthernetDevice::send()` 发现下一跳 IP 不在 `neighbors` 表中时,调用 `request_arp()`: - -1. 构造 `ArpRepr::EthernetIpv4 { Request, src_mac=self.mac, src_ip=self.ip, target_mac=EMPTY, target_ip=next_hop }`。 -2. 用 `send_to()` 通过 driver 发送广播 ARP 请求帧。 -3. 将 `(target_ip, PendingNeighbor { requested_at: now })` 插入 `pending_neighbors`。 -4. 将待发送的 IP 包存入 `pending_packets` 有界队列。 - -当前实现没有最大 ARP 重试计数,也没有独立 `Expired` 状态。只要后续仍有包要发往该 next hop,`EthernetDevice::send()` 会在 `ARP_REQUEST_RETRY=1s` 到期后再次发送 ARP request。 +顺序约束: -**ARP 响应处理**([device/ethernet.rs](net/ax-net/src/device/ethernet.rs)) +- `Router::poll()` 在 smoltcp 前执行,让新 RX packet 进入 `rx_buffer`。 +- DHCP snoop 在 smoltcp 消费 packet 前执行,保留 ingress `InterfaceId`。 +- DHCP ACK/NAK 先生成 `NetworkStateUpdate`,再提交到 smoltcp address list、控制面和 route table。 +- `Router::dispatch()` 在 smoltcp 后执行,把本轮生成的 TX packet 送出。 -收到 ARP 帧时 `process_arp()`: +## 数据面流程 -1. 解析 ARP 包。 -2. 检查目的 MAC(unicast 才检查是否是自身)。 -3. 验证源 MAC 和源 IP 合法性(unicast MAC, 非 broadcast/multicast/unspecified IP)。 -4. 如果是 Request 且目标是自己:构造 Reply 发回。 -5. **无论 Request 还是 Reply**:更新 `neighbors` 表(`src_ip → src_mac, expires_at=now+NEIGHBOR_TTL`)。 -6. 从 `pending_neighbors` 移除该 IP。 -7. Drain `pending_packets`,发送所有下一跳已解析的包。 - -**邻居表 TTL** - -- `NEIGHBOR_TTL = 300s`(5 分钟)。 -- 过期后条目从 `neighbors` 移除,后续发送触发新的 ARP 请求。 -- `pending_neighbors` 中的条目在 `poll_dhcp` 或下一次发送时检查 `requested_at`,超时后重发 ARP。 - ---- +数据面由 RX path、TX path 和 loopback fast path 组成。真实设备通过 worker 和有界队列与协议核心解耦。 -## 5. TCP Socket 操作流程 - -### 5.1 Connect +### RX Path ```mermaid sequenceDiagram - participant App as 应用 - participant Sock as TcpSocket - participant Control as NetControl - participant SockSet as SOCKET_SET + participant Driver as EthernetDriver + participant RxW as device_rx_worker + participant Queue as shared RX queue participant Poll as net-poll + participant Router as Router::poll() + participant Smol as smoltcp Interface + participant Sock as SocketSet - App->>Sock: connect(remote_addr) - Sock->>Control: select_route(remote_addr.addr) - Control-->>Sock: RouteDecision { source, dev, next_hop } - Sock->>Sock: state: Idle → Connecting - Sock->>SockSet: smoltcp socket.connect(remote, local) - Sock->>Poll: request_poll() - Sock->>SockSet: poll_connect(): socket.state() == Connected? - alt Connected - Sock-->>App: Ok - else still connecting - Sock->>Poll: Pollable::register() + request_poll() → yield - Note over Sock: poll_io() resumes and retries closure after readiness/timeout - end -``` - -`TcpSocket::connect()`([tcp.rs](net/ax-net/src/tcp.rs)): - -1. `start_connect()` 查路由获取源地址,在 smoltcp socket 上调用 `connect()`。 -2. `request_poll()` 唤醒 net-poll worker 处理 SYN 发送和 SYN-ACK 接收。 -3. `send_poller` 把一次 `poll_connect()` 检查交给 `poll_io()`;如果返回 `WouldBlock`,由 `Pollable::register()` 注册 waker,任务挂起,ready/timeout 后重新执行闭包。 -4. 连接成功返回 `Ok(())`,连接失败返回对应错误(`ConnectionRefused` 等)。 - -### 5.2 Listen + Accept - -`listen()`([tcp.rs](net/ax-net/src/tcp.rs)): - -1. 状态转换 `Idle → Listening`。 -2. 分配端口(如果未绑定)。 -3. 在 `LISTEN_TABLE` 注册 `(IpListenEndpoint, backlog)`。`ListenTable` 支持同端口不同地址 listen(per-address listen),用 `Vec` 存储。 -4. 设置 `DeviceBinding`(仅当 `bound_if.is_some()` 时)。 - -`accept()`([tcp.rs](net/ax-net/src/tcp.rs)): - -```rust -fn accept(&self) -> AxResult { - let bound_endpoint = self.bound_endpoint()?; - self.general.recv_poller(self, || { - request_poll(); - let accepted = { - let mut sockets = SOCKET_SET.inner.lock(); - LISTEN_TABLE.accept(bound_endpoint, &mut sockets)? - }; - Ok(TcpSocket::new_connected(accepted.handle, ...).into()) - }) -} + Driver-->>RxW: IRQ/OOB readiness + RxW->>Driver: Device::recv() + Driver-->>RxW: IP packet in local PacketBuffer + RxW->>Queue: push(RxPacket{interface_id, QueuedPacket}) + RxW->>Poll: request_poll() + Poll->>Router: drain queue into rx_buffer + Router->>Sock: snoop_tcp_packet() + Router->>Router: DHCP snoop(interface_id, packet) + Poll->>Smol: Interface::poll(router, sockets) + Smol->>Sock: update socket state / wake smoltcp wakers ``` -Accept 依赖 RX 流程中的 `snoop_tcp_packet()` 预创建 child socket。当 SYN 到达时,listen table 先在 `SocketSet` 中创建一个 listen 状态的 child socket 并推入 `syn_queue`;随后 smoltcp 消费 SYN,把 child socket 推进到 `SynReceived` 并发送 SYN-ACK。三次握手完成后,`LISTEN_TABLE.accept()` 将该 socket 从 listen pool 移出并返回。 - -### 5.3 Send / Recv - -见 TX 流程中的 TCP send 部分。TCP `recv()`([tcp.rs](net/ax-net/src/tcp.rs))在 `recv_poller_with` 中循环读取 smoltcp socket 的 RX buffer,直到用户 buffer 填满或 RX 队列空。 - ---- - -## 6. UDP Socket 操作流程 - -### 6.1 Send +数据结构转换: -UDP `send()`([udp.rs](net/ax-net/src/udp.rs))比 TCP 多了几个分支: - -1. **MSG_MORE corking**:如果 `SendFlags::MORE` 被设置,数据缓存在 `self.cork` 中而非立即发送。Cork buffer 上限 `UDP_TX_BUF_LEN`。后续不带 MORE 的 send 会 flush cork buffer。 -2. **目标地址解析**:connected socket 用 `remote_endpoint()`,unconnected socket 用 `options.to`。 -3. **路由选择**:`select_route(dst_addr)` 获取源地址。 -4. **smoltcp socket.send()**:写入 UDP payload + metadata(endpoint, local_address)。 - -零长度 UDP payload(只有 IP+UDP 头)是合法的,smoltcp `send(0, ...)` 处理。 - -### 6.2 Recv - -UDP `recv()`([udp.rs](net/ax-net/src/udp.rs)): - -```rust -fn recv(&self, mut dst: impl Write, mut options: RecvOptions) -> AxResult { - // 确定 expected_remote:Any(addr_out) / AnyDiscard / Expecting(connected_peer) - self.general.recv_poller_with(self, extra_nb, || { - request_poll(); - self.with_smol_socket(|socket| { - if !socket.can_recv() { Err(AxError::WouldBlock) } - else { - let result = socket.recv(); // 或 peek() for MSG_PEEK - match result { - Ok((src, meta)) => { - // 检查 expected_remote 匹配 - // 写入 dst,处理 truncation - } - Err(Exhausted) => Err(WouldBlock), - } - } - }) - }) -} +```text +driver RX buffer + -> device_rx_worker local PacketBuffer + -> shared BoundedPacketQueue + -> Router.rx_buffer + -> RxToken -> smoltcp Interface::poll() ``` -Connected UDP socket 只接受来自 peer 的包,但 `MSG_PEEK` 与普通 `recv()` 的消费语义不同: - -- `MSG_PEEK`:如果队首包来自非 peer,返回 `WouldBlock`,不消费该包。 -- 普通 `recv()`:先从 smoltcp UDP 队列取出一个 datagram,再检查 peer;如果该包来自非 peer,则丢弃该包并返回 `WouldBlock`,不会继续扫描队列后面的 datagram。 +`RxPacket` 保存 ingress `InterfaceId`,用于 DHCP 分发和诊断。队列满时丢包并记录 warning,避免网络热路径无界增长。 -这是当前实现的保守语义,避免为 UDP 维护额外 deferred queue。需要“跳过非 peer 并继续扫描”时,应在 UDP 层显式设计一个有界暂存策略,不能复用 raw socket 的 wire-packet deferred 格式。 - ---- - -## 7. DHCP 流程 +### TX Path ```mermaid sequenceDiagram - participant Sock as SocketSet - participant Poll as net-poll worker - participant Service as Service - participant Dhcp as DhcpState + participant App as socket send/connect + participant Smol as smoltcp socket + participant Poll as net-poll participant Router as Router - participant Control as NetControl - participant Dev as Ethernet device - - Note over Dhcp: phase = Discovering - Poll->>Service: poll() - Service->>Dhcp: poll_packet(now) - alt retry_at <= now && phase == Discovering - Dhcp->>Router: send_on_device(DHCP Discover broadcast) + participant TxQ as per-device TX queue + participant TxW as device_tx_worker + participant Dev as EthernetDevice + + App->>Smol: write socket state/buffer + App->>Poll: request_poll() + Poll->>Smol: Interface::poll() + Smol->>Router: TxToken::consume(IP packet) + Poll->>Router: dispatch() + Router->>Router: select_route_for_source(dst, src) + alt loopback + Router->>Router: inject_loopback_rx_direct() + else Ethernet + Router->>TxQ: enqueue TxPacket(next_hop, bytes) + TxQ-->>TxW: tx_wake.notify() + TxW->>Dev: Device::send(next_hop, packet) end - Note over Dev: === RX 路径 === - Dev-->>Router: DHCP Offer packet - Router->>Service: poll() → snoop callback - Service->>Dhcp: process_packet(interface_id, offer) - Dhcp->>Dhcp: phase → Requesting, offered_address = offer.yiaddr - Dhcp->>Router: send_on_device(DHCP Request) - Note over Dev: === RX 路径 === - Dev-->>Router: DHCP ACK - Router->>Service: poll() → snoop callback - Service->>Dhcp: process_packet(interface_id, ack) - Dhcp-->>Service: DhcpEvent::Configured { address, router, dns } - Service->>Service: handle_dhcp_event() - Service->>Service: set_interface_ipv4(iface, new_addr) - Service->>Control: commit_interface_update(routes) - Note over Dhcp: phase = Bound ``` -### 步骤详解 +dispatch 规则: -**DhcpState 结构**([service.rs](net/ax-net/src/service.rs)) +- IPv4 limited broadcast 发往所有非 loopback 设备。 +- IPv4/IPv6 单播按 `(dst, src)` 查 `select_route_for_source()`。 +- 源地址必须与 route rule 的 source 一致,避免多宿主环境下从错误接口发包。 +- loopback 目的地直接写入 `Router.rx_buffer`。 +- 普通设备 TX 进入 per-device `tx_queue`,由 TX worker 调用 `Device::send()`。 -```rust -struct DhcpState { - interface_id: InterfaceId, - dev: usize, - ifname: String, - mac: EthernetAddress, - metric: u32, - transaction_id: u32, // 由 MAC 派生,见 dhcp_transaction_id() - phase: DhcpPhase, // Discovering → Requesting → Bound - retry_at: Instant, - retry: usize, - offered_address: Option, - server_identifier: Option, - address: Option, - dns_servers: Vec, -} -``` - -**状态机转换**([service.rs](net/ax-net/src/service.rs)) +### Loopback Fast Path -`process_packet()` 按 `(phase, message_type)` 驱动: +loopback 普通 TX 不进入设备队列: -| 当前 phase | 收到消息 | 动作 | -| --- | --- | --- | -| Discovering | Offer | 验证 yiaddr 是 unicast;记录 offered_address + server_id;phase → Requesting | -| Requesting | ACK | 解析 subnet_mask → prefix_len;验证 yiaddr;phase → Bound;返回 `DhcpEvent::Configured` | -| Bound | ACK | 更新地址(续约) | -| 任意 | NAK | reset;如果之前已配置则返回 `DhcpEvent::Deconfigured` | - -**DHCP 包构造** - -`poll_packet()`([service.rs](net/ax-net/src/service.rs))在 retry 到期时构造 DHCP Discover 或 Request: - -- 使用 `dhcp_transaction_id()`(由 MAC 派生,[service.rs](net/ax-net/src/service.rs))。 -- `DHCP_PARAMETER_REQUEST_LIST = [1, 3, 6, 42]`(subnet mask, router, DNS, NTP)。 -- 指数退避:`retry` 最大位移 `DHCP_MAX_RETRY_SHIFT = 4`。 +```text +Router.tx_buffer + -> Router::dispatch() + -> inject_loopback_rx_direct() + -> Router.rx_buffer + -> next Service::poll() / same idle loop +``` -**ACK 后状态提交**([service.rs](net/ax-net/src/service.rs)) +`inject_loopback_rx_direct()` 在写入 RX buffer 前调用 `snoop_tcp_packet()`,因此 loopback TCP SYN 可以在同一轮 poll 中预创建 accept child socket。 -`handle_dhcp_event(DhcpEvent::Configured)` → `commit_network_state()`: +### ARP / Neighbor -1. `set_interface_ipv4(iface, old_addr, new_addr)` — 更新 smoltcp `ip_addrs`:移除旧地址,添加新地址。 -2. `router.ipv4_rules()` — 生成直连路由 + 默认路由(如果有 gateway)。 -3. `control.commit_interface_update()` — 在 `NetControl` 的 state 中更新接口 IPv4、gateway,替换该接口的 DNS 条目,替换路由表中该接口的 IPv4 规则。 +Ethernet TX 需要把 IP packet 发送到 next-hop MAC: -**Bootstrap 等待策略** +```text +Device::send(next_hop, ip_packet) + -> neighbor cache hit: encapsulate Ethernet frame + transmit + -> miss with pending ARP: queue packet in pending_packets + -> miss without pending ARP: send ARP request + queue packet +``` -`dhcp_configured()`([service.rs](net/ax-net/src/service.rs))只需**任一** DHCP 接口配置成功即返回 true。这确保一个断网的网卡不会阻塞启动。 +入站 ARP reply 或 gratuitous ARP 会更新 neighbor 表,并释放等待该 next hop 的 pending packet。neighbor TTL 为 300 秒,ARP retry 间隔为 1 秒。 ---- +## Socket 流程 -## 8. DNS 查询流程 +socket 流程只修改 socket 状态并注册 waker;协议状态机推进交给 net-poll worker。 -```mermaid -sequenceDiagram - participant App as 应用 - participant Lib as dns_query_timeout() - participant Control as NetControl - participant SockSet as SOCKET_SET - participant DnsSock as dns::Socket - participant Poll as net-poll +### TCP Connect - App->>Lib: dns_query_timeout(name, timeout=5s) - Lib->>Control: dns_servers() - Control-->>Lib: [server1, server2, ...] (按 metric 排序) - Lib->>Control: 过滤不可路由的 server - Lib->>SockSet: add(dns::Socket::new(servers, [])) - Lib->>DnsSock: start_query(name, Type::A) - loop deadline 未到 - Lib->>Poll: request_poll() - Lib->>DnsSock: get_query_result(handle) - alt Pending - Lib->>Lib: yield_now() - else Resolved - DnsSock-->>Lib: Vec - Lib-->>App: Ok(addrs) - else Failed - Lib-->>App: Err(ConnectionRefused) - end - end - Lib-->>App: Err(TimedOut) +```text +TcpSocket::connect(remote) + -> choose/bind local endpoint + -> control plane route/source decision + -> smoltcp tcp::Socket::connect() + -> state = Connecting + -> request_poll() + -> poll_io waits for OUT or error ``` -### 步骤详解 +连接完成由 smoltcp 在后续 `Interface::poll()` 中推进。`Pollable::register()` 同时注册 smoltcp send/recv waker 和设备 readiness waker。 -`dns_query_timeout()`([lib.rs](net/ax-net/src/lib.rs)): +### TCP Listen / Accept -1. 获取 DNS server 列表(DHCP + 静态 + fallback,去重排序)。 -2. 用 `select_route()` 过滤掉不可路由的 server。 -3. 创建临时 `dns::Socket`,加入 `SocketSet`。 -4. `start_query()` 发起 A 记录查询。 -5. 循环 `request_poll()` + `get_query_result()`,超时由 monotonic clock 判断。 -6. `DnsSocketGuard` 的 `Drop` 自动移除 socket,避免泄漏。 +```text +TcpSocket::listen(backlog) + -> register endpoint in LISTEN_TABLE + -> state = Listening -超时常量:`DNS_DEFAULT_TIMEOUT = 5s`([lib.rs](net/ax-net/src/lib.rs))。 +incoming SYN + -> Router::poll() + -> snoop_tcp_packet() + -> LISTEN_TABLE.incoming_tcp_packet() + -> create child smoltcp TCP socket + -> enqueue PendingTcp + -> smoltcp consumes SYN and advances child state ---- - -## 9. Poll 循环与 Waker 机制 - -### request_poll() 轻量唤醒 - -所有热路径(socket send/recv/connect/drop、设备 RX/TX、timer)只调用 `request_poll()`([lib.rs](net/ax-net/src/lib.rs)): - -```rust -pub fn request_poll() { - NET_POLL_REQUESTED.store(true, Ordering::Release); - NET_POLL_WAKE.notify_one(true); -} +TcpSocket::accept() + -> LISTEN_TABLE.accept() + -> return first acceptable child + -> construct connected TcpSocket ``` -- `NET_POLL_REQUESTED` 是一个 `AtomicBool`,Release store 保证可见性。 -- `NET_POLL_WAKE` 是一个 `Waker`(基于 `ax_task`),`notify_one(true)` 表示如果已有 pending wake 则合并(不重复唤醒)。 - -### net-poll worker 主循环 +accept readiness 由 `ListenTableEntryInner.accept_poll` 维护。pending child 的 recv/send readiness 会唤醒 listener 的 accept waiters。 -```rust -fn net_poll_worker() { - loop { - let delay = next_poll_delay(); - let timed_out = NET_POLL_WAKE.wait_timeout_until(delay, - || NET_POLL_REQUESTED.load(Ordering::Acquire)); - if !timed_out { - NET_POLL_REQUESTED.store(false, Ordering::Release); - } - poll_until_idle(); - } -} -``` +### TCP/UDP Send-Recv -1. `next_poll_delay()`([lib.rs](net/ax-net/src/lib.rs))向 smoltcp 询问下一次 poll 截止时间(TCP retransmit、DHCP retry、keep-alive 等),结合 monotonic clock 计算睡眠时长。空闲时回退到 100ms。 -2. `wait_timeout_until()` 等待 wake 或超时。 -3. 被 wake 唤醒(非超时)时清除 `NET_POLL_REQUESTED` 标志。 -4. 进入 `poll_until_idle()`。 - -### poll_until_idle() 原子互斥 +通用阻塞逻辑来自 `GeneralOptions`: ```rust -fn poll_until_idle() { - POLL_AGAIN.store(true, Ordering::Release); - loop { - // CAS 获取 POLLING_INTERFACES 锁 - if POLLING_INTERFACES.compare_exchange(false, true, Acquire, Acquire).is_err() { - return; // 已有其他 poller 在运行 - } - while POLL_AGAIN.swap(false, AcqRel) { - while poll_once() {} // 循环直到 smoltcp 返回 idle - } - POLLING_INTERFACES.store(false, Release); - if !POLL_AGAIN.load(Acquire) { return; } - } +pub fn send_poller_with AxResult, T>( + &self, + pollable: &P, + extra_nonblocking: bool, + f: F, +) -> AxResult { + block_on(timeout( + self.send_timeout(), + poll_io(pollable, IoEvents::OUT, self.nonblocking() || extra_nonblocking, f), + ))? } ``` -- `POLLING_INTERFACES`:原子标志,保证同一时刻只有一个线程进入协议栈。 -- `POLL_AGAIN`:在 poll 期间如果有新的 `request_poll()` 到来,设置此标志使循环重新执行。 +`poll_io()` 流程: -### poll_once() 锁顺序 +1. 先执行一次操作闭包。 +2. 成功则返回。 +3. `WouldBlock` 且 nonblocking/`MSG_DONTWAIT` 则立即返回。 +4. 否则注册 waker 并挂起。 +5. 被 socket readiness、设备 readiness 或 timeout 唤醒后重试。 -```rust -fn poll_once() -> bool { - get_service().poll(&mut SOCKET_SET.inner.lock()) -} -``` +UDP connected socket 在 recv 时过滤 peer;`MSG_MORE` 会把多次 send 合并为一个 datagram,并固定第一次 send 的 remote/source。 -`Service::poll()` 内部锁顺序固定为: +### Raw Socket -``` -1. Service lock (get_service()) -2. SocketSet lock (SOCKET_SET.inner.lock()) - ├─ Router::poll() — drain RX queue → rx_buffer,snoop DHCP client/server - ├─ handle_dhcp_event() — DHCP ACK/NAK 后提交接口、路由、DNS 更新 - ├─ DHCP server replies — SoftAP DHCP server 通过 Router::send_on_device() 发回复 - ├─ smoltcp Interface::poll(timestamp, router, sockets) - │ ├─ Router::receive() / Router::transmit() (phy::Device trait) - │ └─ TCP/UDP/ICMP state machine - ├─ poll_dhcp() — DHCP client retry packets - ├─ reap_orphans() — 回收已完成 teardown 的 orphan TCP socket - └─ Router::dispatch(timestamp, sockets) — tx_buffer → 设备/loopback 快速路径 -3. 释放 SocketSet lock -4. 释放 Service lock -5. waker 的实际唤醒由 PollSet/smoltcp socket waker 路径触发;socket poller 被唤醒后会重新执行原闭包 -``` +raw socket 处理 IP 层 packet: -**严格禁止**: +- send 时按 remote 选择 source,或使用显式绑定地址。 +- loopback ICMP 走本地快速路径。 +- connected raw socket 使用 peer filter。 +- `deferred_rx` 保存被 peer filter 暂存的 wire packet,保证 `MSG_PEEK` 和后续 recv 不破坏 packet 格式。 -- 禁止在持有设备锁(`DeviceHandle.inner`)时进入 `Service` 或 `SocketSet` 锁。 -- 禁止在持有 control 写锁(`NetControl.state.write()`)时进入 smoltcp poll。 -- 锁的获取和释放严格嵌套,不允许跨层级持有。 +## 控制协议流程 -### Waker 注册路径 +控制协议不是独立线程,它们挂在 `Service::poll()` 中运行。 -Socket readiness 由各 backend 的 `Pollable::register()` 注册。`GeneralOptions` 不保存全局 `general.waker`,它只提供 `register_waker()` 辅助,把同一个 task waker 注册到符合 `DeviceBinding` 的设备唤醒路径。 +### DHCP Client -典型路径: +DHCP 状态机: +```text +Discovering --Offer--> Requesting --ACK--> Bound + ^ | | | + | | +--NAK/reset------+ + +--retry---+--timeout/retry-------------+ ``` -socket.send/recv/connect returns WouldBlock - → poll_io() 调用 socket Pollable::register() - → TCP/UDP/raw 将 waker 注册到 smoltcp socket recv/send waker 或本地 PollSet - → GeneralOptions::register_waker() 将 waker 注册到匹配 DeviceBinding 的设备路径 - → task yields - → net-poll worker calls poll_until_idle() - → smoltcp updates socket readiness - → smoltcp/PollSet/device waker 唤醒等待任务 - → task resumes, retries send/recv -``` - -`NetPollWake` 实现了 `core::task::Wake` trait([lib.rs](net/ax-net/src/lib.rs)),使设备 driver 可以通过标准 waker 接口唤醒 poll worker。 - ---- -## 流程总结 +入站 packet 路径: -| 流程 | 热路径入口 | 锁层级 | 延迟来源 | -| --- | --- | --- | --- | -| RX | IRQ → rx_wake → rx_worker → rx_queue | 设备锁 → RX 队列(无 Service 锁) | driver recv + 1 次 copy | -| TX | socket.send → smoltcp buffer → dispatch → tx_queue | Service 锁 → SocketSet 锁 → 路由表读锁 | smoltcp TCP 状态机 + ARP | -| TCP connect | socket.connect → smoltcp connect → request_poll → poll | Service 锁 → SocketSet 锁 | RTT × 1.5(SYN + SYN-ACK) | -| DHCP | poll_dhcp → send_on_device → RX → process_packet | Service 锁 → 路由表写锁 | DHCP server 响应时间 | -| DNS | dns_query_timeout → add socket → poll loop | Service 锁 → SocketSet 锁 | DNS server RTT | -| Poll | request_poll → net-poll worker → poll_until_idle | POLLING_INTERFACES 原子 → Service 锁 → SocketSet 锁 | smoltcp poll duration | - ---- - -## 10. Unix Domain Socket 流程 - -### 10.1 Stream Transport(socketpair / connect-accept) - -Unix stream 基于 `ringbuf::HeapRb` 实现全双工双向通道。`new_channels(pid)` 创建一对交叉连接的 `Channel`: - -```rust -(client_tx → server_rx) + (server_tx → client_rx) -shared: poll_update (Arc), cmsg queues (s2c + c2s) +```text +Router::poll() + -> snoop(interface_id, packet) + -> DhcpState::process_packet(interface_id, packet, timestamp) + -> DhcpEvent::Configured / Deconfigured + -> Service::handle_dhcp_event() + -> commit_network_state() ``` -**Bind + Listen + Accept 流程**: - -1. `bind(addr)` — 在 `ABSTRACT_BINDS` 或 `UnixNamespace` 中注册 `BindSlot`(stream bind + dgram bind)。Stream bind 存储在 `slot.stream: Mutex>`。 -2. `listen()` — Unix 中为 no-op(`Bind` 已准备就绪)。 -3. `accept()` — `block_on(transport.accept())`,内部 `bind.inner.accept.recv()` 阻塞等对端的 `connect()` 调用。 +提交内容: -**Connect 流程**: +- smoltcp `Interface` IP address list。 +- `NetControl.state.interfaces` 的 IPv4/gateway。 +- DNS registry。 +- route table 中该接口的 IPv4 rules。 -1. `connect(remote_addr)` — 查 `with_slot(remote_addr, |slot| transport.connect(slot, local_addr))`。 -2. `StreamTransport::connect()` 创建新的 `(client_channel, server_channel)` 对。 -3. 将 `server_channel` 通过 `bind.inner.accept.send(server_channel)` 推给监听端。 -4. 返回后,对端 accept 解除阻塞,两端各自持有 `client_channel` / `server_channel`。 +出站 DHCP packet 由 `poll_dhcp()` 生成,再通过 `Router::send_on_device()` 从指定设备广播。 -**Cmsg 管道**: +### DHCP Server -- 每个 `Channel` 维护 `tx_cmsg` 和 `rx_cmsg` 两个 `CmsgQueue`(`Arc>>`)。 -- `sendmsg` 时记录 `start_byte = tx_bytes_total + 1, end_byte = start_byte + len - 1`,推入 `tx_cmsg`。 -- `recvmsg` 时从 `rx_cmsg` 前端检查:如果 `rx_bytes_total >= entry.start_byte`,释放 cmsg 给调用者;同时用 `end_byte` 限制本次 recv 长度(cmsg 边界语义)。 +内置 DHCP server 用于 SoftAP 场景。它在 Router RX snoop 中接收 Discover/Request,生成 Offer/Ack 后通过 `send_on_device()` 从绑定设备发出。它不依赖 smoltcp DHCP socket。 -### 10.2 Datagram Transport +### DNS Query -Unix datagram 基于 `async_channel::unbounded()` 无界通道: - -```rust -struct Packet { - data: Vec, - cmsg: Vec, - sender: UnixSocketAddr, -} +```text +dns_query_timeout(name, timeout) + -> dns_servers() + -> filter routable DNS server by control plane route lookup + -> SOCKET_SET.add(dns::Socket) + -> start_query() + -> loop: + request_poll() + get_query_result() + pending -> yield / timeout check + -> DnsSocketGuard::drop() removes socket ``` -**Bind**:注册 slot 时创建 `(tx, rx)` 通道对,`tx` 存入 `Bind` 供 connect/send 方使用,`rx` 存入 `DgramTransport::data_rx`。 - -**Connect**:从 slot 获取 `Bind::connect()`(Clones `data_tx`),更新 `connected: RwLock>`。 +错误语义: -**Send**:读取用户数据到 `Vec`,构造 `Packet`,通过 `connected.data_tx.try_send()` 或查 `with_slot(options.to, |slot| slot.dgram.lock().as_ref().unwrap().data_tx.try_send())` 发送。 +- 无 DNS server:`NotFound`。 +- DNS server 不可路由:`NoSuchDeviceOrAddress`。 +- 查询超时:`TimedOut`。 +- DNS socket 无 free slot:`ResourceBusy`。 +- 名称非法或过长:`InvalidInput`。 -**Recv**:从 `data_rx.recv()` 或 `try_recv()` 取 `Packet`,处理 cmsg、包截断、sender address。 +## Local Transport 流程 -**SO_PASSCRED**:设置后 `send()` 自动添加 `UnixCredentials` 到每个包的 cmsg 中。 +AF_UNIX 和 AF_VSOCK 不通过 smoltcp `Interface`,但复用 `SocketOps` 和 `Pollable`。 ---- - -## 11. Vsock Stream 流程 +### Unix Stream / Datagram -Vsock 基于 `rdif-vsock` 驱动 + 全局 `VSOCK_CONN_MANAGER`: +Unix socket 使用 `Transport` 分发: -```mermaid -sequenceDiagram - participant App as 应用 - participant Sock as VsockSocket - participant Trans as VsockStreamTransport - participant Mgr as VSOCK_CONN_MANAGER - participant Driver as rdif-vsock - - App->>Sock: bind(VsockAddr { cid=VMADDR_CID_ANY, port=0 }) - Sock->>Trans: bind() - Trans->>Mgr: create_connection(conn_id, local_addr, None, Idle) - Trans->>Trans: state: Idle → Idle (不改变) - - App->>Sock: listen() - Sock->>Trans: listen() - Trans->>Mgr: listen(local_addr) - Trans->>Driver: driver.listen(local_addr) - Trans->>Trans: state: Idle → Listening - - App->>Sock: accept() - Sock->>Trans: accept() - Trans->>Trans: recv_poller() 循环 - alt 有连接 - Trans->>Mgr: can_accept(local_port)? - Trans->>Mgr: accept(local_port) → (conn_id, peer_addr) - Trans-->>App: Ok(VsockSocket(新 VsockStreamTransport)) - else 无连接 - Trans->>Trans: WouldBlock → yield → retry - end - - App->>Sock: connect(peer_addr) - Sock->>Trans: connect() - Trans->>Mgr: allocate_port() + create_connection() - Trans->>Driver: driver.connect(conn_id) - Trans->>Trans: send_poller() 等 Connected +```text +UnixSocket + -> Transport::Stream(StreamTransport) + -> Transport::Dgram(DgramTransport) ``` -Connection Manager([vsock/connection_manager.rs](net/ax-net/src/vsock/connection_manager.rs))维护 per-connection 状态机:`Idle → Listening/Connecting → Connected → Closed`。 - -Vsock stream 的 send/recv 直接从 `Connection` 的 ring buffer 读写数据。 - -### 11.1 Driver 事件处理(vsock-poll worker) +abstract namespace 存在内存 map 中;path namespace 通过 `register_unix_namespace()` 注入。Unix stream accept 使用 transport 自己的 `Pollable` 和 `poll_io()`,不调用 `request_poll()`。 -`vsock-poll` 是独立的 vsock 轮询线程([device/vsock.rs](net/ax-net/src/device/vsock.rs)),引用计数管理(`start_vsock_poll`/`stop_vsock_poll`),自适应轮询频率(100μs → 500μs → 2ms → 10ms 的四级退避)。 +### Vsock Stream -`handle_vsock_event()` 将 driver 的 4 类事件分派给 `VSOCK_CONN_MANAGER`: +vsock 只在 `vsock` feature 下启用: -| 事件 | handler | 动作 | -| --- | --- | --- | -| `VsockEvent::ConnectionRequest` | `on_connection_request()` | 查 `listen_queues` → 创建新 `Connection`(state=Connected) → 推入 `AcceptQueue` → `wake()` 唤醒 accept 等待者 | -| `VsockEvent::Received` | `on_data_received()` | 查 `connections` → `push_rx_data()` 写入 RX ring buffer → `wake_rx()` 唤醒 recv 等待者 | -| `VsockEvent::Disconnected` | `on_disconnected()` | 查 `connections` → state=Closed, rx_closed=true, tx_closed=true → `wake_rx()` 通知 recv 端 EOF | -| `VsockEvent::Connected` | `on_connected()` | 查 `connections` → state=Connected → `wake_connect()` 唤醒 connect 等待者 | -| `VsockEvent::CreditUpdate` | `on_credit_update()` | 查 `connections` → `tx_wait_queue_notify()` 唤醒因 buffer 空间不足而等待的 send 调用 | - -`VsockEvent::Received` 的特殊处理:如果 `Connection` RX buffer 已满(`rx_buffer_free() == 0`),事件重新推入 `PENDING_EVENTS` 全局队列,下次 poll 再尝试,避免数据丢失。 +```text +VsockSocket + -> VsockTransport::Stream + -> vsock::connection_manager + -> rdif_vsock::Interface event path +``` ---- +vsock 不进入 `SocketSet`,也不使用 Router。设备事件由 vsock device/event loop 推进。 -## 12. ICMP Loopback Echo 流程 +## 并发与锁边界 -`RawSocket::send()` 对 loopback 地址有特殊处理([raw.rs](net/ax-net/src/raw.rs)): +运行时流程需要维持固定边界,避免应用线程、设备 worker 和协议核心互相阻塞。 -1. 检查目的地址是否是 loopback(`127.0.0.1` 或 `::1`)。 -2. 如果是,**不经过 smoltcp 发送**。而是解析 ICMP echo request(type=8, code=0)。 -3. 调用 `build_loopback_icmp_reply()`:将 type 改为 0(echo reply),清空 code,重新计算 ICMP checksum。 -4. 将 reply 存入 `self.loopback_rx` 队列。 -5. 下次 `recv()` 时优先消费 `loopback_rx` 中的内容。 +### 典型锁路径 -这允许 `ping 127.0.0.1` 在不需要完整 IP stack 回环的情况下正确工作。 +```text +net-poll: + SERVICE -> SOCKET_SET.inner -> Service::poll() -Raw socket 还有一个容易误读的 receive 细节:smoltcp raw socket 队列中保存的是完整 IP packet,而 public raw API 通常交付协议 payload。为了同时支持 connected peer 过滤和 `MSG_PEEK`,`raw.rs` 使用两个本地暂存点: +Router RX/TX: + Router queue locks -> RouteTable read lock -> per-device TX queue -- `loopback_rx`:保存本地生成的 loopback reply,`recv()` 优先消费。 -- `deferred_rx`:保存被 peer 过滤暂存下来的**完整 wire packet**,而不是已经剥离后的 payload。 +device worker: + DeviceHandle.inner -> Device::recv/send -> bounded queue -> request_poll() -因此 raw deferred packet 在再次交付前会重新经过 IP 解析,再调用统一的 `deliver_packet()`。这个约束很重要:如果 deferred queue 存 payload 而不是完整 IP packet,后续 `MSG_PEEK`/peer filtering 会把 packet 格式解释错。 +TCP listen/accept: + SOCKET_SET.inner -> LISTEN_TABLE bucket ---- +control query: + NetControl.state -> RouteTable +``` -## 13. TCP Accept 完整流程图 +禁止路径: -```mermaid -sequenceDiagram - participant Remote as 远端 SYN - participant Eth as EthernetDevice - participant Router as Router::poll() - participant Smol as smoltcp Interface - participant ListenTbl as LISTEN_TABLE - participant SockSet as SOCKET_SET - participant App as accept() - - Remote->>Eth: TCP SYN - Eth->>Router: packet 进入 rx_buffer - Router->>Router: snoop_tcp_packet(bytes) - Router->>ListenTbl: incoming_tcp_packet(src, dst, sockets) - ListenTbl->>ListenTbl: 查 port bucket → 匹配目的地址的 ListenTableEntryInner - ListenTbl->>SockSet: add(new TcpSocket) - ListenTbl->>ListenTbl: socket.listen(dst.port) - ListenTbl->>ListenTbl: syn_queue.push_back(PendingTcp) - - Router->>Smol: Interface::poll() - Smol->>Smol: process SYN → SynReceived state - Smol->>Smol: send SYN-ACK (via TX path) - - Remote->>Eth: TCP ACK (三次握手完成) - Smol->>Smol: socket.state → Established - Smol->>SockSet: socket can_accept() = true - - App->>ListenTbl: accept(bound_endpoint, sockets) - ListenTbl->>ListenTbl: 遍历 syn_queue - ListenTbl->>SockSet: is_acceptable(handle)? - alt Established - ListenTbl-->>App: AcceptedTcp { handle, local_ep, remote_ep } - else 仍在握手 - ListenTbl-->>App: WouldBlock → retry - end -``` +- 设备 worker 进入 `Service` 或 `SocketSet`。 +- socket 热路径同步执行完整 interface poll。 +- 持设备锁等待 socket readiness。 +- 持 `SocketSet` 锁做可能阻塞的用户 IO。 -关键状态检查: +## 流程速查 -- `is_closed_without_data()`:TCP socket 处于 `Closed/TimeWait/Closing/LastAck` 且无未读数据 → 跳过并从 `SocketSet` 移除。 -- `is_acceptable()`:TCP state 为 `Established/CloseWait/FinWait1/FinWait2/Closing/LastAck/TimeWait` → 可以 accept,应用可通过 `recv()` 读取已到达数据或获知 EOF。 -- `SYN_RECEIVED` → 不可 accept,仍在握手中。 +| 场景 | 入口 | 推进者 | 结果 | +| --- | --- | --- | --- | +| 应用发送 TCP 数据 | `TcpSocket::send()` | net-poll worker | smoltcp 生成 IP packet,Router dispatch 到设备 | +| 设备收到包 | `device_rx_worker` | net-poll worker | Router RX buffer,smoltcp 处理 socket 状态 | +| TCP accept | `Router::poll()` SYN snoop + `accept()` | net-poll worker | child socket 进入 accept queue | +| DHCP 获取地址 | `DhcpState::process_packet()` | `Service::poll()` | 更新接口、route、DNS 和 smoltcp 地址 | +| DNS 查询 | `dns_query_timeout()` | caller + net-poll worker | 临时 DNS socket 查询并自动移除 | +| Unix socketpair | `UnixSocket` transport | transport PollSet | 不经过 smoltcp/Router | diff --git a/docs/docs/architecture/net/integration.md b/docs/docs/architecture/net/integration.md index fe74f0d564..0ff3d0d33a 100644 --- a/docs/docs/architecture/net/integration.md +++ b/docs/docs/architecture/net/integration.md @@ -3,107 +3,280 @@ sidebar_position: 9 sidebar_label: "系统集成" --- -# 系统接入 +# 系统集成 -本文说明 ArceOS、StarryOS 和 Axvisor 如何接入 `ax-net`,以及各自负责的边界。 +`ax-net` 是 ArceOS、StarryOS 和 Axvisor 共享的网络栈实现。它统一维护接口、地址、路由、DNS、ARP、socket 状态和协议栈 poll 机制;上层系统只负责把平台设备、系统调用 ABI 或虚拟化管理意图转换为 `ax-net` 的公开 API。 -## ArceOS / ax-runtime +核心源码: -`ax-runtime` 是网络栈初始化的主要入口。它负责: +| 源码 | 集成职责 | +| --- | --- | +| [os/arceos/modules/axruntime/src/devices.rs](os/arceos/modules/axruntime/src/devices.rs) | runtime 侧设备收集、IRQ 注册、`init_network()`、动态 Wi-Fi/SoftAP 注册、vsock 初始化 | +| [os/arceos/modules/axruntime/src/irq.rs](os/arceos/modules/axruntime/src/irq.rs) | 将 platform IRQ 注册能力适配为 `ax_net::EthernetIrqRegistrar` | +| [os/arceos/modules/axruntime/src/unix_ns.rs](os/arceos/modules/axruntime/src/unix_ns.rs) | 将 ArceOS 文件系统命名空间适配为 Unix socket namespace | +| [os/StarryOS/kernel/src/file/net.rs](os/StarryOS/kernel/src/file/net.rs) | Linux `ifreq`/`SIOCGIF*`/`FIONREAD` 等网络 ioctl 适配 | +| [os/StarryOS/kernel/src/file/packet.rs](os/StarryOS/kernel/src/file/packet.rs) | `AF_PACKET`、`sockaddr_ll`、packet socket ioctl 和模拟 ARP reply | +| [os/StarryOS/kernel/src/file/netlink.rs](os/StarryOS/kernel/src/file/netlink.rs) | `RTM_GETLINK`、`RTM_GETADDR` 等 netlink 查询 | +| [os/StarryOS/kernel/src/syscall/net/opt.rs](os/StarryOS/kernel/src/syscall/net/opt.rs) | `getsockopt()`/`setsockopt()`,包括 `SO_BINDTODEVICE` | +| [os/StarryOS/kernel/src/pseudofs/proc.rs](os/StarryOS/kernel/src/pseudofs/proc.rs) | `/proc/net/arp`、`/proc/net/dev` 等 procfs 视图 | +| [net/ax-net/src/lib.rs](net/ax-net/src/lib.rs) | `init_network()`、`register_device_with_config()`、`init_vsock()`、public facade | +| [net/ax-net/src/config.rs](net/ax-net/src/config.rs) | `NetworkConfig`、`InterfaceInfo`、`InterfaceId`、`DeviceBinding` 等跨系统数据模型 | -- 在平台设备 probe 完成后收集 Ethernet 设备。 -- 将 `rd-net` 设备包装为 `ax-net` 的 `EthernetDriver`(由 `RdNetDriver` 实现,[driver.rs](net/ax-net/src/device/driver.rs))。 -- 注册网络 IRQ adapter(`set_ethernet_irq_registrar()`,[ethernet.rs](net/ax-net/src/device/ethernet.rs))。 -- 构造结构化 `NetworkConfig`。 -- 调用 `ax_net::init_network(net_devs, config)`([lib.rs](net/ax-net/src/lib.rs))。 -- 在 `vsock` feature 下调用 `ax_net::init_vsock(vsock_devs)`([lib.rs](net/ax-net/src/lib.rs))。 +## 集成模型 -`ax-runtime` 不负责: +系统集成的基本原则是:网络状态只有一份,位于 `ax-net`;外部系统不复制接口表、路由表或 socket domain。 -- 维护接口 registry。 -- 维护路由表。 -- 解析 StarryOS Linux socket ABI。 -- 直接读写 smoltcp `SocketSet`。 +```mermaid +flowchart TB + Platform["platform / driver probe"] --> Runtime["ax-runtime devices.rs"] + Runtime --> Init["ax_net::init_network()"] + Runtime --> DynDev["ax_net::register_device_with_config()"] + Runtime --> Vsock["ax_net::init_vsock()"] + + User["Linux / POSIX application"] --> StarrySyscall["StarryOS syscall layer"] + StarrySyscall --> SocketOps["ax_net::SocketOps"] + StarrySyscall --> IfaceAbi["ioctl / netlink / procfs / AF_PACKET"] + + Hypervisor["Axvisor / management logic"] --> Config["NetworkConfig"] + Config --> Init + + Init --> AxNet["ax-net"] + DynDev --> AxNet + Vsock --> AxNet + SocketOps --> AxNet + IfaceAbi --> AxNet + + subgraph AxNetState["ax-net state"] + Control["NetControl: interfaces / DNS / routes"] + Service["Service: smoltcp Interface / SocketSet / DHCP"] + Router["Router as MultiDevice"] + end + + AxNet --> Control + AxNet --> Service + AxNet --> Router +``` + +各层边界: + +| 层级 | 负责 | 不负责 | +| --- | --- | --- | +| runtime / platform | 收集设备、注册 IRQ、传入结构化配置、注册动态设备 | 维护路由表、解析 Linux socket ABI、直接访问 `SocketSet` | +| `ax-net` | 接口 registry、路由、DNS、socket、协议栈 poll、多设备 dataplane | 平台设备发现、Linux `ifreq` 编解码、虚拟机管理策略 | +| StarryOS | Linux syscall 参数校验、ABI 结构体编解码、namespace 可见性过滤 | 复制第二套路由表、直接驱动 smoltcp、固定假设 `eth0` | +| Axvisor | 描述管理面/服务面网络意图、选择接口绑定策略 | 自行实现 TCP/UDP/ARP/DNS 状态 | + +## ArceOS Runtime + +### 初始化入口 + +`ax-runtime` 是设备进入 `ax-net` 的主要入口。动态和静态设备路径都遵循同一顺序: + +```rust +// os/arceos/modules/axruntime/src/devices.rs, 简化示意 +ax_net::set_ethernet_irq_registrar(&crate::irq::NET_IRQ_REGISTRAR); +register_unix_namespace(); + +let config = parse_network_config(); +let (nics, wireless) = collect_net_devices(); + +ax_net::init_network(nics, config); +register_wireless_devices(wireless); +``` + +这条路径完成三件事: + +- 将 runtime 发现的 Ethernet 设备包装成 `ax_net::EthernetDriver`。 +- 将结构化 `NetworkConfig` 交给 `ax-net`,由 `ax-net` 创建 `lo`、Ethernet 接口、路由、DHCP 状态和 DNS registry。 +- 在主网络栈初始化后注册需要独立控制面的 Wi-Fi/SoftAP 设备。 + +`parse_network_config()` 是系统配置到 `NetworkConfig` 的转换点。接口地址、DHCP、DNS、metric 和设备匹配策略应在这里进入结构化配置,而不是在 StarryOS 或驱动层再维护一份网络状态。 + +### IRQ 适配 + +`ax-net` 的 Ethernet 设备只依赖抽象的 `EthernetIrqRegistrar`。runtime 侧通过 `set_ethernet_irq_registrar()` 注入平台 IRQ 注册能力: + +```rust +ax_net::set_ethernet_irq_registrar(&crate::irq::NET_IRQ_REGISTRAR); +``` + +IRQ 处理函数只负责唤醒设备路径,不进入 smoltcp poll。协议栈推进仍由 `net-poll` worker 串行完成,避免 IRQ 上下文或应用线程重入 `SocketSet`。 + +### Unix Socket Namespace + +Unix domain socket 的路径名绑定需要文件系统命名空间协助。`ax-runtime` 在启用 `fs-ng` 时注册 namespace adapter: + +```rust +ax_net::unix::register_unix_namespace(crate::unix_ns::AxFsUnixNamespace); +``` + +该适配层只处理 pathname socket 与 VFS namespace 的关系;Unix socket 的连接、收发、poll 和生命周期仍位于 `ax-net`。 + +### 动态 Wi-Fi 与 SoftAP + +带 `wifi_control()` 的设备会走动态注册路径。runtime 从驱动读取 link policy,并把 OOB RX 唤醒函数设置为 `ax_net::notify_oob_rx`: + +```rust +ctrl.set_rx_wake(ax_net::notify_oob_rx); +let policy = ctrl.link_policy(); +``` + +随后 runtime 构造 `NetConfig` 并调用: + +```rust +ax_net::register_device_with_config(driver, config); +``` + +这条路径适合 SoftAP 或运行期新增的静态地址设备。`ax-net` 会分配新的 `InterfaceId`,创建路由规则,更新 smoltcp IP address list,启动设备 worker,并可按配置启用 DHCP server。 + +### Vsock + +`vsock` feature 下,runtime 收集 virtio-vsock 等设备并调用: + +```rust +ax_net::init_vsock(vsock_devs); +``` + +如果平台没有 vsock 设备,也应调用空列表初始化,使上层查询得到明确的“已初始化但无设备”状态。vsock 不参与 IP 路由、ARP、DNS 或 Ethernet dataplane;它只复用 `ax-net` 的 socket facade 和 poll 语义。 ## ArceOS API 层 -ArceOS API 层通过 `SocketOps` 访问 `ax-net`: +ArceOS API 层通过 `SocketOps` 和 `Socket` 枚举访问 `ax-net`: ```text ax-api / ax-posix-api - -> ax-net Socket / SocketOps + -> ax_net::Socket + -> SocketOps -> tcp / udp / raw / unix / vsock ``` -应用侧通常不直接接触 `InterfaceId`,除非需要类似 `SO_BINDTODEVICE` 的接口绑定语义。 +API 层只做语言级或 POSIX 风格的入口适配。具体协议状态、端口冲突检查、socket option、设备绑定、poll readiness、DNS 查询都由 `ax-net` 内部处理。 + +典型调用关系: -## StarryOS +| 上层操作 | `ax-net` 入口 | 说明 | +| --- | --- | --- | +| `socket(AF_INET, SOCK_STREAM)` | `Socket::Tcp(TcpSocket::new())` | 创建 TCP socket,加入统一 socket facade | +| `connect()` | `SocketOps::connect()` | TCP/UDP/raw 各自根据地址族和 route decision 处理 | +| `bind()` | `SocketOps::bind()` | 绑定本地地址时可推导 `DeviceBinding` | +| `poll()` / `select()` / `epoll()` | `SocketOps::poll()` | 查询 readiness 并注册 waker | +| `getaddrinfo()` / DNS 查询 | `dns_query()` / `dns_query_timeout()` | DNS server 来自控制面 registry,并按可达性过滤 | -StarryOS 在 Linux ABI 层接入 `ax-net`。它负责 syscall 参数和 Linux 数据结构,不维护第二套网络状态。 +应用通常不直接接触 `InterfaceId`。只有需要接口约束时,才通过 `bind_device()` 或 Linux ABI 层的 `SO_BINDTODEVICE` 建立 `DeviceBinding`。 + +## StarryOS Linux ABI + +StarryOS 负责把 Linux ABI 转换为 `ax-net` API。它不维护第二套接口 registry、路由表、ARP 表或 socket poller。 ```mermaid flowchart TB - User["Linux user program"] - Syscall["StarryOS syscall/net"] - FileNet["file/net.rs"] - Packet["file/packet.rs"] - Proc["/proc/net/*"] - AxNet["ax-net"] - - User --> Syscall - Syscall --> FileNet - Syscall --> Packet - FileNet --> AxNet - Packet --> AxNet - Proc --> AxNet + App["Linux user program"] --> Syscall["StarryOS syscall/net"] + Syscall --> Inet["AF_INET / AF_INET6 socket"] + Syscall --> Packet["AF_PACKET socket"] + Syscall --> Netlink["AF_NETLINK socket"] + Syscall --> Ioctl["SIOCGIF* ioctl"] + Syscall --> Proc["/proc/net/*"] + + Inet --> AxSocket["ax_net::SocketOps"] + Packet --> AxIface["ax_net::interfaces()/interface_by_id()"] + Netlink --> AxIface + Ioctl --> AxIface + Proc --> AxProc["ax_net::arp_entries()/interfaces()"] ``` -### ioctl +### Namespace 可见性 + +StarryOS 的接口查询先经过可见性过滤: + +```rust +fn visible_interfaces() -> impl Iterator { + ax_net::interfaces() + .into_iter() + .filter(|info| in_root_net_ns() || info.kind == InterfaceKind::Loopback) +} +``` + +语义: + +- root network namespace 可以看到所有接口。 +- 非 root namespace 只暴露 loopback 视图。 +- 当前没有为每个 namespace 复制 `ax-net` 的 route table、socket domain 或协议栈实例。 + +因此,namespace 适配属于 ABI 可见性层,不是 `ax-net` 内部的多网络命名空间实现。 -StarryOS `SIOCGIF*` 系列 ioctl 从 `ax-net` 查询接口: +### ioctl 与 ifreq -- `SIOCGIFCONF` 遍历 `ax_net::interfaces()`。 -- `SIOCGIFADDR` / `SIOCGIFBRDADDR` / `SIOCGIFNETMASK` 按接口名查询 IPv4。 -- `SIOCGIFHWADDR` 返回真实 MAC 或 loopback 类型。 -- `SIOCGIFINDEX` 返回 `InterfaceInfo::id.to_linux_ifindex()`。 -- `SIOCGIFFLAGS` 从 `InterfaceFlags` 映射 Linux `IFF_*`。 +`file/net.rs` 实现 `SIOCGIF*` 查询。数据来源必须是 `ax-net` 接口快照: -### SO_BINDTODEVICE +| ioctl | 数据来源 | +| --- | --- | +| `SIOCGIFCONF` | 遍历 `ax_net::interfaces()`,再应用 namespace 可见性过滤 | +| `SIOCGIFFLAGS` | `InterfaceInfo::flags` 映射到 Linux `IFF_*` | +| `SIOCGIFADDR` | `InterfaceInfo::ipv4.address` | +| `SIOCGIFDSTADDR` | loopback 返回自身地址,Ethernet 返回 `0.0.0.0` | +| `SIOCGIFBRDADDR` | loopback 返回自身地址,Ethernet 根据 IPv4 CIDR 计算广播地址 | +| `SIOCGIFNETMASK` | IPv4 CIDR prefix 转换为 netmask | +| `SIOCGIFHWADDR` | Ethernet 返回真实 MAC,loopback 返回 loopback 硬件类型 | +| `SIOCGIFMTU` | `InterfaceInfo::mtu` | +| `SIOCGIFINDEX` | `InterfaceInfo::id.to_linux_ifindex()` | +| `SIOCGIFMETRIC` / `SIOCGIFMAP` / `SIOCGIFTXQLEN` | 返回 Linux 兼容的固定或空结构值 | +| `FIONREAD` | 转发到底层 socket 的 `recv_available()` | -StarryOS 将 Linux 传入的接口名解析为 `InterfaceId`: +所有按接口名查询的 ioctl 都应先解析 ifreq 中的 name,再通过 `ax_net::interface_by_name()` 获取快照。这样多网口、loopback 和动态注册接口都能走同一套路径。 + +### Socket Option + +StarryOS 的 `SO_BINDTODEVICE` 负责在 Linux 字符串接口名和 `ax-net` 的 `DeviceBinding` 之间转换: ```text -SO_BINDTODEVICE="eth1" +setsockopt(SO_BINDTODEVICE, "eth1") -> ax_net::interface_by_name("eth1") - -> SetSocketOption::BindToDevice(Some(id)) + -> SetSocketOption::BindToDevice(Some(interface_id)) + -> GeneralOptions::set_device_binding() + +getsockopt(SO_BINDTODEVICE) + -> socket DeviceBinding + -> ax_net::interface_by_id(interface_id) + -> interface name ``` -`getsockopt(SO_BINDTODEVICE)` 则反向从 socket 的 `DeviceBinding` 查询接口名。 +其它 socket option 通过 `GetSocketOption`、`SetSocketOption` 和 `Configurable` 分发到具体 socket。`SO_TYPE`、`TCP_INFO`、超时、nonblocking、`SO_REUSEADDR` 等语义应以 `ax-net` 的 socket 状态为准。 ### AF_PACKET -StarryOS 保留 Linux `sockaddr_ll` 编解码,接口信息来自 `ax-net`: +`AF_PACKET` 由 StarryOS 的 `PacketSocket` 实现,接口信息来自 `ax-net`: + +- 创建 packet socket 需要 root network namespace。 +- `bind(sockaddr_ll)` 中 `sll_ifindex == 0` 时绑定第一个可见 Ethernet 接口。 +- `sll_ifindex != 0` 时通过 `InterfaceId::from_linux_ifindex()` 和 `interface_by_id()` 找到接口。 +- `SockAddrLl::from_interface()` 填充 ifindex、硬件类型、地址长度和 MAC。 +- packet socket ioctl 支持 `SIOCGIFINDEX`、`SIOCGIFFLAGS`、`SIOCGIFHWADDR`。 + +`PacketSocket::send_packet()` 只模拟有限的 ARP reply 场景,用于让 Linux 用户态工具在 QEMU 中看到预期的 gateway ARP 行为。它不是通用二层转发路径,也不绕过 `ax-net` 的 IP dataplane。 -- `sll_ifindex == 0`:绑定第一个可见 Ethernet 接口。 -- `sll_ifindex != 0`:通过 `InterfaceId::from_linux_ifindex()` 和 `ax_net::interface_by_id()` 查询接口。 -- `SockAddrLl::from_interface(info, protocol)` 填充 ifindex、硬件类型和 MAC。 -- `send_packet()` 保留旧的 ARP reply 模拟能力,并基于当前绑定接口的 gateway 和 MAC 工作。 +### Netlink 与 procfs -### `/proc/net/arp` +StarryOS 的 netlink 和 procfs 视图也应复用 `ax-net` 状态: -`/proc/net/arp` 使用 `ax_net::arp_entries()`,device 字段必须是真实接口名,不写死 `eth0`。 +| 视图 | 数据来源 | 说明 | +| --- | --- | --- | +| `RTM_GETLINK` | `ax_net::interfaces()` | 生成 `RTM_NEWLINK`,包含 ifindex、name、flags、MAC 等属性 | +| `RTM_GETADDR` | `ax_net::interfaces()` | 生成 IPv4 address dump | +| `/proc/net/arp` | `ax_net::arp_entries()` | device 字段使用真实接口名 | +| `/proc/net/dev` | `ax_net::interfaces()` | 按接口生成兼容视图 | -## Axvisor +这些路径用于 Linux 兼容层观测网络状态,不应创建独立的接口或 ARP 缓存。 -Axvisor 不维护自己的网络接口模型。它通过结构化 `NetworkConfig` 表达网络意图: +## Axvisor 接入 -- 管理面接口。 -- VM 服务面接口。 -- 静态地址或 DHCP。 -- 默认路由 metric。 -- 接口级 DNS。 +Axvisor 应通过 `NetworkConfig` 描述网络意图,例如: -普通 TCP/UDP 连接交给 `ax-net` route decision 自动选择接口。需要固定接口时使用 `bind_device(InterfaceId)`。 +- 管理面接口使用静态地址或 DHCP。 +- VM 服务面接口配置独立 metric。 +- DNS server 来自接口级静态配置或全局 fallback。 +- 需要固定出接口的管理连接使用 `bind_device(InterfaceId)`。 + +示例: ```rust let mgmt = ax_net::interface_by_name("eth0").ok_or(AxError::NoSuchDevice)?; @@ -111,14 +284,50 @@ let sock = ax_net::tcp::TcpSocket::new(); sock.bind_device(mgmt.id)?; ``` -## root net namespace +普通 TCP/UDP 连接不需要 Axvisor 自行选择设备。目的地址、metric、接口 UP 状态和 socket 绑定约束统一交给 `ax-net` route decision。 + +## 集成约束 + +### 状态所有权 + +- 接口 ID、接口名、IPv4、gateway、metric、DNS 和 route table 由 `ax-net` 控制面维护。 +- TCP/UDP/raw/Unix/vsock socket 状态由 `ax-net` socket 层维护。 +- StarryOS ioctl、netlink、procfs 只读取 `ax-net` 快照。 +- runtime 只传入设备和配置,不持有可变网络状态副本。 + +### 线程与 poll 边界 + +- runtime IRQ 和设备 worker 可以唤醒网络栈,但不直接调用 smoltcp poll。 +- socket 热路径只请求 poll,不同步推进整个协议栈。 +- `net-poll` worker 独占执行 `Service::poll()`、smoltcp `Interface::poll()` 和 `SocketSet` 处理。 +- StarryOS syscall 层不应持有 Linux ABI 锁后再进入设备锁。 + +### 接口命名与 ifindex + +- `lo` 固定为 `InterfaceId::LOOPBACK`,Linux ifindex 为 1。 +- Ethernet 接口默认按发现顺序命名为 `eth0`、`eth1`。 +- Linux `ifindex` 和 `InterfaceId` 直接映射。 +- 外部系统不得把 Router 内部 `dev` 索引暴露为 ifindex。 + +### namespace 限制 + +当前 StarryOS namespace 集成是可见性过滤,不是完整 Linux network namespace: + +- 没有 per-namespace route table。 +- 没有 per-namespace socket bind domain。 +- 没有 per-namespace ARP/DNS 状态。 +- 非 root namespace 主要只看到 loopback。 + +需要完整 network namespace 时,应在 `ax-net` 上方设计 namespace domain,而不是在 StarryOS 局部复制接口表。 -当前 StarryOS 初期 root net namespace 可以看到所有接口。非 root namespace 侧的完整 Linux net namespace 隔离尚未实现,现有路径主要做可见性过滤,不复制 `ax-net` 的 domain 或 route table。 +## 接入检查清单 -## 边界原则 +新增或修改系统接入路径时,应检查: -- `ax-net` 统一维护接口、地址、路由、DNS、ARP 和 socket 状态。 -- ArceOS runtime 只做设备和 IRQ 装配。 -- StarryOS 只做 Linux ABI。 -- Axvisor 只表达网络意图。 -- 具体驱动和平台发现不进入 `ax-net`。 +- 是否通过 `NetworkConfig`、`InterfaceInfo`、`InterfaceId`、`DeviceBinding` 等公开模型传递网络语义。 +- 是否避免固定 `eth0`、固定 ifindex 或固定 gateway。 +- 是否只由 `ax-net` 维护 route table、DNS registry 和 ARP entries。 +- 是否使用 `ax_net::interfaces()`、`interface_by_name()`、`interface_by_id()` 查询接口。 +- 是否将 Linux ABI 结构体编解码留在 StarryOS,而不是下沉到 `ax-net`。 +- 是否在动态设备注册后调用 `request_poll()` 或依赖 `register_device_with_config()` 的唤醒路径。 +- 是否避免在 IRQ、设备 worker 或 syscall 热路径中直接推进 smoltcp poll。 diff --git a/docs/docs/architecture/net/sockets.md b/docs/docs/architecture/net/sockets.md index afaba2dc1f..707896a17e 100644 --- a/docs/docs/architecture/net/sockets.md +++ b/docs/docs/architecture/net/sockets.md @@ -5,35 +5,60 @@ sidebar_label: "Socket 系统" # Socket 系统 -`ax-net` 的 socket 抽象层向上暴露统一 API,向下对接 smoltcp(IP 类 socket)或自包含传输(Unix/vsock)。本文从源码层描述 backend 分层、端口仲裁、状态机和通用 poll 设施。 - -## Backend 分层 - -`Socket` 枚举([socket.rs](net/ax-net/src/socket.rs))聚合 5 类 backend,统一实现 `SocketOps` + `Configurable`: - -```rust -// socket.rs -pub enum Socket { - Tcp(Box), - Udp(Box), - Raw(Box), - Unix(Box), - #[cfg(feature = "vsock")] - Vsock(Box), -} +`ax-net` 的 socket 层向上提供统一的 POSIX-like socket facade,向下分别连接 smoltcp IP socket、内核 Unix domain socket transport 和可选 vsock transport。IP 类 socket 共享单个 smoltcp `SocketSet`,但 TCP 监听、UDP bind 冲突、raw packet 过滤、Unix namespace 和 vsock connection manager 都由 `ax-net` 在协议核心外补齐。 + +核心源码: + +| 源码 | 职责 | +| --- | --- | +| [socket.rs](net/ax-net/src/socket.rs) | 统一地址、send/recv 选项、`SocketOps` trait、`Socket` 枚举分发 | +| [general.rs](net/ax-net/src/general.rs) | 通用 socket 选项、超时、nonblocking、`SO_BINDTODEVICE`、poll helper | +| [wrapper.rs](net/ax-net/src/wrapper.rs) | 全局 smoltcp `SocketSet` 包装和 UDP bind side table | +| [state.rs](net/ax-net/src/state.rs) | TCP 等 socket 的轻量状态门禁 | +| [listen_table.rs](net/ax-net/src/listen_table.rs) | TCP listen bucket、SYN/accept 队列、accept waker | +| [tcp.rs](net/ax-net/src/tcp.rs) | TCP stream socket、端口仲裁、connect/listen/accept、orphan 接入 | +| [udp.rs](net/ax-net/src/udp.rs) | UDP datagram socket、connected peer、MSG_MORE corking、route-aware source selection | +| [raw.rs](net/ax-net/src/raw.rs) | Raw IP socket、ICMP loopback、peer filter、deferred RX | +| [unix/](net/ax-net/src/unix/mod.rs) | Unix stream/datagram transport、abstract/path namespace | +| [vsock/](net/ax-net/src/vsock/mod.rs) | 可选 AF_VSOCK facade 和 stream transport | + +## 设计边界 + +socket 层通过 `SocketOps` trait 和 `Socket` 枚举把系统调用语义映射到协议栈内部对象。它将 AF_INET、AF_UNIX、AF_VSOCK 的地址统一为 `SocketAddrEx`,将 `bind/connect/listen/accept/send/recv/shutdown` 统一为 trait 方法,并通过 `GeneralOptions` 维护 `O_NONBLOCK`、`SO_REUSEADDR`、超时和 `SO_BINDTODEVICE` 等通用选项。 + +IP 类 socket(TCP/UDP/raw)持有 smoltcp `SocketHandle`,注册到全局 `SocketSetWrapper`。socket 层补齐 smoltcp 不直接提供的 POSIX 语义——TCP accept queue(`ListenTable`)、UDP wildcard bind 冲突(`udp_binds` side table)、raw connected-peer 过滤。出接口选择由控制面 `NetControl` 在 bind/connect 时决策,实际发包由 `Router::dispatch()` 在 net-poll worker 中完成;socket 操作本身不同步推进 `Interface::poll()`,只调用 `request_poll()` 请求 worker 推进。 + +Unix domain socket 和 vsock 不经过 smoltcp,各自维护独立的 transport、namespace 和连接状态,但共享 `SocketOps`/`Configurable`/`Pollable` 入口,向上层呈现一致的 socket facade。 + +典型关系如下: + +```mermaid +flowchart TB + Syscall["syscall / ABI layer"] --> Facade["Socket enum + SocketOps"] + Facade --> General["GeneralOptions"] + Facade --> Inet["TCP / UDP / Raw"] + Facade --> Unix["Unix transport"] + Facade --> Vsock["Vsock transport"] + + Inet --> SocketSet["SocketSetWrapper"] + SocketSet --> Smol["smoltcp SocketSet"] + Inet --> Control["NetControl route/bind decision"] + Inet --> Poll["Pollable + poll_io"] + Poll --> NetPoll["request_poll() / net-poll worker"] + + Inet --> TcpSide["TCP_BOUND_PORTS / ListenTable"] + SocketSet --> UdpSide["udp_binds side table"] + Unix --> UnixNs["abstract/path namespace"] + Vsock --> VsockMgr["connection_manager"] ``` -| Backend | 类型 | 协议核心 | 源码 | -| --- | --- | --- | --- | -| `TcpSocket` | SOCK_STREAM / AF_INET | smoltcp TCP socket | [tcp.rs](net/ax-net/src/tcp.rs) | -| `UdpSocket` | SOCK_DGRAM / AF_INET | smoltcp UDP socket | [udp.rs](net/ax-net/src/udp.rs) | -| `RawSocket` | SOCK_RAW / AF_INET | smoltcp raw socket + ICMP loopback | [raw.rs](net/ax-net/src/raw.rs) | -| `UnixSocket` | SOCK_STREAM/SOCK_DGRAM / AF_UNIX | 自包含 `Transport`(不经 smoltcp) | [unix/mod.rs](net/ax-net/src/unix/mod.rs) | -| `VsockSocket` | SOCK_STREAM / AF_VSOCK | `rdif-vsock` 驱动 + ring buffer | [vsock/mod.rs](net/ax-net/src/vsock/mod.rs) | +## 公共 Facade + +公共 facade 定义跨协议族共享的数据形状和操作入口。系统调用层只需要持有 `Socket`,不需要知道底层是 smoltcp、Unix transport 还是 vsock connection manager。 -IP 类 socket(TCP/UDP/Raw/DNS)通过 `SOCKET_SET.add()` 注册到全局 `SocketSet`,获得 `SocketHandle`。Unix 和 vsock 不依赖 smoltcp,各自维护连接状态。 +### 地址与选项 -`SocketAddrEx` 统一地址类型: +`SocketAddrEx` 是跨地址族的统一地址类型: ```rust // socket.rs @@ -45,242 +70,324 @@ pub enum SocketAddrEx { } ``` -`SocketOps` trait 是所有 backend 的统一接口: +send/recv 选项保留 Linux `MSG_*` 语义: + +```rust +pub struct SendOptions { + pub to: Option, + pub flags: SendFlags, + pub cmsg: Vec, +} + +pub struct RecvOptions<'a> { + pub from: Option<&'a mut SocketAddrEx>, + pub flags: RecvFlags, + pub cmsg: Option<&'a mut Vec>, + pub truncated: Option<&'a mut bool>, +} +``` + +其中 `MSG_DONTWAIT` 只影响当前调用,不修改 socket 自身的 `O_NONBLOCK`;`MSG_PEEK`、`MSG_TRUNC`、`MSG_MORE` 由具体 transport 按协议语义解释。 + +### SocketOps + +`SocketOps` 是所有 backend 的统一接口: ```rust -// socket.rs pub trait SocketOps: Configurable { fn bind(&self, local_addr: SocketAddrEx) -> AxResult; fn connect(&self, remote_addr: SocketAddrEx) -> AxResult; - fn listen(&self, _backlog: usize) -> AxResult { Err(AxError::OperationNotSupported) } - fn accept(&self) -> AxResult { Err(AxError::OperationNotSupported) } + fn listen(&self, _backlog: usize) -> AxResult { + Err(AxError::OperationNotSupported) + } + fn accept(&self) -> AxResult { + Err(AxError::OperationNotSupported) + } fn send(&self, src: impl Read + IoBuf, options: SendOptions) -> AxResult; fn recv(&self, dst: impl Write + IoBufMut, options: RecvOptions<'_>) -> AxResult; + fn recv_available(&self) -> AxResult { + Err(AxError::OperationNotSupported) + } fn local_addr(&self) -> AxResult; fn peer_addr(&self) -> AxResult; fn shutdown(&self, how: Shutdown) -> AxResult; } ``` -TCP 实现完整 stream 接口(含 `listen`/`accept`);UDP 实现 datagram 的 bind/connect/send/recv/poll,不支持 `listen`/`accept`;Raw socket 不实现 `listen`/`accept`;Unix/vsock 有各自独立的 transport trait。 +默认实现只给出“不支持”的语义,具体 backend 再按协议覆盖。例如 TCP 支持 `listen/accept`,UDP/raw 不支持;Unix stream 支持 accept,Unix datagram 不按 TCP listen 语义工作;vsock 提供 stream transport。 -## SocketSetWrapper +### Backend 分发 -全局 socket 容器([wrapper.rs](net/ax-net/src/wrapper.rs))在 smoltcp `SocketSet` 之上增加 UDP 端口冲突仲裁: +`Socket` 枚举负责把统一 API 分发到具体 backend: ```rust -// wrapper.rs -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -struct UdpBindKey { - addr: Option, - port: u16, -} - -pub(crate) struct SocketSetWrapper<'a> { - pub inner: Mutex>, - udp_binds: Mutex>, +pub enum Socket { + Udp(Box), + Tcp(Box), + Raw(Box), + Unix(Box), + #[cfg(feature = "vsock")] + Vsock(Box), } ``` -`add()` 和 `with_socket_mut()` 是对 smoltcp API 的薄封装;`remove()` 额外调用 `udp_unbind()`。 +| Backend | 地址族/类型 | 协议核心 | 关键状态 | +| --- | --- | --- | --- | +| `TcpSocket` | AF_INET / SOCK_STREAM | smoltcp TCP socket | `StateLock`、`TCP_BOUND_PORTS`、`ListenTable`、orphan | +| `UdpSocket` | AF_INET / SOCK_DGRAM | smoltcp UDP socket | UDP bind side table、connected peer、cork | +| `RawSocket` | AF_INET / SOCK_RAW | smoltcp raw socket | local/peer filter、loopback RX、deferred RX | +| `UnixSocket` | AF_UNIX / stream,dgram | in-kernel transport | abstract/path namespace、stream/dgram transport | +| `VsockSocket` | AF_VSOCK / stream | rdif-vsock transport | connection manager、stream ring buffers | + +## 共享 Socket 状态 -### UDP 端口仲裁 +共享状态层包含三类内容:通用 socket 选项、smoltcp handle 空间,以及状态转换门禁。它们不表达某个协议的完整语义,只提供所有 backend 复用的基础设施。 -`udp_bind_available()` 实现默认 UDP bind 冲突仲裁。`SO_REUSEADDR` 不在该函数内部处理,而是在 `UdpSocket::bind()` 中通过 `general.reuse_address()` 决定是否跳过 `SOCKET_SET.udp_bind()` 这张 side table: +### GeneralOptions + +`GeneralOptions` 被 TCP、UDP、raw、Unix、vsock transport 复用,用来维护通用 socket option 和阻塞等待入口: ```rust -// wrapper.rs -fn udp_bind_available(binds: &HashMap, key: UdpBindKey) -> bool { - let wildcard = UdpBindKey { addr: None, port: key.port }; - // 精确 bind:同地址同端口冲突 || wildcard 已占用冲突 - if binds.contains_key(&key) || (key.addr.is_some() && binds.contains_key(&wildcard)) { - return false; - } - // Wildcard bind:任何地址已占用同一端口则冲突 - key.addr.is_some() || !binds.keys().any(|bind| bind.port == key.port) +// general.rs +pub(crate) struct GeneralOptions { + nonblock: AtomicBool, + reuse_address: AtomicBool, + send_timeout_nanos: AtomicU64, + recv_timeout_nanos: AtomicU64, + bound_if: AtomicU32, + socket_type: AtomicI32, + domain: i32, + protocol: i32, } ``` -| bind 类型 | 示例 | 拒绝条件 | -| --- | --- | --- | -| 精确 bind | `192.168.1.1:53` | 同地址同端口已存在,或 wildcard `0.0.0.0:53` 已存在 | -| Wildcard bind | `0.0.0.0:53` | 任何地址已占用同一端口 | -| `SO_REUSEADDR` | — | `UdpSocket::bind()` 跳过 wrapper 的 UDP bind side table;smoltcp 自身仍会执行 socket 状态/地址可用性检查 | +构造时固定 `(SOCK_*, AF_*, IPPROTO_*)`,后续 `getsockopt()` 直接从这里读取: -`udp_port_available()` 暴露给 ephemeral port 分配器使用。 - -## TCP 端口仲裁 +| socket | SOCK_* | AF_* | protocol | +| --- | --- | --- | --- | +| TCP | `SOCK_STREAM` | `AF_INET` | `IPPROTO_TCP` | +| UDP | `SOCK_DGRAM` | `AF_INET` | `IPPROTO_UDP` | +| Raw | `SOCK_RAW` | `AF_INET` | 创建时指定的 `IpProtocol` | +| Unix stream | `SOCK_STREAM` | `AF_UNIX` | `0` | +| Unix dgram | `SOCK_DGRAM` | `AF_UNIX` | `0` | +| Vsock stream | `SOCK_STREAM` | `AF_VSOCK` | `0` | -除了 `ListenTable`(管理 `listen()` 端口),`ax-net` 还有独立的 `TCP_BOUND_PORTS`([tcp.rs](net/ax-net/src/tcp.rs))追踪已 bind 但尚未 listen 的端口: +`bound_if` 保存的是稳定的 `InterfaceId`,不是 Router 内部设备索引: ```rust -// tcp.rs -static TCP_BOUND_PORTS: LazyLock>>>> = - LazyLock::new(|| Mutex::new(HashMap::new())); - -fn register_tcp_bound(endpoint: IpListenEndpoint) -> AxResult { - if endpoint.port == 0 { return Ok(()); } - let mut bound_ports = TCP_BOUND_PORTS.lock(); - let bound_addrs = bound_ports.entry(endpoint.port).or_default(); - if bound_addrs.iter().any(|&addr| listen_addrs_conflict(addr, endpoint.addr)) { - return Err(AxError::AddrInUse); - } - bound_addrs.push(endpoint.addr); - Ok(()) +pub fn set_device_binding(&self, binding: DeviceBinding) { + self.bound_if.store( + binding.bound_if.map_or(0, InterfaceId::get), + Ordering::Release, + ); } -fn unregister_tcp_bound(endpoint: IpListenEndpoint) { - if endpoint.port != 0 { - let mut bound_ports = TCP_BOUND_PORTS.lock(); - if let Some(bound_addrs) = bound_ports.get_mut(&endpoint.port) { - if let Some(idx) = bound_addrs.iter().position(|&addr| addr == endpoint.addr) { - bound_addrs.swap_remove(idx); - } - if bound_addrs.is_empty() { bound_ports.remove(&endpoint.port); } - } +pub fn device_binding(&self) -> DeviceBinding { + let raw = self.bound_if.load(Ordering::Acquire); + DeviceBinding { + bound_if: (raw != 0).then_some(InterfaceId::new(raw)), } } ``` -地址冲突判定: +### SocketSetWrapper + +TCP、UDP 和 raw socket 都注册到同一个 smoltcp `SocketSet`,由 `SocketSetWrapper` 持有: ```rust -fn listen_addrs_conflict(a: Option, b: Option) -> bool { - a.is_none() || b.is_none() || a == b +pub(crate) struct SocketSetWrapper<'a> { + pub inner: Mutex>, + udp_binds: Mutex>, } ``` -即 wildcard 与所有地址冲突,两个具体地址仅相等时冲突。这与 `ListenTable` 的同端口多地址 listen 语义一致。 +统一 `SocketSet` 的意义: -`tcp_port_available()` 同时检查两表: +- TCP/UDP/raw 共享同一 handle 空间。 +- net-poll worker 可以一次性推进所有 IP socket。 +- TCP listen table 和 orphan reaper 可以通过 handle 操作 child socket。 +- 不需要为每个接口复制 socket set,wildcard listen 和动态 route 选择保持简单。 + +`SocketSetWrapper` 只封装 smoltcp socket 访问,不持有 `Service` 锁,也不直接唤醒任务: ```rust -fn tcp_port_available(port: u16) -> bool { - LISTEN_TABLE.can_listen(IpListenEndpoint { addr: None, port }) - && !TCP_BOUND_PORTS.lock().contains_key(&port) +pub fn with_socket_mut, R, F>(&self, handle: SocketHandle, f: F) -> R +where + F: FnOnce(&mut T) -> R, +{ + let mut set = self.inner.lock(); + let socket = set.get_mut(handle); + f(socket) } ``` -## Socket 状态机 +### StateLock -`StateLock`([state.rs](net/ax-net/src/state.rs))提供基于 CAS 的轻量状态转换锁,所有 socket 类型共享: +`StateLock` 是 TCP 等 socket 的轻量状态门禁,避免同一个 socket 上并发 `bind/connect/listen` 进入不一致状态: ```rust -// state.rs #[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum State { Idle = 0, Busy = 1, Connecting = 2, - Connected = 3, Listening = 4, Closed = 5 } +pub(crate) enum State { + Idle = 0, + Busy = 1, + Connecting = 2, + Connected = 3, + Listening = 4, + Closed = 5, +} pub struct StateLock(AtomicU8); +``` -impl StateLock { - pub fn get(&self) -> State { - self.0.load(Ordering::Acquire).try_into().expect("invalid state") - } +`lock(expect)` 通过 CAS 把期望状态临时切到 `Busy`,`StateGuard::transit()` 在操作成功后提交新状态,失败时回退旧状态: - pub fn lock(&self, expect: State) -> Result, State> { - match self.0.compare_exchange(expect as u8, State::Busy as u8, - Ordering::Acquire, Ordering::Acquire) { - Ok(_) => Ok(StateGuard(self, expect as u8)), - Err(old) => Err(old.try_into().expect("invalid state")), - } +```rust +pub fn lock(&self, expect: State) -> Result, State> { + match self.0.compare_exchange( + expect as u8, + State::Busy as u8, + Ordering::Acquire, + Ordering::Acquire, + ) { + Ok(_) => Ok(StateGuard(self, expect as u8)), + Err(old) => Err(old.try_into().expect("invalid state")), } } ``` -状态转换图: +典型 TCP 公共状态流: +```text +Idle --bind--> Idle +Idle --listen--> Listening +Idle --connect--> Connecting --established--> Connected +Listening --accept--> Listening +Connected --shutdown/drop--> Closed or orphaned smoltcp socket ``` -Idle ──bind──→ Idle (不变,但注册 endpoint) -Idle ──listen──→ Listening -Idle ──connect──→ Connecting ──SYN/SYN+ACK──→ Connected -Listening ──accept──→ Listening (不变,但生成新 socket) -Connected ──close──→ Closed -``` -`StateGuard::transit()` 在执行操作时临时置为 `Busy`,成功提交新状态,失败回退: +## 端口与监听仲裁 + +端口仲裁是 POSIX 兼容语义的一部分,不能完全交给 smoltcp。`ax-net` 使用 TCP 和 UDP 各自的 side table 表达 wildcard/specific-address 冲突关系。 + +### UDP Bind Side Table + +UDP bind side table 位于 `SocketSetWrapper`,用于补齐 Linux 风格的 wildcard bind 冲突: ```rust -// state.rs -#[must_use] -pub struct StateGuard<'a>(&'a StateLock, u8); - -impl StateGuard<'_> { - pub fn transit(self, new: State, f: impl FnOnce() -> AxResult) -> AxResult { - match f() { - Ok(result) => { self.0.0.store(new as u8, Ordering::Release); Ok(result) } - Err(err) => { self.0.0.store(self.1, Ordering::Release); Err(err) } - } +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +struct UdpBindKey { + addr: Option, + port: u16, +} + +fn udp_bind_available(binds: &HashMap, key: UdpBindKey) -> bool { + let wildcard = UdpBindKey { + addr: None, + port: key.port, + }; + if binds.contains_key(&key) || (key.addr.is_some() && binds.contains_key(&wildcard)) { + return false; } + key.addr.is_some() || !binds.keys().any(|bind| bind.port == key.port) +} +``` + +| bind 类型 | 示例 | 冲突规则 | +| --- | --- | --- | +| 精确地址 | `192.168.1.10:53` | 同地址同端口冲突;同端口 wildcard 已存在也冲突 | +| Wildcard | `0.0.0.0:53` | 任意地址已占用该端口即冲突 | +| `SO_REUSEADDR` | socket option | `UdpSocket::bind()` 跳过 wrapper 的 UDP bind side table;smoltcp 仍执行自身 bind 检查 | + +### TCP Bound Ports + +TCP 除了 listen table,还需要记录“已经 bind 但还没有 listen/connect 完成”的端口所有权: + +```rust +static TCP_BOUND_PORTS: LazyLock>>>> = + LazyLock::new(|| Mutex::new(HashMap::new())); + +fn listen_addrs_conflict(a: Option, b: Option) -> bool { + a.is_none() || b.is_none() || a == b +} +``` + +语义是 wildcard 与所有地址冲突,两个具体地址仅在相等时冲突。ephemeral TCP 端口分配同时检查 listen table 和 bound table: + +```rust +fn tcp_port_available(port: u16) -> bool { + LISTEN_TABLE.can_listen(IpListenEndpoint { addr: None, port }) + && !TCP_BOUND_PORTS.lock().contains_key(&port) } ``` -并发 `bind`/`connect`/`listen` 在同一 socket 上只有第一个成功——后续操作在 `lock(expect)` 时 CAS 失败。 +这里用 wildcard endpoint 检查 listen table 是有意的保守策略:自动分配 ephemeral port 时,只要该端口已经存在任何 listen entry,就不再分配给主动连接 socket。 -## ListenTable +### ListenTable -`ListenTable`([listen_table.rs](net/ax-net/src/listen_table.rs))是 TCP 监听的核心数据结构。65536 个端口 bucket,每端口一个 `Arc>>`,支持同端口不同地址 listen: +`ListenTable` 是 TCP passive open 的核心数据结构。smoltcp 没有“一个 public listen socket 管理多个 child socket”的 POSIX 对象模型,所以 `ax-net` 在外部维护 accept queue: ```rust -// listen_table.rs struct ListenTableEntryInner { listen_endpoint: IpListenEndpoint, - backlog: usize, // clamp 到 LISTEN_QUEUE_SIZE=512 + backlog: usize, syn_queue: VecDeque, accept_poll: Arc, } pub struct ListenTable { - tcp: Box<[ListenTableEntry]>, // [Arc>>; 65536] + tcp: Box<[ListenTableEntry]>, } ``` -### listen/unlisten +`tcp` 是 65536 个端口 bucket,每个 bucket 存放该端口下的多个具体地址 listener。`listen()` 检查 wildcard/specific 冲突后插入 entry: ```rust -pub fn can_listen(&self, endpoint: IpListenEndpoint) -> bool { - self.tcp[endpoint.port as usize].lock().iter() - .all(|entry| !listen_addrs_conflict(entry.listen_endpoint.addr, endpoint.addr)) -} - -pub fn listen(&self, endpoint: IpListenEndpoint, backlog: usize) -> AxResult { - let mut entries = self.tcp[endpoint.port as usize].lock(); - if entries.iter().any(|e| listen_addrs_conflict(e.listen_endpoint.addr, endpoint.addr)) { +pub fn listen(&self, listen_endpoint: IpListenEndpoint, backlog: usize) -> AxResult { + let port = listen_endpoint.port; + let mut entries = self.tcp[port as usize].lock(); + if entries + .iter() + .any(|entry| listen_addrs_conflict(entry.listen_endpoint.addr, listen_endpoint.addr)) + { return Err(AxError::AddrInUse); } - entries.push(ListenTableEntryInner::new(endpoint, backlog)); + entries.push(ListenTableEntryInner::new(listen_endpoint, backlog)); Ok(()) } ``` -### SYN 预创建 - -`snoop_tcp_packet()` 在 `Router::poll()` 中拦截 TCP SYN 包(`tcp_packet.syn() && !tcp_packet.ack()`),调用 `incoming_tcp_packet()` 预创建 socket。 +### SYN 预创建与 accept -`incoming_tcp_packet()` 在 `syn_queue` 未满时创建新 `TcpSocket` 并调用 `socket.listen()`,推入队列供后续 `accept()` 获取。 +`Router::poll()` 在 RX 路径 snoop TCP SYN 包,`incoming_tcp_packet()` 匹配 listen endpoint 后预创建 child smoltcp socket,并推入 listener 的 `syn_queue`。这样每条 pending 连接都有自己的 smoltcp TCP 状态机,可以独立完成 SYN-RECEIVED 到 ESTABLISHED 的推进。 -### accept() +`accept()` 遍历 `syn_queue`,清理已经关闭且无数据的 child,返回第一个可接受 socket: ```rust -pub fn accept(&self, endpoint: IpListenEndpoint, sockets: &mut SocketSet) -> AxResult { - let entries = self.tcp[endpoint.port as usize].clone(); +pub fn accept( + &self, + listen_endpoint: IpListenEndpoint, + sockets: &mut SocketSet<'_>, +) -> AxResult { + let entries = self.listen_entry(listen_endpoint.port); let mut table = entries.lock(); - let entry = table.iter_mut() - .find(|e| e.listen_endpoint == endpoint) - .ok_or(AxError::InvalidInput)?; - - // 遍历 syn_queue,跳过 Closed 且无数据的 socket + let Some(entry) = table + .iter_mut() + .find(|entry| entry.listen_endpoint == listen_endpoint) + else { + return Err(AxError::InvalidInput); + }; + + let syn_queue: &mut VecDeque = &mut entry.syn_queue; let mut idx = 0; - while idx < entry.syn_queue.len() { - let handle = entry.syn_queue[idx].accepted.handle; + while idx < syn_queue.len() { + let handle = syn_queue[idx].accepted.handle; if is_closed_without_data(sockets, handle) { - entry.syn_queue.swap_remove_front(idx); + syn_queue.swap_remove_front(idx); sockets.remove(handle); continue; } if is_acceptable(sockets, handle) { - return Ok(entry.syn_queue.swap_remove_front(idx).unwrap().accepted); + return Ok(syn_queue.swap_remove_front(idx).unwrap().accepted); } idx += 1; } @@ -288,66 +395,329 @@ pub fn accept(&self, endpoint: IpListenEndpoint, sockets: &mut SocketSet) -> AxR } ``` -可 accept 的状态:`Established | CloseWait | FinWait1 | FinWait2 | Closing | LastAck | TimeWait`。 +可接受状态包括已经建立以及已经进入关闭流程但仍可被 userspace 观察的 child,例如 `Established`、`CloseWait`、`FinWait*`、`Closing`、`LastAck`、`TimeWait`。 + +## IP Socket Backend + +TCP、UDP 和 raw socket 都持有 smoltcp `SocketHandle`,但它们在 public 语义、side table 和 packet 格式上差异很大。 -## 通用 Poll 与超时 +### TCP Socket -`GeneralOptions`([general.rs](net/ax-net/src/general.rs))为所有 socket 提供通用的非阻塞、超时和 poll 设施: +`TcpSocket` 包装 smoltcp stream socket,并维护 public TCP 状态、端口注册、peer endpoint、keepalive/TCP_INFO 选项和 readiness poll set: ```rust -// general.rs -pub(crate) struct GeneralOptions { - nonblock: AtomicBool, // O_NONBLOCK - reuse_address: AtomicBool, // SO_REUSEADDR - send_timeout_nanos: AtomicU64, // SO_SNDTIMEO (0 = 无限) - recv_timeout_nanos: AtomicU64, // SO_RCVTIMEO - bound_if: AtomicU32, // SO_BINDTODEVICE (InterfaceId, 0 = 未绑定) - socket_type: AtomicI32, // SOCK_STREAM(1)/DGRAM(2)/RAW(3) - domain: i32, // AF_INET(2)/AF_UNIX(1)/AF_VSOCK(40) - protocol: i32, // IPPROTO_TCP(6)/UDP(17)/ICMP(1) +pub struct TcpSocket { + state: StateLock, + handle: SocketHandle, + bound_endpoint: Mutex, + peer_endpoint: Mutex>, + bound_registered: AtomicBool, + general: GeneralOptions, + pending_error: AtomicI32, + keep_idle_secs: AtomicU32, + keep_interval_secs: AtomicU32, + keep_count: AtomicU32, + user_timeout_millis: AtomicU32, + rx_closed: AtomicBool, + poll_rx: Arc, + poll_tx: Arc, + poll_rx_closed: PollSet, } ``` -构造函数按 socket 类型固化 `(socket_type, domain, protocol)`: +TCP socket 的主要职责: -| socket | SOCK_* | AF_* | IPPROTO_* | -| --- | --- | --- | --- | -| `TcpSocket` | 1 (STREAM) | 2 (INET) | 6 (TCP) | -| `UdpSocket` | 2 (DGRAM) | 2 (INET) | 17 (UDP) | -| `RawSocket` | 3 (RAW) | 2 (INET) | 根据 `IpProtocol` | -| `UnixSocket` stream | 1 (STREAM) | 1 (UNIX) | 0 | -| `UnixSocket` dgram | 2 (DGRAM) | 1 (UNIX) | 0 | -| `VsockSocket` | 1 (STREAM) | 40 (VSOCK) | 0 | +- `bind()`:通过控制面校验本地地址并注册 `TCP_BOUND_PORTS`。 +- `connect()`:选择 route/source,绑定 ephemeral port,启动 smoltcp connect,然后 `request_poll()`。 +- `listen()`:把 endpoint 移入 `ListenTable`。 +- `accept()`:从 `ListenTable` 取出 child handle,构造已连接 `TcpSocket`。 +- `send/recv()`:只操作 smoltcp socket buffer,不同步驱动完整 interface poll。 +- `drop()`:必要时把未完全关闭的 smoltcp socket移入 orphan reaper。 -### 核心 Poller +### UDP Socket -`send_poller_with()` 和 `recv_poller_with()` 是通用的阻塞/非阻塞等待入口。当前源码不手写循环,而是把一次 socket 操作闭包交给 `ax_task::future::poll_io()`,再套上 send/recv timeout: +`UdpSocket` 是 datagram backend,保留本地 endpoint、connected peer 和 `MSG_MORE` cork 状态: + +```rust +struct CorkState { + buf: Vec, + remote: IpEndpoint, + source: IpAddress, +} + +pub struct UdpSocket { + handle: SocketHandle, + local_addr: RwLock>, + peer_addr: RwLock>, + general: GeneralOptions, + cork: Mutex>, +} +``` + +设计要点: + +- bind 时通过 `SocketSetWrapper::udp_bind()` 记录 wildcard/specific ownership。 +- connect/sendto 时通过控制面 route decision 选择源地址。 +- connected UDP 保存 `(peer endpoint, selected source)`,recv 时过滤不匹配 peer 的 datagram。 +- `MSG_MORE` 会把多次 send 合并为一个 datagram,并固定第一次 send 的 remote/source,避免后续调用改变目标。 +- drop 时从 UDP bind side table 中移除 handle。 + +### Raw Socket + +`RawSocket` 暴露 IP 层以上、TCP/UDP 以下的 packet-oriented 接口: + +```rust +pub struct RawSocket { + handle: SocketHandle, + ip_version: IpVersion, + local_addr: RwLock>, + peer_addr: RwLock>, + loopback_rx: Mutex)>>, + deferred_rx: Mutex)>>, + ttl: RwLock>, + rx_closed: AtomicBool, + tx_closed: AtomicBool, + general: GeneralOptions, +} +``` + +raw socket 有两个特别路径: + +- `loopback_rx` 保存本地快速路径产生、尚未被 recv 取走的 loopback packet。 +- `deferred_rx` 保存 connected-peer 过滤时暂存的非当前可交付 packet,格式保持为一致的 wire packet,避免 peek/filter 后破坏 smoltcp receive queue 语义。 + +发送时,如果没有显式本地地址,raw socket 通过控制面按 remote 选择 source;loopback 目的地址走本地路径,非 loopback 目的地址交给 smoltcp raw socket 和 Router dispatch。 + +## Local Transport Backend + +Unix 和 vsock 不经过 smoltcp `SocketSet`,但共享 `SocketOps`、`Configurable` 和 `Pollable` 入口。它们的状态机和缓冲区由各自 transport 管理。 + +### Unix Socket + +Unix socket facade 维护公共 local/remote 地址,具体 stream/datagram 语义交给 `Transport`: + +```rust +pub enum UnixSocketAddr { + Unnamed, + Abstract(Arc<[u8]>), + Path(Arc), +} + +pub enum Transport { + Stream(StreamTransport), + Dgram(DgramTransport), +} + +pub struct UnixSocket { + transport: Transport, + local_addr: Mutex, + remote_addr: Mutex, +} +``` + +namespace 分两类: + +- abstract namespace:`ABSTRACT_BINDS: HashMap, BindSlot>`,完全位于内存。 +- path namespace:通过 `register_unix_namespace()` 注入外部 VFS namespace provider。 + +`BindSlot` 同时容纳 stream listener 和 datagram endpoint,因此同一路径下 stream/dgram ownership 由 transport 分别仲裁: + +```rust +pub struct BindSlot { + stream: Mutex>, + dgram: Mutex>, +} +``` + +Unix socket 的 accept 使用 transport 自己的 `Pollable`,不涉及 `request_poll()` 或 smoltcp: + +```rust +let (transport, peer_addr) = + block_on(poll_io(&self.transport, IoEvents::IN, nonblocking, || { + self.transport.try_accept() + }))?; +``` + +### Vsock Socket + +vsock 是可选 feature,不属于 IP 协议,也不通过 smoltcp poll。facade 只把 `SocketOps` 映射到 `VsockTransport`: + +```rust +pub enum VsockTransport { + Stream(VsockStreamTransport), +} + +pub struct VsockSocket { + transport: VsockTransport, +} +``` + +核心连接状态位于 `vsock::connection_manager`,设备事件由 vsock 设备层推进。transport enum 提供 stream variant,并为后续 datagram 扩展保留结构。 + +## Poll 与唤醒 + +socket 阻塞语义基于 `Pollable` + `poll_io()`。应用线程只注册 waker 并等待 readiness;协议栈推进由 net-poll worker 或本地 transport 自己的 poll set 完成。 + +### 通用 poll helper + +`GeneralOptions` 提供 send/recv 两类阻塞 helper: ```rust -// general.rs pub fn send_poller_with AxResult, T>( - &self, pollable: &P, extra_nb: bool, f: F) -> AxResult -{ + &self, + pollable: &P, + extra_nonblocking: bool, + f: F, +) -> AxResult { block_on(timeout( self.send_timeout(), poll_io( pollable, IoEvents::OUT, - self.nonblocking() || extra_nb, + self.nonblocking() || extra_nonblocking, f, ), ))? } ``` -流程:`poll_io()` 先调用 `f()`;如果返回 `WouldBlock` 且当前调用不是 nonblocking,则通过 `pollable.register(context, IoEvents::OUT/IN)` 注册 waker,挂起当前任务,等待 readiness 或 timeout 后重试。`GeneralOptions::register_waker()` 只是提供给具体 socket 的 `Pollable::register()` 使用,用于把同一个 waker 注册到符合 `DeviceBinding` 的设备唤醒路径。 +`poll_io()` 的流程: -`register_waker()` 根据 `DeviceBinding` 选择性注册到匹配的设备: +1. 先执行一次 socket 操作闭包。 +2. 如果成功,直接返回。 +3. 如果返回 `WouldBlock` 且是 nonblocking 或 `MSG_DONTWAIT`,立即返回错误。 +4. 否则调用 `Pollable::register()` 注册 waker,挂起当前任务。 +5. 被唤醒或 timeout 后重试闭包。 + +### IP socket readiness + +TCP/UDP/raw 的 `poll()` 都会先 `request_poll()`,表示需要专用 net-poll worker 推进 smoltcp: ```rust -pub(crate) fn register_waker(&self, waker: &Waker) { +impl Pollable for UdpSocket { + fn poll(&self) -> IoEvents { + request_poll(); + if self.local_addr.read().is_none() { + return IoEvents::empty(); + } + let mut events = IoEvents::empty(); + self.with_smol_socket(|socket| { + events.set(IoEvents::IN, socket.can_recv()); + events.set(IoEvents::OUT, socket.can_send()); + }); + events + } +} +``` + +注册 waker 时分两层: + +- 向 smoltcp socket 注册 recv/send waker,等待协议 socket buffer 状态变化。 +- 通过 `GeneralOptions::register_waker()` 向匹配 `DeviceBinding` 的设备注册 waker,等待设备 RX 触发下一轮 net-poll。 + +```rust +pub fn register_waker(&self, waker: &Waker) { get_service().register_waker(self.device_binding(), waker); } ``` -绑定 `DeviceBinding { bound_if: Some(eth0) }` 时只注册到 eth0 的 waker,避免被无关设备唤醒。 +TCP listener 还有额外 accept waker:`ListenTable::register_accept_waker()` 会把 userspace waker 放到 listener 的 `accept_poll`,并把 `accept_poll` 转成 waker 注册到 pending child 的 recv/send readiness 上。 + +### Local transport readiness + +Unix/vsock 不调用 `request_poll()`。它们的 `Pollable` 由 transport 内部 `PollSet`、channel 或 connection manager 状态驱动。这样 AF_UNIX/AF_VSOCK 的等待路径不会依赖 IP net-poll worker。 + +## 生命周期与清理 + +socket drop 需要清理 public side table,但不能破坏协议栈还需要推进的状态。 + +### TCP orphan + +TCP drop 时,如果 smoltcp socket 已经进入需要继续关闭或 TIME-WAIT 的状态,socket 不会立即从 `SocketSet` 删除,而是交给 orphan reaper: + +```text +TcpSocket::drop + -> unregister_tcp_bound / unlisten if needed + -> if smoltcp socket still needs protocol cleanup: + orphan::register_orphan(handle, timestamp) + else: + SOCKET_SET.remove(handle) +``` + +这样 FIN、LAST-ACK、TIME-WAIT 等状态仍由 net-poll worker 推进,避免应用对象释放后协议状态被过早销毁。 + +### UDP/raw cleanup + +UDP drop 会调用 `SOCKET_SET.remove(handle)`,wrapper 在 remove 中清除 UDP bind side table: + +```rust +pub fn remove(&self, handle: SocketHandle) { + self.udp_unbind(handle); + self.inner.lock().remove(handle); +} +``` + +raw drop 会先 `shutdown(Shutdown::Both)`,再移除 smoltcp raw socket。raw 的 `loopback_rx` 和 `deferred_rx` 是 socket 本地暂存状态,随对象释放。 + +### Listen cleanup + +TCP listener unlisten 时会从 listen table 删除 entry,并销毁尚未 accept 的 child socket handle: + +```rust +pub fn unlisten(&self, listen_endpoint: IpListenEndpoint) { + let handles = { + let mut entries = self.tcp[listen_endpoint.port as usize].lock(); + let Some(idx) = entries + .iter() + .position(|entry| entry.listen_endpoint == listen_endpoint) + else { + return; + }; + entries.swap_remove(idx).into_handles() + }; + for handle in handles { + SOCKET_SET.remove(handle); + } +} +``` + +## 并发边界 + +socket 层并发边界围绕三类锁:`SERVICE`、`SOCKET_SET.inner`、协议 side table。原则是 socket 操作只在必要范围内持锁,并通过 `request_poll()` 交给 net-poll worker 推进协议核心。 + +### 锁顺序 + +典型锁顺序: + +```text +net-poll path: + SERVICE -> SOCKET_SET.inner -> smoltcp sockets + +TCP listen/accept path: + SOCKET_SET.inner -> LISTEN_TABLE bucket + +TCP bind path: + TCP_BOUND_PORTS -> LISTEN_TABLE check + +UDP bind path: + SOCKET_SET.inner / udp_binds + +control-assisted bind/send path: + NetControl.state -> RouteTable +``` + +需要避免的反向路径: + +- 持设备锁时进入 `SocketSet` 或 `Service`。 +- 持 `SocketSet` 时执行可能阻塞的用户 buffer IO。 +- socket 热路径直接调用完整 interface poll。 + +### 热路径原则 + +TCP/UDP/raw 的 send/connect/recv 路径只做三件事: + +1. 操作对应 smoltcp socket 或本地 socket 状态。 +2. 调用 `request_poll()` 请求专用 net-poll worker 推进协议栈。 +3. 在 `WouldBlock` 时通过 `Pollable::register()` 注册 waker 并让出当前任务。 + +这个模型保持应用线程和协议栈驱动线程分离,避免 socket 调用者临时成为 smoltcp interface owner。 diff --git a/docs/docs/architecture/net/testing.md b/docs/docs/architecture/net/testing.md index 565eca8e01..9dd4ae95d8 100644 --- a/docs/docs/architecture/net/testing.md +++ b/docs/docs/architecture/net/testing.md @@ -5,184 +5,435 @@ sidebar_label: "测试与限制" # 测试与限制 -网络栈同时影响 ArceOS、StarryOS 和 Axvisor。修改 `net/ax-net` 时,应按改动范围选择验证集合。 - -## 单元测试 - -推荐至少覆盖: - -- 接口 ID 与 `DeviceBinding` 语义。 -- 多接口 route lookup。 -- longest prefix 优先级。 -- 同前缀 metric 选择。 -- 同 metric 稳定插入顺序。 -- 接口 down / 不可用时跳过 route。 -- default route 查询。 -- bounded RX/TX queue 满时行为。 -- `QueuedPacket` 超 MTU 拒绝、队列路径无每包堆分配。 -- loopback dispatch 直接注入 `Router.rx_buffer`,并在注入前触发 TCP SYN snoop。 -- DHCP bootstrap 和 per-interface 状态。 -- DHCP server Discover/Request 回复。 -- TCP orphan 在 Closed、TIME_WAIT/FIN teardown 超时和 overflow 场景下的回收。 -- TCP/UDP bind 到具体本地地址后保留接口绑定。 -- 未绑定 TCP/UDP connect 按 route decision 选择接口。 - -当前 `ax-net` host 单测可用: +`ax-net` 横跨协议栈核心、设备 dataplane、系统调用 ABI 和运行时设备接入。验证时应按改动影响面选择测试层级:纯数据结构改动优先跑 crate 单测;socket、poll、ioctl、AF_PACKET、netlink 或 procfs 改动需要补 StarryOS QEMU 系统测试;涉及 runtime、架构或平台设备路径时再扩展到对应架构。 + +## 验证分层 + +| 层级 | 覆盖范围 | 典型触发条件 | +| --- | --- | --- | +| `ax-net` 单元测试 | 路由表、接口配置、socket bind 表、TCP listen/orphan、DHCP、DNS、raw deferred RX、队列边界 | 修改 `net/ax-net/src` 内部逻辑 | +| crate lint / fmt | Rust 风格、clippy 约束、feature 组合基础检查 | 修改 Rust 代码 | +| StarryOS syscall 测试 | Linux socket ABI、ioctl、AF_PACKET、netlink、procfs、poll/epoll 行为 | 修改 StarryOS 适配层或 `ax-net` public API | +| 跨架构 QEMU | riscv64、loongarch64、aarch64、x86_64 上的启动、调度、网络 syscall 回归 | 修改同步、原子、poll worker、架构相关接入 | +| 板级或真实设备验证 | IRQ、DMA、真实 NIC、Wi-Fi/SoftAP、链路状态 | 修改 driver/runtime/设备 worker/OOB RX | + +## 推荐命令 + +### 文档改动 + +仅修改 `docs/docs/architecture/net` 文档时,不需要运行 clippy 或 QEMU。建议至少检查 Markdown 结构、代码块闭合和文档内旧术语: ```bash +rg -n "^##|^###" docs/docs/architecture/net +grep -R "^```" -n docs/docs/architecture/net | wc -l +rg -n "Rounte[r]|ServiceCor[e]|device_mas[k]|#L[0-9]+" docs/docs/architecture/net +``` + +### `ax-net` 逻辑改动 + +修改 `net/ax-net` Rust 逻辑后,优先执行: + +```bash +cargo fmt +cargo xtask clippy --package ax-net cargo test -p ax-net ``` -`ax-net` host test 需要相关底层 crate 启用 host-test feature,避免链接内核 percpu 路径。 +`cargo test -p ax-net` 用于 host 单测。若某些 feature 组合依赖内核环境或底层 crate host-test 支持不足,应记录失败原因,并用更贴近目标环境的 `cargo xtask` 验证补足。 -### 测试覆盖建议 +### StarryOS 系统测试 -| 模块 | 关键测试点 | 优先级 | -| --- | --- | --- | -| `RouteTable` | 添加/删除/替换规则、排序验证、默认路由查询、`select_route_if` with mock filter | 高 | -| `ListenTable` | port 冲突、per-address listen、wildcard 冲突、backlog 上限、`can_listen`/`listen`/`unlisten`、SYN 队列溢出、`can_accept`/`accept` | 高 | -| `SocketSetWrapper` | UDP bind 冲突仲裁(精确/wildcard)、add/remove;`SO_REUSEADDR` 跳过 side table 的行为由 `UdpSocket::bind()` 覆盖 | 中 | -| `StateLock` | Idle→Busy CAS、transit 成功/失败回退、状态读取一致性 | 中 | -| `GeneralOptions` | nonblock/send_timeout/recv_timeout、device_binding 读写、Atomic 一致性 | 中 | -| `TcpSocket` | `connect()` 成功/失败、`bind_device()` 无效接口、`poll_connect()` 状态转换、`tcp_info_snapshot()` 字段 | 高 | -| `UdpSocket` | `send()` with MSG_MORE corking、connected/disconnected send、`recv()` truncation | 高 | -| `RawSocket` | `send()` ICMP echo→loopback reply、TTL 读写、IP version 校验、`MSG_PEEK` 与 `deferred_rx` wire-packet 格式 | 中 | -| `UnixSocket` | stream pair send/recv、cmsg 传递、datagram pair、abstract namespace bind/connect | 中 | -| `VsockSocket` | bind/listen/connect/accept 状态转换、send/recv | 低(需 `vsock` feature) | -| `DhcpState` | Discovering→Offer→Requesting→ACK→Bound 状态机、NAK→reset、`process_packet` 校验 | 中 | -| `DhcpServer` | Discover→Offer、Request→Ack、错误 xid/mac/interface 过滤、单客户端租约覆盖 | 中 | -| `orphan` | Closed 立即回收、teardown 超时回收、overflow 只 warn 且保留仍在 teardown 的 socket | 高 | -| DNS | `dns_query_timeout` 超时、不可路由 server 过滤、`DnsSocketGuard` drop | 中 | - -## 接口路由测试 - -建议对 `RouteTable` 进行以下专项测试: +涉及 Linux ABI、socket syscall、poll/epoll、AF_PACKET、netlink 或 `/proc/net/*` 时,使用 `cargo xtask` 跑 StarryOS QEMU: +```bash +cargo xtask starry test qemu --arch riscv64 +cargo xtask starry test qemu --arch loongarch64 +``` + +跨架构回归可扩展到: + +```bash +cargo xtask starry test qemu --arch aarch64 +cargo xtask starry test qemu --arch x86_64 ``` -Test: longest prefix match - Add: 10.0.0.0/8 → dev0, metric=100 - Add: 10.0.2.0/24 → dev1, metric=200 - Query 10.0.0.1 → dev0 (只有 /8 匹配) - Query 10.0.2.15 → dev1 (/24 比 /8 更长) - -Test: metric tie-breaking - Add: 0.0.0.0/0 → dev0, metric=100 - Add: 0.0.0.0/0 → dev1, metric=50 - Query 8.8.8.8 → dev1 (metric 更低) - -Test: insert order stability (same prefix, same metric) - Add rule_a: metric=100, order=0 - Add rule_b: metric=100, order=1 - rule_a 排在 rule_b 前面 - -Test: unavailable interface skip - Add: 0.0.0.0/0 → dev0, metric=100 - Add: 0.0.0.0/0 → dev1, metric=200 - dev0 is not UP → select_route_if with is_usable → returns dev1 - -Test: replace_ipv4_rules_for_interface - Add rules for interface=2 - Call replace with new rules → old rules removed, new rules in place + +需要缩小范围时,优先使用 StarryOS case 选择参数运行 system 组: + +```bash +cargo xtask starry test qemu --arch riscv64 -c qemu-smp1/system ``` -## DHCP 测试 +## `ax-net` 单元测试矩阵 + +### 控制面与路由 + +| 模块 | 关键测试点 | +| --- | --- | +| `InterfaceId` / `DeviceBinding` | Linux ifindex 映射、无绑定/绑定接口读写、无效接口拒绝 | +| `RouteTable` | 添加、删除、替换规则;最长前缀;低 metric 优先;同 metric 插入顺序稳定 | +| `select_route_if()` | 接口 `UP` 过滤、`SO_BINDTODEVICE` 过滤、无路由返回错误 | +| `select_route_for_source()` | 多宿主场景下源地址和出接口一致 | +| DNS registry | DHCP/static/fallback 排序、去重、不可路由 DNS server 过滤 | + +专项路由用例: + +```text +longest prefix: + 10.0.0.0/8 -> dev0 metric=100 + 10.0.2.0/24 -> dev1 metric=200 + query 10.0.2.15 => dev1 -DHCP 逻辑建议覆盖: +metric: + 0.0.0.0/0 -> dev0 metric=100 + 0.0.0.0/0 -> dev1 metric=50 + query 8.8.8.8 => dev1 +source route: + eth0 source=10.0.2.15 + eth1 source=192.168.1.10 + packet source=192.168.1.10 => eth1 + +replace interface rules: + replace IPv4 rules for interface=2 + old connected/default routes are removed + new connected/default routes are installed +``` + +### 设备 dataplane + +| 模块 | 关键测试点 | +| --- | --- | +| `BoundedPacketQueue` | 容量上限、满队列 drop、长度计数一致、wait/wake 行为 | +| `QueuedPacket` | 超 MTU 拒绝、inline buffer 无每包堆分配 | +| `Router::poll()` | 共享 RX queue drain、ingress `InterfaceId` 保留、RX buffer 满时停止 | +| `Router::dispatch()` | 普通 unicast route lookup、limited broadcast 多接口发送、TX queue 满时 drop/warn | +| loopback | TX 直接注入 `rx_buffer`,不经过设备队列,并在注入前执行 TCP SYN snoop | +| waker | 全局设备 waker 与绑定接口 waker 分离 | + +需要重点验证 loopback 的两个性质: + +```text +loopback fast path: + socket send -> smoltcp tx_buffer + Router::dispatch sees LOOPBACK route + inject_loopback_rx_direct writes Router.rx_buffer + next poll consumes packet in same protocol core + +no queue allocation: + loopback packet does not enter shared RX queue + no Vec/to_vec/Box allocation is required for the loopback hop ``` -Test: Discovering → Offer → Requesting transition - 创建 DhcpState(phase=Discovering) - 构造 DHCPOFFER packet (unicast yiaddr, correct xid, correct mac) - process_packet → phase=Requesting, offered_address=yiaddr - 验证未产生 DhcpEvent - -Test: Requesting → ACK → Bound transition - 创建 DhcpState(phase=Requesting, offered_address=addr) - 构造 DHCPACK (subnet_mask 255.255.255.0, yiaddr=addr, router=x, dns=[y]) - process_packet → phase=Bound, 返回 DhcpEvent::Configured - -Test: NAK → reset - 创建 DhcpState(phase=Bound, address=some_addr) - process_packet(DHCPNAK) → phase=Discovering, 返回 DhcpEvent::Deconfigured - -Test: packet filtering - DhcpState for eth0 (interface_id=2) 忽略 interface_id=3 的包 - 错误 xid 或 mac 的包被忽略 + +### Socket 层 + +| 模块 | 关键测试点 | +| --- | --- | +| `SocketSetWrapper` | socket add/remove、UDP bind side table、wildcard/specific 地址冲突 | +| `ListenTable` | backlog、per-address listen、wildcard 冲突、SYN queue、accept wake | +| `GeneralOptions` | nonblocking、send/recv timeout、`SO_REUSEADDR`、`SO_BINDTODEVICE` | +| `TcpSocket` | bind/connect/listen/accept/send/recv/shutdown、`TCP_INFO`、orphan 转移 | +| `UdpSocket` | connected/unconnected send、`MSG_MORE` corking、truncation、`SO_REUSEADDR` | +| `RawSocket` | ICMP echo loopback reply、TTL、IP version 校验、`MSG_PEEK`、deferred RX wire-packet 格式 | +| Unix socket | pathname/abstract namespace、stream/datagram、cmsg、shutdown | +| vsock | bind/listen/connect/accept/send/recv、无设备初始化语义 | + +TCP listen 建议覆盖: + +```text +per-address listen: + listen(127.0.0.1:8080) + listen(10.0.2.15:8080) + both succeed + listen(0.0.0.0:8080) conflicts with either specific listener + +accept queue: + incoming SYN creates pending socket + established pending socket is returned by accept() + closed pending socket is skipped and removed + queue overflow drops new SYN without corrupting existing entries ``` -## TCP 测试 +UDP bind 建议覆盖: + +```text +wildcard conflict: + bind(0.0.0.0:1234) + bind(10.0.2.15:1234) => AddressInUse unless reuse policy allows it + +device binding: + bind(10.0.2.15:1234) derives eth0 binding + send uses route allowed by that binding + waker registration ignores unrelated interfaces +``` +### DHCP 与动态状态 + +| 模块 | 关键测试点 | +| --- | --- | +| DHCP client | Discovering、Requesting、Bound、NAK reset、xid/mac/interface 过滤 | +| DHCP commit | smoltcp address list、interface snapshot、DNS、route table 一致更新 | +| DHCP server | Discover -> Offer、Request -> Ack、客户端 MAC/xid/interface 过滤、租约覆盖 | +| dynamic device | 分配新 `InterfaceId`、写入 registry、安装路由、启动 worker、唤醒 poll | + +DHCP 状态机用例: + +```text +Discovering -> Offer: + DhcpState phase=Discovering + receive DHCPOFFER with correct xid/mac/interface + phase=Requesting + no configured event yet + +Requesting -> ACK: + receive DHCPACK with yiaddr/subnet/router/dns + phase=Bound + emit Configured event + commit updates interface IPv4, DNS and routes + +NAK: + Bound address exists + receive DHCPNAK + phase=Discovering + emit Deconfigured event ``` -Test: SYN pre-create - ListenTable.listen(port=8080, backlog=10) - 调用 incoming_tcp_packet(SYN, dst_port=8080) - → SocketSet 中有新 socket,ListenTableEntryInner.syn_queue.len() == 1 - -Test: SYN queue overflow - backlog=2, 压入 2 个 SYN, 第 3 个 SYN → 丢弃 (warn) - -Test: accept existing connection - 压入 2 个 PendingTcp, 第一个 ESTABLISHED, 第二个 SYN_RECEIVED - accept() → 返回第一个 - -Test: accept skip closed - 第一个 CLOSED without data, 第二个 ESTABLISHED - accept() → 跳过第一个(移除), 返回第二个 - -Test: bind_device - TcpSocket::new().bind_device(nonexistent_id) → NoSuchDevice - TcpSocket::new().bind_device(valid_id) → Ok - -Test: SO_REUSEADDR - UDP: 设置 SO_REUSEADDR 后 UdpSocket::bind() 跳过 SocketSetWrapper 的 UDP side table - TCP: 当前 TCP_BOUND_PORTS 仍做 wildcard/specific 地址冲突检查,不能简单断言两个 TCP socket 设 SO_REUSEADDR 后一定可 bind 同一端口 - -Test: per-address listen - listen(127.0.0.1:8080) 与 listen(10.0.2.15:8080) 可以共存 - listen(0.0.0.0:8080) 与任一具体地址 listener 冲突 - SYN 目的地址只进入匹配 listener 的 syn_queue + +### 生命周期与清理 + +| 模块 | 关键测试点 | +| --- | --- | +| orphan reaper | `Closed` 立即回收、TIME_WAIT/FIN teardown 超时回收、overflow 只回收 Closed | +| socket drop | TCP orphan 化后仍允许 FIN/TIME_WAIT 推进 | +| poll worker | socket drop/request_poll 后 net-poll worker 被唤醒 | +| DNS socket guard | 查询结束后 socket 被释放 | + +orphan 测试应避免强制清理仍在 teardown 的 TCP socket。超过 orphan 上限时,优先回收 `Closed`;仍处于 TIME_WAIT、FIN_WAIT、LAST_ACK、CLOSING 的 socket 应保留到超时或自然关闭。 + +## StarryOS 系统测试矩阵 + +### Socket dataplane + +重点覆盖: + +- TCP loopback connect/send/recv/close。 +- UDP send/recv、connected UDP、非阻塞错误。 +- raw ICMP echo 路径。 +- poll/select/epoll readiness。 +- send 后及时唤醒 waiter,避免依赖应用线程同步 poll。 + +相关测试: + +| 测试 | 覆盖点 | +| --- | --- | +| `syscall-test-socket-dataplane` | TCP/UDP/raw socket dataplane 基础行为 | +| `bugfix-bug-tcp-send-no-epoll-notify` | TCP send 后 epoll waiter 唤醒 | + +### ioctl、netlink 与 procfs + +重点覆盖: + +- `SIOCGIFCONF` 返回非零接口列表。 +- `SIOCGIFINDEX` 与 netlink `RTM_GETLINK` 的 ifindex 一致。 +- `SIOCGIFADDR`、`SIOCGIFNETMASK`、`SIOCGIFBRDADDR` 与 `ax-net` 接口快照一致。 +- `RTM_GETADDR` 至少返回 loopback IPv4。 +- `/proc/net/arp` 行格式正确,device 字段来自真实接口名。 + +相关测试: + +| 测试 | 覆盖点 | +| --- | --- | +| `bugfix-bug-netlink-getlink` | `RTM_GETLINK`、`SIOCGIFTXQLEN`、link 属性 | +| `bugfix-bug-netlink-getaddr` | `RTM_GETADDR`、loopback address、link/address dump | +| `syscall-test-netlink-recvmsg` | netlink recvmsg 基础语义 | +| `bugfix-bug-proc-net-arp` | `/proc/net/arp` 格式与内容 | + +### AF_PACKET + +重点覆盖: + +- `socket(AF_PACKET, SOCK_DGRAM, ...)` 创建和权限语义。 +- `bind(sockaddr_ll)` 按 ifindex 绑定接口。 +- `getsockname()` 返回匹配的 `sockaddr_ll`。 +- packet socket ioctl 返回 ifindex、flags 和 MAC。 +- 模拟 gateway ARP reply 工作,loopback 或未知 peer 不产生错误格式响应。 + +相关测试: + +| 测试 | 覆盖点 | +| --- | --- | +| `bugfix-bug-packet-arping` | AF_PACKET bind、SIOCGIFINDEX、RTM_GETLINK 一致性、模拟 ARP reply | + +### Namespace 可见性 + +重点覆盖: + +- root network namespace 能看到 loopback 和 Ethernet。 +- 非 root namespace 只暴露 loopback 视图。 +- StarryOS 可见性过滤不改变 `ax-net` 全局 route table。 +- `AF_PACKET` 创建和绑定遵守 root namespace 限制。 + +## 回归场景 + +### 多接口路由 + +验证内容: + +- 多个 Ethernet 接口各自有独立 IPv4、metric 和默认路由。 +- 目的地址命中直连路由时优先使用直连接口。 +- 多个默认路由按 metric 选择。 +- `SO_BINDTODEVICE` 或绑定具体本地地址后,只使用匹配接口。 +- smoltcp 已选源地址后,Router dispatch 不从错误接口发送。 + +### Loopback + +验证内容: + +- TCP loopback connect 可以在同一协议核心内推进 SYN。 +- UDP loopback send/recv 不经过外部设备 worker。 +- raw ICMP echo loopback reply 使用正确 wire packet 格式。 +- loopback 路径不会因为共享 RX queue 满而丢包;它受 `Router.rx_buffer` 容量约束。 + +### Poll 与唤醒 + +验证内容: + +- socket 热路径只请求 poll,不同步执行 smoltcp poll。 +- RX worker 入队后唤醒 net-poll worker。 +- TX worker 队列 drain 后不阻塞协议核心。 +- accept queue、UDP recv、TCP send/recv 都能唤醒对应 waker。 +- `poll_at()` 定时器可推进 TCP retransmit、TIME_WAIT、DHCP 等事件。 + +### DNS 与 DHCP + +验证内容: + +- DHCP ACK 后接口地址、默认路由和 DNS 同步可见。 +- DHCP NAK 后旧地址、旧路由和 DHCP DNS 被清理。 +- 静态 DNS、DHCP DNS、fallback DNS 按 metric 和来源去重排序。 +- 不可路由 DNS server 被跳过,查询尝试下一个 server。 + +### ABI 兼容 + +验证内容: + +- `SO_BINDTODEVICE` set/get 往返正确。 +- `SIOCGIFINDEX`、`AF_PACKET sockaddr_ll.sll_ifindex`、netlink ifindex 使用同一 `InterfaceId`。 +- `/proc/net/arp` 不暴露固定 QEMU gateway stub,也不写死 `eth0`。 +- `TCP_INFO`、`SO_TYPE`、`FIONREAD` 从真实 socket 状态返回。 + +## 调试指南 + +### QEMU 只显示 `STARRY_GROUPED_TEST_FAILED` + +`cargo xtask starry test qemu` 的汇总输出可能只显示匹配到失败模式。定位时应查更早的 test binary 输出: + +```bash +rg -n "STARRY_GROUPED_TEST_FAILED|FAIL:|panic|assert|test-socket|bugfix-bug" target -g "*.log" ``` -## 已知 Debug 定位指南 +排查顺序: -### "no route to destination" +1. 找到第一个打印 `FAIL:` 的 test binary。 +2. 确认是否是网络 testcase,还是其它系统测试间接受网络超时影响。 +3. 对照该 testcase 的源码,确认失败发生在 syscall 返回值、超时、内容不匹配还是权限语义。 +4. 如果 riscv64 和 loongarch64 都失败,优先怀疑协议栈/ABI 逻辑;如果只在单架构失败,再检查原子、调度和定时器。 -可能原因: +### `no route to destination` -1. DHCP 未完成 — 检查 `dhcp_configured()` 返回值。 -2. 默认路由未提交 — 检查 `default_routes()` 是否非空。 -3. 接口未 UP — 检查 `interface_by_id(id).flags` 包含 `UP`。 -4. RX worker 沉睡 — `device.rx_wake` 未被通知。 -5. 设备 TX/RX queue 满 — 检查是否有 worker 未运行或 driver 长时间阻塞。 +常见原因: -检查方法: +- DHCP 未完成,接口没有 IPv4。 +- default route 没有提交。 +- 接口 flags 不包含 `UP`。 +- socket 被 `SO_BINDTODEVICE` 限制到不匹配接口。 +- smoltcp 选择的源地址和 route table 中的接口源地址不一致。 + +建议打印: ```rust -// 在出现错误前插入 -info!("routes: {:?}", ax_net::default_routes()); info!("interfaces: {:?}", ax_net::interfaces()); -info!("arp entries: {:?}", ax_net::arp_entries()); +info!("routes: {:?}", ax_net::default_routes()); +info!("dns: {:?}", ax_net::dns_servers()); ``` -### "address already in use" (UDP) +### `address already in use` -1. 检查是否有其他 socket 绑定了同一 `(addr, port)`。 -2. 检查 wildcard `(0.0.0.0, port)` 是否已存在。 -3. 检查 `SO_REUSEADDR` 是否正确设置。 +排查方向: + +- 是否已有 wildcard bind 占用同一端口。 +- 是否已有具体地址 bind 与新 bind 冲突。 +- TCP listen 是否被 `ListenTable` 的 wildcard/specific 规则拒绝。 +- UDP 是否正确设置 `SO_REUSEADDR`,以及该路径是否应跳过 side table。 +- 绑定具体本地地址时,该地址是否属于当前接口 registry。 ### DHCP 超时 -1. `DHCP_BOOTSTRAP_ATTEMPTS = 200`,每次 interval 10ms → 总超时 ≈ 2 秒。 -2. 检查 DHCP server 是否可达。 -3. 检查 `process_packet()` 中的 xid/mac 校验是否误过滤。 +排查方向: + +- DHCP packet 的 ingress `InterfaceId` 是否匹配对应 `DhcpState`。 +- xid、client MAC、message type 是否被过滤。 +- RX worker 是否把 DHCP reply 入队到共享 RX queue。 +- net-poll worker 是否被唤醒。 +- DHCP server 是否在同一二层网络可达。 + +### AF_PACKET / netlink 不一致 + +排查方向: + +- `SIOCGIFINDEX` 是否来自 `InterfaceId::to_linux_ifindex()`。 +- `RTM_GETLINK` 是否遍历同一份 `ax_net::interfaces()`。 +- `sockaddr_ll.sll_ifindex` 是否能通过 `InterfaceId::from_linux_ifindex()` 反查接口。 +- namespace 可见性过滤是否导致接口在某条路径可见、另一条路径不可见。 + +### `/proc/net/arp` 异常 -### ARP pending 队列溢出 +排查方向: -`ETHERNET_MAX_PENDING_PACKETS = 128`,当大量并发连接的目标 MAC 都未解析时触发。增加 `ETHERNET_MAX_PENDING_PACKETS` 或确保 ARP reply 在预期时间内到达。 +- `ax_net::arp_entries()` 是否有对应 entry。 +- ARP entry 的 interface_id 是否能映射回接口名。 +- procfs 输出是否仍残留固定 gateway 或固定 `eth0` 字段。 +- ARP pending queue 是否已满,导致解析请求被丢弃。 ### net-poll worker 不推进 -1. 检查 `NET_POLL_REQUESTED` atomic 标志。 -2. 检查 `NET_POLL_WAKE` 是否有竞争。 -3. 检查 `poll_until_idle()` 中的 CAS 锁是否一直失败;正常情况下只有 `net-poll` worker 会持续驱动协议栈,socket 热路径只负责 `request_poll()`。 -4. 检查 `next_poll_delay()` 返回值 — 如果 smoltcp `poll_at()` 返回 `None`,idle poll interval 为 100ms。 +排查方向: + +- `request_poll()` 是否被调用。 +- `NET_POLL_REQUESTED` 是否被置位。 +- `NET_POLL_WAKE` 是否唤醒 worker。 +- `POLLING_INTERFACES` CAS 是否长时间被占用。 +- `next_poll_delay()` 是否返回过长 idle interval。 +- 设备 worker 是否卡在 driver `send()` / `receive()` 持锁路径。 + +## 已知限制 + +### 协议与特性范围 + +- IPv6 route、NDP、MLD 和完整 IPv6 socket 语义未作为主路径完善。 +- IGMP/按接口 multicast membership 不完整。 +- DHCP lease renew/rebind、租约过期回收和地址冲突检测仍需继续补齐。 +- DNS 不包含 split DNS、search domain 和完整 `/etc/resolv.conf` 语义。 +- `SO_REUSEPORT`、完整 Linux TCP option 集合和高级拥塞控制不在当前范围。 + +### 架构限制 + +- 协议核心仍是单 smoltcp `Interface + SocketSet`,TCP/UDP 状态机本身不多核并行。 +- 多设备 dataplane 通过 worker 和有界队列解耦,但不是 RSS/NAPI 多队列模型。 +- loopback 已有直接注入快路径,但普通设备 RX/TX 仍存在必要的 packet copy。 +- 尚未实现端到端 zero-copy;这需要 rd-net buffer ownership、packet pool 和 smoltcp token 共同改造。 + +### 系统集成限制 + +- StarryOS network namespace 当前主要是可见性过滤,不是完整 per-namespace network stack。 +- 动态 link down/up、接口热插拔、队列重建和 socket 错误传播仍需完善。 +- 真实硬件 IRQ/DMA 行为需要板级验证,QEMU 不能覆盖全部 timing 和 cache 一致性问题。 + +## 变更验收清单 + +提交网络相关变更前,按影响面检查: + +- 修改 `net/ax-net/src` 逻辑:`cargo fmt`、`cargo xtask clippy --package ax-net`、相关单测。 +- 修改 socket readiness 或 poll:补 StarryOS poll/epoll 或 socket dataplane QEMU 验证。 +- 修改接口、路由、DNS、ARP:补 ioctl、netlink、procfs、multi-interface route 验证。 +- 修改 AF_PACKET:补 `bugfix-bug-packet-arping` 相关路径。 +- 修改 runtime/driver/OOB RX:至少验证 riscv64 QEMU,真实设备改动还应补板级验证记录。 +- 修改文档:检查标题层级、代码块闭合和过期术语。 diff --git a/p.md b/p.md new file mode 100644 index 0000000000..dd9d99611c --- /dev/null +++ b/p.md @@ -0,0 +1,552 @@ +# `ax-net` 多网口支持与性能优化方案 + +## 1. 背景 + +`net/ax-net` 已经完成统一网络栈收敛,核心能力包括 TCP、UDP、raw socket、Unix domain socket、可选 vsock、DNS、DHCP、ARP、poll/waker 和 `rd-net` 设备适配。为了进一步支持多网口、多路由和更稳定的数据面性能,需要在现有单 smoltcp `Interface` 架构上补齐控制面、多设备数据面和 socket 绑定语义。 + +当前需要解决的主要问题: + +- 初始化流程偏向第一个 NIC,接口语义容易隐含为 `eth0`。 +- `NetworkConfig` 不能完整表达每个接口的地址、网关、DNS、DHCP、metric。 +- DHCP、DNS、路由更新等状态偏单接口模型。 +- StarryOS ioctl、AF_PACKET、`/proc/net/arp` 等路径容易出现固定 `eth0` 假设。 +- 旧设备绑定如果使用 `u32 device_mask`,接口集合语义不清晰,扩展性差。 +- socket 热路径不应同步推进完整协议栈,应只请求专用 poll worker 工作。 +- 设备收发、smoltcp poll、`SocketSet` 访问需要清晰锁边界,避免设备路径和协议核心互相阻塞。 + +本方案目标: + +- 注册并管理所有可用 NIC,形成 `lo`、`eth0`、`eth1` 等接口。 +- 支持每个接口独立静态 IPv4 或 DHCP 配置。 +- 支持多条直连路由、默认路由、metric、源地址选择和下一跳选择。 +- 提供统一接口 registry API,供 ArceOS、StarryOS、Axvisor 和诊断工具使用。 +- 保持单 `smoltcp::iface::Interface + SocketSet` 架构,避免 multi-smoltcp domain 带来的 socket 绑定、动态路由和 wildcard listen 聚合复杂度。 +- 通过设备队列解耦、异步 poll、控制面快照、有界队列和减少热路径分配提升性能。 +- 删除旧单网口假设和旧 `u32 device_mask` 兼容路径,保持代码简洁。 + +非目标: + +- 不拆分为多个 smoltcp 实例。 +- 不实现完整 IPv6。 +- 不实现 Linux net namespace 的完整隔离。 +- 不承诺固定性能提升比例,性能效果以 benchmark 为准。 +- 不为了过渡兼容保留复杂 wrapper 或多套状态。 + +## 2. 修改方案 + +### 2.1 总体架构 + +方案保持 **单协议栈核心 + 多设备 Router** 模型: + +```text +public API + -> tcp / udp / raw / unix / vsock + -> interfaces() / default_routes() / dns_query() + +control plane + -> interface registry + -> shared RouteTable + -> DNS registry + -> DeviceBinding + -> network state transaction commit + +single protocol core + -> Service + -> smoltcp Interface + -> Router as smoltcp Device + -> DHCP states + -> DHCP server + -> orphan reaper + -> SocketSetWrapper + -> ListenTable + +multi-device data plane + -> Router.rx_buffer / Router.tx_buffer + -> shared bounded RX queue + -> per-device bounded TX queues + +device workers + -> per-device RX worker + -> per-device TX worker + -> OOB RX poll task for dedicated-poll devices +``` + +核心原则: + +- `Router` 是 smoltcp `Device` 适配层,由 `Service` 独占,不作为全局对象被外部直接调用。 +- 设备 worker 不进入 smoltcp `Interface` 或 `SocketSet`。 +- 设备 worker 与协议核心之间通过有界队列传递 packet。 +- `SocketSetWrapper` 仍然是全局 socket 集合,但 socket 热路径只请求 poll,不同步推进完整网络栈。 +- 控制面状态使用短锁和快照;数据面 poll 只在必要时进入 smoltcp 临界区。 +- 不需要额外拆出 `ServiceCore` / `ServiceHandle` 层级,除非后续出现明确的所有权或锁边界收益。 + +### 2.2 配置模型 + +`NetworkConfig` 改为接口级配置: + +```rust +pub struct NetworkConfig { + pub interfaces: Vec, + pub default_dns_servers: Vec, +} + +pub struct InterfaceConfig { + pub name: String, + pub match_by: InterfaceMatcher, + pub static_ip: Option, + pub dhcp: bool, + pub metric: u32, + pub dns_servers: Vec, +} + +pub enum InterfaceMatcher { + ByOrder(usize), + ByMac(EthernetAddress), + ByDriverName(String), +} +``` + +配置语义: + +- 未显式配置的 Ethernet 接口默认启用 DHCP。 +- `dhcp = true` 与 `static_ip.is_some()` 互斥,配置冲突直接 panic。 +- 显式配置必须能匹配到唯一设备,否则初始化失败。 +- 接口名必须唯一。 +- `lo` 由 `ax-net` 固定创建,不允许外部配置覆盖。 +- 删除旧全局 `AX_IP`、`AX_GW`、`AX_PREFIX_LEN`、`AX_DNS` 单网口语义。 +- `ax-runtime` 只负责把结构化接口配置传入 `ax_net::init_network()`。 + +### 2.3 接口与控制面模型 + +新增统一接口标识和对外快照: + +```rust +pub struct InterfaceId(u32); + +pub struct InterfaceInfo { + pub id: InterfaceId, + pub name: String, + pub kind: InterfaceKind, + pub mac: Option, + pub ipv4: Option, + pub mtu: usize, + pub flags: InterfaceFlags, + pub metric: u32, +} +``` + +`InterfaceId` 是 `ax-net` 内部和对外统一的接口标识。StarryOS 的 Linux `ifindex` 直接由 `InterfaceId` 的数值映射得到,不再维护单独的 `IfIndex`。 + +控制面建议使用一个 `NetControl` 聚合接口 registry、DNS entries 和共享路由表: + +```rust +struct ControlState { + interfaces: Vec, + dns: Vec, +} + +struct NetControl { + state: RwLock, + routes: SharedRouteTable, +} +``` + +设计要求: + +- 查询 API 返回只读快照,不暴露内部锁。 +- 接口、DNS、路由更新需要事务化。 +- 对外查询使用 `default_routes()`,不提供含糊的 `routes()` API。 +- 控制面不直接收发 packet,也不推进 smoltcp poll。 + +### 2.4 路由模型 + +路由规则: + +```rust +pub struct Rule { + pub filter: IpCidr, + pub via: Option, + pub dev: usize, + pub interface_id: InterfaceId, + pub src: IpAddress, + pub metric: u32, + pub order: u64, +} + +pub struct RouteDecision { + pub dev: usize, + pub interface_id: InterfaceId, + pub source: IpAddress, + pub next_hop: IpAddress, + pub metric: u32, +} +``` + +查找规则: + +- 先按最长前缀匹配。 +- 同前缀按 metric 从小到大。 +- metric 相同按插入顺序稳定选择。 +- 接口 down 时跳过该接口路由。 +- 查不到路由返回错误,不 panic。 +- `select_route_if()` 用于带接口可用性过滤的普通路由查询。 +- `select_route_with_binding()` 用于 `SO_BINDTODEVICE` 或本地地址推导出的接口约束。 +- `select_route_for_source(dst, source)` 用于 TX dispatch,保证 smoltcp 已选择的源地址和出接口一致。 + +调用方迁移: + +- TCP `connect()` 使用路由选择本地地址和出接口。 +- UDP `connect()` / `sendto()` 按目标地址和 `DeviceBinding` 查 route,不强制长期缓存完整 `RouteDecision`,避免 DHCP/route 更新后缓存失效。 +- raw socket 查不到路由返回错误。 +- Router TX dispatch 根据 IP 包的源地址和目标地址选择 TX queue。 + +### 2.5 设备队列与 smoltcp Device 适配 + +设备并行优化不能破坏 smoltcp `Device` token 模型。正确边界是: + +```text +device worker + -> RouterQueues + +Router as smoltcp Device + -> receive() consumes Router.rx_buffer + -> transmit() returns TxToken + -> TxToken::consume() writes Router.tx_buffer + +Service::poll() + -> Router::poll() drains worker RX queue into rx_buffer + -> iface.poll(now, &mut router, &mut sockets) + -> Router::dispatch() routes tx_buffer to loopback or per-device TX queue +``` + +建议结构: + +```rust +struct Router { + rx_buffer: PacketBuffer, + tx_buffer: PacketBuffer, + queues: Arc, + devices: Vec>, + table: SharedRouteTable, +} + +struct RouterQueues { + rx: Arc>, +} + +struct DeviceHandle { + interface_id: InterfaceId, + name: String, + inner: Arc>>, + rx_queue: Arc>, + tx_queue: Arc>, + rx_wake: Arc, + tx_wake: Arc, +} +``` + +设计要求: + +- 所有真实设备共享一个 RX queue。 +- 每个真实设备有独立 TX queue。 +- 设备 worker 不访问 `Router` 本体。 +- `Router::poll()` 从 RX queue drain 到 smoltcp `rx_buffer`,并保留 ingress `InterfaceId`。 +- `Router::dispatch()` 从 smoltcp `tx_buffer` 取包,按 route 分发到 loopback 或 per-device TX queue。 +- 不需要单独的 `RouteSnapshot`;控制面和 Router 可以共享同一个 `SharedRouteTable`。 +- 不需要把所有 TX queue 放在 `RouterQueues.tx: Vec<_>` 中;TX queue 放进 `DeviceHandle` 更直接。 + +### 2.6 有界队列和 packet buffer + +队列要求: + +- RX/TX 队列必须有界。 +- RX 满时丢包并记录 warning/统计。 +- TX 满时按 socket/packet 语义返回 `WouldBlock`、重试或丢包。 +- 不使用无界 `SegQueue` 作为网络热路径队列。 +- 第一版可以使用 copy queue,但必须避免无界分配。 + +建议实现: + +```rust +struct BoundedPacketQueue { + inner: Mutex>, + capacity: usize, + len: AtomicUsize, +} + +struct QueuedPacket { + bytes: [u8; STANDARD_MTU], + len: usize, +} +``` + +说明: + +- inline `QueuedPacket` 可以避免每包 `Box<[u8]>` / `Vec::to_vec()` 分配。 +- 不强制引入 `PacketBufPool`;除非后续需要更复杂的 buffer ownership。 +- 端到端 zero-copy 不属于第一阶段目标,后续需要配合 `rd-net` buffer ownership 和 smoltcp token 适配。 + +### 2.7 设备 worker 与 poll 唤醒 + +设备 worker 不直接进入 smoltcp: + +```text +RX IRQ / RX worker + -> receive from device + -> push RxPacket into bounded rx queue + -> request_poll() + +net-poll worker + -> take Service lock + -> take SocketSet lock + -> Service::poll() + +TX worker + -> pop TxPacket from per-device tx queue + -> send through device +``` + +要求: + +- 每个非 loopback 设备可以启动一个 RX worker 和一个 TX worker。 +- IRQ handler 只唤醒对应 RX worker 或设置 queue event,不做重工作。 +- 无设备 IRQ 时,由平台轮询任务或 OOB RX poll task 唤醒设备。 +- RX worker 不忙等。 +- 设备 waker 唤醒对应 RX worker。 +- socket waker 根据 `DeviceBinding` 只注册到允许的设备路径。 + +### 2.8 net-poll worker + +`poll_interfaces()` 不再作为 socket 热路径同步入口。新的主路径是 `net-poll` worker: + +```text +socket send/connect/drop + -> update SocketSet state + -> request_poll() + +device RX + -> enqueue RxPacket + -> request_poll() + +timer + -> request_poll() + +net-poll worker + -> wait wake or deadline + -> poll_until_idle() +``` + +建议 API: + +```rust +pub fn poll_interfaces() { + request_poll(); +} + +pub fn request_poll() { + NET_POLL_REQUESTED.store(true, Ordering::Release); + NET_POLL_WAKE.notify_one(true); +} +``` + +要求: + +- `poll_interfaces()` 保留为 public trigger/debug API,但不再同步执行 `Service::poll()`。 +- `net_poll_worker()` 独占调用 `poll_until_idle()`。 +- `poll_until_idle()` 使用全局原子标志防重入。 +- `poll_until_idle()` 在有工作时批量 poll,不在每次成功 poll 后主动 `yield_now()`。 +- socket send/recv/connect/accept/drop 热路径只 `request_poll()`。 + +### 2.9 控制面状态与事务更新 + +DHCP ACK 等运行期更新不能暴露半更新状态。 + +建议更新对象: + +```rust +struct NetworkStateUpdate { + interface_id: InterfaceId, + dev: usize, + metric: u32, + old_ipv4: Option, + ipv4: Option, + gateway: Option, + dns_source: DnsSource, + dns_servers: Vec, +} +``` + +提交规则: + +- DHCP 解析在 `Service::poll()` 中完成。 +- DHCP ACK/NAK 先生成 `NetworkStateUpdate`。 +- smoltcp `Interface` 的 IP 地址、接口 registry、DNS entries、route table 必须作为一个逻辑事务更新。 +- 对外查询 API 要么看到旧状态,要么看到新状态,不暴露半更新状态。 +- 如果控制面更新逻辑变重,再进一步缩窄写锁范围。 + +### 2.10 Socket 绑定索引与快路径 + +目标: + +- TCP/UDP bind 冲突检查不在热路径扫描整个 `SocketSet`。 +- wildcard bind 与具体地址 bind 的冲突语义统一。 +- 普通 socket 使用 `DeviceBinding { bound_if: Option }`,不再使用 `u32 device_mask`。 + +建议: + +```rust +pub struct DeviceBinding { + pub bound_if: Option, +} +``` + +语义: + +- `None`:未绑定接口,按 route decision 或 wildcard 语义工作。 +- `Some(id)`:由 `SO_BINDTODEVICE`、绑定具体本地地址或 AF_PACKET ifindex 得到。 + +要求: + +- `SO_BINDTODEVICE` 存入通用 socket options。 +- 绑定具体本地地址时,通过控制面反查接口并设置 `DeviceBinding`。 +- TCP 使用独立 bind/listen 侧表维护 wildcard/specific 地址冲突。 +- UDP 使用 `SocketSetWrapper` 内部 side table 维护 bind 冲突。 +- `ListenTable` 支持 per-address listen 和 accept waker。 +- 不承诺完整 `reuseport`;当前重点是 `SO_REUSEADDR` 和 bind/listen 冲突语义。 +- 不必新增统一 `SocketRegistry` 类型,避免重复维护 TCP/UDP 生命周期。 + +### 2.11 DNS 与 DHCP + +DHCP: + +- 每个 Ethernet 接口独立 `DhcpState`。 +- DHCP packet 根据 ingress `InterfaceId` 分发。 +- DHCP ACK 生成 `NetworkStateUpdate`。 +- DHCP NAK 或失败只影响对应接口。 +- DHCP bootstrap 不应要求所有 DHCP 接口成功,避免断开的网卡阻塞系统启动。 + +DNS: + +- DHCP DNS、接口级静态 DNS、全局 fallback DNS 都记录为 `DnsServerEntry`。 +- `DnsServerEntry` 保留 server、interface_id、metric 和 source。 +- `dns_servers()` 返回按 metric 排序、去重后的地址列表。 +- `dns_query_timeout()` 选择 DNS server 前检查可路由性,不可路由则尝试下一个。 +- 不实现 DNS cache、split DNS、`/etc/resolv.conf`。 + +### 2.12 广播、组播与 loopback + +广播: + +- limited broadcast 可发送到所有非 loopback Ethernet 设备。 +- 绑定具体接口时,应尽量限制到该接口。 +- 子网广播按接口 IPv4/prefix 选择接口,后续补齐。 + +组播: + +- IPv4 multicast 至少按绑定接口或 route decision 选择出接口。 +- 未绑定接口的 multicast 使用默认 route 或接口集合策略。 +- IGMP/MLD membership 不在本轮范围。 + +Loopback: + +- `LoopbackDevice` 只作为 `lo` 接口占位。 +- loopback 不启动 RX/TX worker。 +- 普通 smoltcp TX 选中 loopback 时,`Router::dispatch()` 应直接注入 `Router.rx_buffer`,不走共享 RX queue 和设备 worker。 +- loopback 注入前应执行 TCP SYN snoop,保证回环 listen/accept 能在同一 poll 周期推进。 + +### 2.13 对外 API + +正式 API: + +```rust +pub fn init_network(net_devs: EthernetDeviceList, config: NetworkConfig); +pub fn poll_interfaces(); +pub fn request_poll(); + +pub fn interfaces() -> Vec; +pub fn interface_by_name(name: &str) -> Option; +pub fn interface_by_id(id: InterfaceId) -> Option; +pub fn ipv4_config(name: &str) -> Option; +pub fn default_routes() -> Vec; +pub fn arp_entries() -> Vec; + +pub fn dns_query(name: &str) -> AxResult>; +pub fn dns_query_timeout(name: &str, timeout: Duration) -> AxResult>; +pub fn dns_servers() -> Vec; +``` + +说明: + +- `poll_interfaces()` 是轻量触发入口,不作为 socket 热路径同步 poll。 +- `eth0_ipv4_config()` 这类 convenience helper 可以保留,但新代码应优先使用 `ipv4_config(name)` 和接口 registry API。 +- 不提供 `u32 device_mask` 兼容转换。 + +### 2.14 StarryOS、ArceOS 与 Axvisor 接入 + +StarryOS 网络 ABI 层必须从 `lo + eth0` 固定模型迁移到 `ax-net` 接口 registry。 + +涉及方向: + +- `SIOCGIFCONF` 遍历 `ax_net::interfaces()`。 +- `SIOCGIFADDR`、`SIOCGIFBRDADDR`、`SIOCGIFNETMASK` 按接口名查询。 +- `SIOCGIFHWADDR` 返回真实 MAC 或 loopback 类型。 +- `SIOCGIFINDEX` 返回 `InterfaceInfo::id.to_linux_ifindex()`。 +- AF_PACKET bind 使用 ifindex 映射 `InterfaceId`。 +- `/proc/net/arp` 使用 `ax_net::arp_entries()`,device 字段必须是真实接口名。 +- `SO_BINDTODEVICE` 映射到 `DeviceBinding { bound_if: Some(id) }`。 +- StarryOS 不缓存第二套 IPv4/gateway/MAC 状态。 + +ArceOS: + +- `ax-runtime` 构造结构化 `NetworkConfig` 并调用 `ax_net::init_network()`。 +- `ax-api`、`ax-posix-api` 使用统一 socket API。 +- 面向应用的 API 不增加 `eth0` 假设。 + +Axvisor: + +- 复用 `ax-net` 接口 registry、route decision 和 socket API。 +- 管理面、VM 服务面通过接口级配置表达网络意图。 + +## 3. 测试方案 + +单元测试: + +- 多接口 route lookup。 +- 默认路由 metric 选择。 +- route 查不到返回错误,不 panic。 +- DHCP packet 按 `InterfaceId` 分发。 +- `NetworkStateUpdate` commit 前后状态一致。 +- bounded RX/TX queue 满时行为正确。 +- `Router` RX metadata 保留 ingress `InterfaceId`。 +- `DeviceBinding` route/waker 过滤语义正确。 +- TCP/UDP socket 绑定索引冲突检查正确。 +- loopback direct injection 能在同一 poll 周期推进 TCP SYN。 + +集成测试: + +- QEMU 单网卡 DHCP。 +- QEMU 多网卡静态配置。 +- TCP/UDP bind 到 `eth1` 地址后 route decision 选择 `eth1`。 +- StarryOS `SIOCGIFCONF` 能看到多个接口。 +- StarryOS `SIOCGIFADDR`、`SIOCGIFHWADDR`、`SIOCGIFINDEX` 可按接口名查询。 +- StarryOS AF_PACKET bind 到指定 ifindex 后返回正确 `sockaddr_ll`。 +- `/proc/net/arp` device 字段正确。 +- DNS server 来自 DHCP/静态/fallback 时查询路径不退化。 + +性能验证: + +- 对比同步 `poll_interfaces()` 与 `request_poll()` 的 socket 热路径耗时。 +- 多 NIC 同时 RX 时验证 bounded queue、公平性和丢包行为。 +- 长时间 UDP/TCP 压测验证队列不会无界增长。 +- 验证 route/interface/DNS 查询不会长时间阻塞 `net-poll` worker。 + +## 4. 遗留问题 + +- smoltcp 协议栈处理仍然是单实例串行。设备队列解耦不能让 TCP/UDP 协议处理本身多核并行。 +- Zero-copy RX/TX 需要继续改造 `rd-net` buffer ownership、packet pool 和 smoltcp token 适配。 +- 完整 DHCP lease renew/rebind、过期回收、地址冲突检测仍需后续补齐。 +- 完整 IPv6、邻居发现、IPv6 route、AAAA DNS 查询不在本轮范围。 +- IGMP/MLD、按接口 multicast membership 不在本轮范围。 +- split DNS、`/etc/resolv.conf`、net namespace resolver 策略不在本轮范围。 +- 动态 link down/up、接口热插拔、队列重建和 socket 错误传播仍需后续完善。 +- RSS、多队列网卡、per-queue poll、NAPI 类批量调度属于后续更底层 dataplane 优化。 From e6c9c0a21e4d681acf54c5e9064783bf19286dde Mon Sep 17 00:00:00 2001 From: ZCShou <72115@163.com> Date: Tue, 16 Jun 2026 13:01:37 +0800 Subject: [PATCH 27/31] docs(net): document socket option matrix, driver buffer semantics, Unix/vsock internals, and DHCP server boundaries --- docs/docs/architecture/net/api.md | 75 +++ docs/docs/architecture/net/configuration.md | 1 + docs/docs/architecture/net/devices.md | 71 ++- docs/docs/architecture/net/flows.md | 44 ++ docs/docs/architecture/net/overview.md | 2 + docs/docs/architecture/net/sockets.md | 122 ++++- docs/docs/architecture/net/testing.md | 494 +++++++++----------- 7 files changed, 527 insertions(+), 282 deletions(-) diff --git a/docs/docs/architecture/net/api.md b/docs/docs/architecture/net/api.md index 00f328cf68..ff3872ad2a 100644 --- a/docs/docs/architecture/net/api.md +++ b/docs/docs/architecture/net/api.md @@ -456,6 +456,42 @@ pub struct TcpInfo { } ``` +### Option 支持矩阵 + +`GetSocketOption` / `SetSocketOption` 是跨协议的分发格式,并不表示每个 backend 都支持所有 option。backend 返回 `supported = false` 时,统一映射为 `ENOPROTOOPT`;option 值非法时返回具体参数错误。 + +| Option | 主要实现者 | 语义 | +| --- | --- | --- | +| `ReuseAddress` | `GeneralOptions`,UDP/TCP bind 路径读取 | UDP 设置后跳过 wrapper UDP bind side table;TCP 仍受 `TCP_BOUND_PORTS` 和 `ListenTable` 的 wildcard/specific 冲突规则约束 | +| `Error` | `GeneralOptions` | 返回并清理 socket error 状态;当前主要作为 Linux ABI 兼容字段 | +| `DontRoute` | `GeneralOptions` | 保存标志位;普通发送路径仍由控制面 route decision 决定接口 | +| `SendBuffer` / `ReceiveBuffer` | `GeneralOptions`、Unix stream/datagram、vsock | 对 IP socket 主要返回配置值或默认预算;Unix/vsock 按各自 ring/queue 容量返回 | +| `SendBufferForce` | `GeneralOptions` | 兼容 `SO_SNDBUFFORCE` 风格入口,语义上等价设置发送缓冲区预算 | +| `KeepAlive` | `GeneralOptions` + TCP | 通用标志由 `GeneralOptions` 保存;TCP backend 同步到 smoltcp keep-alive 配置 | +| `SendTimeout` / `ReceiveTimeout` | `GeneralOptions` | 被 `send_poller*` / `recv_poller*` 使用,决定阻塞等待超时 | +| `PassCredentials` | Unix stream/datagram | 保存或接受 Linux `SO_PASSCRED` 语义;credentials 由 Unix transport 生成 | +| `PeerCredentials` | Unix stream/datagram | 返回 `UnixCredentials { pid, uid, gid }`,uid/gid 当前使用内核默认值 | +| `SocketType` / `SocketProtocol` / `SocketDomain` | `GeneralOptions` | 由 socket 创建时写入,用于 `getsockopt()` 返回 Linux ABI 可见值 | +| `BindToDevice` | `GeneralOptions` | 保存 `DeviceBinding`,影响 route lookup 和设备 waker 注册 | +| `NoDelay` | TCP | 映射 TCP_NODELAY 行为 | +| `MaxSegment` | TCP | 返回或设置 TCP MSS 相关兼容值,受 smoltcp 能力限制 | +| `TcpKeepIdle` / `TcpKeepInterval` / `TcpKeepCount` | TCP | 校验 Linux 兼容范围后同步到 TCP keepalive 配置 | +| `TcpUserTimeout` | TCP | 保存 Linux `TCP_USER_TIMEOUT` 兼容值 | +| `TcpInfo` | TCP | 从 smoltcp socket 状态和本地默认值合成 `TCP_INFO` snapshot | +| `Ttl` | raw / IP socket backend | raw socket 使用该值控制发送 TTL | +| `RecvErr` | `GeneralOptions` | 保存 IP_RECVERR 兼容标志;当前不提供完整 Linux error queue | +| `NonBlocking` | `GeneralOptions` | 与 `MSG_DONTWAIT` 不同,修改 socket 自身阻塞属性 | + +### TCP_INFO + +`TcpInfo` 的字段来源分三类: + +- 直接来自 smoltcp:连接状态、收发队列长度、窗口估计等。 +- 来自 `tcp.rs` 默认值:MSS、PMTU、初始 RTO、reordering 等 Linux 兼容默认字段。 +- 合成或保守值:smoltcp 未暴露的拥塞控制细节、ECN/SACK/window scale 等字段以保守方式填充。 + +因此 `TCP_INFO` 适合用于 Linux 兼容探测和调试,不应被上层当作完整 Linux TCP 栈的拥塞控制 ABI。 + ## DNS 与名称解析 DNS API 位于 [lib.rs](net/ax-net/src/lib.rs),使用控制面的 DNS registry 和 route decision。 @@ -517,6 +553,45 @@ pub trait NetTxBuffer: Send { `RdNetDriver` 是基于 `rd-net` 的标准适配实现。 +### Driver Buffer 与错误语义 + +`EthernetDriver` 把底层 NIC 的 DMA ring、virtqueue 或 `rd-net` queue 抽象为一次一个 packet 的 buffer ownership: + +```text +RX: + driver.receive() -> Box + EthernetDevice reads packet() + driver.recycle_rx_buffer(rx_buf) + +TX: + driver.alloc_tx_buffer(frame_len) + EthernetDevice fills packet_mut() + driver.transmit(tx_buf) + driver.recycle_tx_buffers() +``` + +错误类型保持小集合,便于 Router 和设备 worker 做统一策略: + +| 错误 | 语义 | +| --- | --- | +| `Again` | 暂无 RX packet、TX 暂不可用或需要稍后重试 | +| `BadState` | 设备未处于可收发状态 | +| `InvalidParam` | packet size 或参数非法 | +| `Io` | 底层设备或传输错误 | +| `NoMemory` | 驱动无法分配 buffer | +| `Unsupported` | 设备不支持该操作 | + +`NetIrqEvents` 是 IRQ summary bitmask。`RX_READY`、`RX_ERROR`、`TX_DONE` 会唤醒相关 worker;`SPURIOUS` 表示没有需要网络栈处理的事件。 + +### RdNetDriver 适配 + +`RdNetDriver` 持有 `rd_net::TxQueue`、`rd_net::RxQueue` 和少量 `pending_rx` 预取缓存。它的设计边界是: + +- RX 预取目标为 `RX_PREFETCH_TARGET = 1`,只减少一次收包路径上的驱动交互,不形成额外无界缓存。 +- TX 分配会把 frame 长度提升到 Ethernet 最小帧长 `ETH_ZLEN = 60`。 +- `rd_net::NetError::Retry` 映射为 `NetDeviceError::Again`,`NoMemory` / `NotSupported` 保留对应语义,link down 或其它底层错误映射为 `Io`。 +- `ax-net` 上层不依赖 `rd-net` 类型;其它 NIC driver 只要实现 `EthernetDriver` 即可接入。 + ### IRQ 注册 ```rust diff --git a/docs/docs/architecture/net/configuration.md b/docs/docs/architecture/net/configuration.md index 84fe3bf7f5..59ffdf2d1d 100644 --- a/docs/docs/architecture/net/configuration.md +++ b/docs/docs/architecture/net/configuration.md @@ -334,6 +334,7 @@ const TCP_INFO_DEFAULT_REORDERING: u32 = 3; | `DHCP_BOOTSTRAP_ATTEMPTS` | 200 | DHCP bootstrap 最大轮数 | | `DHCP_BOOTSTRAP_POLL_INTERVAL` | 10ms | DHCP bootstrap 每轮 sleep | | `DHCP_MAX_RETRY_SHIFT` | 4 | DHCP 指数退避上限,最大 16s | +| `DHCP_SERVER_LEASE_SECS` | 86400s | 内置 SoftAP DHCP server 返回的固定租约时间 | | `NEIGHBOR_TTL` | 300s | ARP neighbor cache TTL | | `ARP_REQUEST_RETRY` | 1s | ARP request 重试间隔 | diff --git a/docs/docs/architecture/net/devices.md b/docs/docs/architecture/net/devices.md index 05f1d12adc..ffb307373c 100644 --- a/docs/docs/architecture/net/devices.md +++ b/docs/docs/architecture/net/devices.md @@ -150,7 +150,24 @@ pub struct EthernetDevice { ### RdNetDriver -`RdNetDriver` 是 `rd-net` 设备到 `EthernetDriver` trait 的适配层。它持有 `rd_net::TxQueue` 和 `rd_net::RxQueue`,并通过少量 RX 预取降低驱动锁竞争。Router 不直接依赖 `rd-net` 类型,只依赖内部 `Device` trait。 +`RdNetDriver` 是 `rd-net` 设备到 `EthernetDriver` trait 的适配层。它持有 `rd_net::TxQueue`、`rd_net::RxQueue` 和一个很小的 `pending_rx` 预取队列。Router 不直接依赖 `rd-net` 类型,只依赖内部 `Device` trait。 + +```text +rd_net::Net + -> RdNetDriver + -> EthernetDriver trait + -> EthernetDevice + -> Router DeviceHandle +``` + +适配策略: + +- RX:`rd_net::RxQueue::receive()` 返回的 packet 被复制到 `VecRxBuffer`,放入 `pending_rx` 或直接交给 `EthernetDevice`。`RX_PREFETCH_TARGET = 1`,只预取一个 packet,避免形成新的缓存层。 +- TX:`alloc_tx_buffer(size)` 返回 `VecTxBuffer`,实际长度为 `max(size, ETH_ZLEN)`,保证 Ethernet 最小帧长 60 字节。 +- IRQ:`handle_irq()` 调用底层 irq handler 后尝试预取 RX packet,并根据结果返回 `NetIrqEvents::RX_READY`、`RX_ERROR` 或 `SPURIOUS`。 +- 错误:`rd_net::NetError::Retry` 映射为 `NetDeviceError::Again`,`NoMemory` / `NotSupported` 保留语义,link down 或其它错误映射为 `Io`。 + +这个适配层仍然是 copy-based 的。它的目标是隔离 `rd-net` ownership 模型,而不是提供端到端 zero-copy。后续如果要做 zero-copy,需要同时改造 `rd-net` buffer ownership、`EthernetDevice` frame 封装和 smoltcp token 生命周期。 ## Router as MultiDevice @@ -582,6 +599,11 @@ pub fn request_poll() { } ``` +设备 readiness 通过两类 waker 分流: + +- `NET_POLL_DEVICE_WAKER`:全局设备 waker,用于告诉 net-poll worker 有协议栈工作需要推进。 +- `register_waker(binding, waker)`:socket readiness waker,只注册到 `DeviceBinding` 允许的接口,避免绑定到 `eth1` 的 socket 被 `eth0` 的 readiness 无意义唤醒。 + 专用 net-poll worker 独占调用 `poll_until_idle()`: ```rust @@ -625,6 +647,53 @@ device RX DHCP client ACK 会通过 `NetworkStateUpdate` 更新 smoltcp address list、控制面接口快照、DNS 和 route table。DHCP server 的 Offer/Ack 使用 `Router::send_on_device(dev, next_hop, packet, timestamp)` 从指定接口发出。 +#### DHCP Client + +DHCP client 属于 `Service` 状态,每个启用 DHCP 的 Ethernet 接口对应一个 `DhcpState`。`Router::poll()` 在把 packet 放入 smoltcp RX buffer 前先做 snoop,DHCP UDP packet 会按 ingress `InterfaceId` 分发给对应 `DhcpState`: + +```text +Ethernet RX frame + -> EthernetDevice strips Ethernet/ARP + -> Router::poll(packet, ingress_if) + -> DhcpState::process_packet(ingress_if, packet) + -> optional DhcpEvent +``` + +`Configured` 事件提交以下状态: + +- smoltcp `Interface` 的 IPv4 address list。 +- `NetControl` 中的接口 IPv4/gateway snapshot。 +- DHCP DNS entries。 +- 该接口的 connected route 和 default route。 + +`Deconfigured` 事件清理同一接口的 DHCP 地址、DNS 和 IPv4 route。这样某个接口 DHCP NAK 不会影响其它接口的静态地址或 DHCP 状态。 + +#### DHCP Server + +内置 DHCP server 用于 SoftAP 或运行期注册的静态服务接口。它不是通用企业 DHCP server,而是一个轻量的 per-interface server: + +```rust +pub struct DhcpServer { + interface_id: InterfaceId, + dev: usize, + server_ip: Ipv4Address, + client_ip: Ipv4Address, + mac: EthernetAddress, + enabled: bool, +} +``` + +设计语义: + +- 只处理进入 `interface_id` 对应接口的 DHCP packet。 +- 主要响应 Discover 和 Request,生成 Offer/Ack。 +- server IP 来自 SoftAP/静态接口地址,client IP 来自 `NetConfig::dhcp_server_client_ip`。 +- 使用固定轻量 lease 时间 `LEASE_SECS = 86400`,不维护复杂租约池。 +- 发送不经过 smoltcp socket,而是直接通过 `Router::send_on_device(dev, next_hop, packet, timestamp)` 从绑定设备发出。 +- 不参与 DHCP client 状态机,也不会更新控制面地址;它服务的是对端客户端。 + +这个边界避免 DHCP server 和 DHCP client 争抢同一个 UDP socket,也让 SoftAP 设备即使不依赖外部 DHCP 服务也能给对端分配一个简单地址。 + ### ARP Entries `arp_entries()` 从所有设备收集邻居表快照: diff --git a/docs/docs/architecture/net/flows.md b/docs/docs/architecture/net/flows.md index bbd9bcc3af..7f41e33aed 100644 --- a/docs/docs/architecture/net/flows.md +++ b/docs/docs/architecture/net/flows.md @@ -429,6 +429,23 @@ Router::poll() 内置 DHCP server 用于 SoftAP 场景。它在 Router RX snoop 中接收 Discover/Request,生成 Offer/Ack 后通过 `send_on_device()` 从绑定设备发出。它不依赖 smoltcp DHCP socket。 +```text +client DHCP Discover/Request + -> device RX + -> Router::poll() + -> DHCP server classifier checks ingress InterfaceId + -> DhcpServer::process_packet() + -> build Offer/Ack + -> Router::send_on_device(dev, client_ip/broadcast, packet) + -> EthernetDevice ARP/Ethernet TX +``` + +DHCP server 和 DHCP client 的职责分离: + +- client 负责本机作为 DHCP 客户端从外部网络获取地址,并通过 `NetworkStateUpdate` 修改控制面。 +- server 负责本机作为 SoftAP/服务接口给对端分配一个固定客户端地址,不修改本机控制面地址。 +- server 发送路径绕过 smoltcp UDP socket,避免和用户 UDP socket 或 DHCP client socket 竞争端口 67/68。 + ### DNS Query ```text @@ -468,6 +485,22 @@ UnixSocket abstract namespace 存在内存 map 中;path namespace 通过 `register_unix_namespace()` 注入。Unix stream accept 使用 transport 自己的 `Pollable` 和 `poll_io()`,不调用 `request_poll()`。 +stream 使用双向 ring buffer;datagram 使用 message queue。两者都通过 `PollSet` 唤醒本地 waiters。 + +```text +Unix stream sendmsg with cmsg + -> write payload to peer RX ring + -> enqueue PendingCmsg { start_byte, end_byte, cmsg } + -> wake peer poll set + +Unix stream recvmsg + -> read bytes from RX ring + -> deliver cmsg when rx offset reaches start_byte + -> stop at cmsg message boundary when needed +``` + +datagram 的 cmsg 与 payload 一起封装在单个 packet 中,因此天然保留消息边界;stream 则依靠 byte offset 维护 ancillary data 与发送调用之间的关系。 + ### Vsock Stream vsock 只在 `vsock` feature 下启用: @@ -481,6 +514,17 @@ VsockSocket vsock 不进入 `SocketSet`,也不使用 Router。设备事件由 vsock device/event loop 推进。 +```text +vsock-poll task + -> rdif_vsock::Interface::poll() + -> event: request / connected / rx / credit / disconnect + -> if blocked, keep event in PENDING_EVENTS + -> VSOCK_CONN_MANAGER updates Connection + -> wake accept/connect/rx/tx waiters +``` + +连接管理器维护 listening、connecting、connected 和 closed 状态。listener 通过 `ListenQueue` 和 `AcceptQueue` 接收连接;每条 established connection 拥有 64KiB RX ring,并通过 credit update 唤醒 TX waiters。设备事件处理使用 4KiB 临时 RX buffer;无法立即交付的事件会留在 pending queue,后续 poll 周期继续推进。 + ## 并发与锁边界 运行时流程需要维持固定边界,避免应用线程、设备 worker 和协议核心互相阻塞。 diff --git a/docs/docs/architecture/net/overview.md b/docs/docs/architecture/net/overview.md index 2eac0315a8..dd3f8d5f5b 100644 --- a/docs/docs/architecture/net/overview.md +++ b/docs/docs/architecture/net/overview.md @@ -71,6 +71,8 @@ TGOSKits 的网络能力收敛在 `net/ax-net`。它是 ArceOS、StarryOS 和 Ax | `vsock-poll` worker | vsock 设备轮询,事件分发到 `VSOCK_CONN_MANAGER` | 自适应频率 sleep(100μs→10ms) | | `{ifname}-oob-poll` | OOB RX 设备(如 SDIO Wi-Fi)的专用 poll task | `OOB_RX_SIGNAL.wait()` | +`NET_POLL_DEVICE_WAKER` 是全局设备 readiness waker。Router 会把它注册给所有允许触发全局协议栈推进的设备;设备 RX/IRQ/OOB 路径只唤醒 worker 和设置 poll 请求,不直接进入 smoltcp `Interface::poll()`。 + ### 全局锁顺序 严格的锁嵌套顺序,防止死锁: diff --git a/docs/docs/architecture/net/sockets.md b/docs/docs/architecture/net/sockets.md index 707896a17e..6dab54f30f 100644 --- a/docs/docs/architecture/net/sockets.md +++ b/docs/docs/architecture/net/sockets.md @@ -538,6 +538,64 @@ let (transport, peer_addr) = }))?; ``` +#### Unix Stream + +Unix stream 使用两组单向 ring buffer 组成全双工连接: + +```text +endpoint A tx -> endpoint B rx +endpoint B tx -> endpoint A rx +``` + +每个方向还带一条 cmsg side channel。stream 的 ancillary data 不按“单个字节”保存,而是绑定到一次 send 调用产生的字节区间: + +```rust +struct PendingCmsg { + start_byte: u64, + end_byte: u64, + cmsg: Vec, +} +``` + +接收端在读到 `start_byte` 后交付该 cmsg,并可在 `end_byte` 处截断一次 recv,使下一次 `recvmsg()` 从下一个带 cmsg 的消息边界开始。这避免了 `MSG_PEEK` 或分段读取时把 ancillary data 和 payload 的对应关系打散。 + +stream listener 的 bind 状态是 `stream::Bind`: + +```text +bind/listen + -> install stream::Bind into BindSlot.stream +connect + -> create paired channels + -> enqueue server-side ConnRequest + -> wake listener poll set +accept + -> receive ConnRequest + -> wrap server-side channel as accepted UnixSocket +``` + +#### Unix Datagram + +Unix datagram 使用 message queue,而不是 byte stream。每个 packet 保存 payload、发送端地址和 cmsg: + +```text +DgramTransport::send + -> resolve peer BindSlot.dgram + -> enqueue Packet { bytes, addr, cmsg } + -> wake receiver poll set +``` + +datagram 的消息边界天然保留;`MSG_TRUNC`、接收缓冲区不足和 cmsg 交付都按单个 packet 处理。path namespace 与 abstract namespace 的 ownership 仍通过同一个 `BindSlot` 管理。 + +#### Credentials + +Unix transport 支持 Linux 风格的 credentials 查询: + +- `PassCredentials(bool)` 接受 `SO_PASSCRED` 设置,用于兼容 Linux 应用的 option 探测。 +- `PeerCredentials(UnixCredentials)` 返回对端或创建者 PID,uid/gid 当前使用内核默认值。 +- stream channel 在连接建立时保存 peer pid;datagram endpoint 返回绑定 transport 的 pid。 + +`CMsgData` 是 `Box`,由 StarryOS 或上层 ABI 保存具体 ancillary payload。`ax-net` 只负责按 socket 语义运输 cmsg,不解析 Linux `cmsghdr` 二进制布局。 + ### Vsock Socket vsock 是可选 feature,不属于 IP 协议,也不通过 smoltcp poll。facade 只把 `SocketOps` 映射到 `VsockTransport`: @@ -554,6 +612,68 @@ pub struct VsockSocket { 核心连接状态位于 `vsock::connection_manager`,设备事件由 vsock 设备层推进。transport enum 提供 stream variant,并为后续 datagram 扩展保留结构。 +#### Vsock Connection Manager + +`vsock::connection_manager` 是 AF_VSOCK stream 的全局状态表: + +```rust +pub enum ConnectionState { + Idle, + Listening, + Connecting, + Connected, + Closed, +} + +pub struct VsockConnectionManager { + connections: BTreeMap>>, + listen_queues: BTreeMap>>, +} +``` + +核心对象: + +| 对象 | 职责 | +| --- | --- | +| `Connection` | 保存 state、local/peer address、RX ring、TX wait queue、RX/connect poll set、半关闭标志和统计 | +| `AcceptQueue` | listener 的已完成连接队列,容量为 `VSOCK_ACCEPT_QUEUE_SIZE` | +| `ListenQueue` | 绑定一个 local port,持有 `AcceptQueue` 和 accept poll set | +| `VSOCK_CONN_MANAGER` | 全局 manager,处理 listen/connect/accept/disconnect 和设备事件 | + +每条连接拥有 `VSOCK_RX_BUFFER_SIZE = 64 KiB` 的 RX ring。设备收到数据后写入对应 connection 的 RX ring 并唤醒 `rx_wakers`;socket `recv()` 从 ring 消费。发送路径调用 `device::vsock_send()`,当 peer credit 或设备侧压力不足时通过 `tx_wait_queue` 短暂等待。 + +vsock 设备层还有一个临时 RX buffer 和 pending event queue: + +- `VSOCK_RX_TMPBUF_SIZE = 4 KiB`:poll task 从 `rdif_vsock::Interface` 拉取事件时使用的临时接收缓冲。 +- `PENDING_EVENTS`:当事件暂时无法完整交付给 manager(例如目标连接 RX ring 空间不足)时保存事件,后续 poll 周期继续处理,避免直接丢弃设备事件。 +- `VsockStats` / `get_vsock_stats()`:导出 connection manager 的连接数、监听数和队列状态,用于诊断 vsock 连接泄漏或 accept backlog 问题。 + +#### Vsock Poll Worker + +vsock 设备不进入 smoltcp poll。`start_vsock_poll()` / `stop_vsock_poll()` 使用引用计数控制一个独立 poll task: + +```text +first active vsock socket + -> start_vsock_poll() + -> spawn vsock-poll task + +last active vsock socket dropped + -> stop_vsock_poll() + -> poll task observes refcount=0 and exits +``` + +poll task 从 `rdif_vsock::Interface` 拉取事件,并分发到 `VSOCK_CONN_MANAGER`: + +| 事件 | manager 动作 | +| --- | --- | +| connection request | 查找 `ListenQueue`,创建 server-side connection,压入 accept queue | +| connected | 将 outgoing connection 置为 `Connected` 并唤醒 connect waker | +| received data | 写 RX ring,唤醒 recv waker | +| credit update | 唤醒 TX wait queue | +| disconnect | 标记 close,唤醒 RX/connect waiters | + +poll 频率自适应:有事件时降低 sleep interval,长时间 idle 时逐步退回较长 interval,避免空轮询占用 CPU。 + ## Poll 与唤醒 socket 阻塞语义基于 `Pollable` + `poll_io()`。应用线程只注册 waker 并等待 readiness;协议栈推进由 net-poll worker 或本地 transport 自己的 poll set 完成。 @@ -639,7 +759,7 @@ TCP drop 时,如果 smoltcp socket 已经进入需要继续关闭或 TIME-WAIT TcpSocket::drop -> unregister_tcp_bound / unlisten if needed -> if smoltcp socket still needs protocol cleanup: - orphan::register_orphan(handle, timestamp) + orphan::add_orphan(handle, timestamp) else: SOCKET_SET.remove(handle) ``` diff --git a/docs/docs/architecture/net/testing.md b/docs/docs/architecture/net/testing.md index 9dd4ae95d8..820f7bd7df 100644 --- a/docs/docs/architecture/net/testing.md +++ b/docs/docs/architecture/net/testing.md @@ -5,324 +5,296 @@ sidebar_label: "测试与限制" # 测试与限制 -`ax-net` 横跨协议栈核心、设备 dataplane、系统调用 ABI 和运行时设备接入。验证时应按改动影响面选择测试层级:纯数据结构改动优先跑 crate 单测;socket、poll、ioctl、AF_PACKET、netlink 或 procfs 改动需要补 StarryOS QEMU 系统测试;涉及 runtime、架构或平台设备路径时再扩展到对应架构。 +本文说明 `ax-net` 现有测试资产、运行方式、覆盖范围和当前限制。测试分三层:`net/ax-net` crate 内单元测试验证协议栈内部数据结构和路由/绑定语义;StarryOS system 测试验证 Linux ABI 观测面;`apps/starry/qemu/dual-net` 验证双网口 DHCP、路由和并发数据面。 -## 验证分层 +## 测试资产 -| 层级 | 覆盖范围 | 典型触发条件 | +| 层级 | 位置 | 作用 | | --- | --- | --- | -| `ax-net` 单元测试 | 路由表、接口配置、socket bind 表、TCP listen/orphan、DHCP、DNS、raw deferred RX、队列边界 | 修改 `net/ax-net/src` 内部逻辑 | -| crate lint / fmt | Rust 风格、clippy 约束、feature 组合基础检查 | 修改 Rust 代码 | -| StarryOS syscall 测试 | Linux socket ABI、ioctl、AF_PACKET、netlink、procfs、poll/epoll 行为 | 修改 StarryOS 适配层或 `ax-net` public API | -| 跨架构 QEMU | riscv64、loongarch64、aarch64、x86_64 上的启动、调度、网络 syscall 回归 | 修改同步、原子、poll worker、架构相关接入 | -| 板级或真实设备验证 | IRQ、DMA、真实 NIC、Wi-Fi/SoftAP、链路状态 | 修改 driver/runtime/设备 worker/OOB RX | +| crate 单元测试 | [net/ax-net/src](net/ax-net/src) 内各 `#[cfg(test)]` 模块 | 验证 `RouteTable`、`NetControl`/`Service`、TCP/UDP 设备绑定、UDP bind 表、TCP listen 表和通用 socket option | +| StarryOS system 测试 | [test-suit/starryos/qemu-smp1/system](test-suit/starryos/qemu-smp1/system) | 验证 Linux socket syscall、ioctl、AF_PACKET、netlink、procfs 等 ABI | +| dual-net 集成测试 | [apps/starry/qemu/dual-net](apps/starry/qemu/dual-net) | 验证两张 virtio-net、双 DHCP、接口绑定下载和并发数据面 | +| xtask 结构自检 | [scripts/axbuild/src/starry/test.rs](scripts/axbuild/src/starry/test.rs) | 验证 `dual-net` app 配置必须包含双网卡、host HTTP fixture 和 guest probe | -## 推荐命令 +## `ax-net` 单元测试 -### 文档改动 - -仅修改 `docs/docs/architecture/net` 文档时,不需要运行 clippy 或 QEMU。建议至少检查 Markdown 结构、代码块闭合和文档内旧术语: +### 运行方式 ```bash -rg -n "^##|^###" docs/docs/architecture/net -grep -R "^```" -n docs/docs/architecture/net | wc -l -rg -n "Rounte[r]|ServiceCor[e]|device_mas[k]|#L[0-9]+" docs/docs/architecture/net +cargo test -p ax-net ``` -### `ax-net` 逻辑改动 - -修改 `net/ax-net` Rust 逻辑后,优先执行: +`ax-net` 单元测试是 host-side Rust 测试,主要覆盖不依赖真实 QEMU 设备的内部逻辑。部分测试会使用 [lib.rs](net/ax-net/src/lib.rs) 中的 `test_support` 构造一个 split-route 测试网络: -```bash -cargo fmt -cargo xtask clippy --package ax-net -cargo test -p ax-net +```text +LOCAL_IF = InterfaceId(2), LOCAL_ADDR = 10.0.2.15 +PEER_IF = InterfaceId(3), PEER_ADDR = 10.0.3.15 ``` -`cargo test -p ax-net` 用于 host 单测。若某些 feature 组合依赖内核环境或底层 crate host-test 支持不足,应记录失败原因,并用更贴近目标环境的 `cargo xtask` 验证补足。 +`network_test_guard()` 用全局 mutex 串行化会初始化全局网络状态的测试,避免 `SERVICE`、`NET_CONTROL`、`SOCKET_SET` 这类全局单例在并发 host test 中互相污染。 -### StarryOS 系统测试 +### RouteTable -涉及 Linux ABI、socket syscall、poll/epoll、AF_PACKET、netlink 或 `/proc/net/*` 时,使用 `cargo xtask` 跑 StarryOS QEMU: +位置:[router.rs](net/ax-net/src/router.rs) -```bash -cargo xtask starry test qemu --arch riscv64 -cargo xtask starry test qemu --arch loongarch64 -``` +| 测试 | 覆盖点 | +| --- | --- | +| `route_lookup_uses_longest_prefix` | 最长前缀优先,`10.0.1.0/24` 优先于默认路由 | +| `route_lookup_uses_metric_for_same_prefix` | 同前缀按 metric 小者优先 | +| `route_lookup_keeps_stable_order_for_equal_metric` | 同前缀、同 metric 时保持插入顺序 | +| `route_lookup_skips_unusable_interface` | `select_route_if()` 可通过闭包跳过不可用接口 | +| `default_routes_only_reports_zero_prefix_ipv4_rules` | `default_routes()` 只导出 IPv4 `0.0.0.0/0` 规则 | +| `bounded_packet_queue_reports_full_and_preserves_order` | 有界队列满时返回错误,并保持 FIFO | -跨架构回归可扩展到: +这些测试对应多网口 route decision 的核心排序规则:最长前缀、metric、稳定顺序和接口可用性过滤。 -```bash -cargo xtask starry test qemu --arch aarch64 -cargo xtask starry test qemu --arch x86_64 -``` +### Service / DHCP 地址状态 -需要缩小范围时,优先使用 StarryOS case 选择参数运行 system 组: +位置:[service.rs](net/ax-net/src/service.rs) -```bash -cargo xtask starry test qemu --arch riscv64 -c qemu-smp1/system -``` +| 测试 | 覆盖点 | +| --- | --- | +| `dhcp_configured_is_true_once_any_interface_has_address` | 多 DHCP 接口中只要任一接口已获得地址,bootstrap 状态即可视为完成 | +| `interface_address_table_handles_loopback_and_two_ethernet_addresses` | smoltcp `Interface` address list 能同时保存 loopback、eth0、eth1 IPv4 | + +这组测试防止网络初始化重新退化为“只看第一个网卡”或“接口地址表只能容纳单 Ethernet 地址”的模型。 -## `ax-net` 单元测试矩阵 +### TCP 设备绑定 -### 控制面与路由 +位置:[tcp.rs](net/ax-net/src/tcp.rs) -| 模块 | 关键测试点 | +| 测试 | 覆盖点 | | --- | --- | -| `InterfaceId` / `DeviceBinding` | Linux ifindex 映射、无绑定/绑定接口读写、无效接口拒绝 | -| `RouteTable` | 添加、删除、替换规则;最长前缀;低 metric 优先;同 metric 插入顺序稳定 | -| `select_route_if()` | 接口 `UP` 过滤、`SO_BINDTODEVICE` 过滤、无路由返回错误 | -| `select_route_for_source()` | 多宿主场景下源地址和出接口一致 | -| DNS registry | DHCP/static/fallback 排序、去重、不可路由 DNS server 过滤 | +| `tcp_info_reports_default_socket_metrics` | `TCP_INFO` 在 closed socket 上返回稳定默认字段 | +| `connect_preserves_bound_interface` | TCP bind 到具体本地地址后,connect 不会被 peer route 改写绑定接口 | +| `connect_uses_peer_route_when_unbound` | wildcard bind 的 TCP connect 根据目的地址 route decision 选择接口 | +| `connect_rejects_unroutable_bound_device` | 显式绑定到不可达接口后,connect 返回错误并保留原绑定 | -专项路由用例: +这组测试覆盖 `SO_BINDTODEVICE` 和本地地址推导出的 `DeviceBinding` 对 TCP connect 的影响。 -```text -longest prefix: - 10.0.0.0/8 -> dev0 metric=100 - 10.0.2.0/24 -> dev1 metric=200 - query 10.0.2.15 => dev1 - -metric: - 0.0.0.0/0 -> dev0 metric=100 - 0.0.0.0/0 -> dev1 metric=50 - query 8.8.8.8 => dev1 - -source route: - eth0 source=10.0.2.15 - eth1 source=192.168.1.10 - packet source=192.168.1.10 => eth1 - -replace interface rules: - replace IPv4 rules for interface=2 - old connected/default routes are removed - new connected/default routes are installed -``` +### UDP 设备绑定 -### 设备 dataplane +位置:[udp.rs](net/ax-net/src/udp.rs) -| 模块 | 关键测试点 | +| 测试 | 覆盖点 | | --- | --- | -| `BoundedPacketQueue` | 容量上限、满队列 drop、长度计数一致、wait/wake 行为 | -| `QueuedPacket` | 超 MTU 拒绝、inline buffer 无每包堆分配 | -| `Router::poll()` | 共享 RX queue drain、ingress `InterfaceId` 保留、RX buffer 满时停止 | -| `Router::dispatch()` | 普通 unicast route lookup、limited broadcast 多接口发送、TX queue 满时 drop/warn | -| loopback | TX 直接注入 `rx_buffer`,不经过设备队列,并在注入前执行 TCP SYN snoop | -| waker | 全局设备 waker 与绑定接口 waker 分离 | +| `connect_preserves_bound_interface` | UDP bind 到具体本地地址后,connect 不会改写绑定接口 | +| `connect_uses_peer_route_when_unbound` | wildcard bind 的 UDP connect 根据目的地址 route decision 选择接口 | +| `connect_rejects_unroutable_bound_device` | 显式绑定到不可达接口后,connect 返回错误并保留原绑定 | -需要重点验证 loopback 的两个性质: +UDP 的测试与 TCP 对齐,重点是 datagram socket 的 connected peer 不应破坏本地地址绑定语义。 -```text -loopback fast path: - socket send -> smoltcp tx_buffer - Router::dispatch sees LOOPBACK route - inject_loopback_rx_direct writes Router.rx_buffer - next poll consumes packet in same protocol core - -no queue allocation: - loopback packet does not enter shared RX queue - no Vec/to_vec/Box allocation is required for the loopback hop -``` +### UDP Bind Side Table -### Socket 层 +位置:[wrapper.rs](net/ax-net/src/wrapper.rs) -| 模块 | 关键测试点 | +| 测试 | 覆盖点 | | --- | --- | -| `SocketSetWrapper` | socket add/remove、UDP bind side table、wildcard/specific 地址冲突 | -| `ListenTable` | backlog、per-address listen、wildcard 冲突、SYN queue、accept wake | -| `GeneralOptions` | nonblocking、send/recv timeout、`SO_REUSEADDR`、`SO_BINDTODEVICE` | -| `TcpSocket` | bind/connect/listen/accept/send/recv/shutdown、`TCP_INFO`、orphan 转移 | -| `UdpSocket` | connected/unconnected send、`MSG_MORE` corking、truncation、`SO_REUSEADDR` | -| `RawSocket` | ICMP echo loopback reply、TTL、IP version 校验、`MSG_PEEK`、deferred RX wire-packet 格式 | -| Unix socket | pathname/abstract namespace、stream/datagram、cmsg、shutdown | -| vsock | bind/listen/connect/accept/send/recv、无设备初始化语义 | +| `udp_bind_rules_allow_distinct_specific_addresses` | 相同端口可绑定到不同具体本地地址;相同地址冲突;wildcard 与具体地址冲突 | +| `udp_bind_rules_reject_specific_after_wildcard` | 已存在 wildcard bind 时拒绝后续具体地址 bind | -TCP listen 建议覆盖: +这些测试补齐 smoltcp UDP socket 之外的 Linux 风格 wildcard/specific bind 仲裁。 -```text -per-address listen: - listen(127.0.0.1:8080) - listen(10.0.2.15:8080) - both succeed - listen(0.0.0.0:8080) conflicts with either specific listener - -accept queue: - incoming SYN creates pending socket - established pending socket is returned by accept() - closed pending socket is skipped and removed - queue overflow drops new SYN without corrupting existing entries -``` +### TCP ListenTable -UDP bind 建议覆盖: +位置:[listen_table.rs](net/ax-net/src/listen_table.rs) -```text -wildcard conflict: - bind(0.0.0.0:1234) - bind(10.0.2.15:1234) => AddressInUse unless reuse policy allows it - -device binding: - bind(10.0.2.15:1234) derives eth0 binding - send uses route allowed by that binding - waker registration ignores unrelated interfaces -``` +| 测试 | 覆盖点 | +| --- | --- | +| `allows_same_port_on_distinct_specific_addresses` | 同端口可以在不同具体地址上 listen | +| `wildcard_listener_conflicts_with_specific_addresses` | wildcard listener 与任一具体地址 listener 冲突 | -### DHCP 与动态状态 +这组测试覆盖 per-address listen 的冲突规则,是 wildcard listen、`0.0.0.0:port` 和多本地地址共存语义的基础。 -| 模块 | 关键测试点 | +### GeneralOptions + +位置:[general.rs](net/ax-net/src/general.rs) + +| 测试 | 覆盖点 | | --- | --- | -| DHCP client | Discovering、Requesting、Bound、NAK reset、xid/mac/interface 过滤 | -| DHCP commit | smoltcp address list、interface snapshot、DNS、route table 一致更新 | -| DHCP server | Discover -> Offer、Request -> Ack、客户端 MAC/xid/interface 过滤、租约覆盖 | -| dynamic device | 分配新 `InterfaceId`、写入 registry、安装路由、启动 worker、唤醒 poll | +| `device_binding_round_trips_none_and_some_interface` | `DeviceBinding` 在 `GeneralOptions` 中可以从 none 到指定接口再回到 none | -DHCP 状态机用例: +`DeviceBinding` 使用 atomic raw ifindex 保存,这个测试验证 public 语义不会因为内部原子编码而丢失。 -```text -Discovering -> Offer: - DhcpState phase=Discovering - receive DHCPOFFER with correct xid/mac/interface - phase=Requesting - no configured event yet - -Requesting -> ACK: - receive DHCPACK with yiaddr/subnet/router/dns - phase=Bound - emit Configured event - commit updates interface IPv4, DNS and routes - -NAK: - Bound address exists - receive DHCPNAK - phase=Discovering - emit Deconfigured event +## StarryOS system 测试 + +### 运行方式 + +完整 QEMU system 组: + +```bash +cargo xtask starry test qemu --arch riscv64 -c qemu-smp1/system ``` -### 生命周期与清理 +常用跨架构回归: -| 模块 | 关键测试点 | -| --- | --- | -| orphan reaper | `Closed` 立即回收、TIME_WAIT/FIN teardown 超时回收、overflow 只回收 Closed | -| socket drop | TCP orphan 化后仍允许 FIN/TIME_WAIT 推进 | -| poll worker | socket drop/request_poll 后 net-poll worker 被唤醒 | -| DNS socket guard | 查询结束后 socket 被释放 | +```bash +cargo xtask starry test qemu --arch riscv64 +cargo xtask starry test qemu --arch loongarch64 +``` -orphan 测试应避免强制清理仍在 teardown 的 TCP socket。超过 orphan 上限时,优先回收 `Closed`;仍处于 TIME_WAIT、FIN_WAIT、LAST_ACK、CLOSING 的 socket 应保留到超时或自然关闭。 +system 测试使用 StarryOS guest 内的 Linux 用户态程序验证 syscall/ABI 层。网络相关用例主要覆盖以下几类。 -## StarryOS 系统测试矩阵 +### Socket Dataplane -### Socket dataplane +| 测试 | 位置 | 覆盖点 | +| --- | --- | --- | +| `syscall-test-socket-dataplane` | [test-suit/starryos/qemu-smp1/system/syscall-test-socket-dataplane](test-suit/starryos/qemu-smp1/system/syscall-test-socket-dataplane) | TCP/UDP/raw socket 数据面基础行为 | +| `bugfix-bug-tcp-send-no-epoll-notify` | [test-suit/starryos/qemu-smp1/system/bugfix-bug-tcp-send-no-epoll-notify](test-suit/starryos/qemu-smp1/system/bugfix-bug-tcp-send-no-epoll-notify) | TCP send 后 epoll waiter 唤醒 | -重点覆盖: +### ioctl / netlink / procfs -- TCP loopback connect/send/recv/close。 -- UDP send/recv、connected UDP、非阻塞错误。 -- raw ICMP echo 路径。 -- poll/select/epoll readiness。 -- send 后及时唤醒 waiter,避免依赖应用线程同步 poll。 +| 测试 | 位置 | 覆盖点 | +| --- | --- | --- | +| `bugfix-bug-netlink-getlink` | [test-suit/starryos/qemu-smp1/system/bugfix-bug-netlink-getlink](test-suit/starryos/qemu-smp1/system/bugfix-bug-netlink-getlink) | `RTM_GETLINK`、`SIOCGIFTXQLEN`、link 属性 | +| `bugfix-bug-netlink-getaddr` | [test-suit/starryos/qemu-smp1/system/bugfix-bug-netlink-getaddr](test-suit/starryos/qemu-smp1/system/bugfix-bug-netlink-getaddr) | `RTM_GETADDR`、loopback address、link/address dump | +| `syscall-test-netlink-recvmsg` | [test-suit/starryos/qemu-smp1/system/syscall-test-netlink-recvmsg](test-suit/starryos/qemu-smp1/system/syscall-test-netlink-recvmsg) | netlink recvmsg 基础语义 | +| `bugfix-bug-proc-net-arp` | [test-suit/starryos/qemu-smp1/system/bugfix-bug-proc-net-arp](test-suit/starryos/qemu-smp1/system/bugfix-bug-proc-net-arp) | `/proc/net/arp` 格式、device 字段和固定 gateway stub 回归 | -相关测试: +### AF_PACKET -| 测试 | 覆盖点 | -| --- | --- | -| `syscall-test-socket-dataplane` | TCP/UDP/raw socket dataplane 基础行为 | -| `bugfix-bug-tcp-send-no-epoll-notify` | TCP send 后 epoll waiter 唤醒 | +| 测试 | 位置 | 覆盖点 | +| --- | --- | --- | +| `bugfix-bug-packet-arping` | [test-suit/starryos/qemu-smp1/system/bugfix-bug-packet-arping](test-suit/starryos/qemu-smp1/system/bugfix-bug-packet-arping) | `AF_PACKET` bind、`SIOCGIFINDEX`、`RTM_GETLINK` 一致性、模拟 gateway ARP reply | -### ioctl、netlink 与 procfs +这些 system 测试验证的是 StarryOS Linux ABI 层是否正确使用 `ax_net::interfaces()`、`InterfaceId`、`arp_entries()` 和 socket facade。它们不替代 `ax-net` crate 单元测试;两者覆盖层级不同。 -重点覆盖: +## dual-net 集成测试 -- `SIOCGIFCONF` 返回非零接口列表。 -- `SIOCGIFINDEX` 与 netlink `RTM_GETLINK` 的 ifindex 一致。 -- `SIOCGIFADDR`、`SIOCGIFNETMASK`、`SIOCGIFBRDADDR` 与 `ax-net` 接口快照一致。 -- `RTM_GETADDR` 至少返回 loopback IPv4。 -- `/proc/net/arp` 行格式正确,device 字段来自真实接口名。 +`apps/starry/qemu/dual-net` 是双网卡集成测试,用于验证多设备初始化、双 DHCP、route table、接口绑定和并发收发。它是 Starry app 级 QEMU 场景,不属于 `test-suit/starryos` system 分组。 -相关测试: +### 运行方式 -| 测试 | 覆盖点 | -| --- | --- | -| `bugfix-bug-netlink-getlink` | `RTM_GETLINK`、`SIOCGIFTXQLEN`、link 属性 | -| `bugfix-bug-netlink-getaddr` | `RTM_GETADDR`、loopback address、link/address dump | -| `syscall-test-netlink-recvmsg` | netlink recvmsg 基础语义 | -| `bugfix-bug-proc-net-arp` | `/proc/net/arp` 格式与内容 | +列出 case: -### AF_PACKET +```bash +cargo xtask starry app list --kind qemu | rg "qemu/dual-net" +``` + +运行 riscv64: -重点覆盖: +```bash +cargo xtask starry app qemu -t qemu/dual-net --arch riscv64 +``` + +运行 x86_64: -- `socket(AF_PACKET, SOCK_DGRAM, ...)` 创建和权限语义。 -- `bind(sockaddr_ll)` 按 ifindex 绑定接口。 -- `getsockname()` 返回匹配的 `sockaddr_ll`。 -- packet socket ioctl 返回 ifindex、flags 和 MAC。 -- 模拟 gateway ARP reply 工作,loopback 或未知 peer 不产生错误格式响应。 +```bash +cargo xtask starry app qemu -t qemu/dual-net --arch x86_64 +``` -相关测试: +QEMU 配置: -| 测试 | 覆盖点 | +| 架构 | 配置文件 | | --- | --- | -| `bugfix-bug-packet-arping` | AF_PACKET bind、SIOCGIFINDEX、RTM_GETLINK 一致性、模拟 ARP reply | +| riscv64 | [apps/starry/qemu/dual-net/qemu-riscv64.toml](apps/starry/qemu/dual-net/qemu-riscv64.toml) | +| x86_64 | [apps/starry/qemu/dual-net/qemu-x86_64.toml](apps/starry/qemu/dual-net/qemu-x86_64.toml) | -### Namespace 可见性 +### 拓扑 -重点覆盖: +```text +guest eth0 + -> virtio-net-pci net0 + -> QEMU user net 10.0.2.0/24 + -> DHCP address 10.0.2.15 + -> host gateway 10.0.2.2 + +guest eth1 + -> virtio-net-pci net1 + -> QEMU user net 10.0.3.0/24 + -> DHCP address 10.0.3.15 + -> host gateway 10.0.3.2 + +host HTTP server + -> 127.0.0.1:18382 on host + -> exposed through each QEMU user net gateway + -> payload size 1 MiB, byte value 68 +``` -- root network namespace 能看到 loopback 和 Ethernet。 -- 非 root namespace 只暴露 loopback 视图。 -- StarryOS 可见性过滤不改变 `ax-net` 全局 route table。 -- `AF_PACKET` 创建和绑定遵守 root namespace 限制。 +`qemu-*.toml` 会启动 host HTTP server: -## 回归场景 +```toml +[host_http_server] +bind = "127.0.0.1" +port = 18382 +body_size = 1048576 +body_byte = 68 +``` -### 多接口路由 +guest 启动后自动执行: -验证内容: +```text +/usr/bin/dual-net-tests.sh +``` -- 多个 Ethernet 接口各自有独立 IPv4、metric 和默认路由。 -- 目的地址命中直连路由时优先使用直连接口。 -- 多个默认路由按 metric 选择。 -- `SO_BINDTODEVICE` 或绑定具体本地地址后,只使用匹配接口。 -- smoltcp 已选源地址后,Router dispatch 不从错误接口发送。 +脚本来自 [apps/starry/qemu/dual-net/c/dual-net-tests.sh](apps/starry/qemu/dual-net/c/dual-net-tests.sh)。[prebuild.sh](apps/starry/qemu/dual-net/c/prebuild.sh) 会安装 `curl`,[CMakeLists.txt](apps/starry/qemu/dual-net/c/CMakeLists.txt) 会把 `curl` 和 `dual-net-tests.sh` 安装进 guest rootfs。 -### Loopback +### Guest 检查项 -验证内容: +`dual-net-tests.sh` 执行以下检查: -- TCP loopback connect 可以在同一协议核心内推进 SYN。 -- UDP loopback send/recv 不经过外部设备 worker。 -- raw ICMP echo loopback reply 使用正确 wire packet 格式。 -- loopback 路径不会因为共享 RX queue 满而丢包;它受 `Router.rx_buffer` 容量约束。 +- `ifconfig eth0` 或 `ip addr show eth0` 能看到 `10.0.2.15`。 +- `ifconfig eth1` 或 `ip addr show eth1` 能看到 `10.0.3.15`。 +- `curl --interface eth0 http://10.0.2.2:18382/payload.bin?...` 能下载至少 1 MiB。 +- `curl --interface eth1 http://10.0.3.2:18382/payload.bin?...` 能下载至少 1 MiB。 +- 串行下载完成后,再并发从 eth0/eth1 下载。 -### Poll 与唤醒 +成功输出包含: -验证内容: +```text +DUAL_NET_ETH0_ADDR_OK +DUAL_NET_ETH1_ADDR_OK +DUAL_NET_FETCH_ETH0_SINGLE_MS=... BYTES=1048576 +DUAL_NET_FETCH_ETH1_SINGLE_MS=... BYTES=1048576 +DUAL_NET_FETCH_ETH0_PARALLEL_MS=... BYTES=1048576 +DUAL_NET_FETCH_ETH1_PARALLEL_MS=... BYTES=1048576 +DUAL_NET_TEST_PASSED +``` + +失败输出以以下格式开始: + +```text +DUAL_NET_TEST_FAILED: ... +``` + +### 覆盖范围 -- socket 热路径只请求 poll,不同步执行 smoltcp poll。 -- RX worker 入队后唤醒 net-poll worker。 -- TX worker 队列 drain 后不阻塞协议核心。 -- accept queue、UDP recv、TCP send/recv 都能唤醒对应 waker。 -- `poll_at()` 定时器可推进 TCP retransmit、TIME_WAIT、DHCP 等事件。 +`dual-net` 覆盖: -### DNS 与 DHCP +- runtime 能收集两张 virtio-net 设备。 +- `NetworkConfig` 默认 DHCP 策略能应用到未显式配置的 Ethernet 接口。 +- `eth0` 和 `eth1` 能通过独立 DHCP 获取不同网段地址。 +- route table 同时存在 `10.0.2.0/24` 和 `10.0.3.0/24` connected route。 +- `curl --interface` 通过 Linux ABI 映射到接口绑定,限制 route lookup。 +- 串行和并发下载验证 per-device TX queue、共享 RX queue、device worker 和 net-poll worker 可以持续推进。 -验证内容: +### xtask 结构自检 -- DHCP ACK 后接口地址、默认路由和 DNS 同步可见。 -- DHCP NAK 后旧地址、旧路由和 DHCP DNS 被清理。 -- 静态 DNS、DHCP DNS、fallback DNS 按 metric 和来源去重排序。 -- 不可路由 DNS server 被跳过,查询尝试下一个 server。 +[scripts/axbuild/src/starry/test.rs](scripts/axbuild/src/starry/test.rs) 中的 `dual_net_qemu_case_exercises_two_interfaces_and_parallel_fetches` 会静态检查 `dual-net` case 的结构: -### ABI 兼容 +- `c/dual-net-tests.sh`、`c/prebuild.sh`、`c/CMakeLists.txt` 必须存在。 +- riscv64 和 x86_64 都必须有 `qemu-*.toml`。 +- QEMU args 必须包含 `net0`、`net1` 两个 virtio-net-pci。 +- net0 必须是 `10.0.2.0/24` 且 DHCP 起始地址为 `10.0.2.15`。 +- net1 必须是 `10.0.3.0/24` 且 DHCP 起始地址为 `10.0.3.15`。 +- `shell_init_cmd` 必须是 `/usr/bin/dual-net-tests.sh`。 +- host HTTP server 必须监听 18382,payload 至少 1 MiB。 -验证内容: +这个结构测试防止 app 配置被误删、改成单网卡或失去自动 guest probe。 -- `SO_BINDTODEVICE` set/get 往返正确。 -- `SIOCGIFINDEX`、`AF_PACKET sockaddr_ll.sll_ifindex`、netlink ifindex 使用同一 `InterfaceId`。 -- `/proc/net/arp` 不暴露固定 QEMU gateway stub,也不写死 `eth0`。 -- `TCP_INFO`、`SO_TYPE`、`FIONREAD` 从真实 socket 状态返回。 +## 常见失败定位 -## 调试指南 +### `DUAL_NET_TEST_FAILED` -### QEMU 只显示 `STARRY_GROUPED_TEST_FAILED` +| 现象 | 优先检查 | +| --- | --- | +| `eth1 did not get 10.0.3.15` | 第二个 virtio-net 是否被 runtime 收集;默认 DHCP 是否应用到未显式配置接口;DHCP packet ingress `InterfaceId` 是否分发正确 | +| eth0 成功、eth1 curl 失败 | `SO_BINDTODEVICE` / `curl --interface` 是否映射到 eth1;route table 是否有 `10.0.3.0/24` connected route | +| 串行成功、并发失败 | RX/TX worker 是否被正确唤醒;队列是否满;net-poll worker 是否持续 poll | +| 下载字节数小于 1 MiB | TCP receive/send readiness、host HTTP server 暴露、QEMU user net 或 curl 超时 | +| QEMU timeout | 是否缺少 `curl`、`ip`、`ifconfig`;shell init command 是否执行到 `DUAL_NET_TEST_PASSED` | + +### `STARRY_GROUPED_TEST_FAILED` `cargo xtask starry test qemu` 的汇总输出可能只显示匹配到失败模式。定位时应查更早的 test binary 输出: @@ -365,16 +337,6 @@ info!("dns: {:?}", ax_net::dns_servers()); - UDP 是否正确设置 `SO_REUSEADDR`,以及该路径是否应跳过 side table。 - 绑定具体本地地址时,该地址是否属于当前接口 registry。 -### DHCP 超时 - -排查方向: - -- DHCP packet 的 ingress `InterfaceId` 是否匹配对应 `DhcpState`。 -- xid、client MAC、message type 是否被过滤。 -- RX worker 是否把 DHCP reply 入队到共享 RX queue。 -- net-poll worker 是否被唤醒。 -- DHCP server 是否在同一二层网络可达。 - ### AF_PACKET / netlink 不一致 排查方向: @@ -384,29 +346,17 @@ info!("dns: {:?}", ax_net::dns_servers()); - `sockaddr_ll.sll_ifindex` 是否能通过 `InterfaceId::from_linux_ifindex()` 反查接口。 - namespace 可见性过滤是否导致接口在某条路径可见、另一条路径不可见。 -### `/proc/net/arp` 异常 +## 当前限制 -排查方向: - -- `ax_net::arp_entries()` 是否有对应 entry。 -- ARP entry 的 interface_id 是否能映射回接口名。 -- procfs 输出是否仍残留固定 gateway 或固定 `eth0` 字段。 -- ARP pending queue 是否已满,导致解析请求被丢弃。 - -### net-poll worker 不推进 - -排查方向: - -- `request_poll()` 是否被调用。 -- `NET_POLL_REQUESTED` 是否被置位。 -- `NET_POLL_WAKE` 是否唤醒 worker。 -- `POLLING_INTERFACES` CAS 是否长时间被占用。 -- `next_poll_delay()` 是否返回过长 idle interval。 -- 设备 worker 是否卡在 driver `send()` / `receive()` 持锁路径。 +### 测试覆盖限制 -## 已知限制 +- crate 单元测试主要覆盖纯 Rust 数据结构和 route/bind 语义,不启动真实 smoltcp 端到端 TCP 会话。 +- `dual-net` 使用 QEMU user networking,不覆盖 tap/bridge、真实 NIC IRQ/DMA、RSS 或多队列网卡。 +- `dual-net` 验证双 DHCP 和接口绑定下载,但不验证 link down/up、热插拔和运行期 route 删除。 +- StarryOS system 测试覆盖 Linux ABI 观测面,不直接检查 `Router` 内部队列长度或每包分配情况。 +- vsock、Unix cmsg、DHCP server、OOB RX 仍需要更多专门测试资产。 -### 协议与特性范围 +### 协议与功能限制 - IPv6 route、NDP、MLD 和完整 IPv6 socket 语义未作为主路径完善。 - IGMP/按接口 multicast membership 不完整。 @@ -420,20 +370,4 @@ info!("dns: {:?}", ax_net::dns_servers()); - 多设备 dataplane 通过 worker 和有界队列解耦,但不是 RSS/NAPI 多队列模型。 - loopback 已有直接注入快路径,但普通设备 RX/TX 仍存在必要的 packet copy。 - 尚未实现端到端 zero-copy;这需要 rd-net buffer ownership、packet pool 和 smoltcp token 共同改造。 - -### 系统集成限制 - - StarryOS network namespace 当前主要是可见性过滤,不是完整 per-namespace network stack。 -- 动态 link down/up、接口热插拔、队列重建和 socket 错误传播仍需完善。 -- 真实硬件 IRQ/DMA 行为需要板级验证,QEMU 不能覆盖全部 timing 和 cache 一致性问题。 - -## 变更验收清单 - -提交网络相关变更前,按影响面检查: - -- 修改 `net/ax-net/src` 逻辑:`cargo fmt`、`cargo xtask clippy --package ax-net`、相关单测。 -- 修改 socket readiness 或 poll:补 StarryOS poll/epoll 或 socket dataplane QEMU 验证。 -- 修改接口、路由、DNS、ARP:补 ioctl、netlink、procfs、multi-interface route 验证。 -- 修改 AF_PACKET:补 `bugfix-bug-packet-arping` 相关路径。 -- 修改 runtime/driver/OOB RX:至少验证 riscv64 QEMU,真实设备改动还应补板级验证记录。 -- 修改文档:检查标题层级、代码块闭合和过期术语。 From 5f770e4bc221c6231966905584dc7b0c29554ea9 Mon Sep 17 00:00:00 2001 From: ZCShou <72115@163.com> Date: Tue, 16 Jun 2026 13:37:50 +0800 Subject: [PATCH 28/31] docs(net): remove obsolete multi-NIC architecture design document --- p.md | 552 ----------------------------------------------------------- 1 file changed, 552 deletions(-) delete mode 100644 p.md diff --git a/p.md b/p.md deleted file mode 100644 index dd9d99611c..0000000000 --- a/p.md +++ /dev/null @@ -1,552 +0,0 @@ -# `ax-net` 多网口支持与性能优化方案 - -## 1. 背景 - -`net/ax-net` 已经完成统一网络栈收敛,核心能力包括 TCP、UDP、raw socket、Unix domain socket、可选 vsock、DNS、DHCP、ARP、poll/waker 和 `rd-net` 设备适配。为了进一步支持多网口、多路由和更稳定的数据面性能,需要在现有单 smoltcp `Interface` 架构上补齐控制面、多设备数据面和 socket 绑定语义。 - -当前需要解决的主要问题: - -- 初始化流程偏向第一个 NIC,接口语义容易隐含为 `eth0`。 -- `NetworkConfig` 不能完整表达每个接口的地址、网关、DNS、DHCP、metric。 -- DHCP、DNS、路由更新等状态偏单接口模型。 -- StarryOS ioctl、AF_PACKET、`/proc/net/arp` 等路径容易出现固定 `eth0` 假设。 -- 旧设备绑定如果使用 `u32 device_mask`,接口集合语义不清晰,扩展性差。 -- socket 热路径不应同步推进完整协议栈,应只请求专用 poll worker 工作。 -- 设备收发、smoltcp poll、`SocketSet` 访问需要清晰锁边界,避免设备路径和协议核心互相阻塞。 - -本方案目标: - -- 注册并管理所有可用 NIC,形成 `lo`、`eth0`、`eth1` 等接口。 -- 支持每个接口独立静态 IPv4 或 DHCP 配置。 -- 支持多条直连路由、默认路由、metric、源地址选择和下一跳选择。 -- 提供统一接口 registry API,供 ArceOS、StarryOS、Axvisor 和诊断工具使用。 -- 保持单 `smoltcp::iface::Interface + SocketSet` 架构,避免 multi-smoltcp domain 带来的 socket 绑定、动态路由和 wildcard listen 聚合复杂度。 -- 通过设备队列解耦、异步 poll、控制面快照、有界队列和减少热路径分配提升性能。 -- 删除旧单网口假设和旧 `u32 device_mask` 兼容路径,保持代码简洁。 - -非目标: - -- 不拆分为多个 smoltcp 实例。 -- 不实现完整 IPv6。 -- 不实现 Linux net namespace 的完整隔离。 -- 不承诺固定性能提升比例,性能效果以 benchmark 为准。 -- 不为了过渡兼容保留复杂 wrapper 或多套状态。 - -## 2. 修改方案 - -### 2.1 总体架构 - -方案保持 **单协议栈核心 + 多设备 Router** 模型: - -```text -public API - -> tcp / udp / raw / unix / vsock - -> interfaces() / default_routes() / dns_query() - -control plane - -> interface registry - -> shared RouteTable - -> DNS registry - -> DeviceBinding - -> network state transaction commit - -single protocol core - -> Service - -> smoltcp Interface - -> Router as smoltcp Device - -> DHCP states - -> DHCP server - -> orphan reaper - -> SocketSetWrapper - -> ListenTable - -multi-device data plane - -> Router.rx_buffer / Router.tx_buffer - -> shared bounded RX queue - -> per-device bounded TX queues - -device workers - -> per-device RX worker - -> per-device TX worker - -> OOB RX poll task for dedicated-poll devices -``` - -核心原则: - -- `Router` 是 smoltcp `Device` 适配层,由 `Service` 独占,不作为全局对象被外部直接调用。 -- 设备 worker 不进入 smoltcp `Interface` 或 `SocketSet`。 -- 设备 worker 与协议核心之间通过有界队列传递 packet。 -- `SocketSetWrapper` 仍然是全局 socket 集合,但 socket 热路径只请求 poll,不同步推进完整网络栈。 -- 控制面状态使用短锁和快照;数据面 poll 只在必要时进入 smoltcp 临界区。 -- 不需要额外拆出 `ServiceCore` / `ServiceHandle` 层级,除非后续出现明确的所有权或锁边界收益。 - -### 2.2 配置模型 - -`NetworkConfig` 改为接口级配置: - -```rust -pub struct NetworkConfig { - pub interfaces: Vec, - pub default_dns_servers: Vec, -} - -pub struct InterfaceConfig { - pub name: String, - pub match_by: InterfaceMatcher, - pub static_ip: Option, - pub dhcp: bool, - pub metric: u32, - pub dns_servers: Vec, -} - -pub enum InterfaceMatcher { - ByOrder(usize), - ByMac(EthernetAddress), - ByDriverName(String), -} -``` - -配置语义: - -- 未显式配置的 Ethernet 接口默认启用 DHCP。 -- `dhcp = true` 与 `static_ip.is_some()` 互斥,配置冲突直接 panic。 -- 显式配置必须能匹配到唯一设备,否则初始化失败。 -- 接口名必须唯一。 -- `lo` 由 `ax-net` 固定创建,不允许外部配置覆盖。 -- 删除旧全局 `AX_IP`、`AX_GW`、`AX_PREFIX_LEN`、`AX_DNS` 单网口语义。 -- `ax-runtime` 只负责把结构化接口配置传入 `ax_net::init_network()`。 - -### 2.3 接口与控制面模型 - -新增统一接口标识和对外快照: - -```rust -pub struct InterfaceId(u32); - -pub struct InterfaceInfo { - pub id: InterfaceId, - pub name: String, - pub kind: InterfaceKind, - pub mac: Option, - pub ipv4: Option, - pub mtu: usize, - pub flags: InterfaceFlags, - pub metric: u32, -} -``` - -`InterfaceId` 是 `ax-net` 内部和对外统一的接口标识。StarryOS 的 Linux `ifindex` 直接由 `InterfaceId` 的数值映射得到,不再维护单独的 `IfIndex`。 - -控制面建议使用一个 `NetControl` 聚合接口 registry、DNS entries 和共享路由表: - -```rust -struct ControlState { - interfaces: Vec, - dns: Vec, -} - -struct NetControl { - state: RwLock, - routes: SharedRouteTable, -} -``` - -设计要求: - -- 查询 API 返回只读快照,不暴露内部锁。 -- 接口、DNS、路由更新需要事务化。 -- 对外查询使用 `default_routes()`,不提供含糊的 `routes()` API。 -- 控制面不直接收发 packet,也不推进 smoltcp poll。 - -### 2.4 路由模型 - -路由规则: - -```rust -pub struct Rule { - pub filter: IpCidr, - pub via: Option, - pub dev: usize, - pub interface_id: InterfaceId, - pub src: IpAddress, - pub metric: u32, - pub order: u64, -} - -pub struct RouteDecision { - pub dev: usize, - pub interface_id: InterfaceId, - pub source: IpAddress, - pub next_hop: IpAddress, - pub metric: u32, -} -``` - -查找规则: - -- 先按最长前缀匹配。 -- 同前缀按 metric 从小到大。 -- metric 相同按插入顺序稳定选择。 -- 接口 down 时跳过该接口路由。 -- 查不到路由返回错误,不 panic。 -- `select_route_if()` 用于带接口可用性过滤的普通路由查询。 -- `select_route_with_binding()` 用于 `SO_BINDTODEVICE` 或本地地址推导出的接口约束。 -- `select_route_for_source(dst, source)` 用于 TX dispatch,保证 smoltcp 已选择的源地址和出接口一致。 - -调用方迁移: - -- TCP `connect()` 使用路由选择本地地址和出接口。 -- UDP `connect()` / `sendto()` 按目标地址和 `DeviceBinding` 查 route,不强制长期缓存完整 `RouteDecision`,避免 DHCP/route 更新后缓存失效。 -- raw socket 查不到路由返回错误。 -- Router TX dispatch 根据 IP 包的源地址和目标地址选择 TX queue。 - -### 2.5 设备队列与 smoltcp Device 适配 - -设备并行优化不能破坏 smoltcp `Device` token 模型。正确边界是: - -```text -device worker - -> RouterQueues - -Router as smoltcp Device - -> receive() consumes Router.rx_buffer - -> transmit() returns TxToken - -> TxToken::consume() writes Router.tx_buffer - -Service::poll() - -> Router::poll() drains worker RX queue into rx_buffer - -> iface.poll(now, &mut router, &mut sockets) - -> Router::dispatch() routes tx_buffer to loopback or per-device TX queue -``` - -建议结构: - -```rust -struct Router { - rx_buffer: PacketBuffer, - tx_buffer: PacketBuffer, - queues: Arc, - devices: Vec>, - table: SharedRouteTable, -} - -struct RouterQueues { - rx: Arc>, -} - -struct DeviceHandle { - interface_id: InterfaceId, - name: String, - inner: Arc>>, - rx_queue: Arc>, - tx_queue: Arc>, - rx_wake: Arc, - tx_wake: Arc, -} -``` - -设计要求: - -- 所有真实设备共享一个 RX queue。 -- 每个真实设备有独立 TX queue。 -- 设备 worker 不访问 `Router` 本体。 -- `Router::poll()` 从 RX queue drain 到 smoltcp `rx_buffer`,并保留 ingress `InterfaceId`。 -- `Router::dispatch()` 从 smoltcp `tx_buffer` 取包,按 route 分发到 loopback 或 per-device TX queue。 -- 不需要单独的 `RouteSnapshot`;控制面和 Router 可以共享同一个 `SharedRouteTable`。 -- 不需要把所有 TX queue 放在 `RouterQueues.tx: Vec<_>` 中;TX queue 放进 `DeviceHandle` 更直接。 - -### 2.6 有界队列和 packet buffer - -队列要求: - -- RX/TX 队列必须有界。 -- RX 满时丢包并记录 warning/统计。 -- TX 满时按 socket/packet 语义返回 `WouldBlock`、重试或丢包。 -- 不使用无界 `SegQueue` 作为网络热路径队列。 -- 第一版可以使用 copy queue,但必须避免无界分配。 - -建议实现: - -```rust -struct BoundedPacketQueue { - inner: Mutex>, - capacity: usize, - len: AtomicUsize, -} - -struct QueuedPacket { - bytes: [u8; STANDARD_MTU], - len: usize, -} -``` - -说明: - -- inline `QueuedPacket` 可以避免每包 `Box<[u8]>` / `Vec::to_vec()` 分配。 -- 不强制引入 `PacketBufPool`;除非后续需要更复杂的 buffer ownership。 -- 端到端 zero-copy 不属于第一阶段目标,后续需要配合 `rd-net` buffer ownership 和 smoltcp token 适配。 - -### 2.7 设备 worker 与 poll 唤醒 - -设备 worker 不直接进入 smoltcp: - -```text -RX IRQ / RX worker - -> receive from device - -> push RxPacket into bounded rx queue - -> request_poll() - -net-poll worker - -> take Service lock - -> take SocketSet lock - -> Service::poll() - -TX worker - -> pop TxPacket from per-device tx queue - -> send through device -``` - -要求: - -- 每个非 loopback 设备可以启动一个 RX worker 和一个 TX worker。 -- IRQ handler 只唤醒对应 RX worker 或设置 queue event,不做重工作。 -- 无设备 IRQ 时,由平台轮询任务或 OOB RX poll task 唤醒设备。 -- RX worker 不忙等。 -- 设备 waker 唤醒对应 RX worker。 -- socket waker 根据 `DeviceBinding` 只注册到允许的设备路径。 - -### 2.8 net-poll worker - -`poll_interfaces()` 不再作为 socket 热路径同步入口。新的主路径是 `net-poll` worker: - -```text -socket send/connect/drop - -> update SocketSet state - -> request_poll() - -device RX - -> enqueue RxPacket - -> request_poll() - -timer - -> request_poll() - -net-poll worker - -> wait wake or deadline - -> poll_until_idle() -``` - -建议 API: - -```rust -pub fn poll_interfaces() { - request_poll(); -} - -pub fn request_poll() { - NET_POLL_REQUESTED.store(true, Ordering::Release); - NET_POLL_WAKE.notify_one(true); -} -``` - -要求: - -- `poll_interfaces()` 保留为 public trigger/debug API,但不再同步执行 `Service::poll()`。 -- `net_poll_worker()` 独占调用 `poll_until_idle()`。 -- `poll_until_idle()` 使用全局原子标志防重入。 -- `poll_until_idle()` 在有工作时批量 poll,不在每次成功 poll 后主动 `yield_now()`。 -- socket send/recv/connect/accept/drop 热路径只 `request_poll()`。 - -### 2.9 控制面状态与事务更新 - -DHCP ACK 等运行期更新不能暴露半更新状态。 - -建议更新对象: - -```rust -struct NetworkStateUpdate { - interface_id: InterfaceId, - dev: usize, - metric: u32, - old_ipv4: Option, - ipv4: Option, - gateway: Option, - dns_source: DnsSource, - dns_servers: Vec, -} -``` - -提交规则: - -- DHCP 解析在 `Service::poll()` 中完成。 -- DHCP ACK/NAK 先生成 `NetworkStateUpdate`。 -- smoltcp `Interface` 的 IP 地址、接口 registry、DNS entries、route table 必须作为一个逻辑事务更新。 -- 对外查询 API 要么看到旧状态,要么看到新状态,不暴露半更新状态。 -- 如果控制面更新逻辑变重,再进一步缩窄写锁范围。 - -### 2.10 Socket 绑定索引与快路径 - -目标: - -- TCP/UDP bind 冲突检查不在热路径扫描整个 `SocketSet`。 -- wildcard bind 与具体地址 bind 的冲突语义统一。 -- 普通 socket 使用 `DeviceBinding { bound_if: Option }`,不再使用 `u32 device_mask`。 - -建议: - -```rust -pub struct DeviceBinding { - pub bound_if: Option, -} -``` - -语义: - -- `None`:未绑定接口,按 route decision 或 wildcard 语义工作。 -- `Some(id)`:由 `SO_BINDTODEVICE`、绑定具体本地地址或 AF_PACKET ifindex 得到。 - -要求: - -- `SO_BINDTODEVICE` 存入通用 socket options。 -- 绑定具体本地地址时,通过控制面反查接口并设置 `DeviceBinding`。 -- TCP 使用独立 bind/listen 侧表维护 wildcard/specific 地址冲突。 -- UDP 使用 `SocketSetWrapper` 内部 side table 维护 bind 冲突。 -- `ListenTable` 支持 per-address listen 和 accept waker。 -- 不承诺完整 `reuseport`;当前重点是 `SO_REUSEADDR` 和 bind/listen 冲突语义。 -- 不必新增统一 `SocketRegistry` 类型,避免重复维护 TCP/UDP 生命周期。 - -### 2.11 DNS 与 DHCP - -DHCP: - -- 每个 Ethernet 接口独立 `DhcpState`。 -- DHCP packet 根据 ingress `InterfaceId` 分发。 -- DHCP ACK 生成 `NetworkStateUpdate`。 -- DHCP NAK 或失败只影响对应接口。 -- DHCP bootstrap 不应要求所有 DHCP 接口成功,避免断开的网卡阻塞系统启动。 - -DNS: - -- DHCP DNS、接口级静态 DNS、全局 fallback DNS 都记录为 `DnsServerEntry`。 -- `DnsServerEntry` 保留 server、interface_id、metric 和 source。 -- `dns_servers()` 返回按 metric 排序、去重后的地址列表。 -- `dns_query_timeout()` 选择 DNS server 前检查可路由性,不可路由则尝试下一个。 -- 不实现 DNS cache、split DNS、`/etc/resolv.conf`。 - -### 2.12 广播、组播与 loopback - -广播: - -- limited broadcast 可发送到所有非 loopback Ethernet 设备。 -- 绑定具体接口时,应尽量限制到该接口。 -- 子网广播按接口 IPv4/prefix 选择接口,后续补齐。 - -组播: - -- IPv4 multicast 至少按绑定接口或 route decision 选择出接口。 -- 未绑定接口的 multicast 使用默认 route 或接口集合策略。 -- IGMP/MLD membership 不在本轮范围。 - -Loopback: - -- `LoopbackDevice` 只作为 `lo` 接口占位。 -- loopback 不启动 RX/TX worker。 -- 普通 smoltcp TX 选中 loopback 时,`Router::dispatch()` 应直接注入 `Router.rx_buffer`,不走共享 RX queue 和设备 worker。 -- loopback 注入前应执行 TCP SYN snoop,保证回环 listen/accept 能在同一 poll 周期推进。 - -### 2.13 对外 API - -正式 API: - -```rust -pub fn init_network(net_devs: EthernetDeviceList, config: NetworkConfig); -pub fn poll_interfaces(); -pub fn request_poll(); - -pub fn interfaces() -> Vec; -pub fn interface_by_name(name: &str) -> Option; -pub fn interface_by_id(id: InterfaceId) -> Option; -pub fn ipv4_config(name: &str) -> Option; -pub fn default_routes() -> Vec; -pub fn arp_entries() -> Vec; - -pub fn dns_query(name: &str) -> AxResult>; -pub fn dns_query_timeout(name: &str, timeout: Duration) -> AxResult>; -pub fn dns_servers() -> Vec; -``` - -说明: - -- `poll_interfaces()` 是轻量触发入口,不作为 socket 热路径同步 poll。 -- `eth0_ipv4_config()` 这类 convenience helper 可以保留,但新代码应优先使用 `ipv4_config(name)` 和接口 registry API。 -- 不提供 `u32 device_mask` 兼容转换。 - -### 2.14 StarryOS、ArceOS 与 Axvisor 接入 - -StarryOS 网络 ABI 层必须从 `lo + eth0` 固定模型迁移到 `ax-net` 接口 registry。 - -涉及方向: - -- `SIOCGIFCONF` 遍历 `ax_net::interfaces()`。 -- `SIOCGIFADDR`、`SIOCGIFBRDADDR`、`SIOCGIFNETMASK` 按接口名查询。 -- `SIOCGIFHWADDR` 返回真实 MAC 或 loopback 类型。 -- `SIOCGIFINDEX` 返回 `InterfaceInfo::id.to_linux_ifindex()`。 -- AF_PACKET bind 使用 ifindex 映射 `InterfaceId`。 -- `/proc/net/arp` 使用 `ax_net::arp_entries()`,device 字段必须是真实接口名。 -- `SO_BINDTODEVICE` 映射到 `DeviceBinding { bound_if: Some(id) }`。 -- StarryOS 不缓存第二套 IPv4/gateway/MAC 状态。 - -ArceOS: - -- `ax-runtime` 构造结构化 `NetworkConfig` 并调用 `ax_net::init_network()`。 -- `ax-api`、`ax-posix-api` 使用统一 socket API。 -- 面向应用的 API 不增加 `eth0` 假设。 - -Axvisor: - -- 复用 `ax-net` 接口 registry、route decision 和 socket API。 -- 管理面、VM 服务面通过接口级配置表达网络意图。 - -## 3. 测试方案 - -单元测试: - -- 多接口 route lookup。 -- 默认路由 metric 选择。 -- route 查不到返回错误,不 panic。 -- DHCP packet 按 `InterfaceId` 分发。 -- `NetworkStateUpdate` commit 前后状态一致。 -- bounded RX/TX queue 满时行为正确。 -- `Router` RX metadata 保留 ingress `InterfaceId`。 -- `DeviceBinding` route/waker 过滤语义正确。 -- TCP/UDP socket 绑定索引冲突检查正确。 -- loopback direct injection 能在同一 poll 周期推进 TCP SYN。 - -集成测试: - -- QEMU 单网卡 DHCP。 -- QEMU 多网卡静态配置。 -- TCP/UDP bind 到 `eth1` 地址后 route decision 选择 `eth1`。 -- StarryOS `SIOCGIFCONF` 能看到多个接口。 -- StarryOS `SIOCGIFADDR`、`SIOCGIFHWADDR`、`SIOCGIFINDEX` 可按接口名查询。 -- StarryOS AF_PACKET bind 到指定 ifindex 后返回正确 `sockaddr_ll`。 -- `/proc/net/arp` device 字段正确。 -- DNS server 来自 DHCP/静态/fallback 时查询路径不退化。 - -性能验证: - -- 对比同步 `poll_interfaces()` 与 `request_poll()` 的 socket 热路径耗时。 -- 多 NIC 同时 RX 时验证 bounded queue、公平性和丢包行为。 -- 长时间 UDP/TCP 压测验证队列不会无界增长。 -- 验证 route/interface/DNS 查询不会长时间阻塞 `net-poll` worker。 - -## 4. 遗留问题 - -- smoltcp 协议栈处理仍然是单实例串行。设备队列解耦不能让 TCP/UDP 协议处理本身多核并行。 -- Zero-copy RX/TX 需要继续改造 `rd-net` buffer ownership、packet pool 和 smoltcp token 适配。 -- 完整 DHCP lease renew/rebind、过期回收、地址冲突检测仍需后续补齐。 -- 完整 IPv6、邻居发现、IPv6 route、AAAA DNS 查询不在本轮范围。 -- IGMP/MLD、按接口 multicast membership 不在本轮范围。 -- split DNS、`/etc/resolv.conf`、net namespace resolver 策略不在本轮范围。 -- 动态 link down/up、接口热插拔、队列重建和 socket 错误传播仍需后续完善。 -- RSS、多队列网卡、per-queue poll、NAPI 类批量调度属于后续更底层 dataplane 优化。 From e860ec1d50a270da63d377fb8c492c96050ee3ed Mon Sep 17 00:00:00 2001 From: ZCShou <72115@163.com> Date: Tue, 16 Jun 2026 14:09:27 +0800 Subject: [PATCH 29/31] fix(ax-net): enlarge shared RX queue and add APK download stress test to dual-net --- apps/starry/qemu/dual-net/c/dual-net-tests.sh | 72 +++++++++++++++++++ apps/starry/qemu/dual-net/qemu-riscv64.toml | 2 +- apps/starry/qemu/dual-net/qemu-x86_64.toml | 2 +- docs/docs/architecture/net/testing.md | 24 ++++++- net/ax-net/src/consts.rs | 13 ++++ net/ax-net/src/router.rs | 11 ++- net/ax-net/src/service.rs | 5 +- scripts/axbuild/src/starry/test.rs | 15 ++++ 8 files changed, 136 insertions(+), 8 deletions(-) diff --git a/apps/starry/qemu/dual-net/c/dual-net-tests.sh b/apps/starry/qemu/dual-net/c/dual-net-tests.sh index a2e2707860..dad71bc293 100644 --- a/apps/starry/qemu/dual-net/c/dual-net-tests.sh +++ b/apps/starry/qemu/dual-net/c/dual-net-tests.sh @@ -10,6 +10,10 @@ need_cmd() { command -v "$1" >/dev/null 2>&1 || fail "missing command $1" } +APK_STRESS_PACKAGE="${APK_STRESS_PACKAGE:-python3}" +APK_STRESS_MIN_BYTES="${APK_STRESS_MIN_BYTES:-8388608}" +APK_STRESS_RETRIES="${APK_STRESS_RETRIES:-3}" + now_ms() { ns="$(date +%s%N 2>/dev/null || true)" case "$ns" in @@ -31,6 +35,30 @@ iface_addr_contains() { grep -qF "$expected" "/tmp/dual-net-$iface.ipaddr" } +prepare_apk_repositories() { + if [ -f /etc/apk/repositories ]; then + sed -i 's|https://|http://|g' /etc/apk/repositories + echo "DUAL_NET_APK_REPOSITORIES_BEGIN" + cat /etc/apk/repositories + echo "DUAL_NET_APK_REPOSITORIES_END" + fi +} + +retry_cmd() { + desc="$1" + shift + attempt=1 + while [ "$attempt" -le "$APK_STRESS_RETRIES" ]; do + if "$@"; then + return 0 + fi + echo "DUAL_NET_RETRY: $desc attempt=$attempt/$APK_STRESS_RETRIES failed" + attempt="$((attempt + 1))" + sleep 2 + done + return 1 +} + fetch_with_iface() { iface="$1" host="$2" @@ -64,10 +92,52 @@ wait_fetch() { rm -f "$out" "/tmp/dual-net-$tag.meta" } +apk_fetch_verify() { + dir="/tmp/dual-net-apk-fetch" + sum="/tmp/dual-net-apk-fetch.sha256" + rm -rf "$dir" "$sum" + mkdir -p "$dir" + + prepare_apk_repositories + + echo "DUAL_NET_APK_UPDATE_BEGIN" + retry_cmd "apk update" apk update || + fail "apk update failed" + echo "DUAL_NET_APK_UPDATE_DONE" + + start="$(now_ms)" + retry_cmd "apk fetch $APK_STRESS_PACKAGE" apk fetch -R -o "$dir" "$APK_STRESS_PACKAGE" || + fail "apk fetch failed for $APK_STRESS_PACKAGE" + end="$(now_ms)" + + total=0 + count=0 + : > "$sum" + for pkg in "$dir"/*.apk; do + [ -e "$pkg" ] || fail "apk fetch produced no .apk files" + apk verify "$pkg" || fail "apk verify failed for $pkg" + sha256sum "$pkg" >> "$sum" + bytes="$(wc -c < "$pkg" | tr -d ' ')" + total="$((total + bytes))" + count="$((count + 1))" + done + + [ "$total" -ge "$APK_STRESS_MIN_BYTES" ] || + fail "apk fetch too small: total=$total min=$APK_STRESS_MIN_BYTES" + + sha256sum -c "$sum" || + fail "apk sha256 verification failed" + + echo "DUAL_NET_APK_FETCH_MS=$((end - start)) BYTES=$total PACKAGES=$count PACKAGE=$APK_STRESS_PACKAGE" + rm -rf "$dir" "$sum" +} + echo "DUAL_NET_TEST_BEGIN" need_cmd ifconfig need_cmd ip need_cmd curl +need_cmd apk +need_cmd sha256sum if iface_addr_contains eth0 10.0.2.15; then echo "DUAL_NET_ETH0_ADDR_OK" @@ -103,4 +173,6 @@ wait_fetch "$pid1" ETH1_PARALLEL parallel_end="$(now_ms)" echo "DUAL_NET_FETCH_PARALLEL_MS=$((parallel_end - parallel_start))" +apk_fetch_verify + echo "DUAL_NET_TEST_PASSED" diff --git a/apps/starry/qemu/dual-net/qemu-riscv64.toml b/apps/starry/qemu/dual-net/qemu-riscv64.toml index 4d86adcaf1..0c435e84d9 100644 --- a/apps/starry/qemu/dual-net/qemu-riscv64.toml +++ b/apps/starry/qemu/dual-net/qemu-riscv64.toml @@ -23,7 +23,7 @@ shell_prefix = "root@starry:" shell_init_cmd = "/usr/bin/dual-net-tests.sh" success_regex = ["(?m)^DUAL_NET_TEST_PASSED\\s*$"] fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^lockdep fatal violation\\s*$", "(?m)^DUAL_NET_TEST_FAILED:"] -timeout = 240 +timeout = 360 [host_http_server] bind = "127.0.0.1" diff --git a/apps/starry/qemu/dual-net/qemu-x86_64.toml b/apps/starry/qemu/dual-net/qemu-x86_64.toml index bacc712310..483ab13c20 100644 --- a/apps/starry/qemu/dual-net/qemu-x86_64.toml +++ b/apps/starry/qemu/dual-net/qemu-x86_64.toml @@ -21,7 +21,7 @@ shell_prefix = "root@starry:" shell_init_cmd = "/usr/bin/dual-net-tests.sh" success_regex = ["(?m)^DUAL_NET_TEST_PASSED\\s*$"] fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^lockdep fatal violation\\s*$", "(?m)^DUAL_NET_TEST_FAILED:"] -timeout = 240 +timeout = 360 [host_http_server] bind = "127.0.0.1" diff --git a/docs/docs/architecture/net/testing.md b/docs/docs/architecture/net/testing.md index 820f7bd7df..d495a516d7 100644 --- a/docs/docs/architecture/net/testing.md +++ b/docs/docs/architecture/net/testing.md @@ -161,7 +161,7 @@ system 测试使用 StarryOS guest 内的 Linux 用户态程序验证 syscall/AB ## dual-net 集成测试 -`apps/starry/qemu/dual-net` 是双网卡集成测试,用于验证多设备初始化、双 DHCP、route table、接口绑定和并发收发。它是 Starry app 级 QEMU 场景,不属于 `test-suit/starryos` system 分组。 +`apps/starry/qemu/dual-net` 是双网卡集成测试,用于验证多设备初始化、双 DHCP、route table、接口绑定、并发收发和较大 APK 下载校验。它是 Starry app 级 QEMU 场景,不属于 `test-suit/starryos` system 分组。 ### 运行方式 @@ -209,6 +209,11 @@ host HTTP server -> 127.0.0.1:18382 on host -> exposed through each QEMU user net gateway -> payload size 1 MiB, byte value 68 + +Alpine APK repositories + -> accessed from guest through QEMU user networking + -> apk fetch -R downloads package files and dependencies + -> apk verify + sha256sum -c validates downloaded files ``` `qemu-*.toml` 会启动 host HTTP server: @@ -227,7 +232,7 @@ guest 启动后自动执行: /usr/bin/dual-net-tests.sh ``` -脚本来自 [apps/starry/qemu/dual-net/c/dual-net-tests.sh](apps/starry/qemu/dual-net/c/dual-net-tests.sh)。[prebuild.sh](apps/starry/qemu/dual-net/c/prebuild.sh) 会安装 `curl`,[CMakeLists.txt](apps/starry/qemu/dual-net/c/CMakeLists.txt) 会把 `curl` 和 `dual-net-tests.sh` 安装进 guest rootfs。 +脚本来自 [apps/starry/qemu/dual-net/c/dual-net-tests.sh](apps/starry/qemu/dual-net/c/dual-net-tests.sh)。[prebuild.sh](apps/starry/qemu/dual-net/c/prebuild.sh) 会安装 `curl`,[CMakeLists.txt](apps/starry/qemu/dual-net/c/CMakeLists.txt) 会把 `curl` 和 `dual-net-tests.sh` 安装进 guest rootfs。`apk` 和 `sha256sum` 来自 Alpine rootfs 的基础工具集。 ### Guest 检查项 @@ -238,6 +243,12 @@ guest 启动后自动执行: - `curl --interface eth0 http://10.0.2.2:18382/payload.bin?...` 能下载至少 1 MiB。 - `curl --interface eth1 http://10.0.3.2:18382/payload.bin?...` 能下载至少 1 MiB。 - 串行下载完成后,再并发从 eth0/eth1 下载。 +- `apk update` 能从 guest 访问 Alpine APK repository。 +- `apk fetch -R -o /tmp/dual-net-apk-fetch python3` 能下载 `python3` 及依赖包。 +- `apk update` 和 `apk fetch` 默认最多重试 3 次,避免外部 mirror 或 QEMU user networking 的短暂抖动导致误报。 +- 下载到本地的 `.apk` 总大小必须不少于 8 MiB。 +- 每个 `.apk` 必须通过 `apk verify`。 +- 生成下载文件的 sha256 清单后,必须通过 `sha256sum -c` 回读校验。 成功输出包含: @@ -248,6 +259,7 @@ DUAL_NET_FETCH_ETH0_SINGLE_MS=... BYTES=1048576 DUAL_NET_FETCH_ETH1_SINGLE_MS=... BYTES=1048576 DUAL_NET_FETCH_ETH0_PARALLEL_MS=... BYTES=1048576 DUAL_NET_FETCH_ETH1_PARALLEL_MS=... BYTES=1048576 +DUAL_NET_APK_FETCH_MS=... BYTES=... PACKAGES=... PACKAGE=python3 DUAL_NET_TEST_PASSED ``` @@ -267,6 +279,8 @@ DUAL_NET_TEST_FAILED: ... - route table 同时存在 `10.0.2.0/24` 和 `10.0.3.0/24` connected route。 - `curl --interface` 通过 Linux ABI 映射到接口绑定,限制 route lookup。 - 串行和并发下载验证 per-device TX queue、共享 RX queue、device worker 和 net-poll worker 可以持续推进。 +- `apk fetch -R` 下载较大的包集合并写入磁盘,验证较长 TCP 流、DNS、默认路由和文件写入路径的组合稳定性。 +- `apk verify` 验证 APK 内置签名/完整性元数据,`sha256sum -c` 验证落盘文件再次读取后的内容一致性。 ### xtask 结构自检 @@ -279,6 +293,8 @@ DUAL_NET_TEST_FAILED: ... - net1 必须是 `10.0.3.0/24` 且 DHCP 起始地址为 `10.0.3.15`。 - `shell_init_cmd` 必须是 `/usr/bin/dual-net-tests.sh`。 - host HTTP server 必须监听 18382,payload 至少 1 MiB。 +- `dual-net-tests.sh` 必须包含 `apk fetch -R`、APK 重试、`apk verify`、`sha256sum -c` 和 `DUAL_NET_APK_FETCH_MS`。 +- QEMU timeout 必须足够覆盖 APK 下载校验流程。 这个结构测试防止 app 配置被误删、改成单网卡或失去自动 guest probe。 @@ -292,6 +308,10 @@ DUAL_NET_TEST_FAILED: ... | eth0 成功、eth1 curl 失败 | `SO_BINDTODEVICE` / `curl --interface` 是否映射到 eth1;route table 是否有 `10.0.3.0/24` connected route | | 串行成功、并发失败 | RX/TX worker 是否被正确唤醒;队列是否满;net-poll worker 是否持续 poll | | 下载字节数小于 1 MiB | TCP receive/send readiness、host HTTP server 暴露、QEMU user net 或 curl 超时 | +| `apk fetch too small` | APK package 依赖集合是否变化;`APK_STRESS_MIN_BYTES` 是否需要随 Alpine 版本调整 | +| `apk verify failed` 或 `sha256sum -c` 失败 | 长连接下载、TCP 重组、文件写入或读回路径存在数据损坏 | +| `apk update` 失败 | guest 默认路由、DNS、外网连通性、Alpine mirror 可达性 | +| 出现 `DUAL_NET_RETRY` 后最终通过 | 外部 APK 下载路径发生过短暂 I/O error,但最终文件完整性校验通过 | | QEMU timeout | 是否缺少 `curl`、`ip`、`ifconfig`;shell init command 是否执行到 `DUAL_NET_TEST_PASSED` | ### `STARRY_GROUPED_TEST_FAILED` diff --git a/net/ax-net/src/consts.rs b/net/ax-net/src/consts.rs index c7d59754ac..506103fbab 100644 --- a/net/ax-net/src/consts.rs +++ b/net/ax-net/src/consts.rs @@ -25,6 +25,19 @@ pub const LISTEN_QUEUE_SIZE: usize = 512; pub const SOCKET_BUFFER_SIZE: usize = 64; +/// Shared device-to-router RX queue capacity. +/// +/// This queue absorbs packets produced by per-device RX workers before the +/// single smoltcp protocol core can drain them. It is intentionally larger than +/// the smoltcp-facing packet buffer: internet downloads and APK index fetches +/// can deliver short RX bursts faster than the net-poll worker gets scheduled, +/// especially on single-core QEMU targets. +/// +/// 256 slots × 1500 bytes = 384 KiB total for the shared RX queue. The queue is +/// still bounded, but large enough to avoid turning ordinary TCP burstiness +/// into packet loss. +pub const DEVICE_RX_QUEUE_SIZE: usize = 256; + /// Per-device TX queue capacity. /// /// Sized to absorb bursty traffic without drops while keeping memory bounded. diff --git a/net/ax-net/src/router.rs b/net/ax-net/src/router.rs index 97efd5d8d5..8b76068753 100644 --- a/net/ax-net/src/router.rs +++ b/net/ax-net/src/router.rs @@ -65,7 +65,7 @@ use spin::RwLock; use crate::{ LISTEN_TABLE, config::{DeviceBinding, InterfaceId, RouteInfo}, - consts::{DEVICE_TX_QUEUE_SIZE, SOCKET_BUFFER_SIZE, STANDARD_MTU}, + consts::{DEVICE_RX_QUEUE_SIZE, DEVICE_TX_QUEUE_SIZE, SOCKET_BUFFER_SIZE, STANDARD_MTU}, device::{ArpEntry, Device}, }; @@ -436,7 +436,7 @@ impl Router { vec![0u8; STANDARD_MTU * SOCKET_BUFFER_SIZE], ); let queues = Arc::new(RouterQueues { - rx: Arc::new(BoundedPacketQueue::new(SOCKET_BUFFER_SIZE)), + rx: Arc::new(BoundedPacketQueue::new(DEVICE_RX_QUEUE_SIZE)), }); Self { rx_buffer, @@ -574,9 +574,10 @@ impl Router { _timestamp: Instant, sockets: &mut SocketSet<'_>, mut snoop: impl FnMut(InterfaceId, &[u8]), - ) { + ) -> bool { // Drain worker-produced packets into the smoltcp-facing RX buffer. // smoltcp later consumes this buffer through Device::receive(). + let mut moved_rx = false; while !self.rx_buffer.is_full() { let Some(packet) = self.queues.rx.pop() else { break; @@ -589,7 +590,9 @@ impl Router { break; }; dst.copy_from_slice(bytes); + moved_rx = true; } + moved_rx || !self.queues.rx.is_empty() } /// Sends a control-plane packet on a specific device. @@ -826,6 +829,8 @@ fn device_rx_worker(device: Arc) { }; if device.rx_queue.push(rx).is_err() { warn!("{}: RX queue is full, dropping packet", device.name); + crate::request_poll(); + ax_task::yield_now(); break; } crate::request_poll(); diff --git a/net/ax-net/src/service.rs b/net/ax-net/src/service.rs index c3642a3f5c..619d5f6c35 100644 --- a/net/ax-net/src/service.rs +++ b/net/ax-net/src/service.rs @@ -656,11 +656,13 @@ impl Service { let timestamp = now(); let mut dhcp_events = Vec::new(); let mut dhcp_server_replies = Vec::new(); + let router_rx_pending; { let dhcp = &mut self.dhcp; let dhcp_server = &mut self.dhcp_server; - self.router + router_rx_pending = self + .router .poll(timestamp, sockets, |interface_id, packet| { for state in dhcp.iter_mut() { if let Some(event) = state.process_packet(interface_id, packet, timestamp) { @@ -697,6 +699,7 @@ impl Service { || dhcp_poll_next || dhcp_server_sent || socket_state_changed + || router_rx_pending } pub fn next_poll_at(&mut self, sockets: &SocketSet) -> Option { diff --git a/scripts/axbuild/src/starry/test.rs b/scripts/axbuild/src/starry/test.rs index 2789d79979..43f4bfa04e 100644 --- a/scripts/axbuild/src/starry/test.rs +++ b/scripts/axbuild/src/starry/test.rs @@ -2475,6 +2475,14 @@ mod tests { .is_some_and(|size| size >= 1024 * 1024), "dual-net case must fetch a payload large enough to expose obvious regressions" ); + assert!( + config + .get("timeout") + .and_then(toml::Value::as_integer) + .is_some_and(|timeout| timeout >= 360), + "{} must leave enough time for the apk package download stability probe", + config_path.display() + ); } let script = fs::read_to_string(&script_path).unwrap(); @@ -2486,6 +2494,13 @@ mod tests { "fetch_with_iface eth0 10.0.2.2", "fetch_with_iface eth1 10.0.3.2", "DUAL_NET_FETCH_PARALLEL_MS", + "apk fetch -R", + "APK_STRESS_MIN_BYTES", + "APK_STRESS_RETRIES", + "DUAL_NET_RETRY", + "apk verify", + "sha256sum -c", + "DUAL_NET_APK_FETCH_MS", "DUAL_NET_TEST_PASSED", "DUAL_NET_TEST_FAILED", ] { From ddfdf3009dab6c182438ca83e67dac4716999644 Mon Sep 17 00:00:00 2001 From: ZCShou <72115@163.com> Date: Tue, 16 Jun 2026 14:59:31 +0800 Subject: [PATCH 30/31] docs(net): add memory and queue model doc and cross-reference from architecture pages --- docs/docs/architecture/net/api.md | 2 +- docs/docs/architecture/net/architecture.md | 1 + docs/docs/architecture/net/configuration.md | 5 +- docs/docs/architecture/net/devices.md | 8 +- docs/docs/architecture/net/flows.md | 2 +- docs/docs/architecture/net/integration.md | 2 +- docs/docs/architecture/net/memory.md | 445 ++++++++++++++++++++ docs/docs/architecture/net/overview.md | 1 + docs/docs/architecture/net/testing.md | 2 +- 9 files changed, 461 insertions(+), 7 deletions(-) create mode 100644 docs/docs/architecture/net/memory.md diff --git a/docs/docs/architecture/net/api.md b/docs/docs/architecture/net/api.md index ff3872ad2a..ba55bdbe47 100644 --- a/docs/docs/architecture/net/api.md +++ b/docs/docs/architecture/net/api.md @@ -1,5 +1,5 @@ --- -sidebar_position: 6 +sidebar_position: 7 sidebar_label: "对外接口" --- diff --git a/docs/docs/architecture/net/architecture.md b/docs/docs/architecture/net/architecture.md index b739a804b5..b448a4a0ec 100644 --- a/docs/docs/architecture/net/architecture.md +++ b/docs/docs/architecture/net/architecture.md @@ -281,6 +281,7 @@ sequenceDiagram | loopback fast path | 回环包直接写入 `rx_buffer`,不经过设备 worker | `Router::poll()` 负责把设备 RX 队列推进到 smoltcp RX buffer;`Router::dispatch()` 负责把 smoltcp TX buffer 中的包按路由分发到 loopback 或真实设备。更细的 worker、队列和 ARP 行为见[多设备实现](devices.md)。 +从驱动 buffer 到用户 buffer、从用户 buffer 到驱动 TX buffer 的完整内存链路见[内存与队列](memory.md)。 ## Device layer diff --git a/docs/docs/architecture/net/configuration.md b/docs/docs/architecture/net/configuration.md index 59ffdf2d1d..23914800c3 100644 --- a/docs/docs/architecture/net/configuration.md +++ b/docs/docs/architecture/net/configuration.md @@ -1,5 +1,5 @@ --- -sidebar_position: 7 +sidebar_position: 8 sidebar_label: "配置参考" --- @@ -271,6 +271,7 @@ pub const RAW_TX_BUF_LEN: usize = 64 * 1024; ```rust pub const STANDARD_MTU: usize = 1500; pub const SOCKET_BUFFER_SIZE: usize = 64; +pub const DEVICE_RX_QUEUE_SIZE: usize = 256; pub const DEVICE_TX_QUEUE_SIZE: usize = 128; pub const ETHERNET_MAX_PENDING_PACKETS: usize = 128; pub const LISTEN_QUEUE_SIZE: usize = 512; @@ -280,11 +281,13 @@ pub const LISTEN_QUEUE_SIZE: usize = 512; | --- | --- | | `STANDARD_MTU` | Router 和 Ethernet 默认 MTU | | `SOCKET_BUFFER_SIZE` | Router RX/TX smoltcp-facing packet buffer 槽位数 | +| `DEVICE_RX_QUEUE_SIZE` | 所有真实设备共享的 device-to-Router RX queue 槽位数 | | `DEVICE_TX_QUEUE_SIZE` | 每设备 TX queue 槽位数 | | `ETHERNET_MAX_PENDING_PACKETS` | ARP resolution pending packet 上限 | | `LISTEN_QUEUE_SIZE` | TCP listen backlog clamp 上限 | Router queue 中的 packet 使用 inline `[u8; STANDARD_MTU] + len`,不为每个 queued packet 分配 `Box<[u8]>`。 +更完整的拷贝边界、队列满行为和内存预算见[内存与队列](memory.md)。 ### Unix Stream Buffer diff --git a/docs/docs/architecture/net/devices.md b/docs/docs/architecture/net/devices.md index ffb307373c..c8fb07cbcc 100644 --- a/docs/docs/architecture/net/devices.md +++ b/docs/docs/architecture/net/devices.md @@ -271,7 +271,7 @@ struct BoundedPacketQueue { - `push()` 满时返回 `Err(packet)`,调用方丢包并记录 warning。 - `pop()` 空时返回 `None`。 - `is_empty()` 只读原子 `len`,用于 worker wait predicate。 -- 队列容量由 `SOCKET_BUFFER_SIZE` 和 `DEVICE_TX_QUEUE_SIZE` 控制。 +- 共享 RX queue 容量由 `DEVICE_RX_QUEUE_SIZE` 控制;per-device TX queue 容量由 `DEVICE_TX_QUEUE_SIZE` 控制。 ### QueuedPacket @@ -305,6 +305,7 @@ RX 需要保存 ingress `InterfaceId`,用于 DHCP 分发、诊断和后续扩 ## 数据路径 数据路径分为设备 RX、smoltcp poll、TX dispatch 和 loopback 快速路径。所有路径都围绕 `Service::poll()` 批量推进。 +端到端的内存所有权、拷贝次数、队列满行为和预算估算见[内存与队列](memory.md)。 ### RX Path @@ -326,7 +327,8 @@ pub fn poll( _timestamp: Instant, sockets: &mut SocketSet<'_>, mut snoop: impl FnMut(InterfaceId, &[u8]), -) { +) -> bool { + let mut moved_rx = false; while !self.rx_buffer.is_full() { let Some(packet) = self.queues.rx.pop() else { break; @@ -338,7 +340,9 @@ pub fn poll( break; }; dst.copy_from_slice(bytes); + moved_rx = true; } + moved_rx || !self.queues.rx.is_empty() } ``` diff --git a/docs/docs/architecture/net/flows.md b/docs/docs/architecture/net/flows.md index 7f41e33aed..c4f496fd18 100644 --- a/docs/docs/architecture/net/flows.md +++ b/docs/docs/architecture/net/flows.md @@ -1,5 +1,5 @@ --- -sidebar_position: 8 +sidebar_position: 9 sidebar_label: "运行时流程" --- diff --git a/docs/docs/architecture/net/integration.md b/docs/docs/architecture/net/integration.md index 0ff3d0d33a..e7262f05ef 100644 --- a/docs/docs/architecture/net/integration.md +++ b/docs/docs/architecture/net/integration.md @@ -1,5 +1,5 @@ --- -sidebar_position: 9 +sidebar_position: 10 sidebar_label: "系统集成" --- diff --git a/docs/docs/architecture/net/memory.md b/docs/docs/architecture/net/memory.md new file mode 100644 index 0000000000..cae3f0decd --- /dev/null +++ b/docs/docs/architecture/net/memory.md @@ -0,0 +1,445 @@ +--- +sidebar_position: 6 +sidebar_label: "内存与队列" +--- + +# 内存与队列 + +`ax-net` 的数据面内存模型以有界队列和明确拷贝边界为核心。当前实现不承诺端到端 zero-copy;它优先保证嵌入式/unikernel 场景下的内存上限、锁边界和协议栈所有权清晰。RX 方向从驱动 buffer 进入 Router 队列,再进入 smoltcp socket buffer,最终复制到用户 buffer;TX 方向从用户 buffer 写入 smoltcp socket buffer,再进入 Router TX buffer、设备 TX queue,最后交给驱动发送。 + +核心源码: + +| 源码 | 职责 | +| --- | --- | +| [consts.rs](net/ax-net/src/consts.rs) | socket buffer、Router packet buffer、设备 RX/TX queue 容量 | +| [router.rs](net/ax-net/src/router.rs) | `BoundedPacketQueue`、`QueuedPacket`、`Router.rx_buffer` / `tx_buffer`、RX/TX worker | +| [service.rs](net/ax-net/src/service.rs) | `Service::poll()` 中的 RX drain、smoltcp poll、TX dispatch 顺序 | +| [device/driver.rs](net/ax-net/src/device/driver.rs) | `rd-net` buffer 适配、`VecRxBuffer` / `VecTxBuffer`、RX prefetch | +| [device/ethernet.rs](net/ax-net/src/device/ethernet.rs) | Ethernet 解封装、ARP pending packet、driver TX buffer 写入 | +| [tcp.rs](net/ax-net/src/tcp.rs)、[udp.rs](net/ax-net/src/udp.rs)、[raw.rs](net/ax-net/src/raw.rs) | 用户 buffer 与 smoltcp socket buffer 之间的收发拷贝 | + +## 总体模型 + +数据面分成四个内存域: + +| 内存域 | 典型对象 | 所有者 | 生命周期 | +| --- | --- | --- | --- | +| Driver buffer | `NetRxBuffer` / `NetTxBuffer`、`rd_net::RxQueue` / `TxQueue` | 具体网卡驱动或 `RdNetDriver` | 单个收发操作或驱动队列周期 | +| Router queue / packet buffer | `QueuedPacket`、`RxPacket`、`TxPacket`、`Router.rx_buffer`、`Router.tx_buffer` | `Router` / device worker | packet 在设备 worker 与 net-poll worker 之间流转期间 | +| smoltcp socket buffer | TCP `SocketBuffer`、UDP/raw `PacketBuffer` | 全局 `SocketSet` 中的具体 socket | socket 生命周期内固定分配 | +| 用户 buffer | syscall 传入的 `Read` / `Write` / `IoBufMut` | 调用者线程 | 单次 `send()` / `recv()` 调用 | + +总体关系如下: + +```mermaid +flowchart LR + subgraph User["用户/系统调用线程"] + UserTx["send/write 用户 buffer"] + UserRx["recv/read 用户 buffer"] + end + + subgraph SocketSet["SocketSet / smoltcp socket buffer"] + TcpBuf["TCP byte buffer"] + UdpRawBuf["UDP/raw packet buffer"] + end + + subgraph Core["net-poll worker: Service + smoltcp Interface + Router"] + RxBuf["Router.rx_buffer
SOCKET_BUFFER_SIZE"] + TxBuf["Router.tx_buffer
SOCKET_BUFFER_SIZE"] + Poll["Interface::poll()"] + Dispatch["Router::dispatch()"] + end + + subgraph Queues["设备 worker 队列"] + SharedRx["shared RX queue
DEVICE_RX_QUEUE_SIZE"] + TxQ0["eth0 TX queue
DEVICE_TX_QUEUE_SIZE"] + TxQ1["eth1 TX queue
DEVICE_TX_QUEUE_SIZE"] + end + + subgraph Driver["驱动/网卡内存"] + RxMem["RX ring / NetRxBuffer"] + TxMem["TX ring / NetTxBuffer"] + end + + RxMem -->|"copy: EthernetDevice::recv"| SharedRx + SharedRx -->|"copy: Router::poll"| RxBuf + RxBuf --> Poll + Poll --> TcpBuf + Poll --> UdpRawBuf + TcpBuf -->|"copy: recv"| UserRx + UdpRawBuf -->|"copy: recv"| UserRx + + UserTx -->|"copy: send"| TcpBuf + UserTx -->|"copy: send"| UdpRawBuf + TcpBuf --> Poll + UdpRawBuf --> Poll + Poll --> TxBuf + TxBuf --> Dispatch + Dispatch -->|"copy: selected dev"| TxQ0 + Dispatch -->|"copy: selected dev"| TxQ1 + TxQ0 -->|"copy: EthernetDevice::send"| TxMem + TxQ1 -->|"copy: EthernetDevice::send"| TxMem +``` + +图中的 queue 都是有界队列。真实设备 RX 先由设备 worker 入队,再由 net-poll worker drain;真实设备 TX 先由 net-poll worker dispatch 到 per-device TX queue,再由设备 TX worker 送入驱动。用户线程只读写 socket buffer 并请求 poll,不直接推进设备队列。 + +关键原则: + +- 设备 worker 不持有 `Service` 或 `SocketSet` 锁。 +- net-poll worker 是唯一推进 smoltcp `Interface` 的线程。 +- Router queue 使用 inline `[u8; STANDARD_MTU]`,避免每包堆分配。 +- queue 满时丢包并 warning,不创建无界 backlog。 +- loopback 普通 TX 直接注入 `Router.rx_buffer`,少一次队列 hop。 + +## 容量常量 + +默认容量集中在 [consts.rs](net/ax-net/src/consts.rs): + +```rust +pub const STANDARD_MTU: usize = 1500; + +pub const TCP_RX_BUF_LEN: usize = 64 * 1024; +pub const TCP_TX_BUF_LEN: usize = 64 * 1024; +pub const UDP_RX_BUF_LEN: usize = 64 * 1024; +pub const UDP_TX_BUF_LEN: usize = 64 * 1024; +pub const RAW_RX_BUF_LEN: usize = 64 * 1024; +pub const RAW_TX_BUF_LEN: usize = 64 * 1024; + +pub const SOCKET_BUFFER_SIZE: usize = 64; +pub const DEVICE_RX_QUEUE_SIZE: usize = 256; +pub const DEVICE_TX_QUEUE_SIZE: usize = 128; +pub const ETHERNET_MAX_PENDING_PACKETS: usize = 128; +``` + +| 常量 | 作用范围 | 默认内存预算 | +| --- | --- | --- | +| `SOCKET_BUFFER_SIZE` | `Router.rx_buffer` 和 `Router.tx_buffer` 的 packet metadata 槽位;每个 data buffer 是 `STANDARD_MTU * SOCKET_BUFFER_SIZE` | RX 约 96 KiB,TX 约 96 KiB | +| `DEVICE_RX_QUEUE_SIZE` | 所有真实设备共享的 device-to-Router RX queue | 256 × 1500B,约 384 KiB | +| `DEVICE_TX_QUEUE_SIZE` | 每个真实设备独立的 TX queue | 每设备 128 × 1500B,约 192 KiB | +| `TCP_RX_BUF_LEN` / `TCP_TX_BUF_LEN` | 每个 TCP socket 的 smoltcp byte buffer | 每连接约 128 KiB | +| `UDP_RX_BUF_LEN` / `UDP_TX_BUF_LEN` | 每个 UDP socket 的 smoltcp packet data buffer | 每 socket 约 128 KiB,外加 metadata | +| `RAW_RX_BUF_LEN` / `RAW_TX_BUF_LEN` | 每个 raw socket 的 smoltcp packet data buffer | 每 socket 约 128 KiB,外加 metadata | +| `ETHERNET_MAX_PENDING_PACKETS` | ARP 解析期间暂存的待发送 IP packet | 每 Ethernet device 约 192 KiB | + +`DEVICE_RX_QUEUE_SIZE` 有意大于 `SOCKET_BUFFER_SIZE`。前者吸收设备 RX worker 与 net-poll worker 调度之间的短 burst;后者是 smoltcp-facing 的单轮 packet buffer。APK index 下载、TCP slow start 或 QEMU user networking burst 都可能在短时间内产生超过 64 个 MTU packet 的入站积压,因此 RX worker 共享队列需要更大的缓冲。 + +## RX 内存路径 + +RX 从真实设备到用户态 `recv()` 大致经过下面的内存边界: + +```mermaid +flowchart TB + DriverRx["驱动 RX 内存
rd-net RxQueue / NetRxBuffer"] + EthRecv["EthernetDevice::recv()
解析 Ethernet/ARP/IPv4"] + LocalBuf["RX worker 本地 PacketBuffer
1 * STANDARD_MTU"] + SharedRx["shared RX queue
RxPacket + QueuedPacket
DEVICE_RX_QUEUE_SIZE"] + RouterRx["Router.rx_buffer
PacketBuffer InterfaceId
SOCKET_BUFFER_SIZE"] + SmolPoll["smoltcp Interface::poll()"] + SocketRx["TCP/UDP/raw socket RX buffer"] + UserRecv["用户 recv/read buffer"] + + DriverRx -->|"copy or adapter receive"| EthRecv + EthRecv -->|"copy IP payload"| LocalBuf + LocalBuf -->|"copy inline packet"| SharedRx + SharedRx -->|"copy drain"| RouterRx + RouterRx --> SmolPoll + SmolPoll -->|"protocol copy/store"| SocketRx + SocketRx -->|"copy to user"| UserRecv +``` + +```text +NIC / virtqueue / rd-net RX memory + -> NetRxBuffer / VecRxBuffer + -> EthernetDevice::recv() + -> device_rx_worker local PacketBuffer + -> BoundedPacketQueue (QueuedPacket inline copy) + -> Router.rx_buffer (PacketBuffer) + -> smoltcp Interface::poll() + -> TCP/UDP/raw socket RX buffer in SocketSet + -> socket recv() + -> user Write / IoBufMut +``` + +### Driver 到 EthernetDevice + +`RdNetDriver` 把 `rd-net` 的 RX queue 适配成 `EthernetDriver::receive()`。当前适配层是 copy-based: + +```text +rd_net::RxQueue::receive() + -> VecRxBuffer { data: Vec } + -> EthernetDevice::recv() +``` + +`RX_PREFETCH_TARGET = 1`,只允许一个小的预取窗口,避免在 driver adapter 中形成新的大缓存层。`EthernetDevice::recv()` 解析 Ethernet frame: + +- ARP frame:更新 neighbor / pending packet 状态,不进入 smoltcp socket。 +- IPv4 frame:校验链路层目标后,把 IP payload 写入调用方提供的 `PacketBuffer`。 +- 其它 frame:忽略或返回没有可交付 packet。 + +### RX Worker 到共享 RX Queue + +`device_rx_worker` 用一个本地 `PacketBuffer` 暂存从 `Device::recv()` 得到的 IP packet: + +```rust +let mut rx_buffer = PacketBuffer::new( + vec![PacketMetadata::EMPTY; 1], + vec![0u8; STANDARD_MTU], +); +``` + +随后把 packet 复制进共享 RX queue: + +```text +local PacketBuffer slice + -> QueuedPacket { bytes: [u8; STANDARD_MTU], len } + -> RxPacket { interface_id, bytes } + -> RouterQueues::rx.push() +``` + +共享 RX queue 是 `Arc>`,所有非 loopback 设备共用一个队列。它保存 ingress `InterfaceId`,让 DHCP client/server、TCP SYN snoop 和诊断路径知道 packet 来自哪个接口。队列满时: + +- 当前 packet 被丢弃。 +- 打印 `"{ifname}: RX queue is full, dropping packet"`。 +- 调用 `request_poll()` 并 `yield_now()`,给 net-poll worker 机会 drain backlog。 + +### Router.rx_buffer 到 smoltcp socket + +`Service::poll()` 首先调用 `Router::poll()`,把共享 RX queue drain 到 smoltcp-facing `Router.rx_buffer`: + +```rust +while !self.rx_buffer.is_full() { + let Some(packet) = self.queues.rx.pop() else { + break; + }; + let bytes = packet.bytes.as_slice(); + snoop_tcp_packet(bytes, sockets); + snoop(packet.interface_id, bytes); + let Ok(dst) = self.rx_buffer.enqueue(bytes.len(), packet.interface_id) else { + break; + }; + dst.copy_from_slice(bytes); +} +``` + +这一步又发生一次 copy:`QueuedPacket` 的 inline bytes 复制到 `Router.rx_buffer`。随后 smoltcp `Interface::poll()` 通过 `Router::receive()` 获取 `RxToken` 并解析 IP/TCP/UDP/raw,最后写入具体 socket 的 RX buffer: + +- TCP:写入 TCP socket 的 byte stream RX buffer。 +- UDP:写入 UDP packet buffer 和 metadata。 +- raw:写入 raw packet buffer;connected peer 不匹配时,`raw.rs` 可把 packet 暂存到 `deferred_rx`。 + +### Socket RX Buffer 到用户 Buffer + +用户执行 `recv()` 时,不直接接触 Router queue。IP socket 从 smoltcp socket buffer 复制到 syscall 提供的用户 buffer: + +```text +TCP recv: + smoltcp TCP SocketBuffer + -> socket.recv(|buf| dst.write(buf)) + -> user buffer + +UDP recv: + smoltcp UDP PacketBuffer + -> socket.recv() / socket.peek() + -> dst.write(payload) + -> user buffer + +raw recv: + smoltcp raw PacketBuffer or deferred_rx / loopback_rx + -> parse/filter + -> dst.write(payload) + -> user buffer +``` + +阻塞等待由 `GeneralOptions::recv_poller_with()` 处理:如果 socket RX buffer 为空,当前调用注册 waker 并等待;等待期间协议推进仍由 `net-poll` worker 完成。 + +## TX 内存路径 + +TX 从用户态 `send()` 到真实驱动大致经过下面的内存边界: + +```mermaid +flowchart TB + UserSend["用户 send/write buffer"] + SocketTx["TCP/UDP/raw socket TX buffer"] + SmolPoll["smoltcp Interface::poll()"] + RouterTx["Router.tx_buffer
PacketBuffer placeholder ifindex 0
SOCKET_BUFFER_SIZE"] + Dispatch["Router::dispatch()
route by dst + src"] + DevTx["per-device TX queue
TxPacket + QueuedPacket
DEVICE_TX_QUEUE_SIZE"] + EthSend["EthernetDevice::send()
ARP / Ethernet header"] + Pending["ARP pending PacketBuffer
ETHERNET_MAX_PENDING_PACKETS"] + DriverTx["驱动 TX 内存
NetTxBuffer / rd-net TxQueue"] + + UserSend -->|"copy to socket"| SocketTx + SocketTx --> SmolPoll + SmolPoll -->|"generate IP packet"| RouterTx + RouterTx --> Dispatch + Dispatch -->|"copy selected packet"| DevTx + DevTx --> EthSend + EthSend -->|"ARP unresolved"| Pending + Pending -->|"ARP resolved"| EthSend + EthSend -->|"copy Ethernet frame"| DriverTx +``` + +```text +user Read / IoBuf + -> smoltcp TCP/UDP/raw socket TX buffer + -> smoltcp Interface::poll() + -> Router.tx_buffer + -> Router::dispatch() + -> per-device BoundedPacketQueue (QueuedPacket inline copy) + -> device_tx_worker + -> EthernetDevice::send() + -> NetTxBuffer / VecTxBuffer + -> driver transmit / rd-net TX queue +``` + +### 用户 Buffer 到 smoltcp Socket + +socket `send()` 只写协议 socket buffer,并请求 net-poll worker: + +```text +send() + -> socket.can_send() + -> socket.send(|buffer| src.read(buffer)) + -> request_poll() +``` + +TCP 的用户 bytes 进入 TCP TX byte buffer。UDP/raw 发送会申请一个 packet-sized smoltcp buffer,然后把用户 payload 写进去;UDP `MSG_MORE` corking 会在 socket 层暂存第一次 send 的 endpoint/source,最终 flush 时一次性写入 smoltcp UDP packet buffer。 + +### smoltcp 到 Router.tx_buffer + +net-poll worker 执行 `Interface::poll()` 时,smoltcp 根据 TCP/UDP/raw socket 状态生成完整 IP packet。`Router::transmit()` 返回 `TxToken`,`TxToken::consume()` 把 packet 写入 `Router.tx_buffer`: + +```rust +fn consume(self, len: usize, f: F) -> R +where + F: FnOnce(&mut [u8]) -> R, +{ + f(self + .0 + .enqueue(len, TX_INTERFACE_PLACEHOLDER) + .expect("This was checked before creating the TxToken")) +} +``` + +这里的 metadata 使用内部占位 `InterfaceId(0)`,因为真实出接口必须等 IP header 生成后才能按 `(dst, src)` 查 route table。 + +### Router.dispatch 到 per-device TX Queue + +`Router::dispatch()` 从 `Router.tx_buffer` 取完整 IP packet: + +- loopback:直接复制到 `Router.rx_buffer`。 +- IPv4 limited broadcast:复制到所有非 loopback device 的 TX queue。 +- 普通单播:解析 `src/dst`,调用 `select_route_for_source(dst, src)`,把 packet 复制到选中设备的 `tx_queue`。 + +普通 Ethernet TX 入队形态: + +```text +Router.tx_buffer packet slice + -> QueuedPacket { bytes: [u8; STANDARD_MTU], len } + -> TxPacket { next_hop, bytes } + -> DeviceHandle.tx_queue +``` + +per-device TX queue 满时,当前 packet 丢弃并 warning。TX queue 是每个真实设备独立的,避免一个慢设备阻塞其它接口的发送 backlog。 + +### TX Worker 到 Driver + +`device_tx_worker` 从 per-device queue 取 `TxPacket`,持有设备锁调用 `Device::send(next_hop, packet)`。对 Ethernet 设备而言: + +```text +TxPacket IP payload + -> EthernetDevice::send(next_hop) + -> neighbor cache / ARP + -> alloc_tx_buffer(frame_len) + -> copy Ethernet header + IP payload + -> driver.transmit(tx_buf) +``` + +如果 ARP 未解析,`EthernetDevice` 会把 IP packet 暂存在 `pending_packets`,发送 ARP request,等 ARP reply 后再 flush。`pending_packets` 也是有界 `PacketBuffer`,上限由 `ETHERNET_MAX_PENDING_PACKETS` 控制。 + +`RdNetDriver` 的 TX buffer 是 `VecTxBuffer`,分配大小至少为 Ethernet 最小帧长 `ETH_ZLEN = 60`。因此当前普通 Ethernet TX 至少包含两次 copy:用户 buffer 到 smoltcp socket buffer、Router/设备队列到 driver TX buffer;不同协议还可能有额外的协议封装 copy。 + +## Loopback 内存路径 + +loopback 普通 socket TX 是特殊快速路径: + +```mermaid +flowchart LR + UserA["发送端用户 buffer"] + SockA["发送端 socket TX buffer"] + RouterTx["Router.tx_buffer"] + Inject["inject_loopback_rx_direct()
snoop TCP SYN"] + RouterRx["Router.rx_buffer"] + Poll["同一轮 Interface::poll()"] + SockB["接收端 socket RX buffer"] + UserB["接收端用户 buffer"] + + UserA -->|"copy"| SockA + SockA --> Poll + Poll --> RouterTx + RouterTx --> Inject + Inject -->|"copy direct"| RouterRx + RouterRx --> Poll + Poll --> SockB + SockB -->|"copy"| UserB +``` + +```text +user send() + -> smoltcp socket TX buffer + -> Router.tx_buffer + -> Router::dispatch() + -> inject_loopback_rx_direct() + -> Router.rx_buffer + -> smoltcp Interface::poll() + -> peer socket RX buffer + -> user recv() +``` + +这个路径不进入 `DeviceHandle.tx_queue`,也不进入共享 `RouterQueues::rx`。它仍会把 IP packet 从 `Router.tx_buffer` 复制到 `Router.rx_buffer`,但避免了早期实现中的 `to_vec()` 分配和额外 RX queue hop。`inject_loopback_rx_direct()` 在写入 `rx_buffer` 前调用 `snoop_tcp_packet()`,因此 loopback TCP SYN 能在同一轮 poll 中预创建 accept child socket。 + +`send_on_device()` 的 loopback 分支仍可能使用共享 RX queue,这是控制面指定设备发送路径;普通 socket loopback TX 走 direct injection。 + +## 满队列与背压 + +当前普通 Ethernet 数据面不把 Router queue 满映射为用户态 `EAGAIN`: + +| 满的位置 | 行为 | 用户可见性 | +| --- | --- | --- | +| smoltcp socket TX buffer 满 | `send()` 返回 `WouldBlock` 或阻塞等待 | 直接可见 | +| smoltcp socket RX buffer 满 | smoltcp 按协议窗口/丢包策略处理 | 间接可见 | +| shared RX queue 满 | 丢弃入站 packet,warning,request poll + yield | TCP 通过重传恢复,UDP/raw 可能丢包 | +| Router.rx_buffer 满 | 停止 drain,下一轮继续;直接注入失败时丢包 warning | TCP 通过重传恢复,UDP/raw 可能丢包 | +| per-device TX queue 满 | 丢弃出站 packet,warning | TCP 通过重传恢复,UDP/raw 可能丢包 | +| ARP pending queue 满 | 丢弃等待 ARP 的出站 packet,warning | 连接建立或首包可能超时/重传 | +| driver TX buffer 分配失败 | `Device::send()` 返回失败,packet 已离开 Router queue | 协议层后续重传或应用超时 | + +这种策略与很多嵌入式协议栈一致:内部队列保持有界,不把所有链路层瞬时拥塞都反馈到已完成的 socket send 调用。TCP 正确性依赖重传和窗口控制;UDP/raw 本身允许丢包。 + +## 内存预算示例 + +以 2 个 Ethernet 设备、默认常量估算,不含驱动自身 ring/DMA 内存: + +| 项目 | 估算 | +| --- | --- | +| `Router.rx_buffer` | `64 * 1500` ≈ 96 KiB | +| `Router.tx_buffer` | `64 * 1500` ≈ 96 KiB | +| shared RX queue | `256 * 1500` ≈ 384 KiB | +| per-device TX queue | `2 * 128 * 1500` ≈ 384 KiB | +| ARP pending packets | `2 * 128 * 1500` ≈ 384 KiB | +| 每条 TCP 连接 | RX 64 KiB + TX 64 KiB | +| 每个 UDP/raw socket | RX 64 KiB + TX 64 KiB + metadata | + +实际内存还包括 metadata、`VecDeque` 元素开销、socket 对象、route/DNS/interface registry、Unix/vsock buffer 和驱动队列。调整常量时应按“共享一次”“每设备”“每 socket”“每连接”分别乘算。 + +## 不属于当前模型的能力 + +当前实现没有: + +- 端到端 zero-copy。 +- DMA buffer 直接挂入 smoltcp socket buffer。 +- RSS / 多队列 NIC / per-queue poll。 +- Linux `sk_buff` 类动态链式 backlog。 +- `MSG_ZEROCOPY` 或 `io_uring` send/recv path。 + +如果后续要实现 zero-copy,需要同时改造 `rd-net` buffer ownership、`EthernetDevice` frame 封装、Router queue 生命周期和 smoltcp token/socket buffer 接口。单独把某个队列改成 `Arc<[u8]>` 只能减少局部 copy,不能形成完整 zero-copy 数据面。 diff --git a/docs/docs/architecture/net/overview.md b/docs/docs/architecture/net/overview.md index dd3f8d5f5b..1e2d4812f0 100644 --- a/docs/docs/architecture/net/overview.md +++ b/docs/docs/architecture/net/overview.md @@ -135,6 +135,7 @@ smoltcp 原生使用需要一个 `phy::Device` + 一个 `Interface`。`ax-net` ### 收发路径仍有拷贝 RX worker 从 driver buffer 复制到有界队列中的 inline packet,Router 再复制到 smoltcp `PacketBuffer`;TX 方向从 smoltcp `PacketBuffer` 复制到 per-device inline queue,再由 driver 发送。当前队列不再为每个包分配 `Box<[u8]>`,loopback dispatch 也直接写 `rx_buffer`,但端到端 zero-copy 仍需要 `rd-net` buffer ownership、packet pool 和 smoltcp token 适配改造。 +完整内存所有权和队列模型见[内存与队列](memory.md)。 ### DHCP 租约管理不完整 diff --git a/docs/docs/architecture/net/testing.md b/docs/docs/architecture/net/testing.md index d495a516d7..6f373d38c2 100644 --- a/docs/docs/architecture/net/testing.md +++ b/docs/docs/architecture/net/testing.md @@ -1,5 +1,5 @@ --- -sidebar_position: 10 +sidebar_position: 11 sidebar_label: "测试与限制" --- From 9da745c03ccb6a9efd77a8ec066d6f1e125b761d Mon Sep 17 00:00:00 2001 From: ZCShou <72115@163.com> Date: Tue, 16 Jun 2026 17:47:40 +0800 Subject: [PATCH 31/31] feat(starry): add aarch64 and loongarch64 dual-net QEMU test configs --- apps/starry/qemu/dual-net/qemu-aarch64.toml | 32 +++++++++++++++++ .../qemu/dual-net/qemu-loongarch64.toml | 34 +++++++++++++++++++ docs/docs/architecture/net/testing.md | 14 ++++++++ scripts/axbuild/src/starry/test.rs | 2 +- 4 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 apps/starry/qemu/dual-net/qemu-aarch64.toml create mode 100644 apps/starry/qemu/dual-net/qemu-loongarch64.toml diff --git a/apps/starry/qemu/dual-net/qemu-aarch64.toml b/apps/starry/qemu/dual-net/qemu-aarch64.toml new file mode 100644 index 0000000000..d71e9ab3e8 --- /dev/null +++ b/apps/starry/qemu/dual-net/qemu-aarch64.toml @@ -0,0 +1,32 @@ +args = [ + "-nographic", + "-m", + "512M", + "-cpu", + "cortex-a53", + "-device", + "virtio-blk-pci,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-aarch64-alpine.img", + "-device", + "virtio-net-pci,netdev=net0", + "-netdev", + "user,id=net0,net=10.0.2.0/24,dhcpstart=10.0.2.15", + "-device", + "virtio-net-pci,netdev=net1", + "-netdev", + "user,id=net1,net=10.0.3.0/24,dhcpstart=10.0.3.15", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/dual-net-tests.sh" +success_regex = ["(?m)^DUAL_NET_TEST_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^lockdep fatal violation\\s*$", "(?m)^DUAL_NET_TEST_FAILED:"] +timeout = 360 + +[host_http_server] +bind = "127.0.0.1" +port = 18382 +body_size = 1048576 +body_byte = 68 diff --git a/apps/starry/qemu/dual-net/qemu-loongarch64.toml b/apps/starry/qemu/dual-net/qemu-loongarch64.toml new file mode 100644 index 0000000000..1a1e063860 --- /dev/null +++ b/apps/starry/qemu/dual-net/qemu-loongarch64.toml @@ -0,0 +1,34 @@ +args = [ + "-machine", + "virt", + "-cpu", + "la464", + "-nographic", + "-m", + "512M", + "-device", + "virtio-blk-pci,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-loongarch64-alpine.img", + "-device", + "virtio-net-pci,netdev=net0", + "-netdev", + "user,id=net0,net=10.0.2.0/24,dhcpstart=10.0.2.15", + "-device", + "virtio-net-pci,netdev=net1", + "-netdev", + "user,id=net1,net=10.0.3.0/24,dhcpstart=10.0.3.15", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/dual-net-tests.sh" +success_regex = ["(?m)^DUAL_NET_TEST_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^lockdep fatal violation\\s*$", "(?m)^DUAL_NET_TEST_FAILED:"] +timeout = 360 + +[host_http_server] +bind = "127.0.0.1" +port = 18382 +body_size = 1048576 +body_byte = 68 diff --git a/docs/docs/architecture/net/testing.md b/docs/docs/architecture/net/testing.md index 6f373d38c2..4db42f05d2 100644 --- a/docs/docs/architecture/net/testing.md +++ b/docs/docs/architecture/net/testing.md @@ -177,16 +177,30 @@ cargo xtask starry app list --kind qemu | rg "qemu/dual-net" cargo xtask starry app qemu -t qemu/dual-net --arch riscv64 ``` +运行 aarch64: + +```bash +cargo xtask starry app qemu -t qemu/dual-net --arch aarch64 +``` + 运行 x86_64: ```bash cargo xtask starry app qemu -t qemu/dual-net --arch x86_64 ``` +运行 loongarch64: + +```bash +cargo xtask starry app qemu -t qemu/dual-net --arch loongarch64 +``` + QEMU 配置: | 架构 | 配置文件 | | --- | --- | +| aarch64 | [apps/starry/qemu/dual-net/qemu-aarch64.toml](apps/starry/qemu/dual-net/qemu-aarch64.toml) | +| loongarch64 | [apps/starry/qemu/dual-net/qemu-loongarch64.toml](apps/starry/qemu/dual-net/qemu-loongarch64.toml) | | riscv64 | [apps/starry/qemu/dual-net/qemu-riscv64.toml](apps/starry/qemu/dual-net/qemu-riscv64.toml) | | x86_64 | [apps/starry/qemu/dual-net/qemu-x86_64.toml](apps/starry/qemu/dual-net/qemu-x86_64.toml) | diff --git a/scripts/axbuild/src/starry/test.rs b/scripts/axbuild/src/starry/test.rs index 43f4bfa04e..7ea9891c0c 100644 --- a/scripts/axbuild/src/starry/test.rs +++ b/scripts/axbuild/src/starry/test.rs @@ -2428,7 +2428,7 @@ mod tests { case_dir.display() ); - for arch in ["x86_64", "riscv64"] { + for arch in ["x86_64", "aarch64", "riscv64", "loongarch64"] { let config_path = case_dir.join(format!("qemu-{arch}.toml")); assert!( config_path.is_file(),