nsplane · 架构与接口
本页描述 nsplane 作为数据面的 crate 划分、公开接口、引擎模型与各功能模块,以
nsplane仓库的docs/architecture.md、docs/design.md与 tagv0.14.0(b28d649,2026-10-13)为准。从 0.10.x 升级见 0.11 迁移说明,0.12 的本地多身份、0.13 的流网关与 0.14 的按身份路由见同页 §7–§9。定位、原则、路线图与决策见 nsplane。每个公开类型与函数的签名和约定以 rustdoc 为准(cargo doc --workspace --no-deps)。
1. crate
Section titled “1. crate”| crate | 作用 |
|---|---|
nsplane-noise | Noise 协议状态机(Tunn:握手、会话、定时器、原地加解密)、RateLimiter、x25519;无 I/O |
nsplane-packet | 报文缓冲(PacketBuf、PacketPool、SharedPacketPool、PacketBatch)、IP / TCP / UDP / ICMP 头视图、五元组、分片与重组、校验和(按 32 位累加)、带密钥的快速哈希(hash::KeyedState)、UDP 报文构建;共享值类型 PeerId、TransportId、Path、Ecn |
nsplane-core | 无 I/O 引擎核心:对端、密钥路由、定时器、路径策略、过滤链、注入、事件;本地多身份(0.12);分片快照单元 cell::SnapshotCell(0.13) |
nsplane | tokio 驱动:Engine / EngineBuilder / EngineHandle、I/O trait、UdpTransport、通道实现、Splitter / MergeSource、分片阶段、加密工作线程池、统计 |
nsplane-tun | 操作系统 TUN 设备:Linux / Android(virtio-net 卸载)、macOS / iOS utun、Windows Wintun,接管 fd / 句柄 |
nsplane-netstack | 基于 smoltcp(dotns/smoltcp 分支)的用户态 TCP/IP 协议栈,IPv4 与 IPv6 的 TCP 连接和 UDP 流 |
nsplane-acl | 与业务无关的 ACL:按不透明标签与带类型规则(网段、端口、协议、ICMP 类型)判定,规则 ID、策略状态、Flow 判定接口;AclFilter(命名空间、授权、出向规则、pinhole、按流钩子);通用有状态流网关 gate(FlowGate、GateFilter;0.13 起 IPv4 与 IPv6 同等处理、可按 PeerId 绑定);作用域按本地身份运行时替换(0.13);FlowTracker |
nsplane-nat | IPv4 / IPv6 翻译过滤器(RFC 7915,按对端的显式地址映射 EAM,RFC 7757)、conntrack、端口映射、Masquerade、Nat64Lan(路由经 LanRoutes 句柄替换)、Redirect |
nsplane-wss | 可选:WebSocket(TLS 或明文)的数据报载体:拨号端 WssDialer(作为 LinkTransport)与服务端 WssServerTransport(多会话、每会话一个端点,供网关使用);WsFrame 流客户端 / 服务端自 0.11 起弃用 |
nsplane-uapi | 引擎之上的 wg UAPI(Unix 套接字 / Windows 命名管道) |
nsplane-cli | Linux / macOS 开发工具:TUN + 引擎 + UAPI |
nsplane-e2e、nsplane-examples | 不发布:库级端到端测试;可运行的示例,包括单端口 relay 及其 UDP / WSS 客户端传输 |
nsplane-noise ─► nsplane-core ─► nsplane ─► nsplane-tun、nsplane-uapi ─► nsplane-clinsplane-packet ─► nsplane-core、nsplanensplane、nsplane-packet ─► nsplane-netstacknsplane-core、nsplane-packet ─► nsplane-acl、nsplane-natnsplane ─► nsplane-wss依赖方向严格向下;只有 nsplane-core 依赖 nsplane-noise(并重新导出 noise、x25519)。最小客户端只需要 nsplane 与 nsplane-tun;nsplane-acl、nsplane-nat、nsplane-netstack 是独立 crate,不加入就不编译、不链接。所有 crate 都没有可选的 Cargo feature。
2. 公开接口总览
Section titled “2. 公开接口总览”| crate | 入口 | 配套类型 |
|---|---|---|
nsplane-noise | noise::Tunn(encapsulate_in_place / decapsulate_in_place)、noise::rate_limiter::RateLimiter、x25519 密钥 | TunnResult、noise::errors::WireGuardError |
nsplane-packet | PacketBuf(头部 / 尾部预留、advance / reserve_front、extend_from_slice、from_shared,越界返回错误)、PacketPool、SharedPacketPool(alloc、alloc_from、recycle)、PacketBatch、IpPacket、build::udp_packet | Path、TransportId、PeerId、Ecn;Ipv4Header、Ipv6Header、TcpHeader、UdpHeader、IcmpHeader、Fragment、FiveTuple;checksum(sum_words、fold)、hash(KeyedState)、reassembly、protocol;Malformed、BoundsError;HEADROOM、TAILROOM、MAX_BATCH |
nsplane-core | Core(handle_input、handle_datagrams / handle_locals、_deferred 形式与 complete_job、handle_timeout / poll_timeout、poll_output、inject_inbound / inject_outbound / inject_outbound_on、force_handshake、route、peer_stats、recycle、add_local_identity / add_local_identity_on / remove_local_identity / local_identities / peer_id_in、set_local_sources、route_from);trait PathPolicy(select、on_authenticated、observe_every_message)、PacketFilter(inbound、inbound_from、outbound) | CoreConfig、Input、Output、ConfigChange、PeerConfig、AllowedIp、PeerStats、Event、Verdict、Roam、MessageKind、StandardRoaming、CryptoJob、LocalIdentityId、LocalIdentityInfo、cell::SnapshotCell、reasons |
nsplane | EngineBuilder(transport、private_key、policy、filter、fragmenter、crypto_workers、queue_capacity、event_capacity、stats_interval、build)、Engine(handle、wait)、EngineHandle(见 §3.3);trait PacketSource(含 recycle)、PacketSink(含 send_batch_spent)、Transport(都带批量方法)、DynTransport、LinkDialer | UdpTransport(含 UDP 旁路通道、路径 MTU 发现)、LinkTransport、ChannelSource / ChannelSink / ChannelTransport、pipe、pump、MapSink / MapSource(含 with_after)、Splitter(含 new_map)、MergeSource、SwapSink、AbortSink / SinkAbort、FragmentConfig / FragmentStats、EngineStatus、TransportStats、QueueStats / QueueDepth、Peer、Event、DROP_* 丢弃原因、EngineError、TransportError、BuildError、BoxFuture |
nsplane-tun | Tun(create、create_with、Unix 上的 from_fd / from_raw_fd、split、offload、mtu、name) | TunOptions、TunSource、TunSink、Offload、adopt_fd(Unix)、MTU_POLL_INTERVAL |
nsplane-netstack | NetStack(new、split)、NetStackHandle(incoming_tcp、incoming_udp、connect_tcp、connect_tcp_from、bind_udp、stats) | NetStackConfig、NetStackSource、NetStackSink、TcpConnection(AsyncRead + AsyncWrite,支持半关闭)、UdpFlow、UdpReply、UdpSocket、NetStackStats、DEFAULT_MTU、MIN_MTU |
nsplane-acl | AclEngine(install / uninstall / fail、policy_state、with_not_installed、rules、evaluate(&LabelSet, &Flow)、store_namespace / remove_namespace、store_grant / remove_grant、open_pinhole、expire_pinholes、clear_all、generation、pinhole_stats)、AclFilter(new、with_config、with_scope、replace_scopes、scope_generation、stats)、gate::FlowGate(replace(GatePolicy))与 gate::GateFilter、FlowTracker | 规则 Label、LabelSet、RuleId、Rule、ProtocolMatch、PortSet、IcmpTypes、RuleSet、Flow、Decision、PolicyState、NotInstalled、Protocol、IpNet;身份 PeerIdentity(labels、labels_for、local_identity)、PeerLabelMap(set_local_identity);作用域 AclFilterScope、AclFilterScopes、AclFilterScopeError;命名空间 NamespaceId、NamespaceKind、NamespacePolicy、NamespaceMember、OutboundRule、Grant、GrantEnd;pinhole PinholeSpec、PinholeGuard、PinholeId、Direction、PinholeError、PinholeStats;网关 GatePolicy、GateScope、GateBinding、GateGrant、UnboundRule、GateHolds、GateConfig、GateMode、GateDecision、GateReason、GateCounters、GateDivert、DivertedPacket;统计 AclFilterStats、FlowKey、FlowStats |
nsplane-nat | Translator(new、store、set_mtu、ipv4_translated_predicate、stats)、TranslationTableBuilder / TranslationTable(by_eam4、by_peer6、by_peer6_eam4 等)、PortMap、Conntrack、Masquerade、Nat64Lan(new / with_snat_ports 收 LanRoutes 句柄)、LanRoutes(new、replace、load)、Redirect | PeerMapping(peer6、eam6、eam4、local6)、SelfMapping(eam4、eam6)、LanPrefix、TableError、TranslatorStats、PortMapRule、ConntrackConfig、ConntrackStats、Flow、FlowMatch、FlowDirection、TcpState、checksum::transport_valid |
nsplane-wss | WssDialer(new、into_transport)、WssServerTransport::new → (WssServerTransport, WssAcceptor)、WssAcceptor::accept(WebSocketStream) → WssSession | WssConfig(ping_interval(Option)、keepalive、allow_plaintext、reconnect_delay、connect_timeout)、WssTls、BearerProvider、WssDialError、WssDialEvent、WssServerConfig(默认最多 4096 个会话)、WssServerTransportStats、WssSessionStats;已弃用:WssStreamClient、WssStreamServer、frame |
nsplane-uapi | Uapi(new、with_external_transport、with_listen_port、handle_request、serve_stream)、UapiListener | udp_transport、TRANSPORT_ID、socket_path / pipe_path |
3. 引擎模型
Section titled “3. 引擎模型”3.1 核心(nsplane-core,无 I/O)
Section titled “3.1 核心(nsplane-core,无 I/O)”Input::Datagram { path, data } ─┐ ┌─▶ Output::Transmit { path, data }Input::Local { packet } ├─▶ Core::handle_input / handle_datagrams / handle_locals ─┼─▶ Output::Deliver { from, packet }Input::Config (ConfigChange) ─┘ └─▶ Output::EventCore::poll_timeout / handle_timeout(now) 重新协商、保活、过期、限速器复位Core::inject_inbound / inject_outbound / force_handshake / route / peer_stats / recycleCore不做 I/O,也没有自己的时钟。报文原地处理:本地报文在自己的缓冲里加密后作为发送输出,数据报在自己的缓冲里解密后作为交付输出,缓冲经recycle回收。handle_datagrams/handle_locals接收一批报文,结果与逐个调用handle_input完全相同,但整批共享一次调度更新、输出队列余量以及连续报文的会话、路由与对端查找。- 接收按会话索引分流;握手发起报文通过解密静态公钥确定对端,共享同一 relay 端点的对端不需要按端点匹配。
- 入向顺序:原地解密 → 源地址在发送方允许 IP 内(全局最长前缀匹配)→ 过滤链(入向)→
Deliver。出向顺序:按目的地址路由(最长允许 IP 匹配)→ 过滤链(出向)→ 原地加密 → 在策略给出的路径上Transmit。改写地址的过滤器需要把相关地址放进对端的允许 IP。 - 过滤链是洋葱顺序:从线路侧到本地侧安装,入向按安装顺序执行,出向按相反顺序执行。推荐的组合是
[AclFilter, PortMap, Translator]。 - 路径选择与漫游交给
PathPolicy(默认StandardRoaming,即标准 WireGuard 漫游);引擎从不自行改变路径。 - 路径阶梯的钩子(ns 账户模式与 quick-v2 §9 使用,默认都不启用):
inject_outbound_on用对端的当前会话加密一个报文并在指定路径上发送,不经路由、出向过滤与select,也不改变对端路径,用于在候选路径上发探测而数据仍走原路径;没有当前会话时以NO_SESSION丢弃,不触发握手。PacketFilter::inbound_from带上解密报文到达的路径(默认实现调用inbound),探测应答方据此核对确切的来源元组或按原路回复。PathPolicy::observe_every_message为true时,当前路径上的每条认证消息也会调用on_authenticated,用于维持直连或判断其丢失;默认只询问来自其他路径的消息,稳态数据路径上没有这次调用。 _deferred入口供多线程加密使用:加密前的工作(路由、出向过滤)先完成,返回一个CryptoJob,CryptoJob::run只持有该对端隧道的锁完成加解密,complete_job在核心里完成其余部分。只有以CoreConfig::crypto_jobs构建的核心才发出任务,此时每个对端的隧道在自己的互斥锁后面;否则每个对端独占隧道,数据路径不加锁。
3.2 驱动(nsplane,tokio)
Section titled “3.2 驱动(nsplane,tokio)”一个拥有者任务独占 Core 并循环:等待句柄命令、本地报文、收到的数据报或核心的下一个定时器,喂给核心并取出输出。周围的 I/O 任务通过有界队列(默认每个 1024)与它相连:
PacketSource ─► source 任务 ─┐ ┌─► 每个传输的 transmit 任务 ─► Transport::send_batch ├─► 拥有者任务(Core)────┤Transport::recv_batch ─► 每个传输的 recv 任务 ┘ ▲ └─► sink 任务 ─► PacketSink │ EngineHandle 命令(64)// 简化;每个方法都有带默认实现的批量形式(recv_batch、send_batch),// Transport::send_batch 报告失败的数据报数。trait PacketSource { async fn recv(&mut self) -> io::Result<PacketBuf>; fn mtu(&self) -> watch::Receiver<u16>; }trait PacketSink { async fn send(&self, packet: PacketBuf, from: PeerId) -> io::Result<()>; }trait Transport { fn id(&self) -> TransportId; async fn recv(&self, buf: &mut PacketBuf) -> io::Result<(usize, Path)>; async fn send(&self, datagram: &[u8], to: &Path) -> io::Result<()>; }trait PathPolicy { fn select(&self, peer, kind) -> Option<Path>; fn on_authenticated(&self, peer, from, kind) -> Roam; }trait PacketFilter { fn inbound(&self, peer, &mut PacketBuf) -> Verdict; fn outbound(&self, peer, &mut PacketBuf) -> Verdict; }- 批处理。 拥有者被一个报文唤醒时,顺带取走已经排在后面的报文(最多
MAX_BATCH= 64),作为一批交给核心。它从不等待凑满一批,也没有批处理定时器,所以单个报文立即通过。 - 多传输。 一个引擎可同时运行任意多个传输(
add_transport/remove_transport/replace_transport可在运行中调用),每个对端的Path指明用哪个传输。每个传输有自己的发送队列和积压,一个卡住的传输不会拖住其他传输上的对端。 - 反压。 只有一个传输时本地报文从不丢弃:发送队列满时数据报在拥有者任务里积压;拥有者只在某个传输有余量时读取本地报文,且一次不超过最大余量(开加密线程时扣除在途的加密任务,0.10.1 修复),没有余量就停止读取,从而反压本地来源。由收到的数据报或定时器产生、遇到积压已满的数据报被丢弃(
DROP_TRANSMIT_FULL)。接收方向:拥有者只在投递队列有余量(扣除在途的解密任务)时读取收到的数据报,sink 满时数据在传输里等待(UDP 是 socket 缓冲),所以DROP_SINK_FULL在通常情况下为 0(0.11 起不开加密线程时也如此)。引擎不按接收方 sink 的速度给发送方限速,这是待设计的事项。 - 加密工作线程池(可选,
crypto_workers(n),n ≥ 2):同一个对端的加解密分摊到所有工作任务(nonce 由拥有者按序分配,完成后按序交付,防重放检查在完成时做),每个对端的报文在两个方向上都保持顺序;握手、定时器、配置、事件、计数仍在拥有者任务。两端开 2 个工作线程时单条 TCP 流约 14.5 Gbit/s(见 §7)。 - 分片阶段(可选,
fragmenter(FragmentConfig)):在本地路径上、核心之前,使每个本地报文不超过来源的 MTU。超过 MTU 的 IPv6 回 ICMPv6 Packet Too Big;超过上限的 IPv4 在 DF 置位时回 Fragmentation Needed,否则拆成 IPv4 分片;翻译为 IPv6 的目的地址上限低 20 字节。错误报文看起来来自目的端,限速发送。 - 混合本地侧:
Splitter是按闭包把交付报文分给多个 sink 的PacketSink,MergeSource是轮询多个来源、报告最小 MTU 的PacketSource,用于 TUN 与 netstack 并存。 - 卸载:
Tun::create(Linux、Android)协商IFF_VNET_HDR(校验和卸载、TSO,内核支持时加 USO),UdpTransport::bind通过quinn-udp启用 GSO 发送与 GRO 接收(Linux、Android),GRO 数据报是读缓冲的零拷贝切片;不支持时回退到逐报文 I/O。卸载只组合已经到达的报文,从不为凑批等待。TunOptions::offload(false)、UdpTransport::bind_with_offload(.., false)可关闭。
3.3 控制、事件与统计
Section titled “3.3 控制、事件与统计”EngineHandle 的每个调用都是发给拥有者任务的消息,处理完才返回,因此一次配置变更对之后的所有调用可见;引擎停止后返回 EngineError。
| 分类 | 调用 |
|---|---|
| 密钥与对端 | set_private_key、add_or_update_peer、remove_peer、remove_all_peers、set_allowed_ips、set_preshared_key、set_keepalive、set_path、peer_id、public_key、private_key |
| 传输 | add_transport、remove_transport、replace_transport |
| 注入与握手 | inject_inbound、inject_outbound、inject_outbound_on(在指定路径上发送)、force_handshake |
| 生命周期 | suspend、resume、mtu、shutdown |
| 事件 | subscribe:握手完成、认证通过、路径采纳、会话过期、对端统计、丢弃、暂停 / 恢复、MTU 变化;发布从不阻塞,落后的订阅者丢失最旧的事件 |
| 统计 | peers / peer_stats(每个对端的线上收发字节 rx / tx、明文字节 data_rx / data_tx、上次握手、当前路径);transport_stats(每个传输收发的数据报数与字节数、发送失败数);drop_counters(按原因);queue_stats / take_queue_stats(每个有界队列的容量与高水位);fragment_stats;status(以上全部加公钥、MTU、是否暂停,一次调用取得) |
计数只增不减。速率、指标导出以及”直连 / relay”这类标签由调用方(ns)负责:定时采样 status,相减得到速率,把传输 id 与路径映射到自己的标签。按传输的计数由传输自己的任务以每批一次原子累加完成,不经过拥有者任务;替换传输时计数延续,移除时一并删除。
3.4 呈现 × 传输
Section titled “3.4 呈现 × 传输”| 本地侧(PacketSink / Source)\ 传输 | UDP | relay UDP | relay WSS |
|---|---|---|---|
| TUN(内核协议栈) | ✓ | ✓ | ✓ |
nsplane-netstack(无 TUN、无 root) | ✓ | ✓ | ✓ |
| 宿主桥接(iOS / Android fd、通道) | ✓ | ✓ | ✓ |
UDP 之外的传输是嵌入方对 Transport 的实现;示例包实现了单端口 relay 及其 UDP 与 WSS 客户端传输,just e2e-examples 在容器中逐格测试这个矩阵,并与内核 WireGuard 互通。
3.5 本地身份(0.12 起)
Section titled “3.5 本地身份(0.12 起)”一个引擎可以持有多把静态密钥。CoreConfig::private_key 是默认身份(LocalIdentityId::DEFAULT),EngineHandle::add_local_identity / add_local_identity_on(key, transports) 增加其他身份,remove_local_identity 删除身份并连带删除它的对端。对端以(本地身份,远端公钥)为键(PeerConfig::local_identity、Peer::local_identity、peer_id_in、remove_peer_in),同一远端公钥可以在两个身份下各是一个对端。入站握手发起按 mac1 分派到它所针对的身份(mac1 由本地公钥派生,不需要逐身份做 DH),匹配不到任何身份的发起以 UNKNOWN_LOCAL_IDENTITY 丢弃且不应答;cookie 密钥与秘密按身份,握手预算(HandshakeBudget)全引擎共用。限定在指定传输上的身份(add_local_identity_on)只在那些传输上应答握手,其对端也只在那里接受握手应答、cookie 与数据,用来防止持有另一覆盖网公钥的一方做跨网探测。单身份时线上字节与 0.11 一致、开销在噪声内;1 / 4 / 16 个身份的握手发起分别为 129 / 130 / 132 µs。UAPI 与 CLI 只操作默认身份。0.14 起密钥路由也按身份:入站源地址只对收包对端所属身份的允许 IP 检查;Core::set_local_sources(identity, sources) 给身份一个源地址集合,源在集合内的本地报文只在该身份的对端里路由(各集合中最具体的网络胜出,无路由则以 NO_ROUTE 丢弃),源不在任何集合的报文走引擎级回退(默认身份的字典,或多身份持有允许 IP 时按最后插入者拥有前缀的合并字典);Core::route_from(src, dst) 是发送路径的决定,分片阶段也按源与目的找对端,所以路径 MTU 状态与 ICMP too-big 跟随所选身份的对端。同一远端密钥与相同允许 IP 可以同时是多个身份的对端。单身份或没有源集合时路由与开销不变;使用源集合时每往返约 +60 ns。
4. 可选功能与默认值
Section titled “4. 可选功能与默认值”基础客户端是 TUN 设备 + UdpTransport + 对端、没有过滤器的 EngineBuilder。下列功能都是可选的;未安装的功能不在数据路径上。
| 功能 | crate | 启用方式 | 默认 | 未启用时的开销 |
|---|---|---|---|---|
| IPv4 / IPv6 翻译 | nsplane-nat | filter(Box::new(Translator::new(table))),set_mtu 设为隧道 MTU | 未安装 | 无 |
| 服务发布(DNAT / SNAT) | nsplane-nat | filter(Box::new(PortMap::new(rules)?)) | 未安装 | 无 |
| ACL | nsplane-acl | filter(Box::new(AclFilter::new(engine, identity))) | 未安装 | 无 |
| 流计量 | nsplane-acl | filter(Box::new(FlowTracker::new(capacity))) | 未安装 | 无 |
| 分片阶段 | nsplane | fragmenter(FragmentConfig::default()) | 关闭 | 每个本地报文一次 Option 判断 |
| 加密工作线程池 | nsplane | crypto_workers(n),n ≥ 2 | 0 | 每个报文一次 Option 判断;不加锁 |
| 通用流网关 | nsplane-acl | filter(Box::new(GateFilter::new(Arc::new(gate)))),FlowGate::replace(GatePolicy) | 未安装 | 无 |
| 按本地身份的 ACL 作用域 | nsplane-acl | AclFilter::replace_scopes(AclFilterScopes):一个默认作用域加每个 LocalIdentityId 一份,运行时原子替换 | 默认作用域(代际 0) | 一次标志读取 |
| WSS 数据报服务端 | nsplane-wss | WssServerTransport::new 作为传输加入,监听器把升级后的 WebSocket 交给 WssAcceptor::accept | 不使用 | 无 |
| 缓冲回收 | nsplane-packet / nsplane | SharedPacketPool、PipeSink::alloc、ChannelSource::pool;pump 自动把 send_batch_spent 的缓冲还给来源 | 不使用 | 每次回收调用一次原子读取 |
| 用户态协议栈 | nsplane-netstack | NetStack::new(config),split 作为来源与 sink | 不使用 | 无(不是 nsplane 的依赖) |
| 混合本地侧 | nsplane | Splitter / MergeSource | 不使用 | 无 |
| TUN 卸载 | nsplane-tun | Tun::create 默认开启 | 内核支持时开启(Linux、Android) | 关闭时每个报文一次系统调用 |
| UDP GSO / GRO | nsplane | UdpTransport::bind 默认开启 | 开启 | 关闭时每个数据报一次系统调用 |
IPv4-only、IPv6-only 或双栈不需要开关:由传输的绑定地址、TUN 上配置的地址与路由、对端的允许 IP 决定。
5. 报文、翻译与 MTU
Section titled “5. 报文、翻译与 MTU”nsplane-packet的视图是zerocopy引用,解析从不拷贝;PacketBuf带 32 字节头部预留,越界操作返回错误而不是 panic。- 地址模型(0.11 起用 RFC 7757 EAM 命名)。 每个对端的映射是
PeerMapping { peer6, eam6, eam4, local6 }:peer6是对端的原生 IPv6,eam4 ↔ eam6是一条显式地址映射(本地应用看到的 IPv4 与它对应的 IPv6),local6是可选的 IPv6 本地别名;节点自身是SelfMapping { eam4, eam6 };IPv4 LAN 前缀与 IPv6 前缀配对(lan4 ↔ lan6):lan6是低 32 位承载整个 IPv4 地址的 /96,或(0.14 起,RFC 7757 风格)长度为 96 +lan4前缀长度、低位嵌入另一个 IPv4 网络的前缀,于是同一个远端 LAN 网段可以经本地另一个 IPv4 网段到达(lan410.9.1.0/24 配fd64:c::c0a8:100/120时 10.9.1.5 ↔fd64:c::c0a8:105),ICMP / ICMPv6 内层报文同样映射,嵌入网络之外的地址不映射;/96 形式与开销不变。产品的地址规划(例如 Quick v2)如何填这些字段,见仓库docs/specs/translator-address-plan.md。TranslationTable不可变,Translator::store在流量进行中原子替换。远端节点只看到 IPv6。 - 翻译器。 无状态 RFC 7915:TTL / 跳数限制、ICMP / ICMPv6(回显与错误,包括引用的报文与 MTU)、分片(IPv6 分片头)都翻译;TCP / UDP 校验和增量更新。无校验和的分片 IPv4 UDP 先重组(任意到达顺序,最多 256 个数据报、1 MiB、60 s);超过翻译器 MTU 的重组结果丢弃。报文在缓冲内有余量时原地增长 20 字节(带分片头 28 字节),TUN 读取会留出这段余量。
- netstack 的 accept 与端口。 accept 队列满时默认关闭新连接;启用
accept_backpressure后,队列满期间不应答新的 SYN(对端会重传),已完成握手的连接在栈内等待应用接受。connect_tcp_from使用调用方指定的本地端口;临时端口在每个栈上从 49152–65535 中的随机位置开始分配。 - 端口映射与 conntrack。
PortMap把隧道侧的listen地址和端口映射到本地target,可限定对端;入向 DNAT 并记录流,回包 SNAT,引用该流的 ICMP 错误也被改写。Conntrack有界,按协议空闲超时(TCP 感知状态),无后台任务。 - 分片与 Packet Too Big 由引擎的分片阶段统一处理(§3.2);TUN 与 netstack 共用同一套 MTU / MSS 限制,
nsplane-netstack的 MSS 由 MTU 推出。
6. ACL、流网关与应用模式的机制
Section titled “6. ACL、流网关与应用模式的机制”0.11 起 nsplane-acl 与业务无关:来源是一组不透明的标签,规则是带类型的(网段、端口、协议、ICMP 类型、标签),每条规则带调用方给的不透明 规则 ID。产品的身份与策略模型(WireGuard 公钥 / terminate 绑定 / IdP 断言到标签的映射、JSON 策略文档、主机别名、自测、合并、deny scope、节点 L3 授权)由使用方编译成这些输入,规格在仓库 docs/specs/。
AclEngine把全部状态(默认规则集、规则命名空间、定向授权、打开的 pinhole)作为一个不可变快照放在nsplane_core::cell::SnapshotCell后面(0.13 起;之前是ArcSwap,因 arc-swap 1.9.x 的可靠性缺陷而移除):写入方发布新快照(store可并发调用),读取方用read经按线程缓存的闭包读取,值未变时不加锁、不动引用计数(0.14:一次读取 2.9 ns)。策略状态是显式的:未安装(按NotInstalled配置的默认处理)、已安装(可以为空,空即全部拒绝)、失败(fail或clear_all之后,fail-closed,POLICY_FAILED)。evaluate(&LabelSet, &Flow)不依赖报文,供代理在建立连接时判定,返回带规则 ID 的Decision。AclFilter是入向PacketFilter:来源的标签来自PeerIdentity(按对端,或按对端与来源地址);非首个 IPv4 分片跟随首片的判定;出向 TCP / UDP 记录回包许可,本地发起的流的回包可以通过;非 TCP/UDP 报文在作用域规则之后按Icmp/Ip/Any规则判定。- 命名空间。 一个来源属于它所有标签所在命名空间的并集。入向报文先查回包表,再依次看:共同命名空间的规则、定向授权(单向)、本地目的的入向 pinhole;否则以
CROSS_NAMESPACE或DENIED丢弃。Pinholes类命名空间从不扩大权限:没有规则、只允许 pinhole、不被授权引用。 - 出向。 默认不限制;对端所在的每个命名空间都设置了
outbound时才受限,此时出向报文只能匹配出向规则、出向 pinhole 或已接受入向流的回包许可。 - pinhole。 一个标签、方向、协议、目的端口,到期时间由调用方指定,返回
PinholeGuard;来源命名空间必须在pinhole_kinds中列出该类型。guard 被丢弃、到期、命名空间被移除、clear_all或权限被撤销时 pinhole 关闭,依赖它的回包许可随之失效。 - 按流钩子。
AclEngine::generation与PeerIdentity的代际一起标记按对端的标签缓存与按流的判定缓存,变更对下一个报文立即生效;接受所有目的、端口与协议且出向不受限的对端直接旁路。判定与逐包完整判定一致(差分测试)。 - 通用流网关(
gate)。 有状态的按作用域网关:GatePolicy由若干GateScope组成(本地地址、按PeerId的来源绑定及其标签、按标签授权新流的GateGrant、未绑定报文的放行或转交、unbound_addresses),加上全局的GateHolds;按协议的空闲超时、按来源与全局的流表上限、孤立分片与 ICMP 错误的处理、Observe/Enforce模式、通用的转交回调GateDivert。整份策略由FlowGate::replace原子替换。0.13 起 IPv4 与 IPv6 一视同仁:IPv6 走与 IPv4 相同的步骤(有界的扩展头遍历、按 32 位标识的分片、ICMPv6 错误刷新被引用的流、回显请求开流),不带地址的GateBinding按PeerId绑定该对端任一地址族的全部报文(精确的(peer, address)绑定优先;同一作用域里一个对端只能有一条按对端的绑定),出向按目的地授权与保留,流与分片上限两族合计;只有 IPv4 条目的策略不管辖任何 IPv6 地址,其 IPv6 报文照旧放行。ns 的节点 L3 语义如何编译到它上面,以及与旧行为的差异,见仓库docs/specs/node-l3.md。 - 按本地身份的运行时作用域(0.13)。
AclFilterScopes是一个默认AclFilterScope加每个LocalIdentityId一份;AclFilter::replace_scopes原子替换并返回新的代际(scope_generation)。报文按其对端所属本地身份(PeerIdentity::local_identity)的作用域判定,没有就用默认作用域;没有作用域的过滤器只多一次标志读取。 - nsplane 只负责执行;对端来源的生命周期、会合、配对、标签的含义与策略的编译在使用方。示例
app_session演示了在其上做文件传输,acl_gateway演示了带类型规则的网关。
发布对比表(0.11.0,2026-10-07,scripts/bench/wg-compare.sh 经 scripts/bench/slot.sh 运行:两个容器,绑定到同一个 CCD 的物理核,真实 TUN,MTU 1420,iperf3 30 s × 3 取中位数,32 核主机,负载 2–6):
| 配对 | TCP 1 条流 | TCP 4 条流 | UDP 3 Gbit/s 丢包 | 有负载时 ping p50 / p99 |
|---|---|---|---|---|
| nsplane ↔ nsplane | 11.1 Gbit/s | 11.8 Gbit/s | 0.03 % | 2.35 / 4.87 ms |
| nsplane ↔ nsplane,2 个加密线程 | 14.5 Gbit/s | 15.2 Gbit/s | 0.10 % | 1.99 / 4.16 ms |
| nsplane → 内核 WireGuard | 7.7 Gbit/s | 7.1 Gbit/s | 0.05 % | - |
| 内核 WireGuard → nsplane | 5.5 Gbit/s | 5.5 Gbit/s | 0.03 % | 1.65 / 1.90 ms |
| wireguard-go ↔ wireguard-go | 12.7 Gbit/s | 11.3 Gbit/s | 0.18 % | 1.97 / 5.20 ms |
| netstack ↔ netstack(用户态,无 TUN) | 8.6 Gbit/s | 11.0 Gbit/s | 0.00 % | - |
内核 WireGuard 的加密线程不在绑定的核上,所以受绑核限制。nsplane → 内核 WireGuard 与 0.10.1 持平(两版都是 7.7 / 7.1),0.11 默认的接收反压在这个方向上没有代价。
| 微基准 | 结果 |
|---|---|
data_path 核心往返 | 64 B 约 530 ns,1420 B 约 1.34 µs(0.10 至 0.11 不变) |
| 校验和 | 20 / 64 / 1420 B 比 0.10 快 1.3 / 2.6 / 4.6 倍(按 32 位累加) |
翻译器(eam4 路径,完整大小) | 出向约 80–93 ns,入向约 88–102 ns(0.10.0 前约 200 ns) |
| ACL 与流网关 | 命名空间大部分场景比 0.10 快 2–27 %;流网关已建立流的过滤比 0.10 的节点 L3 快 23 %,无关报文放行快 56 % |
| netstack TCP | 3 % 丢包 16 MiB 中位 1.0 s(0.10:12.7 s),1 % 丢包 0.1 s |
8. 安全与协议基线
Section titled “8. 安全与协议基线”继承自分支基线:cookie 覆盖源 IP 与端口;握手限速按源 IP 计数并有设备级总预算;双向执行 Reject-After-Messages,到达 Rekey-After-Messages 时重新协商;16 字节填充;8192 防重放窗口;cookie 回复不改变端点;源允许 IP 用全局最长前缀匹配校验;拒绝格式错误的密钥;保活 / 握手定时器从第一个未应答报文起算;握手重试带抖动。调试输出不含密钥材料。运行时代码没有 unwrap / expect / panic;unsafe 只在 nsplane-tun 的平台模块,其余 crate 声明 #![forbid(unsafe_code)]。
加密实现:ChaCha20-Poly1305 用 aws-lc-rs(构建需要 C 编译器,x86_64 Windows 目标另需 NASM),常量时间比较用 subtle,XChaCha20-Poly1305(cookie)/ BLAKE2s / HMAC 用 RustCrypto,X25519 用 x25519-dalek。
9. 作为依赖使用
Section titled “9. 作为依赖使用”固定 commit 以保证可复现构建:
[dependencies]nsplane = { git = "https://github.com/dotns/nsplane", rev = "<commit>" }nsplane-tun = { git = "https://github.com/dotns/nsplane", rev = "<commit>" }# 按需:nsplane-netstack、nsplane-acl、nsplane-nat、nsplane-uapi自带事件循环的嵌入方(如 ns 的无 I/O quick-runtime)可以直接依赖 nsplane-core。阶段 0 中 ns 只固定 noise 层(nsplane-noise)。本地联合开发时用路径覆盖:
[patch."https://github.com/dotns/nsplane"]nsplane = { path = "../nsplane/crates/nsplane" }nsplane-noise = { path = "../nsplane/crates/nsplane-noise" }最小的 IPv4 客户端(一个 TUN、一个 UDP 套接字、一个对端、无过滤器):
let (source, sink) = Tun::create("wg0")?.split()?;let udp = UdpTransport::bind(UDP, "0.0.0.0:0".parse()?)?;let engine = EngineBuilder::new(source, sink).private_key(key).transport(udp).build()?;let mut peer = Peer::new(server_key);peer.allowed_ips = vec!["10.0.0.0/24".parse()?];peer.persistent_keepalive = Some(25);peer.path = Some(Path { transport: UDP, addr: server, ecn: Ecn::NotEct });engine.handle().add_or_update_peer(peer).await?;engine.wait().await?;10. 开发工具与质量门禁
Section titled “10. 开发工具与质量门禁”nsplane-cli 在前台运行一个用户态隧道(TUN + 引擎 + UAPI),日志写到 stderr,Ctrl-C 停止;用标准 wg 工具配置,参数可由环境变量提供(WG_THREADS、WG_LOG_LEVEL、WG_UAPI_FD、WG_TUN_FD、WG_SUDO)。它不做后台守护。
仓库没有 CI,提交前在本地运行门禁:
| 命令 | 内容 |
|---|---|
just check | fmt、clippy、nextest、doctest、cargo deny、shear、typos、MSRV、示例构建 |
just cross | aarch64-apple-darwin(zig)与 x86_64-pc-windows-gnu(mingw)的 clippy |
just test-windows | 在 wine 下运行 Windows 的库单元测试 |
just e2e | nsplane-cli 在两个容器中与内核 WireGuard 互通 |
just e2e-lib | nsplane-e2e 的容器用例与内核 WireGuard 互通 |
just e2e-examples | 示例在容器中跑呈现 × 传输矩阵与 relay 场景,并与内核 WireGuard 互通 |