跳转到内容

nsplane · 架构与接口

本页描述 nsplane 作为数据面的 crate 划分、公开接口、引擎模型与各功能模块,以 nsplane 仓库的 docs/architecture.md、docs/design.md 与 tag v0.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)。

crate作用
nsplane-noiseNoise 协议状态机(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)
nsplanetokio 驱动: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-natIPv4 / 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-cliLinux / macOS 开发工具:TUN + 引擎 + UAPI
nsplane-e2e、nsplane-examples不发布:库级端到端测试;可运行的示例,包括单端口 relay 及其 UDP / WSS 客户端传输
nsplane-noise ─► nsplane-core ─► nsplane ─► nsplane-tun、nsplane-uapi ─► nsplane-cli
nsplane-packet ─► nsplane-core、nsplane
nsplane、nsplane-packet ─► nsplane-netstack
nsplane-core、nsplane-packet ─► nsplane-acl、nsplane-nat
nsplane ─► nsplane-wss

依赖方向严格向下;只有 nsplane-core 依赖 nsplane-noise(并重新导出 noise、x25519)。最小客户端只需要 nsplane 与 nsplane-tun;nsplane-acl、nsplane-nat、nsplane-netstack 是独立 crate,不加入就不编译、不链接。所有 crate 都没有可选的 Cargo feature。

