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..dad71bc293 --- /dev/null +++ b/apps/starry/qemu/dual-net/c/dual-net-tests.sh @@ -0,0 +1,178 @@ +#!/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" +} + +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 + *[!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" +} + +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" + 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" +} + +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" +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))" + +apk_fetch_verify + +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-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/apps/starry/qemu/dual-net/qemu-riscv64.toml b/apps/starry/qemu/dual-net/qemu-riscv64.toml new file mode 100644 index 0000000000..0c435e84d9 --- /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 = 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-x86_64.toml b/apps/starry/qemu/dual-net/qemu-x86_64.toml new file mode 100644 index 0000000000..483ab13c20 --- /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 = 360 + +[host_http_server] +bind = "127.0.0.1" +port = 18382 +body_size = 1048576 +body_byte = 68 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..ba55bdbe47 --- /dev/null +++ b/docs/docs/architecture/net/api.md @@ -0,0 +1,683 @@ +--- +sidebar_position: 7 +sidebar_label: "对外接口" +--- + +# 对外接口 + +`ax-net` 的 public API 面向三类调用方:启动阶段的 runtime、系统 ABI/socket 层,以及设备驱动适配层。API 设计保持一个原则:外部通过稳定的接口 ID、快照和 trait object 访问网络栈,不直接接触 `Service`、`Router`、smoltcp `SocketSet` 等内部对象。 + +核心 re-export 定义在 [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); +``` + +调用方传入已发现的 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。 + +## 运行时查询 + +查询 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; +``` + +`InterfaceId` 是稳定接口 ID,同时作为 StarryOS/Linux ifindex 来源: + +```rust +pub struct InterfaceId(u32); + +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; +} +``` + +`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, +} +``` + +系统 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(); +``` + +### 路由快照 + +```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。 + +### ARP 快照 + +```rust +pub fn arp_entries() -> Vec; +``` + +`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; + } +} +``` + +`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; + 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; + fn local_addr(&self) -> AxResult; + fn peer_addr(&self) -> AxResult; + fn shutdown(&self, how: Shutdown) -> AxResult; +} +``` + +`Socket` 枚举把统一 API 分发给各 backend: + +```rust +pub enum Socket { + Udp(Box), + Tcp(Box), + Raw(Box), + Unix(Box), + #[cfg(feature = "vsock")] + Vsock(Box), +} +``` + +支持的 backend: + +| 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 +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 */ } +} +``` + +`get_option()` / `set_option()` 在 backend 返回 `supported = false` 时映射为 `ENOPROTOOPT`,调用方不需要自己区分“协议不支持”和“选项值非法”。 + +### Option 集合 + +```rust +define_options! { + 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), + + NoDelay(bool), + MaxSegment(usize), + TcpKeepIdle(u32), + TcpKeepInterval(u32), + TcpKeepCount(u32), + TcpUserTimeout(u32), + TcpInfo(TcpInfo), + + Ttl(u8), + RecvErr(bool), + + NonBlocking(bool), +} +``` + +通用选项由 `GeneralOptions` 实现,协议特有选项由具体 socket backend 继续处理。`TcpInfo` 是 transport-independent TCP_INFO snapshot: + +```rust +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 兼容字段 +} +``` + +### 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。 + +```rust +pub fn dns_servers() -> Vec; +pub fn dns_query(name: &str) -> AxResult>; +pub fn dns_query_timeout(name: &str, timeout: Duration) -> AxResult>; +``` + +语义: + +- `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` 移除。 + +## 设备驱动 API + +设备驱动 API 是 low-level NIC 和 `ax-net` 之间的能力边界。驱动提供 buffer 和 IRQ 语义,协议栈不依赖具体 DMA ring 或虚拟队列实现。 + +### EthernetDriver + +```rust +pub type EthernetDeviceList = Vec>; + +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; +} +``` + +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` 的标准适配实现。 + +### 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 +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` 封装平台 IRQ 回调。IRQ handler 返回 `EthernetIrqOutcome::Wake` 时,Ethernet adapter 会唤醒设备 RX worker 和 net-poll 路径。 + +### 动态设备注册 + +```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, +} + +pub fn register_device_with_config(dev: Box, config: NetConfig); +pub fn notify_oob_rx(); +``` + +`register_device_with_config()` 用于运行期加入静态 IPv4 Ethernet 设备,例如 Wi-Fi AP 模式设备。它会: + +- 创建 `EthernetDevice` 或 OOB RX `EthernetDevice`。 +- 分配新的 `InterfaceId`。 +- 更新 smoltcp address list、接口 registry 和 route table。 +- 启动该设备的 RX/TX worker。 +- 可选启用内置单客户端 DHCP server。 + +`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); +``` + +abstract Unix socket 使用 `ax-net` 内部内存 namespace;path socket 通过注册的 `UnixNamespace` 完成路径绑定和解析。 + +## 兼容语义 + +这些 API 语义影响 syscall 层和测试用例,应作为稳定约定维护。 + +### Per-address Bind/Listen + +TCP listen 和 UDP bind 支持 Linux 风格 wildcard/specific-address 冲突: + +- wildcard 地址与同端口所有具体地址冲突。 +- 两个具体地址只有地址相同时冲突。 +- TCP 由 `TCP_BOUND_PORTS` 和 `ListenTable` 共同维护。 +- UDP 由 `SocketSetWrapper` 的 `udp_binds` side table 维护。 +- `SO_REUSEADDR` 会影响 UDP wrapper side table 注册,但不绕过 smoltcp 自身状态检查。 + +### Ephemeral Port + +TCP/UDP 在 bind port 为 `0` 时分配临时端口。临时端口范围从 `49152` 开始,符合 IANA dynamic/private port 下界。该分配器不是 public API,但影响 `bind(0)` 和自动 bind 行为。 + +### Error Mapping + +常见错误约定: + +| 场景 | 错误 | +| --- | --- | +| 绑定不存在接口 | `AxError::NoSuchDevice` | +| 绑定本机不存在地址 | `AxError::NoSuchDeviceOrAddress` | +| 地址/端口冲突 | `AxError::AddrInUse` | +| 操作不支持 | `AxError::OperationNotSupported` | +| nonblocking 或 `MSG_DONTWAIT` 下 would block | `AxError::WouldBlock` | +| 不支持的 socket option | Linux `ENOPROTOOPT` 映射 | + +### API 使用建议 + +- 新代码使用 `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/architecture.md b/docs/docs/architecture/net/architecture.md new file mode 100644 index 0000000000..b448a4a0ec --- /dev/null +++ b/docs/docs/architecture/net/architecture.md @@ -0,0 +1,342 @@ +--- +sidebar_position: 2 +sidebar_label: "总体架构" +--- + +# 总体架构 + +`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 Api["Public API"] + Init["init_network() / init_vsock()"] + Query["interfaces() / default_routes() / arp_entries()"] + DnsApi["dns_servers() / dns_query()"] + Sockets["TcpSocket / UdpSocket / RawSocket / UnixSocket / VsockSocket"] + PollApi["request_poll() / poll_interfaces()"] + end + + subgraph Control["Control plane"] + NetControl["NetControl"] + IfaceRegistry["Interface registry"] + Routes["RouteTable"] + DnsRegistry["DNS registry"] + Binding["DeviceBinding"] + end + + subgraph Core["Single protocol core"] + Service["Service"] + SmolIface["smoltcp Interface"] + SocketSet["SocketSetWrapper"] + Listen["ListenTable"] + Orphan["TCP orphan reaper"] + DhcpClient["DHCP client states"] + DhcpServer["DHCP server"] + end + + 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 DeviceLayer["Device layer"] + Loopback["LoopbackDevice"] + Ethernet["EthernetDevice"] + RdNet["RdNetDriver"] + Hardware["rd-net / physical NIC"] + end + + Api --> Control + Sockets --> SocketSet + PollApi --> Service + Control --> Service + 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 +``` + +### 组件分层 + +| 层级 | 主要职责 | 关键源码 | 详细文档 | +| --- | --- | --- | --- | +| 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) | + +### TCP/IP 分层映射 + +| 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 集成。 + +## Public API + +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 的稳定能力: + +| 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_*` 选项 | + +Public API 的职责是做边界收敛:上层不需要知道某个 socket 是否由 smoltcp、Unix transport 或 vsock transport 实现,也不需要直接操作 `Service`、`Router` 或 `SocketSet`。具体 API 列表见 [API 参考](api.md)。 + +## 控制面 + +控制面负责“网络配置如何被发现、保存、查询和用于决策”。它不直接收发 packet,也不推进 smoltcp poll;数据面只在需要路由、接口地址或 DNS 信息时读取控制面快照。 + +```mermaid +flowchart TB + subgraph Sources["配置来源"] + Static["NetworkConfig / InterfaceConfig"] + DhcpAck["DHCP ACK"] + SocketBind["bind(addr) / SO_BINDTODEVICE"] + end + + subgraph State["NetControl"] + Registry["Interface registry"] + Routes["SharedRouteTable"] + Dns["DNS registry"] + end + + subgraph Consumers["消费者"] + Query["interfaces() / default_routes() / dns_servers()"] + Dispatch["Router::dispatch() route lookup"] + Socket["socket bind/connect/listen"] + DnsQuery["dns_query() server selection"] + end + + 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 +``` + +控制面由 `NetControl` 持有: + +- `ControlState`:接口 registry 和 DNS server entries。 +- `SharedRouteTable`:路由规则,按最长前缀、metric、插入顺序排序。 +- `DeviceBinding`:表达 `SO_BINDTODEVICE` 或本地地址绑定推导出的接口约束。 + +### 初始化注册 + +`init_network()` 根据 `NetworkConfig` 和发现到的 Ethernet 设备创建接口 registry: + +- `lo` 固定为 `InterfaceId::LOOPBACK`。 +- Ethernet 接口从 ifindex 2 开始按发现顺序分配 `InterfaceId`。 +- 静态 IPv4、DHCP 开关、metric、DNS server 和默认路由在初始化时写入 `NetControl`。 +- `Router` 使用同一份 `SharedRouteTable`,所以控制面的路由更新会直接影响后续 TX dispatch。 + +### 运行期更新 + +DHCP client 在 `Service::poll()` 中运行。收到 DHCP ACK 后,`Service` 通过 `commit_interface_update()` 一次性提交: + +- 接口 IPv4 地址和 flags。 +- smoltcp `Interface` 的 IP address 列表。 +- 当前接口的 IPv4 路由。 +- 当前接口贡献的 DNS server。 + +这里使用事务式更新,是为了避免外部查询看到“地址已更新但路由/DNS 还没更新”的中间状态。 + +### 查询路径 + +`interfaces()`、`interface_by_name()`、`interface_by_id()`、`ipv4_config()`、`default_routes()` 和 `dns_servers()` 都返回快照。调用方拿到的是当时的只读视图,不持有内部锁,也不应该假设快照会随 DHCP 或接口状态变化自动更新。 + +### 路由选择 + +路由表按以下优先级排序: + +1. 最长前缀优先。 +2. 同前缀时低 metric 优先。 +3. 同前缀同 metric 时保留插入顺序。 + +普通查询使用 `select_route(dst)`,会过滤未 `UP` 的接口。TX dispatch 使用 `select_route_for_source(dst, src)`,同时匹配 smoltcp 已经选出的源地址,避免多宿主主机从错误接口发出带另一接口源地址的包。 + +### 绑定约束 + +`DeviceBinding` 是控制面和 socket 语义的连接点: + +- `bind(具体本地地址)` 会推导该地址所属接口。 +- `SO_BINDTODEVICE` 会显式限制 socket 只使用某个接口。 +- wildcard bind 不绑定具体接口,由路由表在发送时选择。 + +这些约束会影响 socket readiness 注册、端口/listen 语义和路由可用性过滤。更细的规则见[控制面](control.md)。 + +## Single protocol core + +Single protocol core 是 `ax-net` 的协议状态中心,由 `Service`、smoltcp `Interface`、全局 `SocketSetWrapper`、`ListenTable`、DHCP 状态和 TCP orphan 回收共同组成。 + +### Socket system + +IP socket 共享 smoltcp `SocketSet`,但 Linux/POSIX 语义由 `ax-net` 自己补齐: + +- `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 关闭语义。 + +Unix domain socket 和 vsock 不走 smoltcp IP 层,但通过同一个 public socket facade 暴露给上层。细节见[Socket 系统](sockets.md)。 + +### 专用 poll worker + +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 +``` + +这个模型避免应用线程和协议栈驱动线程互相抢协议栈锁,也保证 TCP 重传、keepalive、DHCP 和设备收包不会依赖某个应用线程继续运行。 + +## Router as MultiDevice + +`Router` 是 single protocol core 和真实设备之间的适配层。它位于 smoltcp 的 `phy::Device` 边界上,对上提供一个 IP medium 设备,对下管理多个 `DeviceHandle`。 + +| 子组件 | 职责 | +| --- | --- | +| `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 | + +`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 + +设备层把不同来源的网络设备统一成 `ax-net` 内部 `Device` trait: + +| 设备类型 | 主要源码 | 作用 | +| --- | --- | --- | +| `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 通知机制接入实际设备。 + +## 数据面流程 + +### RX 路径 + +```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()"] +``` + +RX worker 只负责从真实设备取包并推入共享 RX 队列。协议处理发生在 `Service::poll()` 内:先由 `Router::poll()` 把 RX 队列 drain 到 `rx_buffer`,再由 smoltcp `Interface::poll()` 交付给 TCP/UDP/raw socket。 + +### TX 路径 + +```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"] +``` + +普通 Ethernet 发送会进入设备 TX worker,由 `EthernetDevice` 完成 ARP 和 Ethernet frame 封装。loopback 不走外部设备队列:`Router::dispatch()` 选中 `InterfaceId::LOOPBACK` 时直接把 IP packet 注入 `rx_buffer`,因此本机回环连接可在同一个 poll 周期内继续推进。 + +### DHCP 和 DNS + +DHCP client/server 都位于 `Service::poll()` 调度内,但不依赖 smoltcp 的普通 socket API: + +- 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 new file mode 100644 index 0000000000..23914800c3 --- /dev/null +++ b/docs/docs/architecture/net/configuration.md @@ -0,0 +1,418 @@ +--- +sidebar_position: 8 +sidebar_label: "配置参考" +--- + +# 配置参考 + +`ax-net` 的配置由结构化 `NetworkConfig`、Cargo feature、运行时设备注册参数和一组集中常量组成。配置目标是明确表达每个接口的意图,避免旧式单网口全局变量和隐式 `eth0` 假设。 + +核心源码: + +| 配置域 | 源码 | +| --- | --- | +| 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] +vsock = ["dep:rdif-vsock"] +``` + +| feature | 作用 | +| --- | --- | +| `vsock` | 启用 `rdif-vsock` 依赖、AF_VSOCK socket backend 和 vsock device 初始化 | + +启用 `vsock` 后导出: + +- `init_vsock(vsock_devs)`。 +- `vsock` 模块。 +- `Socket::Vsock` 变体。 +- `VsockDevice` / `VsockDeviceList` 类型别名。 + +### smoltcp 能力 + +`ax-net` 固定启用的 smoltcp 能力包括: + +- `alloc` +- `log` +- `async` +- `medium-ethernet` +- `medium-ip` +- `proto-ipv4` +- `proto-ipv6` +- `socket-raw` +- `socket-icmp` +- `socket-udp` +- `socket-tcp` +- `socket-dhcpv4` +- `socket-dns` + +Router 对 smoltcp 暴露 `Medium::Ip`,Ethernet frame 处理在 `EthernetDevice` 中完成。 + +## 启动配置模型 + +启动配置通过 `NetworkConfig` 传入 `init_network()`。它描述“哪些设备应该成为哪些接口,以及接口如何获得 IPv4/DNS/metric”。 + +### NetworkConfig + +```rust +#[derive(Debug, Clone, Default)] +pub struct NetworkConfig { + pub interfaces: Vec, + 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, + pub match_by: InterfaceMatcher, + pub static_ip: Option, + pub dhcp: bool, + pub metric: u32, + pub dns_servers: Vec, +} +``` + +字段语义: + +| 字段 | 语义 | +| --- | --- | +| `name` | 对外接口名,例如 `eth0`、`uplink0` | +| `match_by` | 将配置绑定到某个探测到的 Ethernet driver | +| `static_ip` | 静态 IPv4 配置;与 `dhcp` 互斥 | +| `dhcp` | 是否启用 DHCP client | +| `metric` | 接口路由和接口级 DNS 优先级,值越小越优先 | +| `dns_servers` | 绑定到该接口的静态 DNS server | + +### InterfaceMatcher + +```rust +#[derive(Debug, Clone)] +pub enum InterfaceMatcher { + ByOrder(usize), + ByMac(EthernetAddress), + ByDriverName(String), +} +``` + +匹配规则: + +- `ByOrder(0)` 匹配第一个发现的 Ethernet device。 +- `ByMac(mac)` 按 MAC 地址匹配。 +- `ByDriverName(name)` 按 driver 暴露的设备名匹配。 +- 同一设备不能被多个配置匹配。 +- 每个显式配置必须匹配到一个设备。 + +### StaticIpConfig + +```rust +#[derive(Debug, Clone)] +pub struct StaticIpConfig { + pub ip: Ipv4Addr, + pub prefix_len: u8, + pub gateway: Ipv4Addr, +} +``` + +静态接口初始化会: + +- 将 `ip/prefix_len` 加入 smoltcp `Interface` address list。 +- 安装直连路由。 +- 如果 `gateway != 0.0.0.0`,安装默认路由。 +- 将 `dns_servers` 记录为 `DnsSource::Static`。 + +`gateway = 0.0.0.0` 表示不安装默认路由。 + +## 初始化校验 + +`init_network()` 对配置执行 fail-fast 校验。启动阶段配置错误直接 panic,避免系统在半初始化网络状态下运行。 + +### 校验规则 + +| 配置项 | 规则 | +| --- | --- | +| 接口名 | 不能是 `lo`,不能重复 | +| `static_ip` + `dhcp` | 不能同时启用 | +| 静态 IP | 不能是 `0.0.0.0` | +| prefix | 不能大于 32 | +| gateway | 可以是 `0.0.0.0`,表示无默认路由 | +| DNS server | 不能是 `0.0.0.0` | +| matcher | 每个显式配置必须匹配唯一设备 | + +### 默认策略 + +未显式配置的 Ethernet 设备: + +- 名称为 `eth{order}`。 +- `InterfaceId = order + 2`。 +- metric 为 `100`。 +- 默认启用 DHCP。 +- 无静态接口级 DNS。 + +loopback: + +- 名称为 `lo`。 +- `InterfaceId::LOOPBACK == 1`。 +- 地址为 `127.0.0.1/8`。 +- metric 为 `0`。 +- flags 包含 `UP | RUNNING | LOOPBACK`。 + +## DNS 配置 + +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_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; +``` + +| 常量 | 含义 | +| --- | --- | +| `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 + +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; +``` + +这些值用于填充 `TcpInfo` 中无法直接从 smoltcp 获得或需要 Linux 兼容默认值的字段。 + +### 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 | +| `DHCP_SERVER_LEASE_SECS` | 86400s | 内置 SoftAP DHCP server 返回的固定租约时间 | +| `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; + +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![], +}; +``` + +### DHCP 主接口 + fallback DNS + +```rust +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)], +}; +``` + +## 配置建议 + +- 多网口默认路由通过 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 new file mode 100644 index 0000000000..b8f56bb1c1 --- /dev/null +++ b/docs/docs/architecture/net/control.md @@ -0,0 +1,883 @@ +--- +sidebar_position: 3 +sidebar_label: "控制面" +--- + +# 控制面 + +控制面维护 `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 +``` + +关键点: + +- `routes` 是 `Arc>`,`Router` 和 `NetControl` 共享同一实例。 +- `NetControl` 先于 `SERVICE` 初始化,确保 `get_control()` 在 poll worker 启动前可用。 +- DHCP bootstrap 只要求任一 DHCP 接口配置成功即返回,避免断网卡阻塞启动。 + +## 数据模型 + +控制面状态分为三类:对外稳定的接口标识、可快照查询的接口/DNS 状态,以及被 Router 和 `NetControl` 共享的路由表。接口 ID 用于跨模块引用同一接口,接口快照用于系统 ABI 和诊断接口,`NetControl` 负责把这些状态组织成可查询的控制面视图。 + +### 接口标识 + +`InterfaceId(u32)` 是 `ax-net` 内部和对外统一的接口标识,也是 StarryOS Linux ABI 的 ifindex 来源。 + +```rust +// 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` 开始分配,默认命名为 `eth0`、`eth1`。 +- `InterfaceId(0)` 是内部 TX 占位符,不对外暴露。 +- StarryOS 的 `SIOCGIFINDEX`、AF_PACKET `sockaddr_ll.sll_ifindex` 都应通过 `InterfaceId` 映射。 + +### 接口快照 + +对外接口信息使用 `InterfaceInfo`,它是快照,不是内部对象引用: + +```rust +// config.rs +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, +} +``` + +内部状态是 `NetInterface`: + +```rust +// service.rs +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, + } + } +} +``` + +这里特意返回快照,是为了让查询方不持有内部锁,也不依赖接口状态长期不变。DHCP 更新、动态设备注册或后续 link state 更新都可能改变快照内容。 + +### NetControl + +`NetControl` 是控制面的核心对象。它在 `init_network()` 中创建,并早于 `SERVICE` 注册到全局 `NET_CONTROL`。 + +```rust +// service.rs +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, + } + } +} +``` + +初始化时,`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)); +``` + +这个共享关系很关键:控制面查询看到的是 `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 前的决策。 + +### 接口查询 + +只读查询都走 `NetControl` 的读锁: + +```rust +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, + }) +} +``` + +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 + +`RouteTable` 存在于 [router.rs](net/ax-net/src/router.rs),被 `Arc>` 包装为 `SharedRouteTable`。 + +```rust +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, + 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(|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) +} +``` + +TX dispatch 使用 `select_route_for_source()`: + +```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 { + 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,避免外部查询和数据面发送路径看到不一致的网络状态。 + +### 路由规则更新 + +静态接口初始化或 DHCP ACK 后都会生成一组 IPv4 规则: + +```rust +// 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, + )); + } + } + rules +} +``` + +替换某接口 IPv4 规则时使用 `replace_ipv4_rules_for_interface()`: + +```rust +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(); +} +``` + +这保证 DHCP 更新不会留下旧地址或旧默认路由。 + +### DHCP 事务更新 + +DHCP 更新跨越三类状态: + +- smoltcp `Interface` 的 IP address list。 +- `NetControl.state.interfaces` 中的 IPv4/gateway。 +- DNS entries 和 route table。 + +更新入口是 `Service::handle_dhcp_event()`: + +```rust +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); +} +``` + +真正提交在 `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); +} +``` + +`NetControl::commit_interface_update()` 在一个控制面写锁内替换接口状态、DNS 和路由: + +```rust +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 所需状态。 + +## Socket 绑定 + +socket 层通过控制面把本地地址、`SO_BINDTODEVICE` 和 DNS server 可达性统一到接口语义上。绑定结果不直接保存设备索引,而是保存稳定的 `InterfaceId`,后续 route lookup 和 waker 注册再根据它过滤可用接口。 + +### 本地地址推导 + +`bind(具体本地地址)` 会推导接口绑定。核心函数是 `local_binding_for()`: + +```rust +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()), + } +} +``` + +语义: + +- 绑定具体地址:必须是某个接口已经拥有的 IPv4 地址,并推导出 `DeviceBinding { bound_if: Some(id) }`。 +- wildcard bind:返回默认绑定,不限制接口。 +- 该绑定会影响后续 route lookup 和 waker 注册。 + +TCP/UDP bind 会使用这个结果。例如 UDP bind 的设计是: + +```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` 和本地地址推导出的接口约束: + +```rust +// config.rs +#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)] +pub struct DeviceBinding { + pub bound_if: Option, +} +``` + +`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 更新路径进入协议栈。控制面因此不是只读配置表,而是网络状态变化的提交点。 + + + +控制面也服务于运行时静态设备注册,例如 Wi-Fi SoftAP: + +```rust +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(); +} +``` + +`Service::register_static_device()` 会: + +```rust +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 new file mode 100644 index 0000000000..c8fb07cbcc --- /dev/null +++ b/docs/docs/architecture/net/devices.md @@ -0,0 +1,756 @@ +--- +sidebar_position: 5 +sidebar_label: "多设备实现" +--- + +# 多设备实现 + +`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 +``` + +## 设备抽象层 + +设备抽象层把硬件细节限制在 `device/*`,Router 只处理完整 IP packet 和 next-hop IP。这样 Ethernet、loopback、OOB Wi-Fi 等设备可以共享同一个 smoltcp 协议核心。 + +### Device Trait + +内部 `Device` trait 是 Router 与具体设备之间的能力边界: + +```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 wake_rx(&self) {} + + fn register_waker(&self, waker: &Waker); +} +``` + +约束: + +- `recv()` 输出完整 IP packet,不输出 Ethernet frame。 +- `send()` 输入完整 IP packet 和已选好的 `next_hop`。 +- route lookup、source address selection、TCP/UDP/raw 分发都在设备层之上完成。 +- 设备只负责链路层封装、邻居解析、硬件 RX/TX 和 readiness。 + +### LoopbackDevice + +`LoopbackDevice` 是 `lo` 的控制面占位设备: + +```rust +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` 和一个很小的 `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 + +`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, +} +``` + +字段语义: + +- `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`: + +```rust +struct DeviceHandle { + interface_id: InterfaceId, + name: String, + inner: Arc>>, + rx_queue: Arc>, + tx_queue: Arc>, + rx_wake: Arc, + tx_wake: Arc, + rx_waker: Waker, +} +``` + +RX queue 是所有设备共享的,因为 smoltcp 只能从一个 `Router.rx_buffer` 获取 packet;TX queue 是每设备独立的,因为 dispatch 已经决定了出接口。 + +### smoltcp Device 实现 + +Router 对 smoltcp 暴露 `Medium::Ip`,即 smoltcp 看到的是 IP packet 设备,而不是 Ethernet frame 设备: + +```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))) + } + } + + fn transmit(&mut self, _timestamp: Instant) -> Option> { + if self.tx_buffer.is_full() { + None + } else { + Some(TxToken(&mut self.tx_buffer)) + } + } + + 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 + } +} +``` + +ingress `InterfaceId` 在 `Router::poll()` 阶段用于 TCP SYN snoop、DHCP 分发和后续诊断;进入 smoltcp `RxToken` 后只作为 Router 内部元数据保留,smoltcp 本身仍只消费 IP packet。`TxToken` 写入时先使用内部占位接口 ID,真实出接口由 `Router::dispatch()` 解析 IP header 后按 route table 决定。 + +## 队列与 Buffer + +队列层的目标是有界、低分配和清晰所有权:设备 worker 不持有 `Router` 本体,Router 不直接阻塞在硬件收发上。 + +### BoundedPacketQueue + +`BoundedPacketQueue` 是 Router 和设备 worker 之间的有界 FIFO: + +```rust +struct BoundedPacketQueue { + inner: Mutex>, + capacity: usize, + len: AtomicUsize, +} +``` + +语义: + +- `push()` 满时返回 `Err(packet)`,调用方丢包并记录 warning。 +- `pop()` 空时返回 `None`。 +- `is_empty()` 只读原子 `len`,用于 worker wait predicate。 +- 共享 RX queue 容量由 `DEVICE_RX_QUEUE_SIZE` 控制;per-device TX queue 容量由 `DEVICE_TX_QUEUE_SIZE` 控制。 + +### QueuedPacket + +队列中保存的是固定大小 packet buffer,而不是每包堆分配: + +```rust +struct QueuedPacket { + bytes: [u8; STANDARD_MTU], + len: usize, +} +``` + +`QueuedPacket::new(packet)` 会拒绝超过 `STANDARD_MTU` 的 packet。这个设计牺牲了端到端 zero-copy,但给出了明确内存上限,并避免早期 loopback 队列路径中的 `to_vec()` 分配。 + +### RX/TX Packet + +```rust +struct RxPacket { + interface_id: InterfaceId, + bytes: QueuedPacket, +} + +struct TxPacket { + next_hop: IpAddress, + bytes: QueuedPacket, +} +``` + +RX 需要保存 ingress `InterfaceId`,用于 DHCP 分发、诊断和后续扩展;TX 保存的是 route table 已经选择好的 next hop。 + +## 数据路径 + +数据路径分为设备 RX、smoltcp poll、TX dispatch 和 loopback 快速路径。所有路径都围绕 `Service::poll()` 批量推进。 +端到端的内存所有权、拷贝次数、队列满行为和预算估算见[内存与队列](memory.md)。 + +### RX Path + +RX worker 从真实设备获取 packet,写入共享 RX queue: + +```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, + _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; + }; + 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); + moved_rx = true; + } + moved_rx || !self.queues.rx.is_empty() +} +``` + +`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 规则: + +- IPv4 limited broadcast:复制到所有非 loopback 设备。 +- IPv4/IPv6 单播:使用 `select_route_for_source(dst, src)`,确保源地址和出接口一致。 +- IPv6 multicast:Router 层会发往非 loopback 设备;完整 Ethernet IPv6/NDP 不在当前设备层完成范围。 +- 无 route:记录 warning 并丢弃该 packet。 + +普通设备 TX 进入对应设备的 TX queue: + +```rust +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 { + 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()); + } + } +} +``` + +### 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 预创建。 + +`send_on_device()` 的 loopback 分支仍使用 `inject_loopback_rx()` 写入共享 RX queue,主要用于指定设备发送的控制面 packet;普通 socket TX loopback 走 direct injection。 + +## 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(); + } + } +} +``` + +### 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); + } + } +} +``` + +`register_device_waker()` 用于 net-poll worker 的全局设备 readiness;`register_waker(binding, waker)` 用于 socket readiness,只向 `SO_BINDTODEVICE` 或本地地址绑定允许的接口注册。 + +## Ethernet 链路层 + +Ethernet 设备在 IP packet 与真实 Ethernet frame 之间转换,并维护 ARP/neighbor 状态。 + +### RX 处理 + +`EthernetDevice::recv()` 的入站流程: + +1. 从 `EthernetDriver::receive()` 读取一帧。 +2. 解析 `EthernetFrame`。 +3. ARP frame:更新 neighbor 表、处理 gratuitous request/reply、释放 pending packet。 +4. IPv4 frame:校验链路层目标,取出 IP payload,写入 Router 提供的 packet buffer。 +5. 其他协议:忽略或记录。 + +`recv()` 输出给 Router 的始终是 IP packet,而不是 Ethernet frame。 + +### TX 处理 + +`EthernetDevice::send(next_hop, packet)` 的出站流程: + +1. 查询 `neighbors`。 +2. 命中:封装 Ethernet frame 并发送。 +3. 未命中但已有 pending ARP:把 packet 放入 `pending_packets`。 +4. 未命中且需要重试:发送 ARP request,记录 `pending_neighbors`。 + +关键参数: + +- neighbor TTL:300 秒。 +- ARP retry:1 秒。 +- pending packet buffer:有界。 + +### IRQ 与 OOB RX + +Ethernet 支持两种 RX readiness 模式: + +- IRQ 模式:`EthernetIrqRegistrar` 注册硬件 IRQ,IRQ 到来后 `handle_ethernet_irq()` 唤醒 `poll_ready`。 +- OOB RX 模式:用于 SDIO Wi-Fi 等设备,RX 就绪由设备外部线程调用 `notify_oob_rx()`,再唤醒 `{ifname}-oob-poll` 和设备 worker。 + +`register_waker()` 只在存在 IRQ registration 或 OOB RX wake source 时注册: + +```rust +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); +} +``` + +设备 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 +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)` 从指定接口发出。 + +#### 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()` 从所有设备收集邻居表快照: + +```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。 + +不承诺端到端 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 new file mode 100644 index 0000000000..c4f496fd18 --- /dev/null +++ b/docs/docs/architecture/net/flows.md @@ -0,0 +1,567 @@ +--- +sidebar_position: 9 +sidebar_label: "运行时流程" +--- + +# 运行时流程 + +本文描述 `ax-net` 从初始化到运行期收发包、socket 阻塞等待、DHCP/DNS 和本地 transport 的关键流程。流程以源码中的真实边界为准:应用线程只修改 socket 状态并请求 poll,设备 worker 只搬运 packet,专用 net-poll worker 独占推进 smoltcp `Interface` 和全局 `SocketSet`。 + +核心流程涉及: + +| 流程 | 关键源码 | +| --- | --- | +| 初始化 | [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) | + +## 总体时序 + +运行期只有一个协议核心 owner:net-poll worker。socket 调用者和设备 worker 都通过轻量唤醒把工作交给它。 + +```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 + + 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"] +``` + +## 初始化阶段 + +初始化阶段构造控制面、单协议核心和多设备数据面。`init_network()` 是一次性入口,重复调用会 panic。 + +### 配置校验 + +`init_network()` 首先校验 `NetworkConfig`: + +- 接口名不能是保留名 `lo`。 +- `dhcp = true` 不能同时配置 `static_ip`。 +- 静态 IP 不能是 unspecified。 +- 静态 prefix 不能大于 32。 +- DNS server 不能是 unspecified。 +- 每个显式 `InterfaceConfig` 必须匹配一个设备。 +- 一个设备不能被多个 config 同时匹配。 +- 接口名不能冲突。 + +网关 `0.0.0.0` 是有效配置,表示不安装默认路由。 + +### 设备与控制面构建 + +```mermaid +sequenceDiagram + 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 + Init->>Init: collect static DNS / interface snapshot + end + 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 约定: + +- `InterfaceId(1)` 固定为 `lo`。 +- Ethernet 接口从 `InterfaceId(2)` 开始按设备发现顺序分配。 +- `InterfaceId(0)` 是 Router TX 内部占位符,不出现在 public API。 + +### DHCP Bootstrap + +如果存在 DHCP 接口,初始化末尾调用 `wait_for_dhcp_bootstrap()`: + +```rust +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); + } + warn!("DHCP bootstrap timed out"); +} +``` + +bootstrap 等待只要求任一 DHCP 接口成功获得地址。这样一个断开的 DHCP 网口不会阻塞整个系统启动;未配置完成的接口后续仍由 net-poll worker 继续推进。 + +## Poll 主循环 + +poll 主循环由 `net-poll` 线程执行。它合并 socket、设备和 timer 唤醒,并通过 CAS 防止重入。 + +### request_poll + +```rust +pub fn request_poll() { + NET_POLL_REQUESTED.store(true, Ordering::Release); + NET_POLL_WAKE.notify_one(true); +} +``` + +`request_poll()` 不执行协议栈,只设置标志并唤醒 worker。socket 热路径、设备 RX/TX worker、DHCP/DNS 查询都使用这个入口。 + +### 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; + } + } +} +``` + +`poll_once()` 的锁顺序是: + +```text +SERVICE -> SOCKET_SET.inner -> Service::poll() +``` + +`poll_until_idle()` 在仍有工作时批量推进,不主动 yield。它是专用 worker,不需要在每个 packet 后让出给应用线程。 + +### Service::poll + +`Service::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(); + + 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, + ); + } + + 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); + + self.router.dispatch(timestamp, sockets) + || dhcp_poll_next + || dhcp_server_sent + || socket_state_changed +} +``` + +顺序约束: + +- `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 送出。 + +## 数据面流程 + +数据面由 RX path、TX path 和 loopback fast path 组成。真实设备通过 worker 和有界队列与协议核心解耦。 + +### RX Path + +```mermaid +sequenceDiagram + 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 + + 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 +``` + +数据结构转换: + +```text +driver RX buffer + -> device_rx_worker local PacketBuffer + -> shared BoundedPacketQueue + -> Router.rx_buffer + -> RxToken -> smoltcp Interface::poll() +``` + +`RxPacket` 保存 ingress `InterfaceId`,用于 DHCP 分发和诊断。队列满时丢包并记录 warning,避免网络热路径无界增长。 + +### TX Path + +```mermaid +sequenceDiagram + participant App as socket send/connect + participant Smol as smoltcp socket + participant Poll as net-poll + participant Router as Router + 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 +``` + +dispatch 规则: + +- 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()`。 + +### Loopback Fast Path + +loopback 普通 TX 不进入设备队列: + +```text +Router.tx_buffer + -> Router::dispatch() + -> inject_loopback_rx_direct() + -> Router.rx_buffer + -> next Service::poll() / same idle loop +``` + +`inject_loopback_rx_direct()` 在写入 RX buffer 前调用 `snoop_tcp_packet()`,因此 loopback TCP SYN 可以在同一轮 poll 中预创建 accept child socket。 + +### ARP / Neighbor + +Ethernet TX 需要把 IP packet 发送到 next-hop MAC: + +```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 +``` + +入站 ARP reply 或 gratuitous ARP 会更新 neighbor 表,并释放等待该 next hop 的 pending packet。neighbor TTL 为 300 秒,ARP retry 间隔为 1 秒。 + +## Socket 流程 + +socket 流程只修改 socket 状态并注册 waker;协议状态机推进交给 net-poll worker。 + +### TCP Connect + +```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。 + +### TCP Listen / Accept + +```text +TcpSocket::listen(backlog) + -> register endpoint in LISTEN_TABLE + -> state = Listening + +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 + +TcpSocket::accept() + -> LISTEN_TABLE.accept() + -> return first acceptable child + -> construct connected TcpSocket +``` + +accept readiness 由 `ListenTableEntryInner.accept_poll` 维护。pending child 的 recv/send readiness 会唤醒 listener 的 accept waiters。 + +### TCP/UDP Send-Recv + +通用阻塞逻辑来自 `GeneralOptions`: + +```rust +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), + ))? +} +``` + +`poll_io()` 流程: + +1. 先执行一次操作闭包。 +2. 成功则返回。 +3. `WouldBlock` 且 nonblocking/`MSG_DONTWAIT` 则立即返回。 +4. 否则注册 waker 并挂起。 +5. 被 socket readiness、设备 readiness 或 timeout 唤醒后重试。 + +UDP connected socket 在 recv 时过滤 peer;`MSG_MORE` 会把多次 send 合并为一个 datagram,并固定第一次 send 的 remote/source。 + +### Raw Socket + +raw socket 处理 IP 层 packet: + +- send 时按 remote 选择 source,或使用显式绑定地址。 +- loopback ICMP 走本地快速路径。 +- connected raw socket 使用 peer filter。 +- `deferred_rx` 保存被 peer filter 暂存的 wire packet,保证 `MSG_PEEK` 和后续 recv 不破坏 packet 格式。 + +## 控制协议流程 + +控制协议不是独立线程,它们挂在 `Service::poll()` 中运行。 + +### DHCP Client + +DHCP 状态机: + +```text +Discovering --Offer--> Requesting --ACK--> Bound + ^ | | | + | | +--NAK/reset------+ + +--retry---+--timeout/retry-------------+ +``` + +入站 packet 路径: + +```text +Router::poll() + -> snoop(interface_id, packet) + -> DhcpState::process_packet(interface_id, packet, timestamp) + -> DhcpEvent::Configured / Deconfigured + -> Service::handle_dhcp_event() + -> commit_network_state() +``` + +提交内容: + +- smoltcp `Interface` IP address list。 +- `NetControl.state.interfaces` 的 IPv4/gateway。 +- DNS registry。 +- route table 中该接口的 IPv4 rules。 + +出站 DHCP packet 由 `poll_dhcp()` 生成,再通过 `Router::send_on_device()` 从指定设备广播。 + +### DHCP Server + +内置 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 +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 +``` + +错误语义: + +- 无 DNS server:`NotFound`。 +- DNS server 不可路由:`NoSuchDeviceOrAddress`。 +- 查询超时:`TimedOut`。 +- DNS socket 无 free slot:`ResourceBusy`。 +- 名称非法或过长:`InvalidInput`。 + +## Local Transport 流程 + +AF_UNIX 和 AF_VSOCK 不通过 smoltcp `Interface`,但复用 `SocketOps` 和 `Pollable`。 + +### Unix Stream / Datagram + +Unix socket 使用 `Transport` 分发: + +```text +UnixSocket + -> Transport::Stream(StreamTransport) + -> Transport::Dgram(DgramTransport) +``` + +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 下启用: + +```text +VsockSocket + -> VsockTransport::Stream + -> vsock::connection_manager + -> rdif_vsock::Interface event path +``` + +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 和协议核心互相阻塞。 + +### 典型锁路径 + +```text +net-poll: + SERVICE -> SOCKET_SET.inner -> Service::poll() + +Router RX/TX: + Router queue locks -> RouteTable read lock -> per-device TX queue + +device worker: + DeviceHandle.inner -> Device::recv/send -> bounded queue -> request_poll() + +TCP listen/accept: + SOCKET_SET.inner -> LISTEN_TABLE bucket + +control query: + NetControl.state -> RouteTable +``` + +禁止路径: + +- 设备 worker 进入 `Service` 或 `SocketSet`。 +- socket 热路径同步执行完整 interface poll。 +- 持设备锁等待 socket readiness。 +- 持 `SocketSet` 锁做可能阻塞的用户 IO。 + +## 流程速查 + +| 场景 | 入口 | 推进者 | 结果 | +| --- | --- | --- | --- | +| 应用发送 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 new file mode 100644 index 0000000000..e7262f05ef --- /dev/null +++ b/docs/docs/architecture/net/integration.md @@ -0,0 +1,333 @@ +--- +sidebar_position: 10 +sidebar_label: "系统集成" +--- + +# 系统集成 + +`ax-net` 是 ArceOS、StarryOS 和 Axvisor 共享的网络栈实现。它统一维护接口、地址、路由、DNS、ARP、socket 状态和协议栈 poll 机制;上层系统只负责把平台设备、系统调用 ABI 或虚拟化管理意图转换为 `ax-net` 的公开 API。 + +核心源码: + +| 源码 | 集成职责 | +| --- | --- | +| [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` 等跨系统数据模型 | + +## 集成模型 + +系统集成的基本原则是:网络状态只有一份,位于 `ax-net`;外部系统不复制接口表、路由表或 socket domain。 + +```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` 和 `Socket` 枚举访问 `ax-net`: + +```text +ax-api / ax-posix-api + -> ax_net::Socket + -> SocketOps + -> tcp / udp / raw / unix / vsock +``` + +API 层只做语言级或 POSIX 风格的入口适配。具体协议状态、端口冲突检查、socket option、设备绑定、poll readiness、DNS 查询都由 `ax-net` 内部处理。 + +典型调用关系: + +| 上层操作 | `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,并按可达性过滤 | + +应用通常不直接接触 `InterfaceId`。只有需要接口约束时,才通过 `bind_device()` 或 Linux ABI 层的 `SO_BINDTODEVICE` 建立 `DeviceBinding`。 + +## StarryOS Linux ABI + +StarryOS 负责把 Linux ABI 转换为 `ax-net` API。它不维护第二套接口 registry、路由表、ARP 表或 socket poller。 + +```mermaid +flowchart TB + 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()"] +``` + +### 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` 内部的多网络命名空间实现。 + +### ioctl 与 ifreq + +`file/net.rs` 实现 `SIOCGIF*` 查询。数据来源必须是 `ax-net` 接口快照: + +| 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()` | + +所有按接口名查询的 ioctl 都应先解析 ifreq 中的 name,再通过 `ax_net::interface_by_name()` 获取快照。这样多网口、loopback 和动态注册接口都能走同一套路径。 + +### Socket Option + +StarryOS 的 `SO_BINDTODEVICE` 负责在 Linux 字符串接口名和 `ax-net` 的 `DeviceBinding` 之间转换: + +```text +setsockopt(SO_BINDTODEVICE, "eth1") + -> ax_net::interface_by_name("eth1") + -> SetSocketOption::BindToDevice(Some(interface_id)) + -> GeneralOptions::set_device_binding() + +getsockopt(SO_BINDTODEVICE) + -> socket DeviceBinding + -> ax_net::interface_by_id(interface_id) + -> interface name +``` + +其它 socket option 通过 `GetSocketOption`、`SetSocketOption` 和 `Configurable` 分发到具体 socket。`SO_TYPE`、`TCP_INFO`、超时、nonblocking、`SO_REUSEADDR` 等语义应以 `ax-net` 的 socket 状态为准。 + +### AF_PACKET + +`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。 + +### Netlink 与 procfs + +StarryOS 的 netlink 和 procfs 视图也应复用 `ax-net` 状态: + +| 视图 | 数据来源 | 说明 | +| --- | --- | --- | +| `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()` | 按接口生成兼容视图 | + +这些路径用于 Linux 兼容层观测网络状态,不应创建独立的接口或 ARP 缓存。 + +## Axvisor 接入 + +Axvisor 应通过 `NetworkConfig` 描述网络意图,例如: + +- 管理面接口使用静态地址或 DHCP。 +- VM 服务面接口配置独立 metric。 +- DNS server 来自接口级静态配置或全局 fallback。 +- 需要固定出接口的管理连接使用 `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)?; +``` + +普通 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 局部复制接口表。 + +## 接入检查清单 + +新增或修改系统接入路径时,应检查: + +- 是否通过 `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/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 new file mode 100644 index 0000000000..1e2d4812f0 --- /dev/null +++ b/docs/docs/architecture/net/overview.md @@ -0,0 +1,162 @@ +--- +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 能力,向下通过 `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)。 + +| 模块 | 角色 | 关键类型 | +| --- | --- | --- | +| [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` | + +## 能力矩阵 + +| 能力 | 实现方式 | 状态 | +| --- | --- | --- | +| 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` 内部 `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 和队列分配 | 完整 | +| 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` 使用多线程模型,但协议核心串行: + +| 线程 | 职责 | 阻塞点 | +| --- | --- | --- | +| `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())` | +| `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()`。 + +### 全局锁顺序 + +严格的锁嵌套顺序,防止死锁: + +``` +SERVICE (Mutex) + → SOCKET_SET.inner (Mutex) + → TCP_BOUND_PORTS (Mutex>) + → LISTEN_TABLE.tcp[port] (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`)主要由 `{ifname}-rx` / `{ifname}-tx` worker 独立获取。worker 不应在持有设备锁时反向进入 `SERVICE` 或 `SOCKET_SET`,避免设备路径与协议核心互相阻塞。 + +## 核心方案 + +`ax-net` 采用 **单 smoltcp `Interface` + 多设备 `Router`** 架构。详细设计论证见[架构设计 — Single Interface + Multi-Device Router](architecture.md#single-interface--multi-device-router)。 + +### 与 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` + 有界 inline packet queue | +| Zero-copy | `MSG_ZEROCOPY` / `io_uring` | 不支持端到端 zero-copy;Router 队列无每包堆分配,loopback 快速路径少一次队列 hop | + +### 与 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_\* | +| 多接口 | 原生 `netif` + 全局 PCB/socket 管理 | 单 smoltcp 实例 + `Router` 聚合多 NIC + 路由表 + metric | + +## 当前限制 + +### 协议处理串行 + +单 smoltcp 实例意味着 TCP/UDP 协议状态机在同一 `net-poll` worker 上串行执行。设备队列解耦可以减少收发阻塞,但不能让多核并行处理协议状态机。Linux 的 per-CPU softirq 在这里没有对应物。 + +### 收发路径仍有拷贝 + +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 租约管理不完整 + +当前重点覆盖 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/sockets.md b/docs/docs/architecture/net/sockets.md new file mode 100644 index 0000000000..6dab54f30f --- /dev/null +++ b/docs/docs/architecture/net/sockets.md @@ -0,0 +1,843 @@ +--- +sidebar_position: 4 +sidebar_label: "Socket 系统" +--- + +# Socket 系统 + +`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"] +``` + +## 公共 Facade + +公共 facade 定义跨协议族共享的数据形状和操作入口。系统调用层只需要持有 `Socket`,不需要知道底层是 smoltcp、Unix transport 还是 vsock connection manager。 + +### 地址与选项 + +`SocketAddrEx` 是跨地址族的统一地址类型: + +```rust +// socket.rs +pub enum SocketAddrEx { + Ip(SocketAddr), + Unix(UnixSocketAddr), + #[cfg(feature = "vsock")] + Vsock(VsockAddr), +} +``` + +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 +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; +} +``` + +默认实现只给出“不支持”的语义,具体 backend 再按协议覆盖。例如 TCP 支持 `listen/accept`,UDP/raw 不支持;Unix stream 支持 accept,Unix datagram 不按 TCP listen 语义工作;vsock 提供 stream transport。 + +### Backend 分发 + +`Socket` 枚举负责把统一 API 分发到具体 backend: + +```rust +pub enum Socket { + Udp(Box), + Tcp(Box), + Raw(Box), + Unix(Box), + #[cfg(feature = "vsock")] + Vsock(Box), +} +``` + +| 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 状态 + +共享状态层包含三类内容:通用 socket 选项、smoltcp handle 空间,以及状态转换门禁。它们不表达某个协议的完整语义,只提供所有 backend 复用的基础设施。 + +### GeneralOptions + +`GeneralOptions` 被 TCP、UDP、raw、Unix、vsock transport 复用,用来维护通用 socket option 和阻塞等待入口: + +```rust +// 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, +} +``` + +构造时固定 `(SOCK_*, AF_*, IPPROTO_*)`,后续 `getsockopt()` 直接从这里读取: + +| 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` | + +`bound_if` 保存的是稳定的 `InterfaceId`,不是 Router 内部设备索引: + +```rust +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)), + } +} +``` + +### SocketSetWrapper + +TCP、UDP 和 raw socket 都注册到同一个 smoltcp `SocketSet`,由 `SocketSetWrapper` 持有: + +```rust +pub(crate) struct SocketSetWrapper<'a> { + pub inner: Mutex>, + udp_binds: Mutex>, +} +``` + +统一 `SocketSet` 的意义: + +- 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 +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) +} +``` + +### StateLock + +`StateLock` 是 TCP 等 socket 的轻量状态门禁,避免同一个 socket 上并发 `bind/connect/listen` 进入不一致状态: + +```rust +#[repr(u8)] +pub(crate) enum State { + Idle = 0, + Busy = 1, + Connecting = 2, + Connected = 3, + Listening = 4, + Closed = 5, +} + +pub struct StateLock(AtomicU8); +``` + +`lock(expect)` 通过 CAS 把期望状态临时切到 `Busy`,`StateGuard::transit()` 在操作成功后提交新状态,失败时回退旧状态: + +```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 +``` + +## 端口与监听仲裁 + +端口仲裁是 POSIX 兼容语义的一部分,不能完全交给 smoltcp。`ax-net` 使用 TCP 和 UDP 各自的 side table 表达 wildcard/specific-address 冲突关系。 + +### UDP Bind Side Table + +UDP bind side table 位于 `SocketSetWrapper`,用于补齐 Linux 风格的 wildcard bind 冲突: + +```rust +#[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) +} +``` + +这里用 wildcard endpoint 检查 listen table 是有意的保守策略:自动分配 ephemeral port 时,只要该端口已经存在任何 listen entry,就不再分配给主动连接 socket。 + +### ListenTable + +`ListenTable` 是 TCP passive open 的核心数据结构。smoltcp 没有“一个 public listen socket 管理多个 child socket”的 POSIX 对象模型,所以 `ax-net` 在外部维护 accept queue: + +```rust +struct ListenTableEntryInner { + listen_endpoint: IpListenEndpoint, + backlog: usize, + syn_queue: VecDeque, + accept_poll: Arc, +} + +pub struct ListenTable { + tcp: Box<[ListenTableEntry]>, +} +``` + +`tcp` 是 65536 个端口 bucket,每个 bucket 存放该端口下的多个具体地址 listener。`listen()` 检查 wildcard/specific 冲突后插入 entry: + +```rust +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(listen_endpoint, backlog)); + Ok(()) +} +``` + +### SYN 预创建与 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()` 遍历 `syn_queue`,清理已经关闭且无数据的 child,返回第一个可接受 socket: + +```rust +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 { + return Err(AxError::InvalidInput); + }; + + let syn_queue: &mut VecDeque = &mut entry.syn_queue; + let mut idx = 0; + while idx < syn_queue.len() { + let handle = syn_queue[idx].accepted.handle; + if is_closed_without_data(sockets, handle) { + syn_queue.swap_remove_front(idx); + sockets.remove(handle); + continue; + } + if is_acceptable(sockets, handle) { + return Ok(syn_queue.swap_remove_front(idx).unwrap().accepted); + } + idx += 1; + } + Err(AxError::WouldBlock) +} +``` + +可接受状态包括已经建立以及已经进入关闭流程但仍可被 userspace 观察的 child,例如 `Established`、`CloseWait`、`FinWait*`、`Closing`、`LastAck`、`TimeWait`。 + +## IP Socket Backend + +TCP、UDP 和 raw socket 都持有 smoltcp `SocketHandle`,但它们在 public 语义、side table 和 packet 格式上差异很大。 + +### TCP Socket + +`TcpSocket` 包装 smoltcp stream socket,并维护 public TCP 状态、端口注册、peer endpoint、keepalive/TCP_INFO 选项和 readiness poll set: + +```rust +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, +} +``` + +TCP socket 的主要职责: + +- `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。 + +### UDP Socket + +`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() + }))?; +``` + +#### 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`: + +```rust +pub enum VsockTransport { + Stream(VsockStreamTransport), +} + +pub struct VsockSocket { + transport: VsockTransport, +} +``` + +核心连接状态位于 `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 完成。 + +### 通用 poll helper + +`GeneralOptions` 提供 send/recv 两类阻塞 helper: + +```rust +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, + ), + ))? +} +``` + +`poll_io()` 的流程: + +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 +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); +} +``` + +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::add_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 new file mode 100644 index 0000000000..4db42f05d2 --- /dev/null +++ b/docs/docs/architecture/net/testing.md @@ -0,0 +1,407 @@ +--- +sidebar_position: 11 +sidebar_label: "测试与限制" +--- + +# 测试与限制 + +本文说明 `ax-net` 现有测试资产、运行方式、覆盖范围和当前限制。测试分三层:`net/ax-net` crate 内单元测试验证协议栈内部数据结构和路由/绑定语义;StarryOS system 测试验证 Linux ABI 观测面;`apps/starry/qemu/dual-net` 验证双网口 DHCP、路由和并发数据面。 + +## 测试资产 + +| 层级 | 位置 | 作用 | +| --- | --- | --- | +| 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` 单元测试 + +### 运行方式 + +```bash +cargo test -p ax-net +``` + +`ax-net` 单元测试是 host-side Rust 测试,主要覆盖不依赖真实 QEMU 设备的内部逻辑。部分测试会使用 [lib.rs](net/ax-net/src/lib.rs) 中的 `test_support` 构造一个 split-route 测试网络: + +```text +LOCAL_IF = InterfaceId(2), LOCAL_ADDR = 10.0.2.15 +PEER_IF = InterfaceId(3), PEER_ADDR = 10.0.3.15 +``` + +`network_test_guard()` 用全局 mutex 串行化会初始化全局网络状态的测试,避免 `SERVICE`、`NET_CONTROL`、`SOCKET_SET` 这类全局单例在并发 host test 中互相污染。 + +### RouteTable + +位置:[router.rs](net/ax-net/src/router.rs) + +| 测试 | 覆盖点 | +| --- | --- | +| `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、稳定顺序和接口可用性过滤。 + +### Service / DHCP 地址状态 + +位置:[service.rs](net/ax-net/src/service.rs) + +| 测试 | 覆盖点 | +| --- | --- | +| `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 地址”的模型。 + +### TCP 设备绑定 + +位置:[tcp.rs](net/ax-net/src/tcp.rs) + +| 测试 | 覆盖点 | +| --- | --- | +| `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 的影响。 + +### UDP 设备绑定 + +位置:[udp.rs](net/ax-net/src/udp.rs) + +| 测试 | 覆盖点 | +| --- | --- | +| `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 返回错误并保留原绑定 | + +UDP 的测试与 TCP 对齐,重点是 datagram socket 的 connected peer 不应破坏本地地址绑定语义。 + +### UDP Bind Side Table + +位置:[wrapper.rs](net/ax-net/src/wrapper.rs) + +| 测试 | 覆盖点 | +| --- | --- | +| `udp_bind_rules_allow_distinct_specific_addresses` | 相同端口可绑定到不同具体本地地址;相同地址冲突;wildcard 与具体地址冲突 | +| `udp_bind_rules_reject_specific_after_wildcard` | 已存在 wildcard bind 时拒绝后续具体地址 bind | + +这些测试补齐 smoltcp UDP socket 之外的 Linux 风格 wildcard/specific bind 仲裁。 + +### TCP ListenTable + +位置:[listen_table.rs](net/ax-net/src/listen_table.rs) + +| 测试 | 覆盖点 | +| --- | --- | +| `allows_same_port_on_distinct_specific_addresses` | 同端口可以在不同具体地址上 listen | +| `wildcard_listener_conflicts_with_specific_addresses` | wildcard listener 与任一具体地址 listener 冲突 | + +这组测试覆盖 per-address listen 的冲突规则,是 wildcard listen、`0.0.0.0:port` 和多本地地址共存语义的基础。 + +### GeneralOptions + +位置:[general.rs](net/ax-net/src/general.rs) + +| 测试 | 覆盖点 | +| --- | --- | +| `device_binding_round_trips_none_and_some_interface` | `DeviceBinding` 在 `GeneralOptions` 中可以从 none 到指定接口再回到 none | + +`DeviceBinding` 使用 atomic raw ifindex 保存,这个测试验证 public 语义不会因为内部原子编码而丢失。 + +## StarryOS system 测试 + +### 运行方式 + +完整 QEMU system 组: + +```bash +cargo xtask starry test qemu --arch riscv64 -c qemu-smp1/system +``` + +常用跨架构回归: + +```bash +cargo xtask starry test qemu --arch riscv64 +cargo xtask starry test qemu --arch loongarch64 +``` + +system 测试使用 StarryOS guest 内的 Linux 用户态程序验证 syscall/ABI 层。网络相关用例主要覆盖以下几类。 + +### 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 + +| 测试 | 位置 | 覆盖点 | +| --- | --- | --- | +| `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 + +| 测试 | 位置 | 覆盖点 | +| --- | --- | --- | +| `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 | + +这些 system 测试验证的是 StarryOS Linux ABI 层是否正确使用 `ax_net::interfaces()`、`InterfaceId`、`arp_entries()` 和 socket facade。它们不替代 `ax-net` crate 单元测试;两者覆盖层级不同。 + +## dual-net 集成测试 + +`apps/starry/qemu/dual-net` 是双网卡集成测试,用于验证多设备初始化、双 DHCP、route table、接口绑定、并发收发和较大 APK 下载校验。它是 Starry app 级 QEMU 场景,不属于 `test-suit/starryos` system 分组。 + +### 运行方式 + +列出 case: + +```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 +``` + +运行 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) | + +### 拓扑 + +```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 + +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: + +```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 +``` + +脚本来自 [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 检查项 + +`dual-net-tests.sh` 执行以下检查: + +- `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 下载。 +- `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` 回读校验。 + +成功输出包含: + +```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_APK_FETCH_MS=... BYTES=... PACKAGES=... PACKAGE=python3 +DUAL_NET_TEST_PASSED +``` + +失败输出以以下格式开始: + +```text +DUAL_NET_TEST_FAILED: ... +``` + +### 覆盖范围 + +`dual-net` 覆盖: + +- 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 可以持续推进。 +- `apk fetch -R` 下载较大的包集合并写入磁盘,验证较长 TCP 流、DNS、默认路由和文件写入路径的组合稳定性。 +- `apk verify` 验证 APK 内置签名/完整性元数据,`sha256sum -c` 验证落盘文件再次读取后的内容一致性。 + +### xtask 结构自检 + +[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 的结构: + +- `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。 +- `dual-net-tests.sh` 必须包含 `apk fetch -R`、APK 重试、`apk verify`、`sha256sum -c` 和 `DUAL_NET_APK_FETCH_MS`。 +- QEMU timeout 必须足够覆盖 APK 下载校验流程。 + +这个结构测试防止 app 配置被误删、改成单网卡或失去自动 guest probe。 + +## 常见失败定位 + +### `DUAL_NET_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 超时 | +| `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` + +`cargo xtask starry test qemu` 的汇总输出可能只显示匹配到失败模式。定位时应查更早的 test binary 输出: + +```bash +rg -n "STARRY_GROUPED_TEST_FAILED|FAIL:|panic|assert|test-socket|bugfix-bug" target -g "*.log" +``` + +排查顺序: + +1. 找到第一个打印 `FAIL:` 的 test binary。 +2. 确认是否是网络 testcase,还是其它系统测试间接受网络超时影响。 +3. 对照该 testcase 的源码,确认失败发生在 syscall 返回值、超时、内容不匹配还是权限语义。 +4. 如果 riscv64 和 loongarch64 都失败,优先怀疑协议栈/ABI 逻辑;如果只在单架构失败,再检查原子、调度和定时器。 + +### `no route to destination` + +常见原因: + +- DHCP 未完成,接口没有 IPv4。 +- default route 没有提交。 +- 接口 flags 不包含 `UP`。 +- socket 被 `SO_BINDTODEVICE` 限制到不匹配接口。 +- smoltcp 选择的源地址和 route table 中的接口源地址不一致。 + +建议打印: + +```rust +info!("interfaces: {:?}", ax_net::interfaces()); +info!("routes: {:?}", ax_net::default_routes()); +info!("dns: {:?}", ax_net::dns_servers()); +``` + +### `address already in use` + +排查方向: + +- 是否已有 wildcard bind 占用同一端口。 +- 是否已有具体地址 bind 与新 bind 冲突。 +- TCP listen 是否被 `ListenTable` 的 wildcard/specific 规则拒绝。 +- UDP 是否正确设置 `SO_REUSEADDR`,以及该路径是否应跳过 side table。 +- 绑定具体本地地址时,该地址是否属于当前接口 registry。 + +### AF_PACKET / netlink 不一致 + +排查方向: + +- `SIOCGIFINDEX` 是否来自 `InterfaceId::to_linux_ifindex()`。 +- `RTM_GETLINK` 是否遍历同一份 `ax_net::interfaces()`。 +- `sockaddr_ll.sll_ifindex` 是否能通过 `InterfaceId::from_linux_ifindex()` 反查接口。 +- namespace 可见性过滤是否导致接口在某条路径可见、另一条路径不可见。 + +## 当前限制 + +### 测试覆盖限制 + +- 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 不完整。 +- 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。 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 组织为六个核心层次和一个辅助层,每一层都面向明确的职责边界。上层依赖下层,但下层不感知上层——这一原则使得同一套组件可以同时服务于多个系统。 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 00336e428d..3babf65978 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", @@ -55,3 +56,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"] } diff --git a/net/ax-net/src/config.rs b/net/ax-net/src/config.rs index 149361f04f..4751676650 100644 --- a/net/ax-net/src/config.rs +++ b/net/ax-net/src/config.rs @@ -1,31 +1,189 @@ -use alloc::vec::Vec; +//! 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; -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 { + /// 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), +} /// 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 { + /// 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, - /// 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. + /// 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 441b928977..506103fbab 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; @@ -9,6 +24,31 @@ pub const RAW_TX_BUF_LEN: usize = 64 * 1024; 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. +/// 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/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 b4add19ef6..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}; @@ -14,6 +36,7 @@ use smoltcp::{ }; use crate::{ + config::InterfaceId, consts::{ETHERNET_MAX_PENDING_PACKETS, STANDARD_MTU}, device::{ArpEntry, Device, EthernetDriver, NetDeviceError, NetIrqEvents}, }; @@ -22,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<()>, @@ -53,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, @@ -68,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, } @@ -138,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) } @@ -255,7 +287,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 { @@ -276,7 +309,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; @@ -474,7 +507,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 { @@ -497,7 +531,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..598bde6954 100644 --- a/net/ax-net/src/device/loopback.rs +++ b/net/ax-net/src/device/loopback.rs @@ -1,32 +1,30 @@ -use alloc::vec; +//! 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 axpoll::PollSet; -use smoltcp::{ - storage::{PacketBuffer, PacketMetadata}, - time::Instant, - wire::IpAddress, -}; +use smoltcp::{time::Instant, wire::IpAddress}; -use crate::{ - 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 } } @@ -37,38 +35,22 @@ impl Device for LoopbackDevice { fn recv( &mut self, - 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(), ()) - .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(). + 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/device/mod.rs b/net/ax-net/src/device/mod.rs index b3c19038c7..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; @@ -7,6 +27,8 @@ use smoltcp::{ wire::{IpAddress, Ipv4Cidr}, }; +use crate::config::InterfaceId; + mod driver; mod ethernet; mod loopback; @@ -21,19 +43,31 @@ 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, - buffer: &mut PacketBuffer<()>, + interface_id: InterfaceId, + buffer: &mut PacketBuffer, timestamp: Instant, snoop: &mut dyn FnMut(&[u8]), ) -> bool; @@ -44,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() } @@ -57,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 9febf018d3..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}; @@ -14,26 +24,32 @@ 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, - /// 已分配给哪个 MAC(简单单客户端记录) + /// Interface that is allowed to feed requests into this server. + interface_id: InterfaceId, + /// 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, server_ip: Ipv4Address, client_ip: Ipv4Address, subnet_mask: Ipv4Address, @@ -43,14 +59,17 @@ impl DhcpServer { client_ip, subnet_mask, dev, + interface_id, leased_to: None, } } - /// 解析一个入站以太网负载(IPv4 包)。若是发给本服务器的 DHCP - /// Discover/Request,返回要广播回去的完整 IPv4 应答包字节。 - pub fn process_packet(&mut self, dev: usize, packet: &[u8]) -> Option> { - if dev != self.dev { + /// 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; } @@ -68,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; } @@ -101,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 b1c4d89500..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, @@ -9,7 +25,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}, }; @@ -20,10 +37,13 @@ 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, - device_mask: AtomicU32, + /// Bound interface id encoded as zero for "not bound". + bound_if: AtomicU32, /// Socket type: SOCK_STREAM (1), SOCK_DGRAM (2), SOCK_RAW (3). socket_type: AtomicI32, @@ -45,7 +65,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, @@ -53,36 +73,50 @@ 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)) } - pub fn set_device_mask(&self, mask: u32) { - self.device_mask.store(mask, Ordering::Release); + /// 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), + Ordering::Release, + ); } - pub fn device_mask(&self) -> u32 { - self.device_mask.load(Ordering::Acquire) + /// Returns the current interface binding. + pub fn device_binding(&self) -> DeviceBinding { + let raw = self.bound_if.load(Ordering::Acquire); + DeviceBinding { + bound_if: (raw != 0).then_some(InterfaceId::new(raw)), + } } + /// 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_mask(), 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, @@ -91,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, @@ -171,6 +206,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 +235,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 } @@ -209,3 +257,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/lib.rs b/net/ax-net/src/lib.rs index 44400de39d..0203fedea0 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] @@ -29,6 +45,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; @@ -46,18 +63,20 @@ pub mod unix; pub mod vsock; mod wrapper; -use alloc::{borrow::ToOwned, boxed::Box, vec, vec::Vec}; +use alloc::{ + borrow::ToOwned, boxed::Box, format, string::String, sync::Arc, task::Wake, vec, vec::Vec, +}; use core::{ future::poll_fn, net::IpAddr, sync::atomic::{AtomicBool, Ordering}, - task::Poll, + task::{Poll, Waker}, time::Duration, }; use ax_errno::{AxError, AxResult, ax_err_type}; use ax_sync::Mutex; -use ax_task::future::block_on; +use ax_task::{WaitQueue, future::block_on}; use axpoll::PollSet; use smoltcp::{ socket::dns::{self, GetQueryResultError, StartQueryError}, @@ -68,7 +87,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, @@ -83,8 +105,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, }; @@ -92,8 +114,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))); /// Registry of wireless control-plane handles, keyed by interface name. /// @@ -119,6 +146,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 @@ -131,104 +165,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); + } + } + 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.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}:"); + 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); - } else { - dhcp_dev = Some(eth0_dev); - dhcp_mac = Some(eth0_address); + info!(" ip: {}/{}", ipv4.address(), ipv4.prefix_len()); + if let Some(gateway) = gateway { + info!(" gw: {}", gateway); + } + } 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( + 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()); + wait_for_dhcp_bootstrap(); + } +} + +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. @@ -246,8 +408,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 @@ -258,7 +423,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) { @@ -267,65 +432,101 @@ 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); +} + +/// Returns ARP/neighbor entries collected from all devices. pub fn arp_entries() -> Vec { get_service().arp_entries() } -/// Stack-agnostic configuration for registering an already-wrapped ethernet -/// device with a static IPv4 and optional services. +/// 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() +} + +/// Runtime configuration for a statically addressed Ethernet device. /// -/// This carries no notion of "Wi-Fi" or "SoftAP" — it is the generic policy the -/// protocol stack applies. Link-type-specific policy (e.g. a SoftAP's choice of -/// addresses and DHCP-server lease) is decided by the caller (board/runtime) and -/// passed in as data. +/// This is used by drivers that appear after the normal device-probe phase, +/// for example Wi-Fi AP mode devices. pub struct NetConfig { - /// Interface name (e.g. `"wlan0"`). - pub name: alloc::string::String, - /// This interface's static address / gateway. + /// 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, run a built-in DHCP server handing out this single address. + /// If set, enables the built-in one-client DHCP server with this client IP. pub dhcp_server_client_ip: Option<[u8; 4]>, - /// Spawn a dedicated poll task woken via [`notify_oob_rx`]. Needed for - /// out-of-band RX devices (e.g. SDIO) that sit outside the ethernet IRQ - /// framework. + /// Whether this device is woken through the out-of-band poll task. pub dedicated_poll: bool, } -/// Registers an already-wrapped ethernet device with a static IPv4 and the -/// services described by `config`. The network service must already be -/// initialized (via [`init_network`]). +/// Registers an extra Ethernet device with a static IPv4 address. /// -/// This is the generic, link-type-agnostic registration entry point. A SoftAP -/// is just one caller that fills in a static IP + DHCP server + dedicated poll. +/// If `dedicated_poll` is set, RX readiness is driven by [`notify_oob_rx`] +/// instead of the shared Ethernet IRQ framework. 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 mac = EthernetAddress(dev.mac_address()); - // A dedicated-poll device gets RX out-of-band (via `notify_oob_rx` → - // `wake_rx`), so its socket wakers must be armed even though it has no - // ethernet IRQ registration. 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 mut s = get_service(); - let dev_idx = s.register_static_device(config.name.clone(), eth_dev, cidr); - if let Some(client) = config.dhcp_server_client_ip { - let client_ip = Ipv4Address::new(client[0], client[1], client[2], client[3]); - let subnet_mask = prefix_to_mask(config.prefix_len); - s.enable_dhcp_server(dev_idx, server_ip, client_ip, subnet_mask); - } + let dev_idx = get_service().register_static_device(config.name.clone(), eth_dev, mac, cidr); + if let Some(client_ip) = config.dhcp_server_client_ip { + let client_ip = Ipv4Address::new(client_ip[0], client_ip[1], client_ip[2], client_ip[3]); + get_service().enable_dhcp_server( + dev_idx, + server_ip, + client_ip, + prefix_to_mask(config.prefix_len), + ); } - info!("{}: up, mac {mac}, ip {cidr}", config.name); - if config.dedicated_poll { + if config.dedicated_poll + && OOB_POLL_TASK_STARTED + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) + .is_ok() + { start_oob_poll_task(config.name); } + request_poll(); } /// Registers a wireless control-plane handle under an interface name. @@ -430,39 +631,77 @@ pub fn notify_oob_rx() { OOB_RX_SIGNAL.wake(); } -/// Spawns the out-of-band RX poll task (idempotent across all such devices). -/// -/// `ifname` names the task (e.g. `wlan0` → `wlan0-poll`). One shared task drives -/// `poll_interfaces()` for every dedicated-poll device, woken by [`notify_oob_rx`]. -fn start_oob_poll_task(ifname: alloc::string::String) { - if OOB_POLL_TASK_STARTED.swap(true, Ordering::AcqRel) { - return; - } +/// Convenience helper for retrieving `eth0` IPv4 configuration. +pub fn eth0_ipv4_config() -> Option { + get_service().eth0_ipv4_config() +} + +fn prefix_to_mask(prefix_len: u8) -> Ipv4Address { + let mask = if prefix_len == 0 { + 0 + } else { + u32::MAX << (32 - prefix_len) + }; + Ipv4Address::from_bits(mask) +} + +fn start_oob_poll_task(ifname: String) { + let task_name = format!("{ifname}-oob-poll"); ax_task::spawn_with_name( - || { + move || { + info!("start OOB network poll task for {ifname}"); block_on(poll_fn(|cx| { - // Register first to avoid lost wakeups. OOB_RX_SIGNAL.register(cx.waker()); poll_interfaces(); get_service().wake_all_devices(); Poll::<()>::Pending })); }, - alloc::format!("{ifname}-poll"), + task_name, ); } -fn prefix_to_mask(prefix_len: u8) -> Ipv4Address { - let bits: u32 = if prefix_len == 0 { - 0 - } else { - u32::MAX << (32 - prefix_len.min(32) as u32) +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) }; - Ipv4Address::from_bits(bits) + let Some(next) = next else { + return IDLE_POLL_INTERVAL; + }; + 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 + } else { + Duration::from_micros(next_micros - now_micros) + } } -pub fn eth0_ipv4_config() -> Option { - get_service().eth0_ipv4_config() +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. @@ -470,22 +709,38 @@ 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); +/// 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() { 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) } @@ -525,7 +780,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) @@ -557,34 +812,45 @@ impl Drop for DnsSocketGuard { } } -fn dhcp_bootstrap() { +fn wait_for_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); @@ -598,9 +864,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); @@ -608,21 +875,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/listen_table.rs b/net/ax-net/src/listen_table.rs index c6465609c6..62f144d397 100644 --- a/net/ax-net/src/listen_table.rs +++ b/net/ax-net/src/listen_table.rs @@ -1,8 +1,36 @@ +//! 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::ops::DerefMut; +use core::task::Waker; use ax_errno::{AxError, AxResult}; use ax_sync::Mutex; +use axpoll::PollSet; use smoltcp::{ iface::{SocketHandle, SocketSet}, socket::tcp::{self, SocketBuffer, State}, @@ -17,15 +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, } @@ -35,15 +72,18 @@ 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 { listen_endpoint, backlog, syn_queue: VecDeque::with_capacity(backlog), + accept_poll: Arc::new(PollSet::new()), } } + /// 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; @@ -54,21 +94,31 @@ impl ListenTableEntryInner { } } + /// Consumes the entry and returns all queued child handles for cleanup. fn into_handles(self) -> Vec { self.syn_queue .into_iter() .map(|pending| pending.accepted.handle) .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 + }) + } } -type ListenTableEntry = Arc>>>; +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); @@ -80,60 +130,89 @@ impl ListenTable { Self { tcp } } - pub fn can_listen(&self, port: u16) -> bool { - self.tcp[port as usize].lock().is_none() + /// 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() + .iter() + .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); - 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(); + /// 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 = { + 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 + /// Returns whether accept() can return a ready child socket. + 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 { + /// Removes and returns one acceptable child socket from the listen queue. + 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); }; @@ -162,22 +241,54 @@ impl ListenTable { Err(AxError::WouldBlock) } + /// Registers a waker for listener readiness and queued child progress. + 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 { + let socket: &mut tcp::Socket = sockets.get_mut(pending.accepted.handle); + socket.register_recv_waker(&accept_waker); + socket.register_send_waker(&accept_waker); + } + } + } + + /// Snoop hook called before smoltcp processes a potential passive open. pub fn incoming_tcp_packet( &self, src: IpEndpoint, dst: IpEndpoint, sockets: &mut SocketSet<'_>, ) { - if let Some(entry) = self.listen_entry(dst.port).lock().deref_mut() { - if !entry.can_accept_endpoint(dst) { - return; - } + 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.syn_queue.len() >= entry.backlog { // SYN queue is full, drop the packet warn!("SYN queue overflow!"); return; } + if entry.has_pending(src, dst) { + 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]), @@ -201,10 +312,18 @@ impl ListenTable { remote_endpoint: src, }, }); + entry.accept_poll.wake(); } } } +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() { @@ -218,3 +337,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/options.rs b/net/ax-net/src/options.rs index 9b3e798dac..3c581e4e4d 100644 --- a/net/ax-net/src/options.rs +++ b/net/ax-net/src/options.rs @@ -1,9 +1,25 @@ +//! 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; 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 +175,7 @@ define_options! { SocketType(i32), SocketProtocol(i32), SocketDomain(i32), + BindToDevice(Option), // --- TCP level options (TCP_*) ---- NoDelay(bool), diff --git a/net/ax-net/src/orphan.rs b/net/ax-net/src/orphan.rs new file mode 100644 index 0000000000..d58a17955f --- /dev/null +++ b/net/ax-net/src/orphan.rs @@ -0,0 +1,172 @@ +//! Orphan socket management for TCP connections. +//! +//! 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. +//! +//! # 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; + +use ax_sync::Mutex; +use smoltcp::{ + iface::{SocketHandle, SocketSet}, + socket::tcp, + time::Instant, +}; +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, +} + +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 + } +} + +#[derive(Clone, Copy)] +enum ReapReason { + /// smoltcp reached the Closed state. + Closed, + /// The orphan exceeded the maximum linger time. + Expired, +} + +/// 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())); + +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. +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 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<'_>) { + let mut removed = Vec::new(); + let remaining_overflow = { + let mut orphans = ORPHAN_SOCKETS.lock(); + orphans.retain(|orphan| { + let socket = sockets.get_mut::(orphan.handle); + 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) + .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) + .then_some(ReapReason::Expired) + } + tcp::State::Closing => orphan + .linger_expired(timestamp) + .then_some(ReapReason::Expired), + _ => { + // Unexpected state for orphan (Listen/SynSent/SynReceived/Established) + // Force remove after 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, + socket.state(), + elapsed / 1_000_000 + ); + Some(ReapReason::Expired) + } else { + None + } + } + }; + + if let Some(reason) = reason { + removed.push((orphan.handle, reason)); + false + } else { + true + } + }); + orphans.len().saturating_sub(ORPHAN_MAX_SOCKETS) + }; + + 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), + } + } + + 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). +#[allow(dead_code)] +pub(crate) fn orphan_count() -> usize { + ORPHAN_SOCKETS.lock().len() +} diff --git a/net/ax-net/src/raw.rs b/net/ax-net/src/raw.rs index 1c42c3ffc1..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::{ @@ -26,13 +45,15 @@ 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, }; +/// Allocates a smoltcp raw socket for one IP version and protocol. pub(crate) fn new_raw_socket( ip_version: IpVersion, ip_protocol: IpProtocol, @@ -47,14 +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, } @@ -63,13 +95,14 @@ 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, 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), @@ -77,10 +110,23 @@ 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); + } + self.general.set_device_binding(DeviceBinding { + bound_if: Some(interface_id), + }); + 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(_)) => { @@ -90,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) => { @@ -100,16 +147,20 @@ impl RawSocket { } } - fn local_address_for(&self, remote: IpAddress) -> IpAddress { + /// 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 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_with_binding(&remote, self.general.device_binding())? + .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 => { @@ -124,6 +175,31 @@ 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, + 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 { @@ -210,15 +286,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 +302,19 @@ 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_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"); 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,14 +328,14 @@ 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(); - self.with_smol_socket(|socket| { + request_poll(); + let written = self.with_smol_socket(|socket| { if !socket.can_send() { return Err(AxError::WouldBlock); } @@ -349,7 +430,9 @@ impl SocketOps for RawSocket { *self.loopback_rx.lock() = Some((local, reply)); } Ok(written) - }) + })?; + request_poll(); + Ok(written) }) } @@ -361,62 +444,53 @@ 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) { - 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)) = 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 - { - 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.loopback_rx.lock() = Some((source, packet)); + return Err(AxError::WouldBlock); + } + return self.deliver_packet(source, &packet, &mut dst, &mut options); + } + 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 { - continue; - } - - if let Some(from) = options.from.as_deref_mut() { - *from = SocketAddrEx::Ip(SocketAddr::new(source.into(), 0)); + 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) }) }) } @@ -447,7 +521,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( @@ -461,12 +535,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 94825cc6a3..b9dbe1ed09 100644 --- a/net/ax-net/src/router.rs +++ b/net/ax-net/src/router.rs @@ -1,5 +1,55 @@ -use alloc::{boxed::Box, vec, vec::Vec}; +//! 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, + format, + string::{String, ToString}, + sync::{Arc, Weak}, + task::Wake, + vec, + vec::Vec, +}; +use core::{ + sync::atomic::{AtomicUsize, Ordering}, + 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,77 +60,373 @@ use smoltcp::{ TcpPacket, }, }; +use spin::RwLock; use crate::{ LISTEN_TABLE, - config::Ipv4InterfaceConfig, - consts::{SOCKET_BUFFER_SIZE, STANDARD_MTU}, + config::{DeviceBinding, InterfaceId, RouteInfo}, + consts::{DEVICE_RX_QUEUE_SIZE, DEVICE_TX_QUEUE_SIZE, SOCKET_BUFFER_SIZE, STANDARD_MTU}, device::{ArpEntry, Device}, }; #[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 { - pub fn new(filter: IpCidr, via: Option, dev: usize, src: IpAddress) -> Self { + /// Creates a route rule before insertion order is assigned. + 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>; +// 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); + +/// Bounded FIFO used between the protocol core and per-device workers. +struct BoundedPacketQueue { + inner: Mutex>, + capacity: usize, + len: AtomicUsize, +} + +impl BoundedPacketQueue { + fn new(capacity: usize) -> Self { + Self { + inner: Mutex::new(VecDeque::with_capacity(capacity)), + capacity, + len: AtomicUsize::new(0), + } + } + + 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); + self.len.store(inner.len(), Ordering::Release); + Ok(()) + } + + fn pop(&self) -> Option { + 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 + } +} + +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, +} + +/// 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 { + /// 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, +} + +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(DEVICE_TX_QUEUE_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 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 {}", + self.name, next_hop + ); + return false; + } + self.tx_wake.notify_one(true); + true + } +} -// TODO(mivik): optimize +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 { + /// 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() } + Self { + rules: Vec::new(), + next_order: 0, + } } - pub fn add_rule(&mut self, rule: Rule) { - let idx = self - .rules + /// 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); + 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)) + }); + } + + /// Selects the best route to `dst` whose interface passes `is_usable`. + 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> { + /// Selects the best route to `dst` that preserves an already chosen source. + 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 remove_ipv4_rules_for_dev(&mut self, dev: usize) { + /// Returns public snapshots of IPv4 default routes. + 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() + } + + /// Removes IPv4 routes owned by one interface. + 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 ) }); } + + /// Atomically replaces IPv4 routes owned by one interface. + 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>; + +/// Virtual smoltcp device that multiplexes all concrete devices. 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 { + /// 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], vec![0u8; STANDARD_MTU * SOCKET_BUFFER_SIZE], @@ -89,164 +435,302 @@ 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(DEVICE_RX_QUEUE_SIZE)), + }); Self { rx_buffer, tx_buffer, + queues, devices: Vec::new(), - table: RouteTable::new(), + table, } } + /// Adds a route to the shared route 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); + /// 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() + .map(|device| device.name.clone()) + .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); + } + + fn start_device_tx_worker(&self, dev: usize) { + let Some(device) = self.devices.get(dev) else { + return; + }; + // Skip loopback: it uses fast path (no worker needed) + if device.interface_id == InterfaceId::LOOPBACK { + return; + } + let device = device.clone(); + let name = format!("{}-tx", device.name); + ax_task::spawn_with_name(move || device_tx_worker(device), name); + } + + fn start_device_rx_worker(&self, dev: usize) { + let Some(device) = self.devices.get(dev) else { + return; + }; + // Skip loopback: packets injected directly in dispatch + if device.interface_id == InterfaceId::LOOPBACK { + return; + } + let device = device.clone(); + let name = format!("{}-rx", device.name); + ax_task::spawn_with_name(move || device_rx_worker(device), name); + } + /// Finds the index of a device by its interface name (e.g. `"wlan0"`). pub fn device_index(&self, name: &str) -> Option { - self.devices.iter().position(|dev| dev.name() == name) + self.devices.iter().position(|device| device.name == name) } + /// Applies an IPv4 address/gateway update to one device and its routes. 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); + } + /// Builds the connected and default IPv4 route rules for one interface. + 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 } + /// Moves device-produced packets into the smoltcp RX buffer. pub fn poll( &mut self, - timestamp: Instant, + _timestamp: Instant, sockets: &mut SocketSet<'_>, - mut snoop: impl FnMut(usize, &[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); + 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; }; - while !self.rx_buffer.is_full() - && dev.recv(&mut self.rx_buffer, timestamp, &mut packet_snoop) - {} + 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(bytes); + moved_rx = true; } + moved_rx || !self.queues.rx.is_empty() } + /// Sends a control-plane packet on a specific device. pub fn send_on_device( &mut self, dev: usize, next_hop: IpAddress, packet: &[u8], - timestamp: Instant, + _timestamp: Instant, ) -> bool { - self.devices[dev].send(next_hop, packet, timestamp) + let device = &self.devices[dev]; + if device.interface_id == InterfaceId::LOOPBACK { + return inject_loopback_rx(&self.queues.rx, next_hop, packet); + } + 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 { - entries.extend(device.arp_entries(timestamp)); + entries.extend(device.inner.lock().arp_entries(timestamp)); } entries } - /// Wakes RX readiness on every device (used by the SDIO WiFi poll task). + /// 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); + device.inner.lock().register_waker(waker); + } + } + + /// Forces all device RX workers to re-check their devices. pub fn wake_all_devices(&self) { for device in &self.devices { - device.wake_rx(); + device.inner.lock().wake_rx(); + device.rx_wake.notify_one(true); } } - 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() - }), - }) + /// 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) { + device.inner.lock().register_waker(&device.rx_waker); + device.inner.lock().register_waker(waker); } - _ => None, - }) + } } - pub fn dispatch(&mut self, timestamp: Instant) -> bool { + /// 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() { + 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]; + 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, + packet.into_inner(), + sockets, + ); + } else { + poll_next |= dev.enqueue_tx(route.next_hop, packet.into_inner()); + } } } IpVersion::Ipv6 => { let packet = smoltcp::wire::Ipv6Packet::new_checked(packet) .expect("got invalid IPv6 packet"); + let src_addr = IpAddress::Ipv6(packet.src_addr()); 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 { - 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::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]; + if dev.interface_id == InterfaceId::LOOPBACK { + 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()); + } } } } @@ -255,6 +739,117 @@ 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, + 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 +/// so smoltcp can immediately consume the injected RX packet. +fn inject_loopback_rx( + rx_queue: &BoundedPacketQueue, + 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, + }; + if rx_queue.push(rx).is_err() { + warn!("Loopback: RX queue full, dropping packet to {}", dst_addr); + return false; + } + 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() { + 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()); + } + } +} + +/// 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]); + + 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: 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); + crate::request_poll(); + ax_task::yield_now(); + break; + } + crate::request_poll(); + received = true; + } + + if !received { + device.inner.lock().register_waker(&device.rx_waker); + device.rx_wake.wait(); + } + } +} + +/// smoltcp TX token backed by the router's temporary TX buffer. pub struct TxToken<'a>(&'a mut PacketBuffer); impl smoltcp::phy::TxToken for TxToken<'_> { @@ -262,13 +857,16 @@ 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, ()) + .enqueue(len, TX_INTERFACE_PLACEHOLDER) .expect("This was checked before creating the 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 => { @@ -301,14 +899,19 @@ fn snoop_tcp_packet(buf: &[u8], sockets: &mut SocketSet<'_>) { } } -pub struct RxToken<'a>(&'a [u8]); +/// smoltcp RX token for one packet queued by the router. +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) } } @@ -321,7 +924,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), )) } @@ -343,3 +952,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_if(&IpAddress::Ipv4(Ipv4Address::new(10, 0, 1, 99)), |_| true) + .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_if(&dst, |_| true).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_if(&dst, |_| true).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 f0ac409156..ad08ab5754 100644 --- a/net/ax-net/src/service.rs +++ b/net/ax-net/src/service.rs @@ -1,13 +1,72 @@ -use alloc::{boxed::Box, vec, vec::Vec}; +//! Network service and control plane. +//! +//! # Lock Ordering Rules +//! +//! 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`) +//! - 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 +//! +//! 3. **TCP_BOUND_PORTS** (`Mutex>>`) +//! - Tracks TCP bind() registrations +//! - Hold duration: registration/unregistration only +//! +//! 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 +//! // ✓ Lightweight trigger: socket paths request the dedicated worker. +//! fn socket_operation() { +//! request_poll() +//! } +//! +//! // ✓ Outer → Inner: SOCKET_SET → LISTEN_TABLE (accept readiness) +//! fn TcpSocket::poll_listener() { +//! let sockets = SOCKET_SET.inner.lock(); +//! LISTEN_TABLE.can_accept(endpoint, &sockets) +//! } +//! ``` +//! +//! # Forbidden Patterns +//! +//! ```ignore +//! // ✗ 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(); +//! waker.wake(); // WRONG: potential self-deadlock +//! ``` + +use alloc::{boxed::Box, format, 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::{ - iface::{Interface, SocketSet}, + iface::{Interface, PollResult, SocketSet}, phy::ChecksumCapabilities, time::{Duration as SmolDuration, Instant}, wire::{ @@ -18,26 +77,293 @@ use smoltcp::{ }; use crate::{ - SOCKET_SET, config::Ipv4InterfaceConfig, consts::STANDARD_MTU, device::ArpEntry, - dhcp_server::DhcpServer, router::Router, + SOCKET_SET, + config::{ + DeviceBinding, DnsServerEntry, DnsSource, InterfaceFlags, InterfaceId, InterfaceInfo, + InterfaceKind, Ipv4InterfaceConfig, RouteInfo, + }, + consts::STANDARD_MTU, + device::{ArpEntry, EthernetDevice}, + dhcp_server::DhcpServer, + 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_else(|| { + ax_err_type!( + NoSuchDeviceOrAddress, + format!("local address {addr} is not assigned to any interface") + ) + }) + } + None => Ok(DeviceBinding::default()), + } + } + + 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(|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}") + ) + })?; + 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); + } + + fn add_interface(&self, interface: NetInterface, routes: Vec) { + self.routes + .write() + .replace_ipv4_rules_for_interface(interface.id, routes); + self.state.write().interfaces.push(interface); + } + + fn allocate_interface_id(&self) -> InterfaceId { + let state = self.state.read(); + let next = state + .interfaces + .iter() + .map(|interface| interface.id.get()) + .max() + .unwrap_or(InterfaceId::LOOPBACK.get()) + .saturating_add(1); + InterfaceId::new(next) + } + + fn contains_interface_name(&self, name: &str) -> bool { + self.state + .read() + .interfaces + .iter() + .any(|interface| interface.name == name) + } +} + pub struct Service { pub iface: Interface, router: Router, + control: Arc, timeout: Option + Send>>>, - dhcp: Option, + dhcp: Vec, dhcp_server: Option, - static_dns: 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,12 +383,23 @@ 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(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), @@ -76,11 +413,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; } @@ -121,7 +458,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) ); @@ -138,6 +476,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 @@ -150,7 +492,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, } @@ -174,6 +521,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, @@ -200,40 +548,81 @@ 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, + dhcp: Vec::new(), dhcp_server: None, - static_dns, } } - pub fn enable_dhcp(&mut self, dev: usize, mac: EthernetAddress) { - self.dhcp = Some(DhcpState::new(dev, mac)); - info!("eth0: DHCP enabled"); - } - - /// 注册一个带静态 IPv4 的设备(如 SoftAP 接口),返回设备索引。 pub fn register_static_device( &mut self, - name: alloc::string::String, - dev: crate::device::EthernetDevice, + name: String, + dev: EthernetDevice, + mac: EthernetAddress, cidr: Ipv4Cidr, ) -> usize { - let dev_idx = self.router.add_device(Box::new(dev)); - self.router.set_ipv4_config(dev_idx, Some(cidr), None); + if self.control.contains_interface_name(&name) { + panic!("interface name conflict: {}", name); + } + + 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)); - info!("{name}: static ip {cidr}"); - dev_idx + self.control.add_interface( + NetInterface { + id: interface_id, + name, + kind: InterfaceKind::Ethernet, + mac: Some(mac), + ipv4: Some(cidr), + gateway: None, + mtu: STANDARD_MTU, + metric, + flags: InterfaceFlags::UP + | InterfaceFlags::RUNNING + | InterfaceFlags::BROADCAST + | InterfaceFlags::MULTICAST, + }, + routes, + ); + self.router.start_device_workers(dev); + dev + } + + 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_empty() } - /// 在指定设备上启用内置 DHCP 服务器(SoftAP 给客户端分配地址)。 pub fn enable_dhcp_server( &mut self, dev: usize, @@ -241,20 +630,26 @@ impl Service { client_ip: Ipv4Address, subnet_mask: Ipv4Address, ) { - self.dhcp_server = Some(DhcpServer::new(dev, server_ip, client_ip, subnet_mask)); + let Some(interface_id) = self.router.interface_id_for_dev(dev) else { + warn!("[dhcp-srv] invalid device index {dev}"); + return; + }; + self.dhcp_server = Some(DhcpServer::new( + dev, + interface_id, + server_ip, + client_ip, + subnet_mask, + )); info!("dev {dev}: DHCP server enabled (lease {client_ip})"); } - /// 按接口名查找设备索引(如 `"wlan0"`)。 + /// Finds the router device index for an interface name such as `wlan0`. pub fn device_index(&self, name: &str) -> Option { self.router.device_index(name) } - /// 运行时把某设备重配为 SoftAP 角色:静态 IP + 内置 DHCP 服务器。 - /// - /// 清掉该设备旧的 DHCP 客户端状态,设置静态地址,并(可选)启动单客户端 - /// DHCP 服务器。供 Wi-Fi 运行时 STA→AP 切换使用,链路层切换(teardown + - /// `start_ap_open`)由调用方在调用本方法前完成。 + /// Reconfigures one wireless device as SoftAP: static IPv4 plus optional DHCP server. pub fn reconfigure_as_ap( &mut self, dev: usize, @@ -262,22 +657,40 @@ impl Service { prefix_len: u8, client_ip: Option, ) { - // 若该设备此前是 DHCP 客户端,撤掉客户端状态及其获得的接口地址。 - if self.dhcp.as_ref().is_some_and(|s| s.dev == dev) { - if let Some(addr) = self.dhcp.as_ref().and_then(|s| s.address) { - Self::set_interface_ipv4(&mut self.iface, Some(addr), None); - } - self.dhcp = None; - } + let Some(interface) = self.interface_for_dev(dev) else { + warn!("dev {dev}: cannot reconfigure AP for unknown device"); + return; + }; + let old_ipv4 = self + .dhcp + .iter() + .find(|state| state.dev == dev) + .and_then(|state| state.address) + .or(interface.ipv4); + self.dhcp.retain(|state| state.dev != dev); let cidr = Ipv4Cidr::new(server_ip, prefix_len); - self.router.set_ipv4_config(dev, Some(cidr), None); - Self::set_interface_ipv4(&mut self.iface, None, Some(cidr)); + self.commit_network_state(NetworkStateUpdate { + interface_id: interface.id, + dev, + metric: interface.metric, + old_ipv4, + ipv4: Some(cidr), + gateway: None, + dns_source: DnsSource::Static, + dns_servers: Vec::new(), + }); match client_ip { Some(client_ip) => { let subnet_mask = mask_from_prefix(prefix_len); - self.dhcp_server = Some(DhcpServer::new(dev, server_ip, client_ip, subnet_mask)); + self.dhcp_server = Some(DhcpServer::new( + dev, + interface.id, + server_ip, + client_ip, + subnet_mask, + )); info!("dev {dev}: reconfigured as AP {cidr}, DHCP server lease {client_ip}"); } None => { @@ -287,145 +700,207 @@ impl Service { } } - /// 运行时把某设备重配为 STA 角色:撤掉 AP 静态 IP / DHCP 服务器, - /// 改用 DHCP 客户端获取地址。链路层关联由调用方先行完成。 + /// Reconfigures one wireless device as STA and restarts DHCP on it. pub fn reconfigure_as_sta(&mut self, dev: usize, mac: EthernetAddress) { - // 撤掉该设备作为 AP 时的 DHCP 服务器与静态地址。 + let Some(interface) = self.interface_for_dev(dev) else { + warn!("dev {dev}: cannot reconfigure STA for unknown device"); + return; + }; if self.dhcp_server.as_ref().is_some_and(|s| s.dev == dev) { self.dhcp_server = None; } - if let Some(cfg) = self.router.ipv4_config_for_dev(dev) { - Self::set_interface_ipv4(&mut self.iface, Some(cfg.address), None); - } - self.router.set_ipv4_config(dev, None, None); + self.dhcp.retain(|state| state.dev != dev); + self.commit_network_state(NetworkStateUpdate { + interface_id: interface.id, + dev, + metric: interface.metric, + old_ipv4: interface.ipv4, + ipv4: None, + gateway: None, + dns_source: DnsSource::Static, + dns_servers: Vec::new(), + }); - // 启用 DHCP 客户端,从新 AP 获取地址。 - self.dhcp = Some(DhcpState::new(dev, mac)); + self.enable_dhcp(interface.id, dev, interface.name, mac, interface.metric); info!("dev {dev}: reconfigured as STA, DHCP client enabled"); } - /// 唤醒所有设备的 RX 就绪(SDIO WiFi 带外收包后由 poll 任务调用)。 - pub fn wake_all_devices(&self) { - self.router.wake_all_devices(); - } - - pub fn dhcp_enabled(&self) -> bool { - self.dhcp.is_some() - } - + /// 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 - .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().any(|state| state.address.is_some()) } pub fn poll(&mut self, sockets: &mut SocketSet) -> bool { let timestamp = now(); let mut dhcp_events = Vec::new(); - let mut dhcp_server_replies: Vec<(usize, Vec)> = 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.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); - } - if let Some(reply) = dhcp_server - .as_mut() - .and_then(|srv| srv.process_packet(dev, packet)) - { - dhcp_server_replies.push((dev, reply)); - } - }); + 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) { + dhcp_events.push(event); + } + } + if let Some(server) = dhcp_server.as_mut() + && let Some(reply) = server.process_packet(interface_id, packet) + { + dhcp_server_replies.push((server.dev, reply)); + } + }); } for event in dhcp_events { self.handle_dhcp_event(event); } - let mut server_sent = false; + let mut dhcp_server_sent = false; for (dev, reply) in dhcp_server_replies { - if self.router.send_on_device( + dhcp_server_sent |= self.router.send_on_device( dev, IpAddress::Ipv4(Ipv4Address::BROADCAST), &reply, timestamp, - ) { - server_sent = true; - } + ); } - 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 || server_sent + + // Reap orphaned TCP sockets using the SocketSet already held by poll_once(). + crate::orphan::reap_orphans(timestamp, sockets); + + self.router.dispatch(timestamp, sockets) + || dhcp_poll_next + || dhcp_server_sent + || socket_state_changed + || router_rx_pending } - 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 interface_for_dev(&self, dev: usize) -> Option { + let interface_id = self.router.interface_id_for_dev(dev)?; + self.control + .state + .read() + .interfaces + .iter() + .find(|interface| interface.id == interface_id) + .cloned() } fn set_interface_ipv4( @@ -446,33 +921,19 @@ 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) + self.control.ipv4_config("eth0") } - 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 wake_all_devices(&self) { + self.router.wake_all_devices(); } - 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 { @@ -492,21 +953,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 { @@ -550,13 +1020,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, @@ -594,3 +1064,64 @@ 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()); + } + + #[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/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 66ce5e4c4e..c31c9a3099 100644 --- a/net/ax-net/src/tcp.rs +++ b/net/ax-net/src/tcp.rs @@ -1,8 +1,35 @@ -use alloc::{vec, vec::Vec}; +//! 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}, sync::atomic::{AtomicBool, AtomicI32, AtomicU32, Ordering}, - task::Context, + task::{Context, Waker}, }; use ax_errno::{AxError, AxResult, LinuxError, ax_bail, ax_err_type}; @@ -21,14 +48,17 @@ 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::*, }; +/// 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]), @@ -50,19 +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, } @@ -85,10 +132,23 @@ impl TcpSocket { keep_count: AtomicU32::new(TCP_KEEPCNT_DEFAULT), user_timeout_millis: AtomicU32::new(TCP_USER_TIMEOUT_DEFAULT_MS), rx_closed: AtomicBool::new(false), + poll_rx: Arc::new(PollSet::new()), + poll_tx: Arc::new(PollSet::new()), poll_rx_closed: PollSet::new(), } } + /// 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); + } + 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, @@ -109,13 +169,17 @@ impl TcpSocket { keep_count: AtomicU32::new(TCP_KEEPCNT_DEFAULT), user_timeout_millis: AtomicU32::new(TCP_USER_TIMEOUT_DEFAULT_MS), rx_closed: AtomicBool::new(false), + poll_rx: Arc::new(PollSet::new()), + poll_tx: Arc::new(PollSet::new()), poll_rx_closed: PollSet::new(), }; 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 } } @@ -248,11 +312,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 } @@ -372,10 +436,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 @@ -384,13 +447,15 @@ 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)?; *self.bound_endpoint.lock() = endpoint; - self.general - .set_device_mask(get_service().device_mask_for(&endpoint)); + if binding.bound_if.is_some() { + self.general.set_device_binding(binding); + } debug!("TCP socket {}: binding to {}", self.handle, local_addr); Ok(()) }) @@ -399,13 +464,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(); + request_poll(); let events = self.poll_connect(); if !events.contains(IoEvents::OUT) { Err(AxError::WouldBlock) @@ -424,6 +487,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 +502,9 @@ 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)); + if binding.bound_if.is_some() { + self.general.set_device_binding(binding); + } debug!("listening on {}", bound_endpoint); Ok(()) })?; @@ -454,12 +519,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(); + 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( @@ -480,7 +545,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) @@ -499,11 +564,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(); + request_poll(); } result } @@ -517,7 +579,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 +626,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 +674,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,17 +682,17 @@ impl SocketOps for TcpSocket { debug!("TCP socket {}: shutting down write side", self.handle); socket.close(); }); - poll_interfaces(); + request_poll(); } } // 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(); - poll_interfaces(); + request_poll(); Ok(()) })?; } @@ -642,7 +704,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(), @@ -654,6 +716,26 @@ impl Pollable for TcpSocket { } fn register(&self, context: &mut Context<'_>, events: IoEvents) { + 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(endpoint, &mut sockets, context.waker()); + } + } + self.with_smol_socket(|socket| { + if events.intersects(IoEvents::IN | IoEvents::RDHUP) { + self.poll_rx.register(context.waker()); + let waker = Waker::from(self.poll_rx.clone()); + socket.register_recv_waker(&waker); + } + if events.contains(IoEvents::OUT) { + self.poll_tx.register(context.waker()); + let waker = Waker::from(self.poll_tx.clone()); + socket.register_send_waker(&waker); + } + }); if events.intersects(IoEvents::IN | IoEvents::OUT | IoEvents::RDHUP) { self.general.register_waker(context.waker()); } @@ -665,13 +747,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); } + + // Unbind from API layer (port registry, etc.) self.unregister_bound_endpoint(); - SOCKET_SET.remove(self.handle); - // This is crucial for the close messages to be sent. - poll_interfaces(); + + 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(); } } @@ -706,14 +814,8 @@ const fn empty_endpoint() -> IpListenEndpoint { } } -fn endpoint_from_ip_endpoint(endpoint: IpEndpoint) -> IpListenEndpoint { - IpListenEndpoint { - addr: Some(endpoint.addr), - port: endpoint.port, - } -} - 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) @@ -731,9 +833,22 @@ 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() { - bound_endpoint.addr = - Some(get_service().get_source_address(&remote_endpoint.addr)); + + // 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_with_binding( + &remote_endpoint.addr, + self.general.device_binding(), + )? + .source, + ); } if bound_endpoint.port == 0 { bound_endpoint.port = get_ephemeral_port()?; @@ -774,13 +889,20 @@ 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 !had_explicit_device_binding && 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(()) }) } + /// 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)?; @@ -789,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()); @@ -799,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(()); @@ -816,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(); @@ -830,10 +955,15 @@ fn unregister_tcp_bound(endpoint: IpListenEndpoint) { } } +/// Returns whether a port is safe for ephemeral TCP allocation. fn tcp_port_available(port: u16) -> bool { - LISTEN_TABLE.can_listen(port) && !TCP_BOUND_PORTS.lock().contains_key(&port) + // 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) } +/// Returns whether two listen/bind addresses conflict on the same port. fn listen_addrs_conflict( a: Option, b: Option, @@ -872,8 +1002,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 +1030,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 +1042,88 @@ 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) + } + ); + } + + #[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 f17326f631..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}, @@ -19,11 +45,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 @@ -36,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( @@ -46,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. @@ -73,16 +105,36 @@ 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); + } + self.general.set_device_binding(DeviceBinding { + bound_if: Some(interface_id), + }); + 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), None => Err(AxError::NotConnected), } } + + /// 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())? + .source) + } } impl Configurable for UdpSocket { @@ -127,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(); @@ -143,11 +196,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,17 +204,27 @@ 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); + } + if binding.bound_if.is_some() { + self.general.set_device_binding(binding); + } *guard = Some(local_endpoint); info!("UDP socket {}: bound on {}", self.handle, endpoint); 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(); + if self.local_addr.read().is_none() { self.bind(SocketAddrEx::Ip(SocketAddr::new( IpAddr::V4(Ipv4Addr::UNSPECIFIED), @@ -174,15 +233,36 @@ 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 + (self.source_for_remote(&remote_addr.addr)?, true) + } else { + // Bound to specific IP, use that address and keep interface + (local_ep.addr, false) + } + } else { + (self.source_for_remote(&remote_addr.addr)?, 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(()) } + /// 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) { @@ -205,7 +285,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() { + self.source_for_remote(&addr.addr)? + } else { + local_ep.addr + } + } else { + self.source_for_remote(&addr.addr)? + }; (addr, src) } None => match self.remote_endpoint() { @@ -246,7 +335,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() { + self.source_for_remote(&addr.addr)? + } else { + local_ep.addr + } + } else { + self.source_for_remote(&addr.addr)? + }; Some((addr, src)) } None => self.remote_endpoint().ok(), @@ -254,7 +352,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). @@ -339,12 +437,12 @@ impl SocketOps for UdpSocket { Ok(cur_read) } })?; - // Flush TX so loopback packets reach the receiver immediately. - poll_interfaces(); + request_poll(); Ok(result) }) } + /// 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), @@ -361,7 +459,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 +531,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 +543,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(); } @@ -459,6 +557,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()); } @@ -472,26 +578,26 @@ 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; 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)] @@ -500,11 +606,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 +618,80 @@ 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) + } + ); + } + + #[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) + } + ); } } 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 b9bd1bc7a1..9d892cc625 100644 --- a/net/ax-net/src/wrapper.rs +++ b/net/ax-net/src/wrapper.rs @@ -1,34 +1,71 @@ +//! 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}; 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 { + /// `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>, - pub new_socket: Event, + /// 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![])), - new_socket: Event::new(), + udp_binds: Mutex::new(HashMap::new()), } } + /// 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); - self.new_socket.notify(1); 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, @@ -38,28 +75,115 @@ impl<'a> SocketSetWrapper<'a> { f(socket) } - pub fn udp_bind_check(&self, addr: IpAddress, port: u16) -> AxResult { + /// 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(()); } - // 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 mut binds = self.udp_binds.lock(); + if !udp_bind_available(&binds, key) { + return Err(AxError::AddrInUse); } + binds.insert(key, handle); 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; + } + let key = UdpBindKey { + addr: (!addr.is_unspecified()).then_some(addr), + port, + }; + 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); debug!("socket {}: destroyed", handle); } } + +/// Implements UDP wildcard/specific-address bind conflict rules. +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) + )); + } +} diff --git a/os/StarryOS/kernel/src/file/net.rs b/os/StarryOS/kernel/src/file/net.rs index e36e18b9d4..fcb5d8779a 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())?; } _ => { @@ -343,6 +364,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..906793c6c5 100644 --- a/os/StarryOS/kernel/src/file/packet.rs +++ b/os/StarryOS/kernel/src/file/packet.rs @@ -1,13 +1,14 @@ use alloc::{borrow::Cow, format, sync::Arc, vec, 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 +21,24 @@ 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 PACKET_HOST: u8 = 0; 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 ARPOP_REQUEST: u16 = 1; +const ARPOP_REPLY: u16 = 2; 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 +55,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 +102,9 @@ impl SockAddrLl { } } -struct PacketFrame { - data: Vec, - from: SockAddrLl, -} - struct PacketSocketState { bound: SockAddrLl, - pending: Option, + pending: Option<(Vec, SockAddrLl)>, } pub struct PacketSocket { @@ -119,9 +118,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 +133,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(()) } @@ -162,7 +162,8 @@ impl PacketSocket { let read = src.read(&mut data)?; data.truncate(read); - if let Some(reply) = build_arp_reply(&data) { + 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(); } @@ -171,15 +172,12 @@ impl PacketSocket { 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,11 +188,14 @@ impl PacketSocket { } } -fn build_arp_reply(request: &[u8]) -> Option { +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] != ETH0_REAL_MAC.len() as u8 + || request[4] != mac.0.len() as u8 || request[5] != 4 || u16::from_be_bytes([request[6], request[7]]) != ARPOP_REQUEST { @@ -203,7 +204,7 @@ fn build_arp_reply(request: &[u8]) -> Option { 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) { + if !is_modeled_peer_ipv4(&info, request_target_protocol) { return None; } @@ -214,34 +215,55 @@ fn build_arp_reply(request: &[u8]) -> Option { 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()); + 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(PacketFrame { data: reply, from }) + Some((reply, from)) } -fn is_modeled_peer_ipv4(ip: [u8; 4]) -> bool { - ax_net::eth0_ipv4_config() +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 = [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 +291,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/io_mpx/poll.rs b/os/StarryOS/kernel/src/syscall/io_mpx/poll.rs index 1ad8434caa..8e9842b043 100644 --- a/os/StarryOS/kernel/src/syscall/io_mpx/poll.rs +++ b/os/StarryOS/kernel/src/syscall/io_mpx/poll.rs @@ -1,13 +1,17 @@ use alloc::vec::Vec; -use core::mem::{MaybeUninit, offset_of}; +use core::{ + future::poll_fn, + mem::{MaybeUninit, offset_of}, + task::Poll, +}; use ax_errno::{AxError, AxResult}; use ax_runtime::hal::time::TimeValue; use ax_task::{ - current, - future::{self, block_on, poll_io}, + current, future, + future::{block_on, interruptible}, }; -use axpoll::IoEvents; +use axpoll::{IoEvents, Pollable}; use linux_raw_sys::general::{POLLNVAL, RLIMIT_NOFILE, pollfd, timespec}; use starry_signal::SignalSet; use starry_vm::{vm_read_slice, vm_write_slice}; @@ -58,6 +62,36 @@ fn write_poll_revents(fds: UserPtr, poll_fds: &[pollfd]) -> AxResult<()> Ok(()) } +fn collect_ready_poll_events( + fds: &FdPollSet, + revent_indices: &[usize], + poll_fds: &mut [pollfd], +) -> usize { + let mut res = 0usize; + for ((fd, events), revent_index) in fds.0.iter().zip(revent_indices.iter()) { + let mut result = fd.poll(); + if result.contains(IoEvents::IN) { + result |= IoEvents::RDNORM; + } + if result.contains(IoEvents::OUT) { + result |= IoEvents::WRNORM; + } + // POSIX: POLLHUP and POLLERR are always reported in revents, + // even if not requested in events. They must NOT be masked out. + let always_report = + result & (IoEvents::HUP | IoEvents::ERR | IoEvents::RDHUP | IoEvents::NVAL); + result &= *events; + result |= always_report; + + let revents = &mut poll_fds[*revent_index].revents; + *revents = result.bits() as _; + if *revents != 0 { + res += 1; + } + } + res +} + fn do_poll( poll_fds: &mut [pollfd], timeout: Option, @@ -96,40 +130,25 @@ fn do_poll( let fds = FdPollSet(fds); with_blocked_signals(sigmask, || { - match block_on(future::timeout( - timeout, - poll_io(&fds, IoEvents::empty(), false, || { - let mut res = 0usize; - for ((fd, events), revent_index) in fds.0.iter().zip(revent_indices.iter()) { - let mut result = fd.poll(); - if result.contains(IoEvents::IN) { - result |= IoEvents::RDNORM; - } - if result.contains(IoEvents::OUT) { - result |= IoEvents::WRNORM; - } - // POSIX: POLLHUP and POLLERR are always reported in revents, - // even if not requested in events. They must NOT be masked out. - let always_report = - result & (IoEvents::HUP | IoEvents::ERR | IoEvents::RDHUP | IoEvents::NVAL); - result &= *events; - result |= always_report; - - let revents = &mut poll_fds[*revent_index].revents; - *revents = result.bits() as _; - if *revents != 0 { - res += 1; - } - } - if res > 0 { - Ok(res as _) - } else { - Err(AxError::WouldBlock) - } - }), - )) { - 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()), } }) } @@ -165,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, 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 146c6f5b92..f4c3f12b9d 100644 --- a/os/arceos/modules/axruntime/src/devices.rs +++ b/os/arceos/modules/axruntime/src/devices.rs @@ -123,65 +123,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() } /// A wireless device that registers *after* `init_network`: its already-wrapped 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 857bbd4dda..28bc70b7d7 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; @@ -624,6 +624,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)) diff --git a/scripts/axbuild/src/starry/test.rs b/scripts/axbuild/src/starry/test.rs index 358896080c..7ea9891c0c 100644 --- a/scripts/axbuild/src/starry/test.rs +++ b/scripts/axbuild/src/starry/test.rs @@ -2409,6 +2409,116 @@ 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", "aarch64", "riscv64", "loongarch64"] { + 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" + ); + 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(); + 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", + "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", + ] { + 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("../.."); 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"); 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};