控制面
控制面维护 ax-net 的接口、地址、路由、DNS 和 socket 设备绑定状态,为协议核心和系统 ABI 提供查询与决策入口。对应到 Linux,相关职责分布在 netdevice、地址管理、FIB/route table、resolver 配置和 socket bind 状态中;对应到 lwIP/smoltcp,则是 netif 配置、地址管理和路由选择逻辑。
核心源码:
| 源码 | 职责 |
|---|---|
config.rs | 控制面公开数据模型:InterfaceId、InterfaceInfo、NetworkConfig、RouteInfo、DeviceBinding |
service.rs | NetControl、接口 registry、DNS registry、DHCP commit、route 查询入口 |
router.rs | RouteTable、Rule、RouteDecision、TX dispatch route lookup |
general.rs | socket 通用选项中的 SO_BINDTODEVICE / DeviceBinding 存取 |
tcp.rs、udp.rs、raw.rs | connect/send/bind 时使用控制面做地址、路由和设备绑定决策 |
源码表将配置数据、控制面状态、路由规则和 socket 绑定分别定位到其维护模块。设计边界基于这些所有者区分只读快照、原子提交和实际 packet 处理,避免控制面膨胀为第二个数据面。
1. 设计边界
控制面是 NetControl 持有的只读状态层,通过 ax_sync::SpinRwLock 保护接口 registry、DNS registry 和共享路由表。它的查询接口(interfaces()、select_route()、dns_servers() 等)只持读锁、返回快照,不进入 Service 或 SocketSet 锁,也不接触设备收发队列。协议状态机推进、包收发和 socket payload 读写全部由数据面的 Service::poll() 和设备 worker 在独立的锁层级中完成。
1.1 无线控制面句柄
WIFI_CONTROLS 是一个全局 Mutex<Vec<(String, rd_net::WifiControlHandle)>>,存储无线设备的控制面句柄。当无线设备注册时,runtime 在 driver 的 WifiControlHandle 被消费进数据面 EthernetDriver 之前捕获一份,按接口名索引。这允许 StarryOS wireless-extensions ioctl 或类似的运行时管理接口通过名称找到对应设备的 WifiControl,在不需要持有 Service 或 SocketSet 锁的情况下完成模式切换。
1.2 状态来源与消费者
控制面图把写入来源、原子提交边界和读取方放在同一视图中, 强调 NetControl 保存的是协议核心与系统 ABI 共享的状态快照。DeviceBinding 不写入全局路由表,而是在 socket 查询时附加接口约束;这一区别决定了绑定行为不会修改其他 socket 的选路结果。
控制面状态有四类写入路径:init_network() 构造初始状态;DHCP ACK/NAK 通过 commit_interface_update() 原子替换某接口的地址、DNS 和路由规则;register_device_with_config() 运行期新增静态 IPv4 设备;reconfigure_wifi() 在 STA/AP 模式切换时更新对应接口 IPv4/DHCP 角色。除初始构造外,这些路径都在 Service 锁内同步更新 smoltcp IP address list、NetControl 快照和 RouteTable,避免数据面与查询面看到不一致状态。
SharedRouteTable(Arc<RwLock<RouteTable>>)同时被 NetControl(查询侧)和 Router(TX dispatch 侧)持有,两者指向同一实例。控制面通过 select_route_with_binding() 提供 socket 级别的路由查询;Router::dispatch() 通过 select_route_for_source() 做实际发包时的出接口选择。两者共享同一套路由规则,但查询时机和过滤条件不同。
2. 初始化流程
init_network() 是启动阶段控制面状态的构造入口。它按固定顺序构建 loopback、Ethernet 接口、静态地址、DNS registry 和共享路由表,然后把所有状态提交给 NetControl 和 Service。运行期新增设备和 Wi-Fi 模式切换会在同一套 NetControl/RouteTable 上追加或替换状态。
关键点:
routes是Arc<RwLock<RouteTable>>,Router和NetControl共享同一实例。NetControl先于SERVICE初始化,确保get_control()在 poll worker 启动前可用。- DHCP bootstrap 只要求任一 DHCP 接口配置成功即返回,避免断网卡阻塞启动。
初始化要点列表说明路由表共享和全局发布顺序是控制面一致性的基础,DHCP 等动态状态在此之后才能安全提交。数据模型章节将这些约束落实到接口 ID、快照、registry 和 DNS entry。
3. 数据模型
控制面状态分为三类:对外稳定的接口标识、可快照查询的接口/DNS 状态,以及被 Router 和 NetControl 共享的路由表。接口 ID 用于跨模块引用同一接口,接口快照用于系统 ABI 和诊断接口,NetControl 负责把这些状态组织成可查询的控制面视图。
3.1 接口标识
InterfaceId(u32) 是 ax-net 内部和对外统一的接口标识,也是 StarryOS Linux ABI 的 ifindex 来源。
// 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<Self> {
if ifindex > 0 {
Some(Self(ifindex as u32))
} else {
None
}
}
}
约定:
InterfaceId::LOOPBACK == 1,固定对应lo。- Ethernet 接口从
2开始分配,默认命名为eth0、eth1。 InterfaceId(0)是内部 TX 占位符,不对外暴露。- StarryOS 的
SIOCGIFINDEX、AF_PACKETsockaddr_ll.sll_ifindex都应通过InterfaceId映射。
接口标识列表说明内部 ID 与 Linux ifindex 使用同一稳定数值,并提供显式转换函数。接口快照在这个身份基础上附加名称、地址和 flags,而不暴露 registry 内部引用。
3.2 接口快照
对外接口信息使用拥有型 InterfaceInfo,把名称、InterfaceId、地址、flags 和设备类型作为同一控制面快照返回。查询方不会持有内部 registry 引用或读锁,因此可以安全完成 ioctl、netlink 和诊断编码,而不会阻塞后续 DHCP 提交。
// config.rs
pub struct InterfaceInfo {
pub id: InterfaceId,
pub name: String,
pub kind: InterfaceKind,
pub mac: Option<EthernetAddress>,
pub ipv4: Option<Ipv4InterfaceConfig>,
pub mtu: usize,
pub flags: InterfaceFlags,
pub metric: u32,
}
内部状态是 NetInterface:
// service.rs
pub(crate) struct NetInterface {
pub id: InterfaceId,
pub name: String,
pub kind: InterfaceKind,
pub mac: Option<EthernetAddress>,
pub ipv4: Option<Ipv4Cidr>,
pub gateway: Option<Ipv4Address>,
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 更新都可能改变快照内容。
3.3 控制面状态
NetControl 是控制面的核心对象。它在 init_network() 中创建,并早于 SERVICE 注册到全局 NET_CONTROL。
// service.rs
struct ControlState {
interfaces: Vec<NetInterface>,
dns: Vec<DnsServerEntry>,
}
pub struct NetControl {
state: RwLock<ControlState>,
pub(crate) routes: SharedRouteTable,
}
impl NetControl {
pub(crate) fn new(
interfaces: Vec<NetInterface>,
routes: SharedRouteTable,
dns: Vec<DnsServerEntry>,
) -> Self {
Self {
state: RwLock::new(ControlState { interfaces, dns }),
routes,
}
}
}
初始化时,lib.rs 构造 loopback、Ethernet 接口、静态 DNS 和共享路由表,然后把同一份 routes 同时交给 Router 和 NetControl:
// lib.rs, 简化示意
let routes: SharedRouteTable = Arc::new(ax_sync::SpinRwLock::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。
3.4 DNS 注册表
DNS server 以带来源接口、metric 和来源类型的 DnsServerEntry 保存,而不是无上下文地址列表。NetControl 按路由偏好排序并去重,使 DHCP、静态配置和 fallback server 能共存,同时在接口状态删除时精确移除对应来源。
pub enum DnsSource { Dhcp, Static, Fallback }
pub(crate) struct DnsServerEntry {
pub server: Ipv4Address,
pub interface_id: InterfaceId,
pub metric: u32,
pub source: DnsSource,
}
DnsServerEntry 的字段让控制面能够在接口 lease 变化时按来源精确替换,而不是清空整个 resolver 配置。下表把三个来源映射到创建时机和 metric,说明 fallback 为什么总排在具有接口上下文的 server 之后。
| 来源 | 创建时机 | 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。
4. 查询决策
查询入口只返回快照或 route decision,不把内部锁、Router 设备索引以外的可变对象暴露给调用方。公共 API 通过 lib.rs facade 进入 NetControl,socket 实现则直接使用 crate 内部查询函数完成 bind/connect/send 前的决策。
4.1 接口查询
接口只读查询都通过 NetControl.state 的读锁取得拥有型快照,并在返回前释放锁。interfaces()、按名称或 ID 查询以及本地地址推导共享同一 registry,避免 ABI 层各自解释接口状态并产生不一致结果。
pub fn interfaces(&self) -> Vec<InterfaceInfo> {
let state = self.state.read();
state.interfaces.iter().map(NetInterface::to_info).collect()
}
pub fn interface_by_name(&self, name: &str) -> Option<InterfaceInfo> {
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<InterfaceInfo> {
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<Ipv4InterfaceConfig> {
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:
// lib.rs
pub fn interfaces() -> Vec<InterfaceInfo> {
get_control().interfaces()
}
pub fn interface_by_name(name: &str) -> Option<InterfaceInfo> {
get_control().interface_by_name(name)
}
pub fn interface_by_id(id: InterfaceId) -> Option<InterfaceInfo> {
get_control().interface_by_id(id)
}
pub fn ipv4_config(name: &str) -> Option<Ipv4InterfaceConfig> {
get_control().ipv4_config(name)
}
接口查询代码表明 snapshot 在释放控制面读锁后交给调用者,避免 ABI 编码期间阻塞状态提交。路由表是另一份共享状态,通过独立读写锁同时服务查询与 Router dispatch。
4.2 路由表
RouteTable 存在于 router.rs,被 Arc<RwLock<_>> 包装为 SharedRouteTable。
pub type SharedRouteTable = Arc<RwLock<RouteTable>>;
#[derive(Debug)]
pub struct Rule {
pub filter: IpCidr,
pub via: Option<IpAddress>,
pub dev: usize,
pub interface_id: InterfaceId,
pub src: IpAddress,
pub metric: u32,
pub order: u64,
}
pub struct RouteTable {
rules: Vec<Rule>,
next_order: u64,
}
每条规则同时保存两类索引:
dev:Router.devices的内部索引,用于 TX dispatch 找到真实设备。interface_id:对外稳定接口 ID,用于查询、绑定和 Linux ifindex 映射。
这两个值不能混用。dev 是 Router 内部位置,interface_id 是公共语义。
4.2.1 排序策略
路由规则在新增或按接口替换后立即按前缀长度、metric 和稳定插入序排序,使后续查询无需在热路径重复重排。排序规则同时服务 socket 预选路和 Router 实际 dispatch,任何变化都会影响多网卡出口优先级。
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))
});
}
优先级:
- 最长前缀匹配。
- 低 metric 优先。
- 插入顺序稳定。
排序要点保证最长前缀与 metric 在所有查询入口中一致,插入序只负责稳定打破完全相同的候选。查询策略在此顺序上再应用目标、源地址与设备绑定过滤。