crate入口配套类型
nsplane-noisenoise::Tunn(encapsulate_in_place / decapsulate_in_place)、noise::rate_limiter::RateLimiter、x25519 密钥TunnResult、noise::errors::WireGuardError
nsplane-packetPacketBuf(头部 / 尾部预留、advance / reserve_front、extend_from_slice、from_shared,越界返回错误)、PacketPool、SharedPacketPool(alloc、alloc_from、recycle)、PacketBatch、IpPacket、build::udp_packetPath、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-coreCore(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
nsplaneEngineBuilder(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、LinkDialerUdpTransport(含 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-tunTun(create、create_with、Unix 上的 from_fd / from_raw_fd、split、offload、mtu、name)TunOptions、TunSource、TunSink、Offload、adopt_fd(Unix)、MTU_POLL_INTERVAL
nsplane-netstackNetStack(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-aclAclEngine(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-natTranslator(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)、RedirectPeerMapping(peer6、eam6、eam4、local6)、SelfMapping(eam4、eam6)、LanPrefix、TableError、TranslatorStats、PortMapRule、ConntrackConfig、ConntrackStats、Flow、FlowMatch、FlowDirection、TcpState、checksum::transport_valid
nsplane-wssWssDialer(new、into_transport)、WssServerTransport::new → (WssServerTransport, WssAcceptor)、WssAcceptor::accept(WebSocketStream) → WssSessionWssConfig(ping_interval(Option)、keepalive、allow_plaintext、reconnect_delay、connect_timeout)、WssTls、BearerProvider、WssDialError、WssDialEvent、WssServerConfig(默认最多 4096 个会话)、WssServerTransportStats、WssSessionStats;已弃用:WssStreamClient、WssStreamServer、frame
nsplane-uapiUapi(new、with_external_transport、with_listen_port、handle_request、serve_stream)、UapiListenerudp_transport、TRANSPORT_ID、socket_path / pipe_path
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::Event
Core::poll_timeout / handle_timeout(now) 重新协商、保活、过期、限速器复位
Core::inject_inbound / inject_outbound / force_handshake / route / peer_stats / recycle
  • Core 不做 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 构建的核心才发出任务,此时每个对端的隧道在自己的互斥锁后面;否则每个对端独占隧道,数据路径不加锁。

一个拥有者任务独占 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) 可关闭。

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 与路径映射到自己的标签。按传输的计数由传输自己的任务以每批一次原子累加完成,不经过拥有者任务;替换传输时计数延续,移除时一并删除。

本地侧(PacketSink / Source)\ 传输UDPrelay UDPrelay WSS
TUN(内核协议栈)✓✓✓
nsplane-netstack(无 TUN、无 root)✓✓✓
宿主桥接(iOS / Android fd、通道)✓✓✓

UDP 之外的传输是嵌入方对 Transport 的实现;示例包实现了单端口 relay 及其 UDP 与 WSS 客户端传输,just e2e-examples 在容器中逐格测试这个矩阵,并与内核 WireGuard 互通。

一个引擎可以持有多把静态密钥。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。

基础客户端是 TUN 设备 + UdpTransport + 对端、没有过滤器的 EngineBuilder。下列功能都是可选的;未安装的功能不在数据路径上。

功能crate启用方式默认未启用时的开销
IPv4 / IPv6 翻译nsplane-natfilter(Box::new(Translator::new(table))),set_mtu 设为隧道 MTU未安装无
服务发布(DNAT / SNAT)nsplane-natfilter(Box::new(PortMap::new(rules)?))未安装无
ACLnsplane-aclfilter(Box::new(AclFilter::new(engine, identity)))未安装无
流计量nsplane-aclfilter(Box::new(FlowTracker::new(capacity)))未安装无
分片阶段nsplanefragmenter(FragmentConfig::default())关闭每个本地报文一次 Option 判断
加密工作线程池nsplanecrypto_workers(n),n ≥ 20每个报文一次 Option 判断;不加锁
通用流网关nsplane-aclfilter(Box::new(GateFilter::new(Arc::new(gate)))),FlowGate::replace(GatePolicy)未安装无
按本地身份的 ACL 作用域nsplane-aclAclFilter::replace_scopes(AclFilterScopes):一个默认作用域加每个 LocalIdentityId 一份,运行时原子替换默认作用域(代际 0)一次标志读取
WSS 数据报服务端nsplane-wssWssServerTransport::new 作为传输加入,监听器把升级后的 WebSocket 交给 WssAcceptor::accept不使用无
缓冲回收nsplane-packet / nsplaneSharedPacketPool、PipeSink::alloc、ChannelSource::pool;pump 自动把 send_batch_spent 的缓冲还给来源不使用每次回收调用一次原子读取
用户态协议栈nsplane-netstackNetStack::new(config),split 作为来源与 sink不使用无(不是 nsplane 的依赖)
混合本地侧nsplaneSplitter / MergeSource不使用无
TUN 卸载nsplane-tunTun::create 默认开启内核支持时开启(Linux、Android)关闭时每个报文一次系统调用
UDP GSO / GROnsplaneUdpTransport::bind 默认开启开启关闭时每个数据报一次系统调用

IPv4-only、IPv6-only 或双栈不需要开关:由传输的绑定地址、TUN 上配置的地址与路由、对端的允许 IP 决定。

  • 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 网段到达(lan4 10.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 ↔ nsplane11.1 Gbit/s11.8 Gbit/s0.03 %2.35 / 4.87 ms
nsplane ↔ nsplane,2 个加密线程14.5 Gbit/s15.2 Gbit/s0.10 %1.99 / 4.16 ms
nsplane → 内核 WireGuard7.7 Gbit/s7.1 Gbit/s0.05 %-
内核 WireGuard → nsplane5.5 Gbit/s5.5 Gbit/s0.03 %1.65 / 1.90 ms
wireguard-go ↔ wireguard-go12.7 Gbit/s11.3 Gbit/s0.18 %1.97 / 5.20 ms
netstack ↔ netstack(用户态,无 TUN)8.6 Gbit/s11.0 Gbit/s0.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 TCP3 % 丢包 16 MiB 中位 1.0 s(0.10:12.7 s),1 % 丢包 0.1 s

继承自分支基线: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。

固定 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?;

nsplane-cli 在前台运行一个用户态隧道(TUN + 引擎 + UAPI),日志写到 stderr,Ctrl-C 停止;用标准 wg 工具配置,参数可由环境变量提供(WG_THREADS、WG_LOG_LEVEL、WG_UAPI_FD、WG_TUN_FD、WG_SUDO)。它不做后台守护。

仓库没有 CI,提交前在本地运行门禁:

命令内容
just checkfmt、clippy、nextest、doctest、cargo deny、shear、typos、MSRV、示例构建
just crossaarch64-apple-darwin(zig)与 x86_64-pc-windows-gnu(mingw)的 clippy
just test-windows在 wine 下运行 Windows 的库单元测试
just e2ensplane-cli 在两个容器中与内核 WireGuard 互通
just e2e-libnsplane-e2e 的容器用例与内核 WireGuard 互通
just e2e-examples示例在容器中跑呈现 × 传输矩阵与 relay 场景,并与内核 WireGuard 互通