From 8c850474d933ef1f924dd0f9bbf481a260632499 Mon Sep 17 00:00:00 2001 From: sunhaosheng-kylin Date: Mon, 10 Aug 2026 16:53:52 +0800 Subject: [PATCH] net: add BusyBox ifconfig SIOC* ioctl support Route traditional network-device ioctls through SocketFileOps into knet, sharing ROUTE_STATE with rtnetlink and aligning address/flag/MTU/errno behavior with Linux 6.8. Co-authored-by: Cursor --- Cargo.lock | 1 + net/knet/Cargo.toml | 3 +- net/knet/docs/design.md | 31 +- net/knet/docs/security.md | 17 +- net/knet/src/device/ethernet.rs | 30 +- net/knet/src/device/mod.rs | 5 + net/knet/src/interface.rs | 557 ++++++++++++++++++++++++++++++++ net/knet/src/lib.rs | 1 + net/knet/src/netlink/mod.rs | 21 +- net/knet/src/netlink/route.rs | 326 ++++++++++++++++++- net/knet/src/netlink/tests.rs | 1 + net/knet/src/netlink/wire.rs | 1 + net/knet/src/socket/file.rs | 5 + net/knet/src/stack/router.rs | 89 ++++- posix/fs/docs/design.md | 2 +- posix/fs/docs/security.md | 3 +- 16 files changed, 1052 insertions(+), 41 deletions(-) create mode 100644 net/knet/src/interface.rs diff --git a/Cargo.lock b/Cargo.lock index ab8e8c7f9..afe859329 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2557,6 +2557,7 @@ dependencies = [ "lazyinit", "linux-raw-sys", "log", + "osvm", "ringbuf", "smoltcp", "tipc", diff --git a/net/knet/Cargo.toml b/net/knet/Cargo.toml index 2e07c829a..f55581e9f 100644 --- a/net/knet/Cargo.toml +++ b/net/knet/Cargo.toml @@ -30,7 +30,8 @@ kvfs = { workspace = true } kio = { workspace = true, features = ["alloc"] } kpoll = { workspace = true } tipc = { workspace = true, optional = true } -linux-raw-sys = { workspace = true, features = ["general"] } +linux-raw-sys = { workspace = true, features = ["general", "ioctl", "if_arp"] } +osvm = { workspace = true } bitflags = { workspace = true } enum_dispatch = { workspace = true } etherparse = { workspace = true } diff --git a/net/knet/docs/design.md b/net/knet/docs/design.md index b3306f637..c5961c8a9 100644 --- a/net/knet/docs/design.md +++ b/net/knet/docs/design.md @@ -126,6 +126,7 @@ core/kruntime | `GeneralOptions` | 统一管理 nonblock、reuseaddr、超时和设备 mask | | `SocketOps` | 上层 socket syscall 使用的统一操作接口 | | `netlink` | 提供 AF_NETLINK socket、kobject uevent 和有限 rtnetlink 控制面 | +| `interface` | 提供 BusyBox `ifconfig` 使用的传统 SIOCGIF*/SIOCSIF* socket ioctl,与 rtnetlink 共用 `ROUTE_STATE` | | `unix` | 提供 Unix domain stream 与 datagram transport;pathname 地址通过 kvfs 查找或创建 socket inode | | `vsock` | 在 `vsock` feature 下提供 virtio-vsock stream 支持 | @@ -199,17 +200,19 @@ build_initial_state v ROUTE_STATE │ RTM_NEWLINK / RTM_NEWADDR / RTM_NEWROUTE / RTM_NEWNEIGH + │ SIOCSIFFLAGS / SIOCSIFADDR / SIOCSIFNETMASK / SIOCSIFBRDADDR / SIOCSIFMTU v updated ROUTE_STATE ──sync_netlink──> Service / Router / Interface / devices │ - └─RTM_GETLINK / RTM_GETADDR / RTM_GETROUTE──> netlink dump + ├─RTM_GETLINK / RTM_GETADDR / RTM_GETROUTE──> netlink dump + └─SIOCGIF* / SIOCGIFCONF───────────────────> traditional ioctl ``` | 从 | 到 | 触发条件 | |----|----|----------| | 初始 state | `ROUTE_STATE` | `init_network` 调用 `build_initial_state` 与 `init_route_state` | -| `ROUTE_STATE` | 更新后的 `ROUTE_STATE` | `apply_newlink`、`apply_newaddr`、`apply_newroute`、`apply_newneigh` | -| 更新后的 `ROUTE_STATE` | data-plane 同步 | `update_route_state` 调用 `SERVICE.lock().sync_netlink` | +| `ROUTE_STATE` | 更新后的 `ROUTE_STATE` | `apply_newlink`、`apply_newaddr`、`apply_newroute`、`apply_newneigh`,以及 `interface` ioctl SET 路径 | +| 更新后的 `ROUTE_STATE` | data-plane 同步 | `mutate_route_state` 调用 `SERVICE.lock().sync_netlink` | ## 算法流程 @@ -281,6 +284,23 @@ updated ROUTE_STATE ──sync_netlink──> Service / Router / Interface / dev 4. mutation 请求构造新的 `RtnetlinkState`,失败时返回 netlink error。 5. 成功更新后同步到 `Service`,带 `NLM_F_ACK` 时返回 ack。 +### 传统网络设备 ioctl + +1. `sys_ioctl` 把 socket fd 委托给 `SocketFileOps::ioctl`。 +2. `interface::sock_dev_ioctl` 按命令分发到 SIOCGIF*/SIOCSIF* 处理函数。 +3. GET 路径从 `ROUTE_STATE` 读取 link/addr;SET 路径检查特权凭据后经 `mutate_route_state` 更新。 +4. `SIOCGIFCONF` 支持 NULL size probe、按 40 字节 LP64 `ifreq` 写入,以及容量不足时的静默截断。 +5. `SIOCSIFADDR` 使用 Linux classful 默认 prefix/broadcast;`0.0.0.0` 删除主 IPv4 地址。 +6. 未实现命令继续返回 `ENOTTY`。 + +当前支持矩阵: + +| 命令 | 行为 | +|------|------| +| `SIOCGIFCONF` / `SIOCGIFNAME` / `SIOCGIFFLAGS` / `SIOCGIFADDR` / `SIOCGIFNETMASK` / `SIOCGIFBRDADDR` / `SIOCGIFHWADDR` / `SIOCGIFMTU` / `SIOCGIFINDEX` | 查询 | +| `SIOCGIFMETRIC` / `SIOCGIFTXQLEN` | BusyBox 展示兼容占位 | +| `SIOCSIFFLAGS` / `SIOCSIFADDR` / `SIOCSIFNETMASK` / `SIOCSIFBRDADDR` / `SIOCSIFMTU` | 设置 | + ## 并发模型 - 全局 `SERVICE` 是 `LazyInit>`,串行化 smoltcp `Interface` 和 `Router` 访问。 @@ -310,8 +330,9 @@ TCP、raw IP、IPv6 和 DNS 继续通过 smoltcp socket set 推进,协议推 ### 控制面状态与 data-plane 同步 -rtnetlink mutation 先更新 `RtnetlinkState`,再同步到 `Service`、`Router`、smoltcp `Interface` 和 `NetDevice`。 -这种结构让 netlink dump 有稳定的 ABI presentation,也让路由表和设备地址能通过同一状态来源重建。 +rtnetlink mutation 与传统 ioctl mutation 都先更新 `RtnetlinkState`,再同步到 `Service`、`Router`、smoltcp `Interface` 和 `NetDevice`。 +这种结构让 netlink dump 和 `ifconfig` ioctl 共用单一事实源,也让路由表和设备地址能通过同一状态来源重建。 +`Router::sync_netlink` 会跳过 `IFF_UP` 已清除接口上的路由,使 `ifconfig eth0 down` 后不再选中该接口的外发路由。 代价是 dump 当前读取控制面状态,运行时统计、驱动瞬时状态和 smoltcp 内部计数没有完整反查。 ### pathname Unix socket 显式传递凭据 diff --git a/net/knet/docs/security.md b/net/knet/docs/security.md index fa42f1ae6..a6e57b487 100644 --- a/net/knet/docs/security.md +++ b/net/knet/docs/security.md @@ -44,14 +44,16 @@ driver layer / smoltcp / ringbuf - **网络输入**:Ethernet、ARP、IPv4、TCP、UDP、raw IP 包; - **设备输入**:driver 提供的 `NetBufHandle`、IRQ 唤醒、RX/TX 缓冲区生命周期; -- **控制面输入**:netlink header、attribute、route mutation、neighbor 更新; +- **控制面输入**:netlink header、attribute、route mutation、neighbor 更新, + 以及传统 socket ioctl 的 `ifreq` / `ifconf` 用户结构; - **Unix / vsock 输入**:peer socket 数据、连接建立与关闭事件, 以及 pathname Unix socket 经 kvfs 解析的路径和 inode; - **上层 syscall 语义输入**:由 `posix/net` 传入的 socket 地址族、 - socket option、阻塞语义和权限决策结果。 + socket option、阻塞语义和权限决策结果; + SET 类网络 ioctl 额外读取当前任务凭据以近似 `CAP_NET_ADMIN`。 -本模块不直接解引用用户指针, -而是信任 `posix/net` 先完成用户内存访问、长度校验和权限检查。 +本模块通过 `osvm` 对 ioctl 用户结构做 checked copy,不直接解引用用户指针。 +对普通 socket payload,本模块继续信任 `posix/net` 先完成用户内存访问、长度校验和权限检查。 本模块也不直接执行 DMA 编程, 但会消费由 driver 层提供的网络缓冲区, 因此仍需把 driver buffer 生命周期视为关键边界。 @@ -215,6 +217,8 @@ vtable 回调保持原指针与静态生命周期,不释放该测试计数器 | T-02 | smoltcp socket handle 已删除后继续访问 | 高 | listener 关闭、accepted child 清理与并发 socket 操作交错 | `SocketSet` 访问由 mutex 串行化;`ListenTable::unlisten` drain 后删除 handle;调用点需避免缓存 handle 后跨释放点使用 | | T-03 | ring buffer index 推进超过实际写入或读取长度 | 高 | `advance_write_index` 或 `advance_read_index` 的 count 与切片来源不一致 | count 由持有 channel mutex 时取得的同一批 slices 计算;unsafe 注释固定不变量 | | T-04 | 恶意 netlink 消息越界读取或构造非法状态 | 高 | 用户传入短 header、畸形 attr、非法 family、非法 ifindex | `NlMsgHeader::read`、`parse_attrs`、`parse_ip_by_family` 和 route index 检查拒绝非法输入 | +| T-04a | 恶意 `ifreq`/`ifconf` 越界写或信息泄漏 | 高 | 超大 `ifc_len`、非法用户指针 | 固定 LP64 ABI 尺寸,全部用户读写经 `osvm` checked copy;`SIOCGIFCONF` 输出记录先清零。多数 `SIOCGIF*` GET 路径在用户提交的 `ifreq` 上覆盖目标字段后回写,尾部 padding 可能保留用户输入,但不读未初始化内核内存 | +| T-04b | 非特权调用者修改网络接口 | 高 | 无特权进程调用 `SIOCSIFFLAGS`/`SIOCSIFADDR` 等 | SET 路径用 `current_cred().is_privileged()` 近似 `CAP_NET_ADMIN`,失败返回 `EPERM` | | T-05 | ARP spoofing 污染 neighbor cache | 中 | 外部主机发送伪造 ARP reply 或 request | 当前校验 unicast MAC、广播和本机目标 IP;完整邻居安全策略仍依赖网络隔离 | | T-06 | listen backlog 被 SYN 洪泛占满 | 中 | 大量连接请求命中同一 listener | backlog 被 clamp 到 `LISTEN_QUEUE_SIZE`;超限丢弃并记录 warn | | T-07 | raw socket 被无权限调用者创建 | 中 | syscall 层没有实施权限门禁 | `knet` 层只封装 raw socket 行为;权限策略应保留在 `posix/net::sys_socket` | @@ -296,6 +300,8 @@ vtable 回调保持原指针与静态生命周期,不释放该测试计数器 - Ethernet 设备只处理 IPv4 ARP,IPv6 NDP、非 Ethernet 链路和多队列 NIC 抽象仍待扩展。 - crate 内 UDP 数据路径当前只支持 IPv4;IPv6 UDP 继续由 smoltcp DNS 路径使用,普通 UDP socket 不提供 IPv6 收发。 - vsock-TIPC bridge v1 不转发 TIPC handles 或 memrefs,也不为 vsock send credit 建立持久重试队列。 +- 传统 ioctl 当前只覆盖 BusyBox `ifconfig` 常用的 IPv4 查询/设置命令;改 MAC、IPv6、接口别名、ethtool 等仍返回 `ENOTTY`。 +- SET ioctl 使用 `euid == 0` 近似 Linux `CAP_NET_ADMIN`,尚未接入完整 capability 模型。 ## 审计清单 @@ -303,8 +309,9 @@ vtable 回调保持原指针与静态生命周期,不释放该测试计数器 - 每个 `unsafe` 块均有 `SAFETY:` 注释。 - 新增 smoltcp socket handle 的生命周期受 `SOCKET_SET` 保护。 -- 新增 route 或 device mutation 通过 `update_route_state` 或等效同步路径更新 data-plane。 +- 新增 route 或 device mutation 通过 `mutate_route_state` 或等效同步路径更新 data-plane。 - 新增 netlink parser 先校验 header 长度、attribute 长度、family 和 index。 +- 新增 socket ioctl 命令校验 `ifreq`/`ifconf` 尺寸、接口名 NUL 终止、地址族和权限,并清零用户输出 padding。 - 新增 ring buffer 操作的 advance count 来自同一锁内同一批 slices。 - Unix stream 的 write index 发布、方向关闭和空队列 EOF 判定保持同一个 per-direction 排序点,方向锁内不执行用户复制或 `PollSet` 唤醒,shutdown 在释放 `channel` mutex 后执行 waiter 唤醒。 - 新增外部网络输入使用 checked parser。 diff --git a/net/knet/src/device/ethernet.rs b/net/knet/src/device/ethernet.rs index a08cd9052..5c254c6d4 100644 --- a/net/knet/src/device/ethernet.rs +++ b/net/knet/src/device/ethernet.rs @@ -15,7 +15,7 @@ use crate::{ buf::{PacketBuf, PacketOwner}, consts::ETHERNET_MAX_PENDING_PACKETS, device::NetDevice as NetDeviceOps, - ip::{IpAddress, Ipv4Cidr}, + ip::{IpAddress, Ipv4Address, Ipv4Cidr}, packet, wire::{ ArpIpv4Packet, ArpOperation, ETHERNET_HEADER_LEN, EtherType, EthernetFrameRef, MacAddress, @@ -45,6 +45,11 @@ pub struct EthernetDevice { inner: ClassDevice, neighbors: HashMap>, ip: Ipv4Cidr, + /// Independent directed broadcast from the control plane. + /// + /// `None` means this address has no directed broadcast (for example after + /// address deletion, or for prefixes without a broadcast). + broadcast: Option, pending_tx: VecDeque, } @@ -54,15 +59,21 @@ impl EthernetDevice { /// Create a new Ethernet device wrapper. pub fn new(name: String, inner: ClassDevice, ip: Ipv4Cidr) -> Self { + let broadcast = ip.broadcast(); Self { name, inner, neighbors: HashMap::new(), ip, + broadcast, pending_tx: VecDeque::with_capacity(ETHERNET_MAX_PENDING_PACKETS), } } + fn directed_broadcast(&self) -> Option { + self.broadcast + } + /// Spawn the RX poll task for an initialized network stack. pub(crate) fn spawn_rx_task(irq: usize) { let _ = ktask::spawn_with_name( @@ -381,7 +392,9 @@ impl NetDeviceOps for EthernetDevice { } packet.set_ifindex(ifindex); - if next_hop.is_broadcast() || self.ip.broadcast().map(IpAddress::Ipv4) == Some(next_hop) { + if next_hop.is_broadcast() + || self.directed_broadcast().map(IpAddress::Ipv4) == Some(next_hop) + { self.send_ipv4_packet_to(ifindex, MacAddress::BROADCAST, &packet); return false; } @@ -436,14 +449,23 @@ impl NetDeviceOps for EthernetDevice { &mut self, name: Option<&str>, ipv4_addr: Option, + broadcast: Option<[u8; 4]>, neighbors: &[(IpAddress, [u8; 6])], ) { if let Some(name) = name { self.name = name.into(); } - if let Some(ipv4_addr) = ipv4_addr { - self.ip = ipv4_addr; + match ipv4_addr { + Some(ipv4_addr) => { + self.ip = ipv4_addr; + self.broadcast = broadcast.map(Ipv4Address::from_octets); + } + None => { + // Address deletion must drop the old primary so ARP replies stop. + self.ip = Ipv4Cidr::new(Ipv4Address::UNSPECIFIED, 0); + self.broadcast = None; + } } self.neighbors.clear(); diff --git a/net/knet/src/device/mod.rs b/net/knet/src/device/mod.rs index 594f46098..231f0130c 100644 --- a/net/knet/src/device/mod.rs +++ b/net/knet/src/device/mod.rs @@ -69,10 +69,15 @@ pub trait NetDevice: Send + Sync { ) -> Result<(), PollRegisterError>; /// Synchronizes device state prepared by the control-plane adapter. + /// + /// `ipv4_addr` of `None` clears the device primary IPv4 address. + /// `broadcast` is the independent directed-broadcast address from the + /// control plane (`None` means the address has no directed broadcast). fn sync_netlink( &mut self, _name: Option<&str>, _ipv4_addr: Option, + _broadcast: Option<[u8; 4]>, _neighbors: &[(IpAddress, [u8; 6])], ) { } diff --git a/net/knet/src/interface.rs b/net/knet/src/interface.rs new file mode 100644 index 000000000..79a158ce1 --- /dev/null +++ b/net/knet/src/interface.rs @@ -0,0 +1,557 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2025 KylinSoft Co., Ltd. +// See LICENSES for license details. + +//! Traditional network-device ioctl support for BusyBox `ifconfig`. +//! +//! This module implements the common SIOCGIF*/SIOCSIF* commands used by +//! BusyBox and net-tools against an AF_INET datagram socket. Control-plane +//! state is shared with rtnetlink through [`crate::netlink::ROUTE_STATE`]. + +use core::mem::MaybeUninit; + +use kerrno::{KError, KResult, LinuxError}; +use kvfs::VfsFile; +use linux_raw_sys::ioctl::{ + SIOCGIFADDR, SIOCGIFBRDADDR, SIOCGIFCONF, SIOCGIFFLAGS, SIOCGIFHWADDR, SIOCGIFINDEX, + SIOCGIFMETRIC, SIOCGIFMTU, SIOCGIFNAME, SIOCGIFNETMASK, SIOCGIFTXQLEN, SIOCSIFADDR, + SIOCSIFBRDADDR, SIOCSIFFLAGS, SIOCSIFMTU, SIOCSIFNETMASK, +}; +use osvm::{read_vm_bytes, write_vm_bytes}; + +use crate::netlink::{ + ARPHRD_ETHER, ARPHRD_LOOPBACK, IFF_BROADCAST, IFF_MULTICAST, IFF_UP, del_primary_ipv4_addr, + ipv4_mask_from_prefix, link_state_for_ifindex, link_state_for_name, primary_ipv4_addr, + route_state, set_link_mtu, set_link_up, set_primary_ipv4_addr, set_primary_ipv4_broadcast, + set_primary_ipv4_netmask, +}; + +/// `IFNAMSIZ` from Linux UAPI. +const IFNAMSIZ: usize = 16; +/// LP64 `sizeof(struct ifreq)`: 16-byte name + 24-byte union (`ifmap`). +const IFREQ_SIZE: usize = 40; +/// LP64 `sizeof(struct ifconf)`: `int` + pad + pointer. +const IFCONF_SIZE: usize = 16; +const AF_INET: u16 = 2; +const SOCKADDR_SIZE: usize = 16; + +#[repr(C)] +#[derive(Clone, Copy)] +struct IfReqBytes { + bytes: [u8; IFREQ_SIZE], +} + +impl IfReqBytes { + fn zeroed() -> Self { + Self { + bytes: [0; IFREQ_SIZE], + } + } + + fn name_bytes(&self) -> [u8; IFNAMSIZ] { + let mut name = [0u8; IFNAMSIZ]; + name.copy_from_slice(&self.bytes[..IFNAMSIZ]); + name + } + + fn set_name(&mut self, name: &str) -> KResult<()> { + if name.len() >= IFNAMSIZ { + return Err(KError::from(LinuxError::ENODEV)); + } + self.bytes[..IFNAMSIZ].fill(0); + self.bytes[..name.len()].copy_from_slice(name.as_bytes()); + Ok(()) + } + + fn parse_name(&self) -> KResult { + let raw = self.name_bytes(); + let end = raw.iter().position(|&b| b == 0).unwrap_or(IFNAMSIZ); + if end == 0 { + return Err(KError::from(LinuxError::ENODEV)); + } + core::str::from_utf8(&raw[..end]) + .map(alloc::string::String::from) + .map_err(|_| KError::from(LinuxError::EINVAL)) + } + + fn set_flags(&mut self, flags: u16) { + self.bytes[IFNAMSIZ..IFNAMSIZ + 2].copy_from_slice(&flags.to_ne_bytes()); + } + + fn flags(&self) -> u16 { + u16::from_ne_bytes([self.bytes[IFNAMSIZ], self.bytes[IFNAMSIZ + 1]]) + } + + fn set_ivalue(&mut self, value: i32) { + self.bytes[IFNAMSIZ..IFNAMSIZ + 4].copy_from_slice(&value.to_ne_bytes()); + } + + fn ivalue(&self) -> i32 { + i32::from_ne_bytes([ + self.bytes[IFNAMSIZ], + self.bytes[IFNAMSIZ + 1], + self.bytes[IFNAMSIZ + 2], + self.bytes[IFNAMSIZ + 3], + ]) + } + + fn set_mtu(&mut self, mtu: i32) { + self.set_ivalue(mtu); + } + + fn mtu(&self) -> i32 { + self.ivalue() + } + + fn set_sockaddr_in(&mut self, addr: [u8; 4]) { + let mut sa = [0u8; SOCKADDR_SIZE]; + sa[0..2].copy_from_slice(&AF_INET.to_ne_bytes()); + // sin_port stays zero. + sa[4..8].copy_from_slice(&addr); + self.bytes[IFNAMSIZ..IFNAMSIZ + SOCKADDR_SIZE].copy_from_slice(&sa); + } + + fn sockaddr_in_addr(&self) -> KResult<[u8; 4]> { + let family = u16::from_ne_bytes([self.bytes[IFNAMSIZ], self.bytes[IFNAMSIZ + 1]]); + if family != AF_INET { + return Err(KError::from(LinuxError::EINVAL)); + } + Ok([ + self.bytes[IFNAMSIZ + 4], + self.bytes[IFNAMSIZ + 5], + self.bytes[IFNAMSIZ + 6], + self.bytes[IFNAMSIZ + 7], + ]) + } + + fn set_hwaddr(&mut self, link_type: u16, mac: [u8; 6]) { + let mut sa = [0u8; SOCKADDR_SIZE]; + sa[0..2].copy_from_slice(&link_type.to_ne_bytes()); + sa[2..8].copy_from_slice(&mac); + self.bytes[IFNAMSIZ..IFNAMSIZ + SOCKADDR_SIZE].copy_from_slice(&sa); + } +} + +fn read_ifreq(arg: usize) -> KResult { + let mut out = IfReqBytes::zeroed(); + let mut uninit = [MaybeUninit::::uninit(); IFREQ_SIZE]; + read_vm_bytes(arg as *const u8, &mut uninit)?; + for (dst, src) in out.bytes.iter_mut().zip(uninit.iter()) { + // SAFETY: `read_vm_bytes` fully initializes `uninit` on success. + *dst = unsafe { src.assume_init() }; + } + Ok(out) +} + +fn write_ifreq(arg: usize, req: &IfReqBytes) -> KResult<()> { + write_vm_bytes(arg as *mut u8, &req.bytes)?; + Ok(()) +} + +fn read_ifconf_header(arg: usize) -> KResult<(i32, usize)> { + let mut uninit = [MaybeUninit::::uninit(); IFCONF_SIZE]; + read_vm_bytes(arg as *const u8, &mut uninit)?; + let mut bytes = [0u8; IFCONF_SIZE]; + for (dst, src) in bytes.iter_mut().zip(uninit.iter()) { + // SAFETY: `read_vm_bytes` fully initializes `uninit` on success. + *dst = unsafe { src.assume_init() }; + } + let len = i32::from_ne_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]); + let ptr = usize::from_ne_bytes(bytes[8..16].try_into().unwrap()); + Ok((len, ptr)) +} + +fn write_ifconf_len(arg: usize, len: i32) -> KResult<()> { + write_vm_bytes(arg as *mut u8, &len.to_ne_bytes())?; + Ok(()) +} + +fn require_net_admin() -> KResult<()> { + if kprocess::current_cred().is_privileged() { + Ok(()) + } else { + Err(KError::from(LinuxError::EPERM)) + } +} + +fn lookup_link_by_req(req: &IfReqBytes) -> KResult { + let name = req.parse_name()?; + link_state_for_name(&name).ok_or_else(|| KError::from(LinuxError::ENODEV)) +} + +fn fill_ifreq_conf_entry(name: &str, addr: [u8; 4]) -> KResult { + let mut req = IfReqBytes::zeroed(); + req.set_name(name)?; + req.set_sockaddr_in(addr); + Ok(req) +} + +fn handle_gifconf(arg: usize) -> KResult { + let (capacity, buf_ptr) = read_ifconf_header(arg)?; + if capacity < 0 { + return Err(KError::from(LinuxError::EINVAL)); + } + + let state = route_state(); + let mut entries = alloc::vec::Vec::new(); + for link in &state.links { + for addr in state + .addrs + .iter() + .filter(|addr| addr.index == link.index as u32 && addr.family == AF_INET as u8) + { + let IpAddress::Ipv4(ipv4) = addr.address else { + continue; + }; + entries.push(fill_ifreq_conf_entry(&link.name, ipv4.octets())?); + } + } + + let needed = (entries.len() * IFREQ_SIZE) as i32; + if buf_ptr == 0 { + write_ifconf_len(arg, needed)?; + return Ok(0); + } + + let capacity = capacity as usize; + let mut written = 0usize; + for entry in &entries { + if written + IFREQ_SIZE > capacity { + break; + } + write_vm_bytes((buf_ptr + written) as *mut u8, &entry.bytes)?; + written += IFREQ_SIZE; + } + write_ifconf_len(arg, written as i32)?; + Ok(0) +} + +fn handle_gifname(arg: usize) -> KResult { + let mut req = read_ifreq(arg)?; + let index = req.ivalue(); + let link = link_state_for_ifindex(index).ok_or_else(|| KError::from(LinuxError::ENODEV))?; + req.set_name(&link.name)?; + write_ifreq(arg, &req)?; + Ok(0) +} + +fn handle_gifflags(arg: usize) -> KResult { + let mut req = read_ifreq(arg)?; + let link = lookup_link_by_req(&req)?; + req.set_flags(link.flags as u16); + write_ifreq(arg, &req)?; + Ok(0) +} + +fn handle_gifaddr(arg: usize) -> KResult { + let mut req = read_ifreq(arg)?; + let link = lookup_link_by_req(&req)?; + let addr = primary_ipv4_addr(link.index as u32) + .ok_or_else(|| KError::from(LinuxError::EADDRNOTAVAIL))?; + let IpAddress::Ipv4(ipv4) = addr.address else { + return Err(KError::from(LinuxError::EADDRNOTAVAIL)); + }; + req.set_sockaddr_in(ipv4.octets()); + write_ifreq(arg, &req)?; + Ok(0) +} + +fn handle_gifnetmask(arg: usize) -> KResult { + let mut req = read_ifreq(arg)?; + let link = lookup_link_by_req(&req)?; + let addr = primary_ipv4_addr(link.index as u32) + .ok_or_else(|| KError::from(LinuxError::EADDRNOTAVAIL))?; + req.set_sockaddr_in(ipv4_mask_from_prefix(addr.prefix_len)); + write_ifreq(arg, &req)?; + Ok(0) +} + +fn handle_gifbrdaddr(arg: usize) -> KResult { + let mut req = read_ifreq(arg)?; + let link = lookup_link_by_req(&req)?; + let addr = primary_ipv4_addr(link.index as u32) + .ok_or_else(|| KError::from(LinuxError::EADDRNOTAVAIL))?; + // Linux copies `ifa_broadcast` even when zero (loopback /31 /32). + let broadcast = addr.broadcast.unwrap_or([0, 0, 0, 0]); + req.set_sockaddr_in(broadcast); + write_ifreq(arg, &req)?; + Ok(0) +} + +fn handle_gifhwaddr(arg: usize) -> KResult { + let mut req = read_ifreq(arg)?; + let link = lookup_link_by_req(&req)?; + let link_type = if link.link_type == ARPHRD_LOOPBACK { + ARPHRD_LOOPBACK + } else { + ARPHRD_ETHER + }; + req.set_hwaddr(link_type, link.mac); + write_ifreq(arg, &req)?; + Ok(0) +} + +fn handle_gifmtu(arg: usize) -> KResult { + let mut req = read_ifreq(arg)?; + let link = lookup_link_by_req(&req)?; + req.set_mtu(link.mtu as i32); + write_ifreq(arg, &req)?; + Ok(0) +} + +fn handle_gifindex(arg: usize) -> KResult { + let mut req = read_ifreq(arg)?; + let link = lookup_link_by_req(&req)?; + req.set_ivalue(link.index); + write_ifreq(arg, &req)?; + Ok(0) +} + +fn handle_gifmetric(arg: usize) -> KResult { + let mut req = read_ifreq(arg)?; + let _link = lookup_link_by_req(&req)?; + // Linux always reports metric 0 for this ioctl. + req.set_ivalue(0); + write_ifreq(arg, &req)?; + Ok(0) +} + +fn handle_giftxqlen(arg: usize) -> KResult { + let mut req = read_ifreq(arg)?; + let _link = lookup_link_by_req(&req)?; + // Compatible placeholder used by BusyBox display paths. + req.set_ivalue(1000); + write_ifreq(arg, &req)?; + Ok(0) +} + +fn handle_sifflags(arg: usize) -> KResult { + // Copy-in before privilege check so bad pointers return EFAULT like Linux. + let req = read_ifreq(arg)?; + require_net_admin()?; + let link = lookup_link_by_req(&req)?; + // Userspace-settable flags that BusyBox may toggle. Only IFF_UP is + // implemented; other changes must not silently succeed. + const IFF_DEBUG: u32 = 1 << 2; + const IFF_NOTRAILERS: u32 = 1 << 5; + const IFF_NOARP: u32 = 1 << 7; + const IFF_PROMISC: u32 = 1 << 8; + const IFF_ALLMULTI: u32 = 1 << 9; + const IFF_DYNAMIC: u32 = 1 << 15; + const USER_SETTABLE: u32 = IFF_UP + | IFF_DEBUG + | IFF_NOTRAILERS + | IFF_NOARP + | IFF_PROMISC + | IFF_ALLMULTI + | IFF_MULTICAST + | IFF_DYNAMIC; + const SUPPORTED_USER_SETTABLE: u32 = IFF_UP; + let changed = (link.flags ^ u32::from(req.flags())) & USER_SETTABLE; + if changed & !SUPPORTED_USER_SETTABLE != 0 { + return Err(KError::from(LinuxError::EOPNOTSUPP)); + } + let want_up = req.flags() as u32 & IFF_UP != 0; + set_link_up(link.index, want_up).map_err(KError::from)?; + Ok(0) +} + +fn handle_sifaddr(arg: usize) -> KResult { + let req = read_ifreq(arg)?; + require_net_admin()?; + let link = lookup_link_by_req(&req)?; + let addr = req.sockaddr_in_addr()?; + if addr == [0, 0, 0, 0] { + del_primary_ipv4_addr(link.index as u32).map_err(KError::from)?; + } else { + let has_broadcast = link.flags & IFF_BROADCAST != 0; + set_primary_ipv4_addr(link.index as u32, addr, has_broadcast).map_err(KError::from)?; + } + Ok(0) +} + +fn handle_sifnetmask(arg: usize) -> KResult { + let req = read_ifreq(arg)?; + require_net_admin()?; + let link = lookup_link_by_req(&req)?; + let mask = req.sockaddr_in_addr()?; + set_primary_ipv4_netmask(link.index as u32, mask).map_err(KError::from)?; + Ok(0) +} + +fn handle_sifbrdaddr(arg: usize) -> KResult { + let req = read_ifreq(arg)?; + require_net_admin()?; + let link = lookup_link_by_req(&req)?; + let broadcast = req.sockaddr_in_addr()?; + set_primary_ipv4_broadcast(link.index as u32, broadcast).map_err(KError::from)?; + Ok(0) +} + +fn handle_sifmtu(arg: usize) -> KResult { + let req = read_ifreq(arg)?; + require_net_admin()?; + let link = lookup_link_by_req(&req)?; + let mtu = req.mtu(); + if mtu <= 0 { + return Err(KError::from(LinuxError::EINVAL)); + } + set_link_mtu(link.index, mtu as u32).map_err(KError::from)?; + Ok(0) +} + +fn is_inet_addr_ioctl(cmd: u32) -> bool { + matches!( + cmd, + SIOCGIFADDR + | SIOCGIFNETMASK + | SIOCGIFBRDADDR + | SIOCSIFADDR + | SIOCSIFNETMASK + | SIOCSIFBRDADDR + ) +} + +fn socket_supports_inet_addr_ioctl(socket: &crate::Socket) -> bool { + matches!( + socket, + crate::Socket::Tcp(_) + | crate::Socket::Udp(_) + | crate::Socket::Raw(_) + | crate::Socket::Packet(_) + ) +} + +/// Dispatches traditional network-device ioctl commands for socket files. +pub(crate) fn sock_dev_ioctl( + socket: &crate::Socket, + _file: &VfsFile, + cmd: u32, + arg: usize, +) -> KResult { + if is_inet_addr_ioctl(cmd) && !socket_supports_inet_addr_ioctl(socket) { + return Err(KError::NotATty); + } + match cmd { + SIOCGIFCONF => handle_gifconf(arg), + SIOCGIFNAME => handle_gifname(arg), + SIOCGIFFLAGS => handle_gifflags(arg), + SIOCGIFADDR => handle_gifaddr(arg), + SIOCGIFNETMASK => handle_gifnetmask(arg), + SIOCGIFBRDADDR => handle_gifbrdaddr(arg), + SIOCGIFHWADDR => handle_gifhwaddr(arg), + SIOCGIFMTU => handle_gifmtu(arg), + SIOCGIFINDEX => handle_gifindex(arg), + SIOCGIFMETRIC => handle_gifmetric(arg), + SIOCGIFTXQLEN => handle_giftxqlen(arg), + SIOCSIFFLAGS => handle_sifflags(arg), + SIOCSIFADDR => handle_sifaddr(arg), + SIOCSIFNETMASK => handle_sifnetmask(arg), + SIOCSIFBRDADDR => handle_sifbrdaddr(arg), + SIOCSIFMTU => handle_sifmtu(arg), + _ => Err(KError::NotATty), + } +} + +use smoltcp::wire::IpAddress; + +#[cfg(unittest)] +mod tests { + use core::mem::size_of; + + use unittest::def_test; + + use super::*; + use crate::netlink::{ + ROUTE_STATE, build_initial_state, inet_abc_len, init_route_state, ipv4_mask_from_prefix, + ipv4_prefix_from_mask, primary_ipv4_addr, route_state, set_link_up, set_primary_ipv4_addr, + set_primary_ipv4_broadcast, set_primary_ipv4_netmask, + }; + + fn init_state() { + let state = build_initial_state( + smoltcp::wire::IpCidr::Ipv4(smoltcp::wire::Ipv4Cidr::new( + smoltcp::wire::Ipv4Address::new(127, 0, 0, 1), + 8, + )), + Some(smoltcp::wire::IpCidr::Ipv4(smoltcp::wire::Ipv4Cidr::new( + smoltcp::wire::Ipv4Address::new(10, 0, 2, 15), + 24, + ))), + Some([0x02, 0, 0, 0, 0, 1]), + Some(smoltcp::wire::IpAddress::Ipv4( + smoltcp::wire::Ipv4Address::new(10, 0, 2, 2), + )), + 1500, + ); + if ROUTE_STATE.is_inited() { + *ROUTE_STATE.write() = state; + } else { + init_route_state(state); + } + } + + #[def_test] + fn abi_sizes_match_lp64_linux() { + assert_eq!(IFREQ_SIZE, 40); + assert_eq!(IFCONF_SIZE, 16); + assert_eq!(size_of::(), 40); + } + + #[def_test] + fn inet_abc_and_mask_helpers() { + assert_eq!(inet_abc_len([10, 0, 2, 15]), Some(8)); + assert_eq!(inet_abc_len([127, 0, 0, 1]), Some(8)); + assert_eq!(inet_abc_len([172, 16, 0, 1]), Some(16)); + assert_eq!(inet_abc_len([192, 168, 1, 1]), Some(24)); + assert_eq!(inet_abc_len([240, 0, 0, 1]), Some(32)); + assert_eq!(inet_abc_len([224, 0, 0, 1]), None); + assert_eq!(ipv4_mask_from_prefix(24), [255, 255, 255, 0]); + assert_eq!(ipv4_prefix_from_mask([255, 255, 255, 128]), Some(25)); + assert!(ipv4_prefix_from_mask([255, 255, 0, 255]).is_none()); + } + + #[def_test(serial)] + fn ifreq_name_and_sockaddr_roundtrip() { + let mut req = IfReqBytes::zeroed(); + req.set_name("eth0").unwrap(); + assert_eq!(req.parse_name().unwrap(), "eth0"); + req.set_sockaddr_in([10, 0, 2, 15]); + assert_eq!(req.sockaddr_in_addr().unwrap(), [10, 0, 2, 15]); + req.set_flags(0x1043); + assert_eq!(req.flags(), 0x1043); + } + + #[def_test(serial)] + fn primary_address_mutation_matches_linux_defaults() { + init_state(); + set_primary_ipv4_addr(2, [192, 168, 1, 10], true).unwrap(); + let addr = primary_ipv4_addr(2).unwrap(); + assert_eq!(addr.prefix_len, 24); + assert_eq!(addr.broadcast, Some([192, 168, 1, 255])); + + set_primary_ipv4_netmask(2, [255, 255, 255, 128]).unwrap(); + let addr = primary_ipv4_addr(2).unwrap(); + assert_eq!(addr.prefix_len, 25); + assert_eq!(addr.broadcast, Some([192, 168, 1, 127])); + + set_primary_ipv4_broadcast(2, [192, 168, 1, 200]).unwrap(); + set_primary_ipv4_netmask(2, [255, 255, 255, 0]).unwrap(); + let addr = primary_ipv4_addr(2).unwrap(); + assert_eq!(addr.prefix_len, 24); + assert_eq!(addr.broadcast, Some([192, 168, 1, 200])); + + set_link_up(2, false).unwrap(); + assert_eq!( + route_state() + .links + .iter() + .find(|link| link.index == 2) + .unwrap() + .flags + & IFF_UP, + 0 + ); + assert!(primary_ipv4_addr(2).is_some()); + } +} diff --git a/net/knet/src/lib.rs b/net/knet/src/lib.rs index 0ffb88916..f3781765d 100644 --- a/net/knet/src/lib.rs +++ b/net/knet/src/lib.rs @@ -18,6 +18,7 @@ extern crate alloc; mod consts; mod device; +mod interface; mod ip; mod link; pub mod netlink; diff --git a/net/knet/src/netlink/mod.rs b/net/knet/src/netlink/mod.rs index ae4d9db41..01189a662 100644 --- a/net/knet/src/netlink/mod.rs +++ b/net/knet/src/netlink/mod.rs @@ -30,7 +30,13 @@ mod socket; mod tests; mod wire; -pub(crate) use route::{build_initial_state, init_route_state, link_state_for_ifindex}; +pub(crate) use route::{ + build_initial_state, del_primary_ipv4_addr, init_route_state, ipv4_mask_from_prefix, + link_state_for_ifindex, link_state_for_name, primary_ipv4_addr, route_state, set_link_mtu, + set_link_up, set_primary_ipv4_addr, set_primary_ipv4_broadcast, set_primary_ipv4_netmask, +}; +#[cfg(unittest)] +pub(crate) use route::{inet_abc_len, ipv4_prefix_from_mask}; pub use socket::{NetlinkSocket, publish_kobject_uevent}; pub(crate) const RT_TABLE_MAIN: u8 = wire::route::TABLE_MAIN; pub(crate) const RTN_UNICAST: u8 = wire::route::TYPE_UNICAST; @@ -69,11 +75,11 @@ pub(crate) const IFF_UP: u32 = 1 << 0; pub(super) const IFF_BROADCAST: u32 = 1 << 1; pub(super) const IFF_LOOPBACK: u32 = 1 << 3; pub(super) const IFF_RUNNING: u32 = 1 << 6; -pub(super) const IFF_MULTICAST: u32 = 1 << 12; +pub(crate) const IFF_MULTICAST: u32 = 1 << 12; pub(super) const IFF_LOWER_UP: u32 = 1 << 16; -pub(super) const ARPHRD_LOOPBACK: u16 = 772; -pub(super) const ARPHRD_ETHER: u16 = 1; +pub(crate) const ARPHRD_LOOPBACK: u16 = 772; +pub(crate) const ARPHRD_ETHER: u16 = 1; #[derive(Clone, Debug)] pub(crate) struct LinkState { @@ -94,6 +100,11 @@ pub(crate) struct AddrState { pub(crate) prefix_len: u8, pub(crate) scope: u8, pub(crate) address: IpAddress, + /// Independent IPv4 broadcast address for ioctl/netlink presentation. + /// + /// `None` means the address has no broadcast (for example loopback, IPv6, + /// or prefix length >= 31). + pub(crate) broadcast: Option<[u8; 4]>, } #[derive(Clone, Copy, Debug)] @@ -129,7 +140,7 @@ pub(crate) struct RtnetlinkState { pub(crate) neighs: Vec, } -pub(super) static ROUTE_STATE: LazyInit> = LazyInit::new(); +pub(crate) static ROUTE_STATE: LazyInit> = LazyInit::new(); static_lock! { pub(super) static UEVENT_SUBSCRIBERS: Mutex>> = Mutex::new(Vec::new()); } diff --git a/net/knet/src/netlink/route.rs b/net/knet/src/netlink/route.rs index 4447f6434..947a310d2 100644 --- a/net/knet/src/netlink/route.rs +++ b/net/knet/src/netlink/route.rs @@ -34,6 +34,7 @@ pub(super) struct AddrRequest { pub(super) prefix_len: u8, pub(super) scope: u8, pub(super) address: IpAddress, + pub(super) broadcast: Option<[u8; 4]>, } #[derive(Clone, Copy, Debug)] @@ -70,6 +71,7 @@ struct LinkAttrs { struct AddrAttrs { address: Option, local: Option, + broadcast: Option<[u8; 4]>, } #[derive(Clone, Copy, Debug, Default)] @@ -89,16 +91,307 @@ struct NeighAttrs { impl From for AddrState { fn from(value: AddrRequest) -> Self { + let broadcast = value.broadcast.or_else(|| match value.address { + IpAddress::Ipv4(addr) if value.prefix_len < 31 => { + Some(ipv4_auto_broadcast(addr.octets(), value.prefix_len)) + } + _ => None, + }); Self { index: value.index, family: value.family, prefix_len: value.prefix_len, scope: value.scope, address: value.address, + broadcast, + } + } +} + +fn ipv4_prefix_mask(prefix_len: u8) -> u32 { + if prefix_len == 0 { + 0 + } else if prefix_len >= 32 { + !0 + } else { + !0u32 << (32 - prefix_len) + } +} + +fn ipv4_auto_broadcast(addr: [u8; 4], prefix_len: u8) -> [u8; 4] { + let host = u32::from_be_bytes(addr); + let mask = ipv4_prefix_mask(prefix_len); + (host | !mask).to_be_bytes() +} + +/// Classful prefix length used by Linux `SIOCSIFADDR` defaults. +pub(crate) fn inet_abc_len(addr: [u8; 4]) -> Option { + if addr == [0, 0, 0, 0] || addr == [255, 255, 255, 255] { + return Some(0); + } + match addr[0] { + // Linux `IN_CLASSA` includes 127/8. + 1..=127 => Some(8), + 128..=191 => Some(16), + 192..=223 => Some(24), + // Linux `IN_CLASSE` maps 240–254 to /32; 255.255.255.255 is handled above. + 240..=254 => Some(32), + _ => None, + } +} + +pub(crate) fn ipv4_mask_from_prefix(prefix_len: u8) -> [u8; 4] { + ipv4_prefix_mask(prefix_len).to_be_bytes() +} + +pub(crate) fn ipv4_prefix_from_mask(mask: [u8; 4]) -> Option { + let value = u32::from_be_bytes(mask); + let inverted = !value; + if inverted != 0 && inverted.wrapping_add(1) & inverted != 0 { + return None; + } + Some(value.count_ones() as u8) +} + +pub(crate) fn link_state_for_name(name: &str) -> Option { + if !ROUTE_STATE.is_inited() { + return None; + } + ROUTE_STATE + .read() + .links + .iter() + .find(|link| link.name == name) + .cloned() +} + +pub(crate) fn primary_ipv4_addr(ifindex: u32) -> Option { + if !ROUTE_STATE.is_inited() { + return None; + } + ROUTE_STATE + .read() + .addrs + .iter() + .find(|addr| addr.index == ifindex && addr.family == wire_route::FAMILY_IPV4) + .cloned() +} + +pub(crate) fn set_link_up(ifindex: i32, is_up: bool) -> Result<(), LinuxError> { + update_link_state(LinkUpdateRequest { + index: ifindex, + flags: if is_up { IFF_UP } else { 0 }, + change: IFF_UP, + name: None, + mtu: None, + }) +} + +pub(crate) fn set_link_mtu(ifindex: i32, mtu: u32) -> Result<(), LinuxError> { + mutate_route_state(|state| { + let link = state + .links + .iter_mut() + .find(|link| link.index == ifindex) + .ok_or(LinuxError::ENODEV)?; + let (min_mtu, max_mtu) = if link.link_type == ARPHRD_LOOPBACK { + (0u32, 65_536u32) + } else { + // Linux ETH_MIN_MTU / ETH_MAX_MTU. + (68u32, 65_535u32) + }; + if mtu < min_mtu || mtu > max_mtu { + return Err(LinuxError::EINVAL); + } + link.mtu = mtu; + Ok(()) + }) +} + +fn sync_prefsrc_and_local_route( + state: &mut RtnetlinkState, + ifindex: u32, + old_addr: Option, + new_addr: Option<(IpAddress, u8)>, +) { + if let Some(old) = old_addr { + // Initial eth0 host /32 routes use loopback `oif=1`. Match by destination + // and host scope so address updates do not leave stale local routes. + state.routes.retain(|route| { + !(route.family == wire_route::FAMILY_IPV4 + && route.dst_len == 32 + && route.dst == Some(old) + && route.gateway.is_none() + && route.scope == wire_route::SCOPE_HOST) + }); + for route in &mut state.routes { + if route.oif == ifindex && route.prefsrc == Some(old) { + route.prefsrc = new_addr.map(|(addr, _)| addr); + } + } + } + + if let Some((addr, _prefix_len)) = new_addr { + let exists = state.routes.iter().any(|route| { + route.family == wire_route::FAMILY_IPV4 + && route.dst_len == 32 + && route.dst == Some(addr) + && route.gateway.is_none() + && route.scope == wire_route::SCOPE_HOST + }); + if !exists { + // Mirror Linux local-table host routes: deliver via loopback. + state.routes.push(RouteState { + family: wire_route::FAMILY_IPV4, + dst_len: 32, + table: wire_route::TABLE_MAIN, + protocol: wire_route::PROTOCOL_BOOT, + scope: wire_route::SCOPE_HOST, + route_type: wire_route::TYPE_UNICAST, + oif: 1, + dst: Some(addr), + gateway: None, + prefsrc: Some(addr), + }); + } + for route in &mut state.routes { + if route.oif == ifindex && route.prefsrc.is_none() && route.gateway.is_some() { + route.prefsrc = Some(addr); + } } } } +pub(crate) fn set_primary_ipv4_addr( + ifindex: u32, + addr: [u8; 4], + has_broadcast_flag: bool, +) -> Result<(), LinuxError> { + let prefix_len = inet_abc_len(addr).ok_or(LinuxError::EINVAL)?; + let ip = IpAddress::Ipv4(smoltcp::wire::Ipv4Address::from_octets(addr)); + let broadcast = if has_broadcast_flag && prefix_len < 31 { + Some(ipv4_auto_broadcast(addr, prefix_len)) + } else { + None + }; + + mutate_route_state(|state| { + ensure_link_exists(state, ifindex)?; + let old = state + .addrs + .iter() + .find(|entry| entry.index == ifindex && entry.family == wire_route::FAMILY_IPV4) + .map(|entry| entry.address); + + if let Some(existing) = state + .addrs + .iter_mut() + .find(|entry| entry.index == ifindex && entry.family == wire_route::FAMILY_IPV4) + { + // Linux returns success without rewriting mask/broadcast when the + // primary address value is unchanged. + if existing.address == ip { + return Ok(()); + } + existing.address = ip; + existing.prefix_len = prefix_len; + existing.scope = if ifindex == 1 { + wire_route::SCOPE_HOST + } else { + wire_route::SCOPE_UNIVERSE + }; + existing.broadcast = broadcast; + } else { + state.addrs.push(AddrState { + index: ifindex, + family: wire_route::FAMILY_IPV4, + prefix_len, + scope: if ifindex == 1 { + wire_route::SCOPE_HOST + } else { + wire_route::SCOPE_UNIVERSE + }, + address: ip, + broadcast, + }); + } + + sync_prefsrc_and_local_route(state, ifindex, old, Some((ip, prefix_len))); + Ok(()) + }) +} + +pub(crate) fn del_primary_ipv4_addr(ifindex: u32) -> Result<(), LinuxError> { + mutate_route_state(|state| { + ensure_link_exists(state, ifindex)?; + let Some(pos) = state + .addrs + .iter() + .position(|entry| entry.index == ifindex && entry.family == wire_route::FAMILY_IPV4) + else { + // Linux returns success even when deleting a missing 0.0.0.0 address. + return Ok(()); + }; + let old = state.addrs[pos].address; + state.addrs.remove(pos); + sync_prefsrc_and_local_route(state, ifindex, Some(old), None); + Ok(()) + }) +} + +pub(crate) fn set_primary_ipv4_netmask(ifindex: u32, mask: [u8; 4]) -> Result<(), LinuxError> { + let prefix_len = ipv4_prefix_from_mask(mask).ok_or(LinuxError::EINVAL)?; + mutate_route_state(|state| { + ensure_link_exists(state, ifindex)?; + let addr = state + .addrs + .iter_mut() + .find(|entry| entry.index == ifindex && entry.family == wire_route::FAMILY_IPV4) + .ok_or(LinuxError::EADDRNOTAVAIL)?; + if addr.prefix_len == prefix_len { + return Ok(()); + } + + let IpAddress::Ipv4(ipv4) = addr.address else { + return Err(LinuxError::EADDRNOTAVAIL); + }; + let old_prefix = addr.prefix_len; + let old_auto = if old_prefix < 31 { + Some(ipv4_auto_broadcast(ipv4.octets(), old_prefix)) + } else { + None + }; + if addr.broadcast == old_auto { + addr.broadcast = if prefix_len < 31 { + Some(ipv4_auto_broadcast(ipv4.octets(), prefix_len)) + } else { + None + }; + } + addr.prefix_len = prefix_len; + Ok(()) + }) +} + +pub(crate) fn set_primary_ipv4_broadcast( + ifindex: u32, + broadcast: [u8; 4], +) -> Result<(), LinuxError> { + mutate_route_state(|state| { + ensure_link_exists(state, ifindex)?; + let addr = state + .addrs + .iter_mut() + .find(|entry| entry.index == ifindex && entry.family == wire_route::FAMILY_IPV4) + .ok_or(LinuxError::EADDRNOTAVAIL)?; + if addr.broadcast == Some(broadcast) { + return Ok(()); + } + addr.broadcast = Some(broadcast); + Ok(()) + }) +} + impl From for RouteState { fn from(value: RouteRequest) -> Self { Self { @@ -451,6 +744,7 @@ fn parse_addr_request(payload: &[u8]) -> Result { prefix_len: info.prefix_len, scope: info.scope, address: attrs.local.or(attrs.address).ok_or(LinuxError::EINVAL)?, + broadcast: attrs.broadcast, }) } @@ -511,6 +805,15 @@ fn parse_addr_attrs(family: u8, attrs: Vec>) -> Result { parsed.address = Some(parse_ip_by_family(family, attr.payload)?) } + wire_addr::attr::BROADCAST if family == wire_route::FAMILY_IPV4 => { + if attr.payload.len() != 4 { + return Err(LinuxError::EINVAL); + } + let mut octets = [0u8; 4]; + octets.copy_from_slice(attr.payload); + parsed.broadcast = Some(octets); + } + wire_addr::attr::BROADCAST => {} wire_addr::attr::LABEL => { let _ = parse_string(attr.payload)?; } @@ -593,6 +896,9 @@ fn build_addr_message(request: &NlMsgHeader, addr: AddrState, label: &str) -> Ve let ip_bytes = ip_addr_bytes(addr.address); push_attr(&mut payload, wire_addr::attr::ADDRESS, &ip_bytes); push_attr(&mut payload, wire_addr::attr::LOCAL, &ip_bytes); + if let Some(broadcast) = addr.broadcast { + push_attr(&mut payload, wire_addr::attr::BROADCAST, &broadcast); + } push_attr_str(&mut payload, wire_addr::attr::LABEL, label); build_nlmsg(RTM_NEWADDR, request.seq, NLM_F_MULTI, payload) @@ -681,13 +987,13 @@ fn mutate_route_state(f: F) -> Result<(), LinuxError> where F: FnOnce(&mut RtnetlinkState) -> Result<(), LinuxError>, { - let new_state = { - let mut state = ROUTE_STATE.write(); - f(&mut state)?; - state.clone() - }; + let mut state = ROUTE_STATE.write(); + f(&mut state)?; if SERVICE.is_inited() { - SERVICE.lock().sync_netlink(&new_state); + // Hold the control-plane write lock across data-plane sync so a newer + // mutation cannot publish first and then be overwritten by an older + // snapshot. + SERVICE.lock().sync_netlink(&state); } Ok(()) } @@ -719,6 +1025,7 @@ pub(crate) fn build_initial_state( prefix_len: lo_ip.prefix_len(), scope: wire_route::SCOPE_HOST, address: lo_ip.address(), + broadcast: None, }); state.routes.push(RouteState { family: wire_route::FAMILY_IPV4, @@ -744,6 +1051,12 @@ pub(crate) fn build_initial_state( mac: eth0_mac.unwrap_or([0; 6]), broadcast: [0xff; 6], }); + let eth0_broadcast = match eth0_ip.address() { + IpAddress::Ipv4(addr) if eth0_ip.prefix_len() < 31 => { + Some(ipv4_auto_broadcast(addr.octets(), eth0_ip.prefix_len())) + } + _ => None, + }; state.addrs.push(AddrState { index: 2, family: match eth0_ip.address() { @@ -753,6 +1066,7 @@ pub(crate) fn build_initial_state( prefix_len: eth0_ip.prefix_len(), scope: wire_route::SCOPE_UNIVERSE, address: eth0_ip.address(), + broadcast: eth0_broadcast, }); state.routes.push(RouteState { family: wire_route::FAMILY_IPV4, diff --git a/net/knet/src/netlink/tests.rs b/net/knet/src/netlink/tests.rs index f5c30df53..a4f9472cb 100644 --- a/net/knet/src/netlink/tests.rs +++ b/net/knet/src/netlink/tests.rs @@ -147,6 +147,7 @@ fn test_add_replace_and_delete_address() { prefix_len: 24, scope: wire_route::SCOPE_UNIVERSE, address: IpAddress::Ipv4(Ipv4Address::new(192, 168, 1, 99)), + broadcast: None, }; add_addr_state(req, NLM_F_CREATE | NLM_F_EXCL).unwrap(); diff --git a/net/knet/src/netlink/wire.rs b/net/knet/src/netlink/wire.rs index bd35ed141..410cc0968 100644 --- a/net/knet/src/netlink/wire.rs +++ b/net/knet/src/netlink/wire.rs @@ -26,6 +26,7 @@ pub(crate) mod addr { pub(crate) const ADDRESS: u16 = 1; pub(crate) const LOCAL: u16 = 2; pub(crate) const LABEL: u16 = 3; + pub(crate) const BROADCAST: u16 = 4; } } diff --git a/net/knet/src/socket/file.rs b/net/knet/src/socket/file.rs index 56c128d07..568be9d2b 100644 --- a/net/knet/src/socket/file.rs +++ b/net/knet/src/socket/file.rs @@ -69,6 +69,11 @@ impl FileOperations for SocketFileOps { socket.send(src, options) } + fn ioctl(&self, file: &VfsFile, cmd: u32, arg: usize) -> VfsResult { + let socket = Self::socket(file)?; + crate::interface::sock_dev_ioctl(&socket, file, cmd, arg) + } + fn poll(&self, file: &VfsFile) -> IoEvents { Self::socket(file) .map(|socket| socket.poll()) diff --git a/net/knet/src/stack/router.rs b/net/knet/src/stack/router.rs index 4d758b831..cde5ad057 100644 --- a/net/knet/src/stack/router.rs +++ b/net/knet/src/stack/router.rs @@ -28,6 +28,23 @@ use crate::{ udp, udp_err, }; +#[derive(Clone, Copy, Debug)] +struct LocalIpv4Addr { + cidr: Ipv4Cidr, + /// Directed broadcast from control plane; `None` means none. + broadcast: Option, +} + +impl LocalIpv4Addr { + fn address(self) -> Ipv4Address { + self.cidr.address() + } + + fn broadcast(self) -> Option { + self.broadcast + } +} + #[derive(Debug)] pub struct Rule { pub filter: IpCidr, @@ -89,7 +106,9 @@ impl RouteTable { pub struct Router { rx_queue: VecDeque, tx_queue: VecDeque, - local_ipv4_addrs: Vec, + local_ipv4_addrs: Vec, + /// Per-device administrative up state mirrored from `ROUTE_STATE` link flags. + device_up: Vec, ipv4_reassembler: Ipv4Reassembler, next_ipv4_identification: u16, pub(crate) devices: Vec>, @@ -101,6 +120,7 @@ impl Router { rx_queue: VecDeque::with_capacity(SOCKET_BUFFER_SIZE), tx_queue: VecDeque::with_capacity(SOCKET_BUFFER_SIZE), local_ipv4_addrs: Vec::new(), + device_up: Vec::new(), ipv4_reassembler: Ipv4Reassembler::new(), next_ipv4_identification: 1, devices: Vec::new(), @@ -156,23 +176,30 @@ impl Router { }; self.devices.remove(pos); self.table.remove_device(pos); + if pos < self.device_up.len() { + self.device_up.remove(pos); + } true } pub fn sync_netlink(&mut self, state: &RtnetlinkState) { + self.device_up.clear(); for (dev_index, device) in self.devices.iter_mut().enumerate() { let ifindex = dev_index as i32 + 1; let link = state.links.iter().find(|link| link.index == ifindex); - let ipv4_addr = state + let is_up = link.is_some_and(|link| link.flags & crate::netlink::IFF_UP != 0); + self.device_up.push(is_up); + let ipv4_entry = state .addrs .iter() - .find(|addr| addr.index == ifindex as u32 && addr.family == 2) - .and_then(|addr_state| match addr_state.address { - SmoltcpIpAddress::Ipv4(addr) => { - Some(Ipv4Cidr::new(addr.into(), addr_state.prefix_len)) - } - SmoltcpIpAddress::Ipv6(_) => None, - }); + .find(|addr| addr.index == ifindex as u32 && addr.family == 2); + let ipv4_addr = ipv4_entry.and_then(|addr_state| match addr_state.address { + SmoltcpIpAddress::Ipv4(addr) => { + Some(Ipv4Cidr::new(addr.into(), addr_state.prefix_len)) + } + SmoltcpIpAddress::Ipv6(_) => None, + }); + let broadcast = ipv4_entry.and_then(|addr| addr.broadcast); let neighbors: Vec<_> = state .neighs .iter() @@ -182,19 +209,37 @@ impl Router { Some((super::from_smoltcp_ip_address(neigh.dst), hardware_addr)) }) .collect(); - device.sync_netlink(link.map(|link| link.name.as_str()), ipv4_addr, &neighbors); + device.sync_netlink( + link.map(|link| link.name.as_str()), + ipv4_addr, + broadcast, + &neighbors, + ); } self.table.clear(); self.local_ipv4_addrs.clear(); self.local_ipv4_addrs .extend(state.addrs.iter().filter_map(|addr| match addr.address { - SmoltcpIpAddress::Ipv4(ipv4) => Some(Ipv4Cidr::new(ipv4.into(), addr.prefix_len)), + SmoltcpIpAddress::Ipv4(ipv4) => Some(LocalIpv4Addr { + cidr: Ipv4Cidr::new(ipv4.into(), addr.prefix_len), + broadcast: addr.broadcast.map(Ipv4Address::from_octets), + }), SmoltcpIpAddress::Ipv6(_) => None, })); for route in state.routes.iter().filter(|route| { route.family == 2 && route.table == RT_TABLE_MAIN && route.route_type == RTN_UNICAST }) { + let link_up = state + .links + .iter() + .find(|link| link.index == route.oif as i32) + .is_some_and(|link| link.flags & crate::netlink::IFF_UP != 0); + if !link_up { + // Mirror Linux fib_sync_down_dev: routes on administratively down + // interfaces are not selected for data-plane forwarding. + continue; + } let dev = route.oif.saturating_sub(1) as usize; if dev >= self.devices.len() { continue; @@ -231,6 +276,10 @@ impl Router { } } + fn is_device_up(&self, dev_index: usize) -> bool { + self.device_up.get(dev_index).copied().unwrap_or(false) + } + pub fn poll(&mut self, timestamp: MonotonicInstant, sockets: &mut SocketSet<'_>) { for expired in self.ipv4_reassembler.remove_expired(timestamp) { self.queue_icmpv4_error( @@ -241,6 +290,10 @@ impl Router { ); } for dev_index in 0..self.devices.len() { + if !self.is_device_up(dev_index) { + // Administratively down interfaces stop RX like Linux `__dev_close`. + continue; + } let ifindex = dev_index as i32 + 1; while self.rx_queue.len() < SOCKET_BUFFER_SIZE { let packet = { @@ -296,7 +349,11 @@ impl Router { ) }; if is_multicast { + let device_up = self.device_up.clone(); for (dev_index, dev) in self.devices.iter_mut().enumerate() { + if !device_up.get(dev_index).copied().unwrap_or(false) { + continue; + } poll_next |= dev.send_ip_packet( dev_index as i32 + 1, dst_addr, @@ -419,13 +476,19 @@ impl Router { return false; }; + let device_up = self.device_up.clone(); let mut poll_next = false; for (dev_index, dev) in preceding_devices.iter_mut().enumerate() { + if !device_up.get(dev_index).copied().unwrap_or(false) { + continue; + } poll_next |= dev.send_ip_packet(dev_index as i32 + 1, dst_addr, packet.clone(), timestamp); } - poll_next |= - last_device.send_ip_packet(device_count as i32, dst_addr, packet, timestamp); + if device_up.get(device_count - 1).copied().unwrap_or(false) { + poll_next |= + last_device.send_ip_packet(device_count as i32, dst_addr, packet, timestamp); + } return poll_next; } diff --git a/posix/fs/docs/design.md b/posix/fs/docs/design.md index c6ee8298b..44471a5bf 100644 --- a/posix/fs/docs/design.md +++ b/posix/fs/docs/design.md @@ -92,7 +92,7 @@ core/ksyscall | `xattr` | 实现 pathname、no-follow 和 fd 三种目标形式的 set/get/list/remove xattr,复制有界输入名称和值,并用单个有界 writer 处理 list size query/`ERANGE` | | `mount` | 把 Linux mount flags 映射到 `kvfs::MountFlags`,按注册的 `FileSystemType` 查找实现,并处理非递归 bind 与 remount | | `stat` | 转换 VFS metadata、access 检查和 statfs 信息 | -| `ioctl` | 处理 `FIONBIO`、inode FIEMAP,并把其它命令转交 `FileLike::ioctl`;未识别命令返回 `ENOTTY`,仅 `debug!` 记录 | +| `ioctl` | 处理 `FIONBIO`、inode FIEMAP,并把其它命令转交 `VfsFile::ioctl` / `FileOperations::ioctl`;socket 网络设备 ioctl 由 `knet::SocketFileOps` 解析用户参数;未识别命令返回 `ENOTTY`,仅 `debug!` 记录 | | `sync` | 将同步请求转发到文件系统或打开对象所在文件系统 | ## 调用约束 / 执行上下文 diff --git a/posix/fs/docs/security.md b/posix/fs/docs/security.md index 064706ee2..e2ad26e94 100644 --- a/posix/fs/docs/security.md +++ b/posix/fs/docs/security.md @@ -52,8 +52,9 @@ kfd resources / kvfs / device and pipe implementations | fd 编号 | 几乎所有 fd syscall | 已关闭 fd、类型不匹配、fd 复用、`CLOEXEC`/非阻塞标志混淆 | | 当前进程状态 | `chdir`、`openat`、`close_range` | 进程资源锁、fs_struct 更新、umask 和当前目录语义 | | VFS magic link | `/proc/self/fd/`、`/proc//fd/` | 跨进程 fd 访问、目标进程退出、fd 表并发变化、no-follow 策略 | -| VFS/设备对象 | `open`、`ioctl`、`mount`、`syncfs` | 设备特定 ioctl、终端对象、mount flags、文件系统实现差异 | +| VFS/设备对象 | `open`、`ioctl`、`mount`、`syncfs` | 设备特定 ioctl、终端对象、socket 网络设备 ioctl、mount flags、文件系统实现差异 | | FIEMAP 可变长输出 | `ioctl(FS_IOC_FIEMAP)` | 用户声明的 extent 数量、header 后数组地址、后端返回的块映射 | +| 网络设备 ioctl | socket fd 上的 `SIOCGIF*` / `SIOCSIF*` | `ifreq`/`ifconf` 用户结构由 `knet` 通过 `osvm` 校验;本层只做 fd 查找与对象委托 | | fd-to-fd 复制 | `sendfile`、`copy_file_range`、`splice` | 源目标类型错误、offset 指针 TOCTOU、同文件重叠、阻塞语义 | 本 crate 不直接访问 MMIO、PIO、DMA、固件表或架构内联汇编。 -- Gitee