nsplane · 0.11 迁移说明(给 ns 与 nsgw)
本页面向 nsplane 的使用方(ns、nsgw),说明从 0.10.x 升到 0.11.0 要改什么、可以删掉什么、行为上有哪些变化。依据 nsplane 仓库 tag
v0.11.0(d788269,2026-10-08)。逐条的英文条目见仓库CHANGELOG.md的[0.11.0]一节;每个被移出 nsplane 的概念,其规格在仓库docs/specs/。
1. 总则:nsplane 不再含产品概念
Section titled “1. 总则:nsplane 不再含产品概念”2026-10-06 起,nsplane 是与业务无关的数据面(ADR docs/decisions/2026-10-06-business-agnostic-scope.md):
- nsplane 只认网段、端口、协议、ICMP 类型、不透明的标签(label)与规则 ID;不认识主体、组、realm、terminate、IdP、服务、授权、Quick 地址这类产品概念。
- 原来移植进 nsplane 的 ns 概念(身份断言、JSON 策略文档、
crates_acl()预设、节点 L3 模型、Quick v2 地址命名)在 0.11 删除,不提供兼容层。 - 这些概念改为规格文档交给使用方自己实现,附带对照数据,用来证明实现与旧行为一致。
just check的最后一步扫描公开接口与 rustdoc 中的产品词汇,以后不会再加回产品预设。
规格(仓库 docs/specs/) | 内容 | 对照数据 |
|---|---|---|
acl-generic-api.md | nsplane-acl 通用接口的设计说明:逐项去向、类型、gate 的 API、24 行编译对照表 | - |
acl-source-identity.md | 如何把 WireGuard 公钥、terminate 绑定、IdP 断言映射成标签 | 35 个用例(注明出处) |
acl-policy-document.md | 旧 JSON 策略文档(hosts / acls / tests)、主机别名展开、自测语义、合并与 deny scope、crates_acl 预设,以及 ns 账户过滤器如何对应到新接口 | data/acl-crates-acl-parity.json(12,181 个报文) |
node-l3.md | 节点 L3 语义(Node / Service / Subnet 授权、源绑定、同属主规则、Subnet LAN DNS 准入、模式、原因映射、gateway consumer)如何编译到通用流网关,以及与旧行为的差异 | data/node-l3-differential.json(6,361 步) |
translator-address-plan.md | Quick v2 地址规划如何对应到翻译器的通用 EAM 字段 | - |
2. 必须改的(不兼容)
Section titled “2. 必须改的(不兼容)”2.1 nsplane-acl
Section titled “2.1 nsplane-acl”| 0.10 | 0.11 |
|---|---|
AccessRequest、AclDecision | Flow + LabelSet 交给 AclEngine::evaluate,返回 Decision(接受时带 RuleId,拒绝时带 reasons 常量) |
CompiledPolicy(compile、is_allowed、validate_tests、permit_all) | RuleSet::new(Vec<Rule>)、RuleSet::matching;放行全部用 AclEngine::with_not_installed(NotInstalled::Accept) 或一条 ProtocolMatch::Any 规则 |
AclEngine::{load, store, clear, policy, is_allowed} | install、uninstall、fail(自己的编译失败时调用)、rules、policy_state |
AclPolicy / AclRule / AclAction / AclTest、merge_layered、apply_deny_scope | 删除。ns 自己解析文档、展开主机别名、合并来源、去掉禁止网段,最后得到 Vec<Rule>(见 acl-policy-document.md)。每条规则的来源写进它的 RuleId |
AclFilterConfig::crates_acl(local)、FragmentMode::ALLOW_ONLY | 显式设置字段:stateful_replies: false、allow_other_protocols: false、fragments: FragmentMode::AllowOnly { ttl: 15 s, capacity: 4096 }、accept_to_local: local、accept_icmp_echo_reply: true、ipv6: Ipv6Mode::Accept |
SourceAssertion(WgPeerKey / Terminate / External)、TerminateBinding、wg_peer_anchor | PeerIdentity::labels(peer) / labels_for(peer, remote) 返回 LabelSet;标签文本由 ns 自己定(例如 key:<hex>),见 acl-source-identity.md |
PeerIdentityMap | PeerLabelMap(insert(peer, LabelSet)、insert_by_source 按前缀表) |
| 命名空间成员 / 授权端 / pinhole 用字符串主体 | 用 Label:NamespaceMember::label、GrantEnd::Label、PinholeSpec::label、memberships(&Label) |
NamespaceId::is_app 与 "app:" 前缀、allow_app_pinholes、NotAppNamespace | NamespaceKind::{Rules, Pinholes}、pinhole_kinds、NotPinholeNamespace;命名空间 ID 不再有含义 |
NamespacePolicy::policy: AclPolicy | NamespacePolicy::rules: Vec<Rule> |
OutboundRule / Grant 的字符串 proto / ports | Vec<ProtocolMatch>;原来的”TCP 与 UDP 任意端口”写成 [Tcp(PortSet::Any), Udp(PortSet::Any)] |
Error::{InvalidCidr, InvalidDst, UnknownAlias, TestsFailed, InvalidPolicy} | Error::{InvalidRule, InvalidNamespace, InvalidGrant}(#[non_exhaustive]);网段由调用方解析(IpNet: FromStr) |
AclFilterStats | 新增 internal(0.10.0)、policy_failed、policy_state 字段,用结构体字面量构造的代码要补上 |
2.2 节点 L3 → 通用流网关(nsplane_acl::gate)
Section titled “2.2 节点 L3 → 通用流网关(nsplane_acl::gate)”整个 NodeL3*、GatewayConsumer*、PeerKeyMap、PeerPublicKeys、NODE_L3_SCHEMA_VERSION 删除,换成通用的 FlowGate:
| 0.10 | 0.11 |
|---|---|
NodeL3Gate(new、with_limits、with_clock) | FlowGate::new(GateConfig) / with_clock,上限在 GateLimits、超时在 GateTimeouts |
apply / apply_from_source / withdraw_source、NodeL3Config | FlowGate::replace(GatePolicy),整份替换并返回代际;来源、墓碑、阶段、schema 版本由 ns 自己管理 |
NodeL3Mode::{Disabled, Observe, Enforce} | GateMode::{Off, Observe, Enforce} |
| Node / Service / Subnet 授权、对端绑定 | GateScope { local, bindings: Vec<GateBinding>, grants: Vec<GateGrant>, unbound, unbound_addresses };授权写成”标签 → 目的前缀 / 协议 / 端口”,绑定按 PeerId |
| gateway consumer 转交 | UnboundRule { action: Divert } + GateDivert 回调 + DivertedPacket |
NodeL3Reason(与 ns 一一对应) | 通用的 GateReason 与 "flow gate: *" 丢弃字符串;ns 自己映射回自己的原因(映射表见 node-l3.md) |
编译步骤见 node-l3.md 的 24 行对照表。与 ns 旧行为的已知差异(都已接受并写进规格):
- 转交候选只在同一个网关代际内有效。
- 网关本身不做”未知对端”丢弃,由 ACL 的
UNKNOWN_PEER与网关的未绑定规则覆盖。 - Subnet 端口 53535 请求在表满时只计一次数。
- Subnet 授权打开的流在作用域变化时按当前授权重新判定,仍匹配就保留(旧网关直接丢弃)。
- 作用域没变的重新编译后,分片与放行状态最多保留 30 s。
- 丢弃原因的细分方式不同。
另外:网关只支持 IPv4,策略里出现 IPv6 一律以 GatePolicyError::Ipv6 拒绝。没有 PeerId 绑定、但属于本作用域的地址,必须列进 GateScope::unbound_addresses,否则发往它们的出站报文不受作用域模式约束。
2.3 nsplane-nat 翻译器改名(行为逐字节不变)
Section titled “2.3 nsplane-nat 翻译器改名(行为逐字节不变)”| 0.10 | 0.11 |
|---|---|
PeerMapping { node6, node4, alias4, alias6 } | PeerMapping { peer6, eam6, eam4, local6 } |
SelfMapping { self4, node4 } | SelfMapping { eam4, eam6 } |
TranslationTableBuilder::peer_with_native_alias4 | peer_with_peer6_eam4 |
TranslationTable::{native_alias4, by_native_alias4, by_alias4, by_alias6, by_node4, by_node6} | {peer6_eam4, by_peer6_eam4, by_eam4, by_local6, by_eam6, by_peer6} |
TableError::Alias4IsSelf4 | TableError::Eam4IsSelf |
Quick v2 的地址规划如何填这些字段,见 translator-address-plan.md。
2.4 其他
Section titled “2.4 其他”nsplane-netstack:NetStackStats新增icmp_ignored字段,用结构体字面量构造的代码要补上。nsplane-wss:WssStreamClient、WssStreamServer和WsFrame编解码标为 deprecated,没有删除;ns 在 M6 切换前可以继续使用。替代方式是在WssDialer上跑 WireGuard 对端;网关侧对应的是新的WssServerTransport。
3. 默认行为的变化
Section titled “3. 默认行为的变化”| 变化 | 影响 |
|---|---|
| 不开 crypto worker 时,投递 sink 满了会让接收端等待,不再丢弃 | 默认配置下 DROP_SINK_FULL 基本为 0;数据在传输里等待(UDP 是 socket 缓冲)。sink 卡住时,排在后面的握手和保活也会延迟。有负载时延迟 e2e:丢失的 ping 505 → 0,p99 14.2 → 5.95 ms |
Event::Authenticated 只在来源变化时发 | 被提升为直连的对端不再每个包都广播一次;PathPolicy::on_authenticated 仍然每条消息调用 |
NetStackConfig::datagram_capacity 默认 128 → 256 | UDP 突发不再丢包;一个卡住的 flow 最坏占用约 512 KiB |
netstack TCP 处理发给自己连接的 PTB / Frag Needed,并有尾部丢包探测(smoltcp fork v0.14.0-nsplane.6) | 小 MTU 路径不再卡死;3 % 丢包时 16 MiB 从 12.7 s 降到 1.0 s |
AclEngine::clear_all 之后状态为 Failed(全部拒绝,POLICY_FAILED) | 原来是 NO_POLICY;uninstall 回到”未安装” |
非 TCP/UDP 的入站报文在作用域规则之后,按带类型的规则(Icmp / Ip / Any)判定 | 规则里没有这类条目的策略,行为不变 |
| 0.10.1:开 crypto worker 时,在途加密任务计入本地队列余量 | 修复负载高时本地报文按 transmit full 被丢弃 |
4. 新能力(ns 可以用来删除自己的代码)
Section titled “4. 新能力(ns 可以用来删除自己的代码)”逐项的”ns 可以删除什么”写在 nsplane 仓库 docs/task/20261003-2200-ns-local-side.md 每一项的 Done 行里。概要:
| 需求 | nsplane 提供 |
|---|---|
| MG-4 / MG-5 / MG-6 | Splitter::new_map(先改写再分流)、MapSink::with_after(投递成功后回调)、SwapSink(可替换的 sink)+ AbortSink / SinkAbort(取消时放弃正在等待的发送,返回 Ok) |
| MR-1 / MR-2 / MR-3 | SharedPacketPool、PipeSink::alloc / recycle、ChannelSource::pool;merge、Android slot、iOS host_tun、Wintun 的 source 都能回收缓冲;PacketSink::send_batch_spent,pump 自动把写完的缓冲还给来源 |
| MT-3 / MT-4 | Tun::create_with(name, TunOptions):Wintun DLL 版本和哈希固定(WintunPin)、同名独占、遗留幽灵网卡自动替换、两个协议族都设置并读回 MTU;TunSource::name / TunSink::name 返回实际网卡名 |
| MD-7 | InboundDestinations trait 与 EngineHandle::set_inbound_destination_source:入站目的地址由调用方无锁更新 |
| MQ-17 | 见第 3 节 |
| MB-x7、MF-5 | 见第 3 节;单流发送没有变慢(15 对交替测量,+1.5–2.1 %) |
| MF-3 | crypto_workers >= 2 时同一对端的加密分摊到所有 worker;两端开 2 个 worker 时 14.5 / 15.2 Gbit/s |
| MF-4 | 翻译器每个包快约 50–60 %,但只占 ns 测到的 30 % 差距的一小部分;其余在 nsplane-nat 之外,建议 ns 自查 TranslatorStats::grown_copies、IPv4 MTU 少 20 字节、TUN 上的 IPv4 TSO |
| MW-7 | WssConfig::ping_interval(None) 完全不发 ping |
| 网关(nsgw) | WssServerTransport + WssAcceptor:接收调用方已升级、已鉴权的 WebSocket,每个会话一个 [100::n]:0 端点,默认最多 4096 个会话,一个慢会话不会拖住其他会话 |
5. ns 侧待办清单
Section titled “5. ns 侧待办清单”- 把依赖固定到
v0.11.0。 - 按
acl-source-identity.md把对端来源映射成标签,替换SourceAssertion。 - 按
acl-policy-document.md在 ns 里实现策略文档的解析、别名展开、合并、deny scope 与自测,产出Vec<Rule>;用acl-crates-acl-parity.json验证。 - 按
node-l3.md把NodeL3Config编译成GatePolicy,自己维护来源、代际与原因映射;用node-l3-differential.json验证;填好unbound_addresses。 - 适配翻译器的 EAM 新名字(
translator-address-plan.md)。 - 补上
AclFilterStats/NetStackStats的新字段。 - 用第 4 节的新能力删除 ns 里残留的逐报文循环(MG、MR、MT、MD-7)。
- 在 Windows 上按仓库
docs/task/20261006-1700-windows-smoke-for-ns.md跑一次冒烟测试,并把结果反馈给 nsplane。 - M6 切换后停用
WsFrame流载体,通知 nsplane 删除。
6. 仍开着、需要协商的
Section titled “6. 仍开着、需要协商的”- relay v2 节点客户端放在哪里: 建议放在 ns 或 nsshared。nsplane 只提供通用载体(
LinkTransport、WssDialer、UDP 旁路通道、PathPolicy),因为 relay v2 的线上格式是产品协议。 - 网关(nsgw)可能提的通用需求: 一次性替换整套对端并报告每个对端的结果;删除对端时拿到最后一次计数;R3 的 IPv6 / NAT64。
- nsplane 推迟的项: 只在不开 worker 时生效的排队延迟上限;包装层转发
send_batch_spent与EngineHandle::recycle;netstack 第二次尾部探测或多块 SACK、入站 ICMP 校验和;测量工具拿不到 engine 丢包计数。ns 如果需要其中某项,请作为需求提出。
7. 0.12.0 的补充:本地多身份
Section titled “7. 0.12.0 的补充:本地多身份”0.12.0(2026-10-12)只新增一项功能:本地多身份,供 ns 让一个节点同时加入多个互相独立的覆盖网,并在每个网里使用不同的静态密钥。
- 接口:
EngineHandle::{add_local_identity, add_local_identity_on, remove_local_identity, local_identities, peer_id_in, remove_peer_in};对端用PeerConfig::local_identity指定身份(None为默认身份,即原来的私钥);非默认身份下对端的其他设置经add_or_update_peer或ConfigChange::InLocalIdentity。 - 默认身份就是原来的私钥:
set_private_key、peer_id(key)、UAPI 与 CLI 不变(UAPI 只显示默认身份)。 - 防关联:不同的密钥只防按公钥关联。要防按地址关联,每个身份用自己的传输,并用
add_local_identity_on把身份限定在它的传输上;否则持有另一覆盖网公钥的一方可以在高负载时用 cookie 回复探测节点是否持有该密钥。 PeerConfig、PeerStats、ConfigChange各多一个字段或变体,用完整结构体字面量或穷举匹配的代码要更新。- 接入说明与开销测量见仓库
docs/task/20261011-0101-local-identities.md。
8. 0.13.0 的补充:按对端的流网关与去 arc-swap
Section titled “8. 0.13.0 的补充:按对端的流网关与去 arc-swap”0.13.0(2026-10-12)是冻结期间的第二次例外,应 ns 的两项请求:按对端、与地址族无关的流网关(GF-1..4),以及从数据路径与公开 API 去掉 arc-swap(AS-1..2)。
- 破坏:
GatePolicyError::Ipv6删除(replace不再拒绝 IPv6 条目;删掉对它的匹配分支,依赖该拒绝的策略改为自己不放 IPv6 条目);Nat64Lan::new与with_snat_ports改收LanRoutes句柄而不是Arc<ArcSwap<Vec<LanRoute>>>——用LanRoutes::new(routes)建、保留一个克隆、用LanRoutes::replace换。 - 流网关:
GatePolicy的作用域、绑定、unbound_addresses、授权、保留与释放都接受 IPv6;不带地址的GateBinding按PeerId绑定该对端的全部报文(之前空列表什么都不绑);find_flow能找到 IPv6 流(混合地址族返回None);新错误DuplicatePeerBinding。 - 运行时作用域:
AclFilterScopes、AclFilter::replace_scopes/scope_generation、PeerIdentity::local_identity、PeerLabelMap::set_local_identity。 - 快照:
nsplane_core::cell::SnapshotCell(new、load、load_full、store)与Snapshot守卫,供使用方替换自己的ArcSwap。 - 性能:首轮干净的 A/B 与 0.12 持平;带作用域的
other_protocols路径 +3.7 %;64 B 报文的 NAT64 翻译 +1..6 ns;安静主机上的完整 A/B 留到发布后。
ns 升到 0.13 后可以关闭两项等待:经 membership 的 Subnet L3(运行时替换本地身份的出向来源)与 Enforce 下对 Node 原生 N6 的出向拒绝(按对端绑定后对 IPv6 的出向按目的地判定)。
9. 0.14.0 的补充:LAN 前缀映射与按身份的路由
Section titled “9. 0.14.0 的补充:LAN 前缀映射与按身份的路由”0.14.0(2026-10-13)是冻结期间的第三次例外,应 ns 的两项请求(LP、LR)并修复 0.13.0 的读取开销回退(OP)。全部为增量,没有破坏性变化。
- LAN 前缀映射:
LanPrefix的lan6可以是 /(96 +lan4长度) 并在低位嵌入另一个 IPv4 网络,lan4_to_lan6(a) = lan6 | (a & host_mask),反向同理;/96 形式不变。LanRoute不变,解析的是嵌入地址。 - 按身份的密钥路由:
Core::set_local_sources(identity, sources)/EngineHandle::set_local_sources、Core::route_from(src, dst)、LocalIdentityInfo::sources(EngineStatus::local_identities的LocalIdentity::sources)。入站源检查只用收包对端所属身份的允许 IP;未在任何源集合内的本地报文按Core::route的引擎级回退路由。 - 快照单元:
SnapshotCell::read(缓存的闭包读取);store可并发调用。 - 性能:一次引擎快照读取 2.9 ns(0.13.0 为 15.8 ns);残余见任务
20261013-1600-perf-residuals,入向分片路径的 2.7 ns 回退在 Unreleased 修复。
ns 用 LAN 前缀映射实现了重叠 LAN 的本地虚拟网段(2026-10-13);按身份的路由(同一 Node 身份在两个 membership 里同时可达)尚未接入。