NSGW · 新架构设计(基于 nsplane 与 nsshared)
状态:草案,未批准(2026-10-05)。 本页是计划,不是现状描述:它描述打算做的工作,不能作为任何代码行为的依据。现有页面(概览、架构与功能)描述的仍是当前实现。
依据的提交:
| 仓库 | 提交 | 引用前缀 |
|---|---|---|
| nsplane | 22721f7 | nsplane: |
| nsshared | fc1c884(v0.6.0) | nsshared: |
| nsgw | 3d31b5f8 | nsgw: |
ns-shared(v1,tag archive/v1-20261002) | 7c8214d | ns-shared: |
| nsd(线上的控制面) | 6bce2220 | nsd: |
| docs(本站) | 7301adb | docs: |
| 归档的现状文档 | 42dfc98 | nsiod-docs: |
| 归档的节点 | 744e950 | ns-archive: |
| 归档的控制面 | 3a41482 | nsd-archive: |
本页不加限定地说 NSD 时,指的是 nsd:,即新网关在 R0 与 R2 中对接验证的控制面;归档的那一个在若干要紧之处不同(Exit 与 Egress),不是验收对象。
本计划细化迁移计划(里程碑 M0 至 M9,决定 D1 至 D6)里 nsgw 的部分;目标架构见新架构设计、Quick 协议 v2 与会合协议。与该页不一致之处列在与迁移计划的差异。
网关作为 nsgw 仓库的一条新线,在两个库之上重构:nsplane 负责 WireGuard 与报文路径,nsshared 负责 所有其他组件也要编解码的契约。迁移决定已经退役的东西不移植。
| 阶段 | 交付 | 状态 |
|---|---|---|
| R0 基础 | 新线、依赖集、NSD 客户端 | 已交付:2026-10-08 合并进 next |
| R1 relay v2 | 建在 nsshared-proto 上的 relay、会合、登记表与区域 | 开发可以开始;L10 已交付,生产完成条件只等 relay 自己的证据 |
| R2 网关换引擎 | 公网入口、Exit 与 Egress 运行在 nsplane 引擎上,对接今天的 NSD 在隔离环境中验证 | 可以开始;网关规模的实测由它自己做(L4) |
| 协议选定 | 新网关用上新的控制协议 | 等待 L11 与 NSD 的工作;排期未决(G11) |
| R3 账户切换 | 网关成为一个节点、账户 relay 独立成进程、node6、IPv6 的 Exit 与 Egress;旧线归档 | 等待 R1、R2、NSD 工作包与 L8 |
| R4 名字与收尾 | DNS 记录供数 | 等待 R3 与 L9 |
已定:东西放在哪里(放置规则),在 nsgw 仓库开 next 新线(G1)、从 main 移植(G2),两个库以
固定提交的 git 依赖取得(G6),v1 控制契约是网关的一个临时 crate(G4),按产品身份做的准入在网关里求值(G5),nsplane 引擎是唯一的 WireGuard 实现(G3),内核的 nft 与 TPROXY 留在网关(G7),
R2 在隔离环境中验收(G8),Exit 与公网入口都不设专用的 WSS 通路(G14、G20),账户 relay 独立成进程(G21),新线从级联分支取什么(G15 至 G17),由谁得出准入的对端集合、引擎如何对账与计费(G18、G19),以及控制协议要重新设计(G9)。
未决,按需要它们的先后排列:
| 用于 | 决定 |
|---|---|
| 向其他仓库提需求,这些周期最长 | G11 至 G13(新控制协议的排期与保证) |
| R3 | G10(是否更换内核后端) |
R0 已交付。今天就能开始的是 R1 的开发和 R2:它们都不等待其他仓库的开发。仍然要经过其他仓库的是: R3 所需的 nsd 的发布边界与 M6 工作包;R1 生产完成所需的 nsshared 的 L10 已在 2026-10-07 发布。
已知的粗略规模:
| 部分 | 规模 |
|---|---|
| R1 | 取代旧 relay 的 34,315 行源码与 36,532 行测试。在钉住的提交上做的模块清点把其中 23,979 行源码归为移植(区域与二进制)、10,336 行归为重新推导;另有 3,876 行区域协议要从 ns-shared 移植。另外新增:登记表存储、信箱、登记状态的复制 |
| R2 | 账户网关共八个 crate、139,150 行源码(含内联测试)。在钉住的提交上做的模块清点,按决定 G14 与 G20 去掉的 WSS 路径调整后,把 92,700 行归为移植、12,790 行归为重新推导、183 行归入网关自己的 v1 契约 crate、33,477 行没有去向:即已退役的路径,其中 nsgw-wg 12,211 行、nsgw-wss 18,005 行。nsgw-api 与 nsgw-routing 中只为 connector 服务的部分尚未从移植数里扣除,ACME 运行时的 584 行也没有扣除:G21 之后它在网关里没有监听可服务。这些数字是对旧代码行的归类,不是对新代码规模的预测。新增:引擎接入与期望状态视图 |
| 控制协议 | 四个仓库合计 16,000 至 30,000 行改动,66 至 110 人日,置信度低 |
| R0、R3、R4 | 未估算 |
R0 至 R2 没有人日估计;每个阶段的计划在开始之前给出。
决定东西放在哪里的规则(所有者,2026-10-05)。
| 规则 | 对网关的含义 |
|---|---|
| 多个组件都要编解码的契约放在 nsshared | nsgw 不定义任何 nsd 或 ns 也要理解的线上类型。有一个带期限的例外(G4):今天 NSD 的 v1 契约是网关的一个临时 crate,R3 移除 |
| 数据面放在 nsplane | WireGuard、进程内的报文处理、承载 WireGuard 报文的各种载体,以及数据路径上按报文和按流的过滤,都属于 nsplane。nsgw 不含 WireGuard 实现。nsplane 按它自己 2026-10-06 的决定不带业务语义:它认识网段、端口、协议与不透明的标签,不认识主体、组与 realm。按产品身份做的准入(例如把公网入口的访问者对照 NSD 下发的策略)在网关里决定(G5) |
| 主机集成留在 nsgw | 编排 Linux 的 nft 与 TPROXY 规则、策略路由,以及围绕它们的计数与对账,属于网关的主机集成,不是数据面(决定 G7) |
留在 nsgw 的是网关自己的行为:relay 运行时及其区域协议(只在 relay 实例之间使用)、NSD 客户端、公网边缘、 Exit 与 Egress 的编排及其主机集成、计量、装配。
nsgw:crates/*/src 共 173,465 行(含内联测试模块),nsgw:crates/*/tests 共 37,981 行。生产代码行数按各内联
测试模块的起始位置估算。
| crate | src 行数 | 生产代码(估) | 职责 |
|---|---|---|---|
nsgw-server | 47,867 | 2.56 万 | 二进制;装配,另含鉴权运行时、用量上报、配置 |
nsgw-keynet-relay | 34,315(另有 tests/ 36,532) | 不超过 2.97 万 | 独立 relay 二进制(设计文档中称 nsgw-quick-relay) |
nsgw-wg | 20,045 | 1.06 万 | mac1 中继、内核与用户态 WireGuard、Exit、Egress 的 WG leg |
nsgw-wss | 19,968 | 0.98 万 | WSS 监听:入口 connector、客户端会话、不透明中继、Exit 承载 |
nsgw-control | 18,795 | 0.84 万 | NSD 客户端:登记、配置流、realm 快照、上报 |
nsgw-envoy | 14,117 | 0.68 万 | Envoy ADS 与快照 |
nsgw-api | 12,984 | 0.68 万 | admin、metrics、公网入口 listener |
nsgw-routing | 4,379 | 0.25 万 | 路由快照 |
nsgw-gateway-egress | 995 | 0.07 万 | Egress 的 ACL 与代理核心 |
影响计划的事实:
- 依赖。 nsgw 以路径依赖引用七个 v1 crate(
nsgw:Cargo.toml,workspace.dependencies:common、control、acl、tunnel-ws、telemetry、keynet-proto、keynet-quick),CI 经workspace.metadata.release的ns-shared-rev固定版本。它们在 nsshared 钉住的提交上都不存在(nsshared 成员)。其中control的面最宽:nsgw-control引用约 100 项,并向其他 crate 再导出 48 个消息类型 (nsgw:crates/nsgw-control/src/lib.rs)。 - WireGuard。
nsgw:crates/nsgw-wg/src/lib.rs(WgPlane)有两个实现:KernelWg(wireguard-uapi、rtnetlink,生产默认)与UserspaceWg(boringtun 0.7.1、tuncrate)。 - 加密库。
ring被锁定:nsgw:deny.toml禁用aws-lc-rs。nsplane 禁用ring,两个库都使用aws-lc-rs(约定对比)。ns-shared:crates/control/Cargo.toml自身固定了ring,所以继续依赖归档的 crate 也去不掉它。 - IPv4。 地址计划(
nsgw:crates/nsgw-server/src/config.rs,wg_tunnel_ip)、数据面(BringUpConfig.address)、 nft(Exit 的ip族表、Egress 的ipv4_addr集合)、ACL 主机匹配(nsgw:crates/nsgw-wss/src/acl_gate.rs)、 路由快照与 Envoy cluster 目标都以 IPv4 为结构。 - WSS 路径。
nsgw:crates/nsgw-wss/src/serve.rs(classify_request)提供四条:/relay是发布端拨入的带帧 connector,网关在它上面为公网入口开流;/client是节点侧的 terminate 会话,带mode=wss-relay时是不透明中继的 客户端一侧;/wss-relay是不透明中继的发布端一侧;/exit-wss/v1是 Exit 承载。 - 公网入口有两种传输。
SelectedTransport::Wg与SelectedTransport::Wss(nsgw:crates/nsgw-api/src/public_ingress.rs);后者需要一个在/relay上登记的在线 connector (nsgw:crates/nsgw-wss/src/registry.rs,open_public_ingress_stream)。NSD 从发布端服务的tunnel字段取策略 (nsd:crates/nsd-distribute/src/public_edge.rs,parse_transport_policy):ws或wss表示只走 connector, 两种偏好策略也都优先选择在线的 connector(nsgw:crates/nsgw-routing/src/public_ingress.rs,TransportPolicy::select_for_public_ingress)。 - relay。 没有其他 nsgw crate 依赖
nsgw-keynet-relay。它的套接字循环、预算、存储引擎、mesh、复制与 owner 栅栏是可复用的结构;所有处理函数都解码KNR1、16 字节 slot 与 KeyBinding 标记。谁可以注册写死在nsgw:crates/nsgw-keynet-relay/src/core.rs(RegisterCryptoWork::execute)里,没有准入抽象。区域成员协议不在 这个 crate 里:它的帧与状态机是ns-shared:crates/keynet-proto/src/region.rs与region_quick_owner.rs, 共 3,876 行。 - 两条控制通道。 配置经推送的配置流与轮询的 realm 快照两条通道到达网关,范围不同;见控制协议。
- 结构。
runtime/full.rs10,437 行,nsgw-envoy/src/lib.rs12,214 行,nsgw-wss/src/registry.rs8,946 行;nsgw-server内含鉴权运行时与用量上报;relay 有 92 个集成测试目标;起门禁作用的 nextest profile 会重试失败的 测试(nsgw:.config/nextest.toml,profileci)。 - 旧线仍在变化。 nsgw 的本地分支
feat/cascaded-relay(6438534e,未推送)比main多 313 个提交、约 31,600 行(新增nsgw-trunkcrate、级联中继);ns-shared 有对应的本地分支(9ef6755,多 57 个提交、约 11,200 行)。该分支也改动了级联之外的文件:config_stream.rs、用量上报与区域测试。它对用量落盘队列的改动只有 级联字段。对两个分支的审查结果见 G2 与 G17。 - 契约有三份拷贝。 ns 与 nsd 不依赖 ns-shared,各自维护一份线上类型,靠对拍用的 fixture 保持一致;nsgw 是 共享版的唯一使用者。
网关今天的其他行为(移植必须保留的)见需要保留的行为。
不移植的部分
Section titled “不移植的部分”新线只包含迁移之后仍然存在的东西。迁移计划的决定 D1、D4 已退役的路径不做”先移植再删除”,而是留在旧线,由 旧线服务到切换为止。
| 不移植 | 旧位置 | 取代者 |
|---|---|---|
mac1 中继、register_source、反射探测、探测集合 | nsgw:crates/nsgw-wg/src/relay.rs、relay/*(含测试 9,027 行) | relay v2(nsgw-relay),R3 接入 |
| 不透明 WSS 中继 | nsgw:crates/nsgw-wss/src/serve.rs 中的 /wss-relay 与 /client?mode=wss-relay | relay v2 的 WSS 载体 |
| 节点侧 terminate 会话 | nsgw-wss 中的 /client,包括经它到达的 Egress(nsgw:crates/nsgw-wss/src/registry.rs,handle_gateway_owned_client_open) | 无(D4)。R3 之前节点需要 UDP 可用才能使用 Egress;R3 之后 UDP 被阻断的节点由 relay v2 的 WSS 载体覆盖 |
| Exit 的 WSS 承载 | nsgw:crates/nsgw-wss/src/serve.rs 中的 /exit-wss/v1,连同 gateway-exit-wss 令牌角色 | R2 中没有:Exit 与 Egress 一样需要 UDP 可用。R3 起网关是一个节点,UDP 被阻断的节点经 relay v2 的 WSS 载体到达它(G14) |
| 公网入口的 WSS connector | nsgw:crates/nsgw-wss/src/serve.rs 与 registry.rs 中的 /relay,连同 relay 角色的令牌与公网入口的 Wss 传输 | 引擎上的 WireGuard。R2 中,策略要求 connector 的路由不暴露。R3 起,UDP 被阻断的发布端经 relay v2 的 WSS 载体到达(G20) |
| 内核 WireGuard 后端及其 Exit WSS overlay | nsgw:crates/nsgw-wg/src/kernel.rs、carrier.rs | nsplane 引擎 |
| BoringTun 后端 | nsgw:crates/nsgw-wg/src/userspace.rs | nsplane 引擎 |
| 只做中继的网关构建 | nsgw:crates/nsgw-server/src/runtime/slim.rs | R3 再定 |
| 级联中继 | feat/cascaded-relay 上的 nsgw-trunk 及相关代码 | 建立在上面两种中继之上,R3 之前不可能出现在新线;之后若仍需要,基于 relay v2 重新设计 |
退役一个处理入口,不等于退役它的词汇。在 M6 契约(L8)之前,新网关仍以 terminate 链路模式登记,因为 NSD 靠这个
能力投影 Exit 与 Egress;仍为发布端、Egress 与 Exit 的对端终结
WireGuard;仍会解码并跳过已退役的中继事件。R0 的阶段计划要带一张矩阵说明这些:处理入口已移除、字段解码后忽略、
词汇仍然必需。它还要带一张改名表:每个旧的内部名字对应的新名字,以及每个属于对外词汇的旧词可以留在哪些地方。
有了 G14 与 G20,新网关不再提供任何 WebSocket 流服务。Exit、Egress 与公网入口在节点一侧共用一条路:引擎上的
WireGuard,R3 起网关是一个节点。三者仍然不同的是主机一侧:nft 转发与 NAT、TPROXY 拦截、Envoy。网关不接受任何
WebSocket:relay v2 的载体 /ns/v2 由 nsgw-relayd 提供,R3 起网关作为客户端接入 relay(G21)。
去掉 connector 改的是公网服务的契约,不只是网关。今天 tunnel 设为 ws 或 wss 的服务按策略必须使用它,
prefer_wss 优先使用它。这些取值在 M6 契约里要么有新的含义,要么废弃;这由 nsd 与 ns 一起定(L8)。
十九个 crate,分六层。一个 crate 只依赖更低层的 crate,或一条列入清单的同层依赖。“代码”一列说明每个 crate 的 代码从哪里来:
- 移植:分三步,各自单独提交。第一步,从旧线一个记录在案的提交取来选定的代码,并通过它自己的测试;导入、 可见性与适配层的修改以及文件拆分属于这一步。第二步,改成新线的名字,不改行为,同样的测试仍然通过。第三步, 才是行为的改变。
- 重新推导:按目标契约新写。旧模块及其测试是一份清单:每个旧测试要么对应到一个新测试,要么记为放弃。
- 新增:没有前身。
| 层 | crate | 负责 | 代码 | 建在什么上 | 首次交付 |
|---|---|---|---|---|---|
| 基础 | nsgw-contract-v1 | 今天 NSD 的 v1 控制契约:事件、请求、上报与声明的类型,以及纯校验与规范化规则,附取自 NSD 的字节向量。只带序列化依赖,没有 I/O。临时的(G4):R3 时由 nsshared 的 M6 载荷 profile(L8)取代并移除 | 移植:v1 control 与 common 中被保留代码用到的类型与纯规则;不含它们的传输、认证客户端与主机辅助函数 | - | R0 |
nsgw-policy | 网关对 NSD 下发的主体策略做准入求值:主体与组、机器锚点和 WireGuard 公钥到主体的绑定、已验证的主体集合、策略自带的自测,以及指明命中主体的判定结果。纯逻辑,没有 I/O。长期服务公网入口;在 R3 给出按节点写的规则之前,也服务 Egress 与对端准入(G5) | 移植:v1 acl(ns-shared:crates/acl,2,981 行,其中生产代码约 1,800 行),去掉它对 telemetry 的依赖 | nsgw-contract-v1 | R2 | |
nsgw-metrics | 指标注册表与句柄;用量计量 trait 及其主体与事件类型,以及角色切换时的边界词汇 | 移植:nsgw-api 的 metrics,但其中 UsageMeter 的聚合与确认状态归 nsgw-usage | - | R0 | |
| 机制 | nsgw-relay | relay v2 核心:注册、presence、反射、DATA 转发、预算、标记库、UDP 与 /ns/v2 载体。准入(公共 relay 任意身份,账户 relay 按 NSD 下发的名单)、转发与复制的副作用、持久存储,都是它定义的 trait | 重新推导;清单:nsgw-keynet-relay 的 core、carrier、store、service | nsshared-proto | R1 |
nsgw-plane | 建在 nsplane 引擎上的 WireGuard 面:终结设备、TUN 本地侧、对端对账、路由 | 移植:nsgw-wg 的 reconciler、routes。新增:以引擎为后端的设备 | nsplane、nsplane-tun | R2 | |
| 视图 | nsgw-realm | 不可变、按 realm 限定的期望状态视图,原子发布:对端、策略、路由、Exit 与 Egress 配置,以及 NSD 令牌的密钥集与声明校验。其他 crate 读取配置的唯一接口(控制协议) | 重新推导;清单:nsgw-control 的 realm_config_snapshot,以及 auth 中的声明部分连同 jwks、jwt_crypto | nsgw-contract-v1 | R0 |
| 服务 | nsgw-routing | 公网入口的服务与后端路由,及其 ACL 门 | 移植;公网入口的 ACL 门来自 nsgw-wss 的 acl_gate 与 registry | nsgw-policy | R2 |
nsgw-envoy | Envoy ADS、证书、监听与路由配置 | 移植 | nsgw-routing、nsgw-edge-auth | R2 | |
nsgw-edge-auth | 公网边缘鉴权:ext_authz、OIDC、JWT authority、鉴权方法链的编解码 | 移植:nsgw-server/src/auth、nsgw-control 的 edge_auth | nsgw-policy,取今天 auth/token.rs 在用的已验证主体类型(VerifiedSubjectSet) | R2 | |
nsgw-exit | Exit:nft 转发与 NAT、配额、计数,挂在 nsgw-plane 报告的接口上 | 移植:nsgw-wg 的 gateway_exit | nsgw-plane | R2 | |
nsgw-egress | Egress:TPROXY 拦截、策略路由、TCP 与 UDP 代理、服务下限 | 移植:nsgw-wg 的 gateway_egress 与 nsgw-gateway-egress(后者今天自己构造 v1 策略引擎) | nsgw-plane、nsgw-policy | R2 | |
nsgw-usage | 实现 nsgw-metrics 的用量计量 trait:聚合、编号、落盘队列、确认状态 | 移植:nsgw-server 的 gateway_usage_* | nsgw-metrics | R2 | |
nsgw-node | 网关作为一个节点:身份与自身记录、派生的覆盖地址、对端记录状态、relay v2 的节点一侧(注册、presence、反射、DATA) | 新增;从 nsgw-server 接过 keys 与身份的保管 | nsgw-plane、nsshared-proto | R3 | |
nsgw-rendezvous | /rendezvous/v1:信箱、登记表、目录,以及它们的存储 | 重新推导;清单:quick/* | nsshared-proto | R1 | |
nsgw-relay-region | 成员身份、mesh、复制、快照、一跳转发、owner 栅栏;实现 nsgw-relay 的转发与复制副作用 | 移植:region/*,以及 ns-shared 中的区域成员协议(keynet-proto 的 region.rs、region_quick_owner.rs),然后改为 12 字节 slot | nsgw-relay;自有的 v2 帧 | R1 | |
| 集成 | nsgw-control | NSD 客户端运行时:登记、配置流、realm 快照轮询、心跳、上报。nsgw-realm 的唯一写入者 | 移植:原 crate,加上 v1 control 的传输与认证客户端 | nsgw-contract-v1、nsgw-realm | R0 |
nsgw-api | admin、健康检查、指标端点、公网入口 listener | 移植 | nsgw-routing、nsgw-realm | R2 | |
| 二进制 | nsgw-server | 账户网关:配置、本地服务下限、启动顺序、排空、监听分发;持久化的身份与隧道地址在 R3 交给 nsgw-node 之前也在这里 | 移植:boot、drain、config。重新推导:runtime/full.rs 的装配 | 账户侧 | R0 |
nsgw-relayd | relay 进程:公共 relay(开放准入),R3 起也是账户 relay(按名单准入,G21) | 移植:nsgw-keynet-relay 的 main、config、operations | relay 族 | R1 |
四个旧 crate 没有同名的后继:nsgw-wss 只留下公网入口的 ACL 门(归 nsgw-routing)与对端准入(归属由 G18 决定);
nsgw-wg 拆成 nsgw-plane、nsgw-exit 与 nsgw-egress;nsgw-gateway-egress
(995 行)并入 nsgw-egress;nsgw-keynet-relay 拆成 relay 族(nsgw-relay、nsgw-relay-region、
nsgw-rendezvous、nsgw-relayd)。
nsgw-server 的 ACME 运行时(wss_acme.rs,WssAcmeRuntime)在网关里没有监听可服务(G21),不移植进
nsgw-server。旧的 relay 二进制只接受静态证书路径;nsgw-relayd 要不要接过这个运行时,由 R1 的阶段计划决定。有两块在表里没有去向,由阶段计划确定:nsgw-api 的审计环与公网边缘审计类型(AuditRingBuffer、edge_audit),Envoy 与
边缘鉴权都在用;已退役的 nsgw-wg kernel.rs 里的能力与链路辅助函数(preflight_capability、link_index),
Exit、Egress 与路由都要调用。
依赖规则:
- 机制与服务都不依赖
nsgw-control或nsgw-api,只有二进制依赖它们。今天账户网关的其他每个 crate 都依赖nsgw-control,nsgw-wss还为了指标模块里的用量主体辅助函数而依赖nsgw-api(nsgw:crates/nsgw-api/src/metrics.rs,machine_usage_subject、usage_subject_from_acl_subject),并且 实现了定义在nsgw-api里的公网入口 trait(nsgw:crates/nsgw-api/src/public_ingress.rs,PublicIngressWssEgress、PublicIngressAcl)。用量词汇移到nsgw-metrics,ACL 的 trait 连同它的门移到nsgw-routing,connector 的 trait 不移植(G20)。 - 机制不知道 NSD,也不知道 realm:
nsgw-plane拿到的是一组对端,nsgw-relay拿到的是一个准入判定。两者都不 依赖nsgw-contract-v1。 - 消息类型来自
nsgw-contract-v1,配置来自nsgw-realm,后者只由nsgw-control写入。任何 crate 都不知道 一项配置来自哪条通道。 - 用量经
nsgw-metrics的计量 trait 上报。nsgw-usage实现它,由二进制注入;机制与同层的服务都不引用nsgw-usage。这个 trait 不只是字节计数器:Exit 或 Egress 的一次切换,要把持久化捕获、确认、暂停与放行 同对端准入门排好先后(nsgw:crates/nsgw-server/src/usage_role_coordinator.rs,UsageRoleCoordinator)。 这套先后顺序的词汇属于 trait,持久化的一半属于nsgw-usage,跨服务的编排属于nsgw-server的装配。 - relay 族只依赖
nsshared-proto与nsgw-metrics,不依赖账户侧:它的依赖闭包里没有nsgw-contract-v1。 账户侧也不依赖 relay 族的任何 crate(G21):nsgw-node以客户端身份说 relay v2,建立在nsshared-proto上。 还有一条依赖未定:R3 时nsgw-relayd要从 NSD 取得名单并向它上报用量,它为此链接哪个客户端、哪个上报器,由 R3 的设计写明。 nsgw-relay既不依赖nsgw-relay-region,也不依赖nsgw-rendezvous。区域实现核心定义的副作用,信箱由 二进制托管。今天核心直接引用区域模块,载体直接调用 quick broker (nsgw:crates/nsgw-keynet-relay/src/core.rs、carrier.rs);没有这些 trait,这两条依赖会原样回来。- 准入判定调用
nsgw-policy,它是网关唯一的策略求值器;其他 crate 都不自己求值规则。机制与 relay 族不依赖它。 - 每个 nsplane crate 有一份允许直接依赖它的 nsgw crate 名单:引擎与
nsplane-tun只有nsgw-plane。 R3 之前不使用nsplane-acl与nsplane-wss。 - 同层依赖逐条列出并写明理由。清单从移植时服务之间确实需要的开始:今天
nsgw-envoy使用nsgw-routing。 nsgw-relay与nsgw-rendezvous暴露的是处理”一个已升级的 WebSocket”与”一个 UDP 报文”的函数,不是监听器。 套接字归二进制所有并按路径分发,与合并的模拟服务相同 (模拟服务的组合),所以监听及其路径属于nsgw-relayd,不属于哪个库。- 两个二进制里只有配置、启动顺序与排空,R3 之后不再有别的东西。
对新线每个 crate 的规则:
- 分层由脚本检查,不靠评审(R0 第 5 步)。
- 移植来的代码使用新线的名字。旧名字分两类,分开处理。对外词汇随它的契约保留
(不移植的部分):线上字符串、配置键、指标名、admin API、落盘格式。每一类只在一处定义:
v1 线上字符串在
nsgw-contract-v1,配置键与指标名各在一个模块,其他源文件都不直接写出它们。已退役设计的 内部标识符在移植的第二步改名:crate、模块、类型与函数的名字,注释与日志文本,例如keynet、KNR1、mac1、WgPlane与KernelWg。重新推导的 crate 从一开始就用新名字;它的风险是照着清单把旧概念带进来, 由同一项检查兜住(R0 第 5 步)。 - 复用了代码的 crate 带一份
PROVENANCE.md:来源提交与路径,移植通过自身测试时的提交,以及旧路径和旧名字 到新路径和新名字的对应关系。重新推导的 crate 记录它用作清单的旧模块与提交。 - 对移植来的路径跟踪旧线上的修复:一项台账检查对这些路径运行
git log,范围从记录的提交到旧线的最新提交; 其中任何一个提交既没有记为已应用、也没有标为不适用,检查就失败。只作清单用的模块里的修复要读一遍,适用的 就变成一个测试。改名之后,旧线上的修复不能再当作补丁直接应用,要按记录的对应关系手工改写。 - 每个 crate 一个集成测试目标(
tests/main.rs,按领域分模块)。 - 子系统的开关是”是否链接这个 crate”,不是共享模块里的
cfg(nsgw:crates/nsgw-server/src/auth/mod.rs约有 100 处)。 - 监听方报告自己实际绑定的地址,测试绑定端口 0 再读回。
- 每个阶段派工之前,阶段计划要带一张模块地图:范围内的每个旧模块,连同行数、目标 crate 与处理方式(移植、
重新推导、不移植)。文件行数门禁从 R0 起启用,没有豁免条目:一个源文件最多 1,500 行生产代码
(所有者,2026-10-06),数到第一个内联测试模块为止。测试代码用同样的上限(所有者,2026-10-09):
一个测试文件最多 1,500 行;一个内联测试模块最多 1,500 行,同所在文件的生产部分分开计数。旧网关里最大的
测试是源文件末尾 7,000 多行的内联测试,只有第一条规则是看不到它们的。对测试代码的检查随 R2 加入;在它
进入
next之前,两个阶段都在评审中执行这条规则。
需要 nsshared 与 nsplane 提供的
Section titled “需要 nsshared 与 nsplane 提供的”编号是稳定的标识,不重复使用。L3(Exit 的地址转换)与 L7(Egress 的拦截)已被决定 G7 撤销:它们是网关自己的
工作,见 Exit 与 Egress。L6(nsplane-wss 的接受端)已被决定 G14 与 G20 于 2026-10-06 撤销:
网关不接受任何 WebSocket 流服务。L2(在 nsplane-acl 中求值 NSD 的主体策略)已被决定 G5 于 2026-10-06 撤销:
nsplane 保持不带业务语义,网关自己在 nsgw-policy 里做准入求值。L4(网关规模的实测)作为工作保留,但不再是
对库的需求(所有者,2026-10-06):由网关在 R2 自己测。L1(v1 网关控制契约)已被决定 G4 于 2026-10-06 撤销:它是网关的一个
临时 crate nsgw-contract-v1,不再是对 nsshared 的需求;它的内容列在表后。
nsshared
Section titled “nsshared”| # | 内容 | 钉住提交上的状态 | 用于 |
|---|---|---|---|
| L8 | M6 的载荷 profile:全部改用节点 ID 与 node6,relay 注册名单,与已退役的 v1 链路模式相区别的 relay 资格,节点侧各族契约 | 没有已实现的契约 | R3 |
| L11 | 控制资源与会话协议(控制协议):带范围的资源模型、规范化登记表与向量、清单、租约与撤销下限、确认结果、纯逻辑的准入状态机。它的版本与所承载的载荷 profile 分开 | 已有一份较早的设计(ns-archive:docs/plan/PLAN-119.md);它的共享线上格式在任何本地检出中都找不回来 | 网关的协议选定 |
| L9 | M8 中有第二方的契约:短名申请(节点到 relay)与 relay 向权威 DNS 服务器供数的接口 | 没有已实现的契约。TXT 编码已有(公网 DNS 发现) | R4 |
| L10 | 记录标记与登记表发布状态的持久化导出与恢复、确定性合并,附向量;以及下面的发布规则 | nsshared:crates/nsshared-proto/src/record.rs(RecordMarks)与 rendezvous/registry.rs(RegistryEntry)的状态原先是私有的,不提供恢复,锁存密钥冲突时也不保留冲突的那条记录。已在 nsshared v0.6.0(fc1c884,2026-10-07)交付:RecordMarks 保留冲突证据,并有 merge、encode、decode;RegistryEntry 按记录修订跟踪发布级别、锁存发布冲突,并有 merge、encode、decode;附带向量。不在其中的:Mailbox 的导出与恢复,以及记录过了删除宽限之后仍保留的任何状态 | R1 的生产完成条件;新线在 R1 钉住 v0.6.0 |
nsgw-contract-v1(原 L1)的内容,按 nsgw 今天引用和自行定义的归纳:
- 签名信封及其校验规则;登记、认证、心跳的载荷;规范的 API 名
gateway.realm_config_snapshot; - 网关在本地定义的信任引导:
/api/v1/info文档、control_trust.v1、按顺序排列的控制端点、信封 pin 的交集、 Noise 公钥与 base64url 的 SHA-256 SPKI pin。照搬 v1control会把更早的引导方式带回来; - 网关收到的配置流事件与 realm 快照,包括公网边缘的依赖类型,它们的代际决定一条路由能否暴露
(
ns-shared:crates/control/src/messages.rs中的PublicEdgeAuthDependency、PublicEdgeCertificateDependency、GatewayAuthJwtAuthority); - 网关上报、服务上报与用量上报,以及本地服务下限背后的声明类型;
- 能力与 scope 词表,包括仍然必需的 v1 词汇(
terminate); - NSD 签发、网关校验的 WSS 与 Exit 令牌的 claim 集合;
- 序列化的主体策略;
- 签名 URL 的规范请求,它由 NSD 签发(
nsd:crates/nsd-api/src/signed_url.rs,mint_query)、由网关校验 (nsgw:crates/nsgw-server/src/auth/mod.rs,verify_signed_url); - 外层传输参数(Noise 模式、QUIC pin 规则、分帧上限)。
不属于它的:已退役的中继与配对传输事件,以及中继探测集合——使用方把它们当作未知事件跳过,如同今天的
ControlMessage::Unknown,因为 NSD 仍会发送;还有只属于部署的设置,例如来源 IP 的信任配置。
L10 中的发布规则补的是规范本身的缺口。relay v2 规范说登记条目的 DNS 选择跟随最近一次被接受的发布;在以不同
顺序接受发布的区域成员之间,这句话没有意义,而且在 v0.5.0(c6f6c24)上,一条更旧的记录也能改变已保存的档位
(即使记录判定为未变化,RegistryEntry::apply_publish 仍会赋值)。先写进规范、再进向量与代码的规则是:改变
档位必须使用一条新签名的、issued_at 更大的记录;较低的修订永不改变档位;同一修订上档位不同的两次发布锁存
一个发布冲突,在更大的修订到来之前抑制 DNS 输出。节点的代价是每次改档位多签一条记录。
R1 可以在已有的基础上开始:记录、 relay v2、会合、 登记表与目录规则。它的生产完成条件不再等待 L10,L10 已交付。区域成员协议不在 nsshared 中, 留在 nsgw(nsshared 的未决问题)。
nsplane
Section titled “nsplane”| # | 内容 | 钉住提交上的状态 | 用于 |
|---|---|---|---|
| L4 | 网关规模下的表现,由网关自己在 R2 实测,不向 nsplane 提需求(所有者,2026-10-06)。事先不设阈值(所有者,2026-10-08):没有可以据以推导的生产实测数据。用同样的拓扑跑一遍内核 WireGuard 作为参照,记录下来的结果由所有者判断。共两次:第一个检查点之前测引擎加 TUN 本地侧、挂很多对端;R2 完成时测每种服务的完整路径。届时 nsplane 的星型拓扑测量工具若已具备就使用(nsplane:docs/task/20261006-1300-nsgw-requests.md,NG-6);否则网关用 nsplane 的示例与 bench 脚本自己搭一个简版 | 网关规模下没有数据。钉住的提交上公布的引擎对引擎数字是:单条与四条 TCP 流 10.3 与 11.4 Gbit/s,开两个加密线程时 11.7 与 12.7(nsplane:CHANGELOG.md,0.10.0)。单个对端的报文现在分摊到各个加密线程,此前记录的“单个对端约 5 Gbit/s”的上限已消除,Translator 也已加快。这些都不是网关的测量:很多对端接在一个引擎之后,本地侧是 TUN,再加内核的 nft 与 TPROXY | R2,第一个检查点之前与完成时 |
| L5 | Linux 内核 WireGuard 后端,仅限设备控制 | 不存在 | 仅当 L4 不达标 |
直接使用、无需新增的:引擎、UdpTransport、nsplane-tun、对端与传输计数、路径 MTU 上限
(嵌入接缝)。
不为 Exit 要求裸报文承载(G14)。专用承载 /exit-wss/v1 用令牌授权一把 WireGuard 公钥,旧的用户态承载正是用
这个对端解密(nsgw:crates/nsgw-wg/src/userspace.rs,decapsulate_carrier_datagram)。通用传输保不住这层绑定:
引擎从报文识别对端,所以持有对端 B 凭据的人可以经授权给对端 A 的承载发送报文。新线不需要这层绑定:R3 起,
经 relay v2 到达的报文先按节点 ID 检查,再由 WireGuard 认证,没有指名某把公钥的令牌。
L5 是退路,不是目标的一部分。内核设备只统一控制接口:它的报文不经过过滤链、可插拔传输、UDP 侧通道与路径 策略。如果它被实现,它要声明不支持这些能力,只服务以裸 UDP 到达网关并需要大流量转发的对端,并且只存在于 nsplane。R2 保留内核的 nft 与 TPROXY,与 L5 无关。
nsplane 在 2026-10-08 发布了 0.11.0,并把 0.11 这条线冻结:只收 bug 修复,需要新接口或优化的请求要等冻结解除
(nsplane:docs/decisions/2026-10-08-feature-freeze-0-11.md)。这个版本把 nsplane-acl 重写成按标签的类型化规则加
一个通用的流网关,去掉了原来带的节点模型。R2 用到的引擎、核心与 nsplane-tun,同本计划钉住的提交相比只差几行,
都不涉及这里引用的条目。新线在 R2 钉住 v0.11.0。由此有两点:R2 的实测如果不理想,就记录并报给所有者,冻结期间
不靠库里的优化来解决;R3 要向 nsplane 提的东西需要先解除冻结。
Exit 与 Egress
Section titled “Exit 与 Egress”这两条路径在 R2 中保留内核实现。引擎取代 WireGuard 设备,把解密后的报文交给一个 TUN 接口;Exit 的 nft 规则
与 Egress 的 TPROXY 规则挂在这个接口上,如同今天挂在 wg0 上(nsgw:crates/nsgw-wg/src/gateway_exit.rs,
GatewayExitConfig,已按接口参数化;gateway_egress.rs,GatewayEgressWgManager)。代码移入 nsgw-exit 与
nsgw-egress,它与已退役的 KernelWg 模块共用的能力与链路辅助函数要抽取出来。
移植必须保留的行为,以及旧行为比其名字所暗示的要弱的地方:
| 行为 | 现状 |
|---|---|
| Exit 的来源准入、保留地址段排除、固定或 masquerade 源地址转换 | nft ip 表,以 fail-closed 方式替换 |
| Exit 的流数上限 | 按源地址计,每个配对一个配额集合(NftScript::write_pairing_quota_rule);配置项的名字暗示有按 profile 汇总的上限,实际并不存在 |
| Exit 的用量 | 每个配对一个具名 nft 计数器;读取失败后,基线在下一次采集时重新建立,其间的字节被丢弃而不是重复计费(GatewayExitManager::reconcile_desired) |
| Exit 的撤回 | 表被替换;conntrack 条目不清除 |
| Egress 的目的地址 | 网关自己的地址加服务端口:NSD 把 service_vip 投影为网关的 VIP(nsd:crates/nsd-distribute/src/notifier.rs,build_gateway_egress_wg_payload) |
| Egress 的准入 | 流与 realm、主体、terminate 锚点、服务及快照代际相关联(authorize_gateway_egress_wg_flow);UDP 流按代际栅栏 |
| 角色变更 | 先持久化一个用量边界并栅栏流量分类,再改变数据面(nsgw:crates/nsgw-server/src/usage_role_coordinator.rs) |
| 隧道地址 | 先取登记时的分配,再取持久化的分配,再取配置的回退值,且须落在配置的前缀内;与之冲突的 WgConfig 被拒绝(nsgw:crates/nsgw-server/src/runtime/shared.rs,resolve_effective_wg_tunnel_ip;runtime/full.rs,guard_wg_config_tunnel_ip)。绑错地址会使 Exit 与 Egress 的来源准入失效 |
| 本地下限 | Egress 目标与 Exit profile 受 gateway_services.toml 约束;修改需要重启,重启时重新上报整个下限 |
归档的控制面对 Egress 的投影方式不同:每个 realm 与服务引用一个稳定的服务地址,另带一个选定属主的提示
(nsd-archive:crates/nsd-policy/src/snapshot/gateway_egress_wg.rs),服务上报里接受的协议也更少。两者不是同一份
契约。R0 与 R2 明确以 nsd: 为生产方;归档线的节点与控制面不是验收对象。
今天的实现中,移植时要么修复、要么指名接受的局限,列在待表态的已知局限。
因为设备换成了引擎,R2 中新增的集成工作:
- 经
EngineHandle读取计数、移除对端或传输,与 nft 和用量落盘队列不构成一个事务。更新顺序、排队任务的 截止点、删除之前要采集什么,在阶段计划里规定,并用不利时序与重启的测试验证。 - 公网入口从主机套接字、以网关的覆盖地址拨向发布端(
nsgw:crates/nsgw-api/src/public_ingress.rs,connect_wg_sender)。如果 Egress 以后在同一地址上的用户态协议栈终结,这些主机连接的回包不能被引入该 协议栈;两者共处一个装配之前,先设计好地址与端口的归属。 - 一个具备 nft、TUN、转发与 TPROXY 的特权测试夹具是必须通过的门。今天的命名空间测试在前置条件缺失时返回成功
(
nsgw:crates/nsgw-wg/src/gateway_exit.rs,netns_available),运行时镜像也没有安装 nftables (nsgw:Dockerfile)。
对 R3(隧道内只有 IPv6):
- nft 表要为 IPv6 重写。起点假设是在内核里做 NAT66;它尚未实现,也未测试。
- 面向公网 IPv4 的 NAT64 没有现成的路径。内核 nft 不提供。nsplane 有无状态的
Translator、只是受限 IPv6 NAPT 的Masquerade,以及Nat64Lan——一个面向有界 LAN 的辅助组件,它的路由接受/0前缀,但不翻译分片、 选项与大多数 ICMP 错误(masquerade)。完整的路径,包括主机上 的回包引导与端口归属,在 R3 的设计中选定;承诺 IPv4 目的地的 profile 不能悄悄失去它。 - Egress 可以继续用 TPROXY,把路由与套接字换成 IPv6 的等价物;也可以在
nsplane-netstack上终结,它在自己的 IPv4 与 IPv6 地址上接受 TCP 与 UDP(netstack API); 两者都不需要Redirect。是否更换后端由对等性证据决定,不与地址切换绑定。
配置经两条通道到达网关。
| 配置流 | realm 快照 | |
|---|---|---|
| 投递 | 推送(SSE、Noise 或 QUIC) | 轮询,默认 20 s(nsgw:crates/nsgw-control/src/realm_config_snapshot.rs) |
| 范围 | 网关的 home realm;事件不带 realm 字段 | 每个 served realm 一个桶(RealmConfigBucket) |
| 实际生效的内容 | home realm 的 RoutingConfig 与 AclConfig;网关全局的公网边缘事件(HTTP 配置、L4 映射、鉴权配置、证书与客户端 CA);GatewayWssRelay;令牌刷新 | 每个 served realm(含 home realm)的 WireGuard 对端、网关、ACL、路由、DNS、Egress、Exit 与 WG 中继配置 |
| 收到但不生效 | 完整运行时中的 WgConfig(nsgw:crates/nsgw-server/src/runtime/full.rs,run,构造 ConfigStreamTask::new 时没有给 WG 消费者);GatewayWgRelay、GatewayExitNftConfig、GatewayEgressWgConfig 只记日志 | 其中可能带有的公网边缘、鉴权与证书材料事件(RealmConfigBucket::apply 丢弃它们) |
配置流是最初的单 realm 设计。拉取接口是在一台网关开始服务多个 realm、却仍只在一个 home realm 登记时加上的
(nsgw:docs/plan/PLAN-058.md),因为配置流面向 home realm,而且无法原子地替换或删除一个完整的多 realm 聚合。
网关已经做对、R2 必须带过来的:公网入口的 ACL
取自路由所属 realm 的桶;路由对照它自己 realm 的路由状态解析;网关全局的公网边缘投影被保留,并在 served 集合
变化时重新求值(nsgw:docs/plan/PLAN-116.md;nsgw:crates/nsgw-wss/src/registry.rs,
evaluate_public_ingress_acl)。
这种安排仍然做不到的:
| 缺陷 | 由两条通道造成 | 无论几条通道都存在 |
|---|---|---|
| 一条公网路由的各项输入(成员关系、路由、ACL、对端、鉴权、证书材料)分别经不同通道到达,没有共同的修订号 | 是 | |
| 配置流断开时轮询也停止:旧网关把轮询绑在配置流的中继许可上 | 是 | |
| 20 s 是重试间隔,不是撤销时限;提示可能丢失,各消费方的新鲜度规则也不同 | 是 | |
| 在第一份快照到达之前,网关运行在一个没有 realm 限定的兼容 ACL 上 | 是 | |
当所有 realm 的中继配对投影都为空、且其中一个 realm 最近有未完成的重绑定时,NSD 对整个多 realm 快照返回 503(nsd:crates/nsd-api/src/envelope.rs,should_defer_gateway_relay_snapshot) | 是 | |
失败或过大的事件被丢弃,而外层响应仍然成功,接收方无法区分残缺的集合与有意为空的集合(nsd:crates/nsd-distribute/src/notifier.rs,build_current_state_event_set) | 是 | |
| 签名保证真实性,不保证顺序:一份较旧的全量替换可以带着新的 nonce 排在较新的之后 | 是 | |
NSD 在构建投影时会写持久状态(prepare_node_l3_projection、refresh_node_l3_rollout) | 是 | |
| 多个 NSD 实例之间,Redis 发布失败只记日志 | 是 | |
| 快照可能带有鉴权密钥与证书私钥,而单次调用的路径不执行配置流那套保密性要求 | 是 |
推与拉并存不是问题所在。问题在于两者的范围、权威、新鲜度与依赖规则互不一致。
ns-archive:docs/plan/PLAN-119.md(“权威双向 Config Stream V2”)在 2026 年 7 月为这个问题获批,没有做完。它的
消费方在 nsgw 的分支 rebase/g5-exit-onto-main 上(e6b5af20,未合入),服务端与物化器在归档控制面的 tag
nsd-dev-pre-reset-20260726 上,而归档控制面与 nsd: 没有共同的提交历史。它声称已冻结的共享线上格式在任何本地
检出中都不存在,调用点也不足以确定其编码。这些源码只作为设计参考与测试场景;不因它们的存在而认为任何关卡已经
通过。该计划约三分之一讲的是 mac1 中继的权威切换,随那种中继一起失效。
方向已定(所有者,2026-10-05):控制协议作为重构的一部分重新设计。下面的设计是设计讨论得出的建议,等待所有者 对 G11 至 G13 的答复。
账户配置使用一个全双工、经认证、带会话 fence 的资源协议,源自 PLAN-119,采用完整依赖组的整体替换,基线在同一条 连接上传送。NSD 发布不可变的完整状态;消费方确认的是实际达成的效果,并且任何失败都不得恢复已被撤销的权限。
R0 至 R2,对接今天的 NSD,不改 NSD:
- 把现有的线上格式留在两个旧式适配器之后,每种配置、每个范围只有一个明确的权威,统一喂给
nsgw-realm这个 按 realm 限定的视图,它只由nsgw-control写入。 - realm 快照的轮询不依赖配置流的中继许可,并拒绝经没有认证保密性的传输获取它。
- 账户侧角色在所需输入齐备之前保持未就绪。来源、realm 范围、通道新鲜度与证书材料有效性分别跟踪;绝不用到达 顺序或本地哈希伪造修订号。
- 保留只经配置流到达的公网边缘输入,以及今天的声明、证书与用量上报;在网关内部暴露效果完成、撤回与计费的 屏障,供以后的适配器使用。
- 把 R2 的控制面保证如实描述为旧式兼容。
契约修订(L11),每项标明相对 PLAN-119 的关系:
| # | 决定 | 相对 PLAN-119 |
|---|---|---|
| 1 | 一个全双工端点与选定的会话:同一套签名帧运行在带保密性的 HTTPS(WebSocket)、Noise 与 QUIC 上;确认、修复与保活都走这条连接。登记、上报与用量仍是单次调用 | 沿用 |
| 2 | nsshared 中共用的带范围模型与纯校验逻辑,键为权威、接收者、realm 或网关全局的范围、种类与稳定 ID;I/O 与效果适配器在 ns 与 nsgw;每个载荷 profile 一份经评审的族登记表 | 修改 |
| 3 | 完整的依赖组置于明确的清单之下,带版本、规范摘要、成员数 / 字节数 / 分块数、精确的依赖引用、清空与移除;基线、更新与修复用同一种有界的事务格式 | 修改:不做逐对象增量 |
| 4 | NSD 先提交源头的变更,再对照一致的源快照编译,发布一个持久的完整头。读取、编码或签名失败永远不算空的或部分的成功,投递永远不推进业务状态 | 沿用并加强 |
| 5 | 每个权威与接收者一个持久的”选定属主”代际;发布、租约续期与确认的接受,在每个 NSD 实例上都要求当前的 fence | 修改 |
| 6 | 授权化身与撤销下限,在合并以及”撤销后以相同内容重新授予”之后仍然保留;带签名的权限租约,其时限独立于传输是否存活 | 新增 |
| 7 | 一个组只有在它声明的运行时、主机、Envoy 与计费效果都成功之后才提交。失败时消费方上报目标、上一次完整提交与已生效的撤回下限;互不相关的组继续推进 | 修改:没有统一回滚 |
| 8 | 公网监听与主机名的网关全局归属,带 realm 限定的鉴权与证书材料引用,以及 realm 之间显式的转移。M6 时:节点 ID 与 node6 引用,以及完整的 relay v2 准入名单 | 修改并新增 |
| 9 | 不可变的密钥版本,按引用计入哈希,只经已验证的保密载体投递,在任何当前或在途状态引用它们期间保留 | 沿用 |
| 10 | 每个接收者明确选定一个协议与 profile;重启后在新的基线到达之前拒绝流量;不悄悄退回 V1;回滚是一个新的经授权的代际 | 修改:没有进程内交接 |
第一版故意不做的,以及何时不再安全:
| 不做 | 何时不再安全 |
|---|---|
| 逐对象增量与消费方重放日志 | 全量替换超出实测的规模或重连预算,或者产品要求每个中间修订都被执行 |
| mac1 中继的权威状态机、参与方确认集合与断连宽限 | 要求网关之间零间隙切换;那需要它自己的协议 |
| 跨机器的通用 prepare 与 commit | 出现新的跨机器原子性要求。特定的屏障(例如 Node L3 的上线证据)保留 |
| 单独的拉取端点或单次调用的确认端点 | 某个必需的代理或载体无法提供全双工;那时要重新决定端点设计,而不是绕过去 |
| 压缩、Merkle 索引 | 容量实测表明需要 |
| 可用于启动的本地投影与密钥缓存 | 要求离线重启后仍能服务(G12) |
任何一项”不做”都不允许出现:残缺的清单、未签名的权限、经非保密载体传送的密钥、缺失的流撤销,或被跳过的效果 确认。
协议何时变更
Section titled “协议何时变更”比较了三种排法。
| 排法 | 代价与风险 | NSD 同时服务两种协议的时间 |
|---|---|---|
| 随 M6 | 重复的契约工作最少。合并风险最高:发布、投递、身份、中继与 IPv6 的效果可能一起失败 | 分批上线期间与回滚窗口内 |
| M6 之前,只对新网关,用今天的载荷 | 多出 10 至 20 人日,用于临时的载荷登记表与单独的浸泡验证。在不同时改寻址的情况下暴露发布与效果确认方面的故障 | 从第一次网关上线到旧客户端的支持截止 |
| M6 之后 | 多出 15 至 30 人日;把已知缺陷带进新的身份体系,并且四个仓库都要再迁移一次 | 从之后的那次上线开始 |
建议:M6 之前,只对新网关(决定 G11)。这需要 NSD 的工作早于迁移决定 D6 所允许的时间,作为 NSD 工作包中第一个 获准的切片。没有这项修订,协议就随 M6 变更,并在该次发布内部先通过控制面的各道门。更换投递方式并不能让新网关 在混池中变得安全;那仍然需要 NSD 在中继资格上的改动。
协议工作的粗略规模,置信度低:nsshared 4,000 至 7,000 行改动,nsd 6,000 至 11,000,nsgw 3,000 至 6,000, ns 3,000 至 6,000;连同载体验证、故障切换演练与评审共 66 至 110 人日。最大的单项风险在 NSD:让那些边构建边写 状态的投影路径产出一份完整、可重复、不可变的发布。
为任何网关选定该协议之前的验收
Section titled “为任何网关选定该协议之前的验收”- 契约:生产方与消费方各自独立的向量;显式的空、清空与 realm 移除;畸形、缺失、重复、乱序与过大的帧;与接收者、 会话、fence、事务和哈希的绑定;重放拒绝;密钥不外泄。
- 载体:经实际部署的各类代理的真实 HTTPS(WebSocket)、Noise 与 QUIC,包括取消、令牌过期、黑洞、重连与背压。 trait 实现或内存中的测试不算数。
- NSD 的权威性,在两种数据库后端上:编译过程中发生变更;读取、签名或编码失败;物化器崩溃;同时出现多个属主 与接管;来自已被 fence 的属主的迟到确认;数据库恢复。
- 应用失败,特别是两个序列:一条公网路由从一个 realm 移到另一个 realm,而新 realm 的证书安装失败;一个 Exit 选择在一个租约内被撤销又以相同内容重新授予。每一步都要说明:网关在服务什么、确认了什么、NSD 记录了什么。
- 重启与过期:在撤回过程中和激活过程中被杀掉的网关,启动时尽管内核规则、Envoy 状态与证书材料文件仍在,也要 拒绝流量,然后取基线。
- 容量:为 served realm 数、组与名单的规模、变更频率与重连并发声明上限,并对照基线成本与撤销延迟实测。
如果运行时无法停止一项旧的授权,仅仅上报失败是不够的:网关要么展示一条确实生效的 fail-closed 路径,要么不被 选定。
一份计划,五个里程碑。R1 与 R2 是相互独立的两条线,各有自己的阶段计划,在 R3 汇合。R1 先开工;R2 在它的阶段计划 批准之后开工。
每份阶段计划都要遵守 R0 带来的两条规则(所有者,2026-10-08)。第一,一个阶段的第一个实现任务,是在验收环境里用真实 组件打通一条最薄的通路,后面的任务在它上面扩展:R0 把容器验收留到了最后,那一个任务靠一次次现场运行才暴露出前面 每个任务本可以用上的事实。第二,每条验收观察都写明从哪里读:哪段输出、哪条记录、哪个端点、哪个计数器或哪个测试能 证明它。
| 阶段 | 交付 | 迁移里程碑 | 前置 |
|---|---|---|---|
| R0 基础 | 新线与 v2 依赖集;然后是 NSD 客户端,以及一个能登记、上报但不承载流量的网关 | M0(nsgw 部分) | 无 |
| R1 relay v2 | nsgw-relay、nsgw-rendezvous、nsgw-relay-region、独立 relay 二进制 nsgw-relayd | M2,之后随 M5 上线 | R0 的基础部分 |
| R2 网关换引擎 | 公网入口、Exit 与 Egress 运行在 nsplane 引擎上,仍用 v1 契约与地址;nsgw-server 只做装配 | 新增;对应 ns 的 M4 | R0 |
| 协议选定 | 为新网关选定控制资源协议(L11),承载今天的载荷。它是 NSD 工作包中的一道门,不是第六个里程碑;若 G11 被否决,并入 R3 | M6 中 NSD 工作的一部分,提前 | R2、L11、NSD 的发布边界、G11 |
| R3 账户切换 | 网关作为节点接入 relay v2,账户 relay 是带名单准入的 nsgw-relayd(G21),node6 寻址,IPv6 的 Exit 与 Egress,M6 的载荷 profile;通过此门之后旧线归档 | M6 | R1、R2、NSD 配套工作、L8 |
| R4 名字与收尾 | 向权威 DNS 供数;本页由 nsgw 的现状文档取代 | M8、M9 | R3、L9 |
- 建立新线(G1)与工作区基线:resolver 3、edition 2024、toolchain 1.96.0,lint 沿用现状;运行时状态路径与 部署身份同旧线分开。十九个 crate 从这里起就全部存在,内容为空但已归层,这样分层检查从第一个提交起就覆盖 整个目标,依赖方也始终能编译。
- 加密库切换为
aws-lc-rs(rustls、tokio-rustls、quinn、rustls-acme、rustls-webpki、本地的jsonwebtokenprovider),反转deny.toml的禁用项,并记录取代原锁定的决定。 - 替换路径依赖:
control与common的契约部分换成网关自己的nsgw-contract-v1(G4),客户端运行时留在nsgw-control;tunnel-ws不再需要(G14、G20);telemetry换成nsshared-telemetry;keynet-*不再需要,只有keynet-proto里的 区域成员协议例外,R1 把它移植进nsgw-relay-region。acl在 R2 移植第一个 ACL 门时换成网关自己的nsgw-policy(G5)。使用不可变的 git 解析结果与提交入库的 lockfile(G6)。 - 附有取自 NSD 的字节向量的
nsgw-contract-v1、nsgw-metrics、nsgw-realm、作为视图写入者的nsgw-control, 以及第一版nsgw-server:经 HTTPS 发现文档建立信任,以terminate链路模式 且不带任何中继模式登记,打开配置流,轮询 realm 快照,心跳并上报。持久化的身份与隧道地址的格式,连同今天的 优先级规则,在这里定下来。旧网关在登记、认证、上报与心跳收到 404 之后退回的那套更老的 JSON 接口不移植 (所有者,2026-10-06):新线只对接钉住版本的 NSD。配置流上出现超限事件时断开并重连,而不是跳过(G17)。 - 门禁:起门禁作用的 nextest profile 不重试;文件行数门禁;分层检查;退役名字检查;修复台账。分层检查是一个读取
cargo metadata --no-deps的脚本,加两个提交入库的文件:deps/layers.toml(crate 到层)与deps/allow.toml(列出的同层依赖与 nsplane 的允许名单)。出现向上的依赖、未列出的同层依赖、环、没有归层的 crate,或名单之外对 nsplane 的依赖,检查就失败;relay 族的闭包规则需要解析后的依赖,是另一项检查。退役名字 检查在源码中搜索一份提交入库的已退役设计标识符清单,并带一份允许名单,对每一处属于对外词汇的旧词写明 文件、词与理由。每项 检查在起门禁作用之前,都先用一个故意写错的输入证明它会失败。修复台账(目标工作区)需要 旧线的检出,在构建之外运行。 - 构建配置:dev 调试信息
line-tables-only,依赖opt-level = 1,清单中不写incremental,保留发布分层 (release、dist、fast)。 - CI 不再旁路检出 ns-shared,也不再使用
ns-shared-rev。 - 配置面:对于保留下来的功能,新线读取与今天相同的文件和相同的
NSGW_环境变量键;对于已退役路径的键,按名 拒绝而不是忽略。所有者此前的决定曾搁置两处清理:没有用处的 Envoy 监听设置public_ingress_envoy_http_listener_port(配置加载之外没有任何代码读它)按名拒绝(所有者,2026-10-06); 重复的wss_listen_port键与旧 WSS 监听的其他键一起拒绝。这一组共十四个:监听地址与端口、对外通告的主机与 端口、令牌受众、静态证书与私钥的路径、四个 ACME 设置、三个会话与流的上限。它们从 R0 起全部按名拒绝 (所有者,2026-10-06),因为网关没有这个监听,R3 时也不会有:relay 的监听及其配置键只存在于nsgw-relayd(G21)。
没有任何一步等待其他仓库。完成条件:质量门通过;二进制向一套隔离的 NSD 登记并收到投影,环境是在本地用容器 起的(钉住版本的 NSD、它自己的数据库与池;所有者,2026-10-06);兼容性 fixture 覆盖”未知事件”与”未知 API”的区别、缺失的 body 与缺省的字段。没有数据面的进程永远不发布到共享的池里。
R1 relay v2
Section titled “R1 relay v2”nsgw-relay:以 12 字节节点 ID 为键的无 I/O 核心。REGISTER、PRESENCE、REFLECT、DATA、STATUS 与 REVOKE 使用nsshared-proto的编解码与校验;记录顺序使用RecordMarks;在 REGISTERED 中宣告能力。UDP 与/ns/v2WSS 载体。标记库以身份为键,采用新的落盘格式,空库启动。准入、转发与复制的副作用、持久存储 都是这个 crate 的 trait,所以它不需要区域就能构建和测试。nsgw-rendezvous:信箱、publish、lookup_node、目录与各项上限。登记表的预算是每个 relay 10^7 条、每条 最多 600 字节,仅记录载荷就约 6 GB,还不含索引、日志与快照暂存;v1 标记库不是为此设计的。存储先出设计 说明再编码:确认一次注册之前提交什么,冲突与高水位状态如何恢复,过期与重放下限的关系,压缩与恢复时间。nsgw-relay-region:沿用现有的 mesh、复制、快照与 owner 栅栏,作为上述副作用的实现,slot 为 12 字节,标记以身份为键,登记记录与发布 状态纳入复制(L10),会合 owner 取代 quick owner。nsgw:crates/nsgw-keynet-relay/src/region/forwarding.rs中进程级全局的REGIONAL_WORK改为实例状态,使一个进程里可以运行多个实例。成员协议的帧与状态机随它一起从keynet-proto移植过来,只在这个 crate 内部使用。nsgw-relayd二进制:配置、运维端点、排空。准入方式由配置选择(G21)。R1 交付开放准入,并在测试里用一份 固定名单走通准入 trait 的名单路径;来自 NSD 的名单在 R3 到位。- 测试:nsshared 的一致性向量(生成器);与 nsshared 模拟服务的行为对照;两实例与三成员区域;冲突、限速与容量上限;带冲突锁存的重启、提交前后的崩溃、分区与 恢复。
R1 不解决的:带签名、可过期、可运行时发现的 relay 目录(目录有版本但没有签名),以及区域成员静默死亡约 50 s 才被发现的问题。
完成条件:ns 的 relay 客户端与登记表客户端对 DEV 独立端口上的该二进制通过,即开发完成;workspace 钉住带有 L10 的 nsshared
版本(v0.6.0)并具备持久性、恢复与容量方面的证据,即生产完成。
R2 网关换引擎
Section titled “R2 网关换引擎”三个检查点,分别晋级,然后做一次组合验收。在第一个检查点承载流量之前,用量上报器及其持久化的落盘队列要先
就位:旧的装配不允许角色配置在此之前生效(nsgw:crates/nsgw-server/src/runtime/full.rs,run),而且每个
检查点都包含它自己的计量以及重启后的恢复。
网关规模的实测由网关自己做,共两次(L4)。第一个检查点承载流量之前:只测引擎,本地侧用 TUN,挂很多对端, 这样在所有东西都移植到引擎上之前就知道引擎有没有上限。R2 完成时:每种服务的完整路径。两次都用同样的拓扑跑一遍 内核 WireGuard 作为参照,把结果连同测量时的硬件与负载一起记录,由所有者决定能否接受。事先不写阈值(所有者, 2026-10-08):没有可以据以推导的生产实测数据。这两次实测不拖住第一个检查点之前的移植工作。
- 公网入口。
nsgw-plane:引擎加 TUN 本地侧与 UDP 传输;对端对账经过对端准入门,排空经EngineHandle; 没有WgPlanetrait,不用boringtun、tuncrate 与wireguard-uapi。nsgw-envoy、nsgw-api、nsgw-policy、nsgw-routing、nsgw-edge-auth。入口传输只有一种:经引擎的 WireGuard。策略要求 connector 的路由 (tunnel设为ws或wss)不暴露,偏好策略则选择 WireGuard(G20)。ACL 门调用nsgw-policy(G5)。 一次基本的请求测试看不出来的对等性也属于这个检查点:一条路由只有在它的鉴权、JWT authority、客户端 CA 与 证书依赖都就绪时才暴露(nsgw:crates/nsgw-envoy/src/lib.rs,http_route_dependency_state);Envoy 与网关 之间的私有监听保持私有;HTTP、TCP 与 UDP 的来源 IP 准入;伪造的转发头与主体头;上游的 authority 与 SNI; HTTPS 失败后不以明文重试。 - Exit。
nsgw-exit:把 nft 实现移植到引擎的 TUN 接口上(Exit 与 Egress),走 UDP。网关不宣告 Exit 的 WSS 承载(G14)。 - Egress。
nsgw-egress:把 TPROXY 客户端一侧与代理核心移植进同一个 crate;只对 UDP 可用的节点提供。 - 位于
nsgw-metrics计量 trait 之后的nsgw-usage,以及只做装配的nsgw-server。大文件在移植时按职责拆分。
为运维与测试原样保留的:就绪与排空、admin 的权限范围及其对机器令牌的拒绝、审计环、构建标识,以及指标名,其中
对端状态改由引擎提供。nsshared-telemetry 只取代 tracing 的初始化;指标注册表是 nsgw-metrics。
阶段计划要带一张按资源与监听器列出的矩阵,覆盖启动、断连、显式为空、抓取失败、过期与恢复,因为今天的行为并 不是同一种策略;每一格都要有意识地保留或修改。
完成条件,在一套隔离的 DEV 环境中:独立的 NSD 实例、数据库与池,使用测试身份,池中只有新线网关。共享 NSD 里
的一个单独 realm 不构成隔离,因为公网池是全局的(nsd:crates/nsd-store/src/repositories/machine.rs,
list_gateways_for_realm)。有两种节点模式不在声明的节点矩阵内(所有者,2026-10-08),因为今天的节点在这两种模式下要求网关提供 WebSocket 端点,且不会退回 WireGuard:经由一台同时是该节点选定 Exit 的网关做 Egress(ns:crates/ns/src/tun_service/bridge.rs,requires_gateway_wss),以及本地代理与 DNS-VIP 连接,它们的路由选择需要 WSS 端点(ns:crates/ns/src/router.rs,primary_gateway_for_source)。验收覆盖声明的现有节点版本、经 WireGuard 的公网入口(TCP 与 UDP)、一条要求 connector
的路由保持不暴露、经 UDP 的 Exit、Egress、运行中的策略撤回、上报、重启,以及 NSD 中断
期间的冷启动;并证明被测路径没有用到任何中继配对。全程由一台网关服务两个 realm:第二个 realm 的路由与 ACL
能工作,不能由第一个 realm 的状态满足,并且在第二个 realm 离开 served 集合时被撤回。Exit 与 Egress 中的特权主机夹具要实际运行并留下运行记录。所有者已在
2026-10-08 授权该夹具:范围是带有本次运行名字的容器、网络与卷,以及本次运行创建的网络命名空间内部的对象;宿主机自身
网络栈上的任何东西都不属于它。生产上网关运行在单租户 Docker 网桥模式:R2 验收这种模式与独立网络命名空间模式,宿主机
网络模式随代码移植但不验收。L4 的两次实测结果报给所有者审阅。
按决定,这个完成条件不证明:与旧网关在同一个池中共存。今天的 NSD 没有一种登记方式能让网关留在池中提供
Exit、Egress 与公网入口、同时又被排除在中继选择之外:健康的网关必须有 WireGuard 端点,而
nsd:crates/nsd-distribute/src/notifier.rs(gateway_relay_endpoint、realm_relay_gateway_ranking)把任何有
端点的网关都当作旧式中继的候选。R3 之前不尝试混池。带入 R2 的已知限制:控制协议中所述的快照
延迟会在重绑定期间扣住配置;阶段计划要演练它,并且不宣称在该窗口内撤销能及时生效。
网关的协议选定
Section titled “网关的协议选定”如果 G11 被接受,控制资源协议在 R2 之后、身份切换之前为新网关选定,只为保留的角色承载今天的载荷。它需要 L11、 NSD 的发布边界与全双工投递,以及控制协议中列出的证据。节点与旧网关仍用今天的协议;部署保持隔离。
R3 账户切换
Section titled “R3 账户切换”输入具备后再细化。
nsgw-node,即网关作为一个节点:它的身份与自身记录、对端记录状态,以及 relay v2 的节点一侧。网关像其他节点 一样向一台 relay 注册,自己不内嵌 relay(G21)。此前由nsgw-server保管的身份交给它。没有哪个库提供 relay v2 的客户端:nsshared 只有编解码。节点自 2026-10-07 起有一个,在它的overlaycrate 里 (ns:crates/overlay/src/relay/,RelaySession、RelayClient):一个不做 I/O 的会话状态机,加上 UDP 与 WSS 载体的驱动,只依赖 nsplane 与nsshared-proto,不依赖节点的其他部分。R3 的设计在三种做法里选一种:依赖这个 crate、把会话状态机移进 nsshared、移植一份;并写明网关是否也拨 WSS 载体还是只用 UDP 载体。- 账户 relay:带名单准入的
nsgw-relayd,独立成进程部署,可以与网关同机,也可以在别处(G21)。它需要来自 NSD 的名单(L8),为此要有同 NSD 的会话和自己的凭据;NSD 把 relay 投影为独立的条目,而不是网关的一个属性。 relay 按源节点 ID 计量它转发的 DATA,区域内每个报文只计一次,并自己向 NSD 上报;归到哪个账户由 NSD 决定。 UDP 被阻断的节点到网关的流量两个进程都看得到,一边是中继字节,一边是服务用量;NSD 知道哪些节点 ID 是网关, 由它保证只计一次费。 - 网关的覆盖地址由其身份键派生(派生);对端为
/128的节点地址;ACL 主体、路由与 Envoy 后端使用节点 ID 与node6。 - Exit 与 Egress 改为 IPv6,含 NAT66 与选定的 NAT64 路径(Exit 与 Egress)。切换不要求更换 内核后端。
- M6 的载荷 profile(L8),nsd、ns 与 nsgw 在同一窗口发布,运行在控制资源协议之上。它取代
nsgw-contract-v1, 后者随之移除。 - NSD 方面:中继选择不再把网关的 WireGuard 端点当作 v1 中继,配置投递与中继快照延迟解耦。
- 用量按节点身份计量;持久化的用量与身份状态的迁移。
- NSD 今天生成、R2 所消费的服务名、别名、虚拟端口与公网域名保持其含义;它们各自如何对应到 R4 的名字,在切换 之前写明。
- relay v2 的验收覆盖访问端、发布端与子网路由器三种角色,以及一个 UDP 被阻断的节点,而不仅是 relay 的编解码。
- 新旧两类参与者要么保持隔离,要么一起切换;旧线归档之前先做一次排空与回滚演练。
- 未决:网关作为一个有自己记录的节点,如果必须讲隧道内控制通道,这些消息就不再只属于节点,要从 ns 移到
nsshared;应答方那时属于
nsgw-node。
R4 名字与收尾
Section titled “R4 名字与收尾”把已校验的记录及其发布档位交给权威 DNS 服务器。这台服务器放在哪里尚未决定;若放在本工作区,它是第三个
二进制,从登记表的发布视图取数(L9、L10),而不是 nsgw-server 里的一个模块。把本站的 nsgw 各页转为现状页,本页随之移除。
待表态的已知局限
Section titled “待表态的已知局限”归档文档为今天的网关记录的局限与偏差(nsiod-docs:,各章的”Divergences & limits”一节),以及本计划建议的
处置。“接受”是指指名接受,并把该限制写进所在阶段的验收。R2 的各项处置已由所有者在 2026-10-08 确认。
| 今天的局限 | 建议 | 阶段 |
|---|---|---|
| 配对消失后,Exit 的具名计数器仍然留着 | 修复:在最后一次采集之后做有界的清理 | R2 |
| Exit 在”最后一次采集到装上拒绝规则”之间、以及”装上规则到计数器取种”之间会少计;计数器读取失败时重新取基线 | 接受(所有者,2026-10-08):不增加切换算法,在 R2 的验收里实测少计的量,不宣称切换无损 | R2 |
| Exit 的撤回是替换表,conntrack 条目留着 | 修复(所有者,2026-10-08):撤回时清除被撤回来源的 conntrack 条目,已建立的流不再有后续报文被转发。固定 SNAT 的地址如果会被网关路由到隧道,就拒绝该配置,这样对每一种被接受的配置这条保证都成立,宿主机上也不用删除任何东西 | R2 |
| 删除对端之前先读一次它的计数;读取与删除之间的字节不计费,因为引擎在删除时不给出最后的计数 | 接受(所有者,2026-10-08);删除时的计数快照要向 nsplane 提需求,而 nsplane 已冻结 | R2 |
ProxyProtocol 来源模式的渲染与直连相同;IPv4 映射的 IPv6 来源只做过模型测试 | 按名拒绝,R0 已经如此;R2 不实现(所有者,2026-10-08) | R2 |
| 鉴权方法按管理员书写的顺序执行,JWT 与 mTLS 先在 Envoy 强制;规范声称有固定优先级 | 两者都保持代码现状,用测试固定;audience 为空的行为列为已知限制(所有者,2026-10-08) | R2 |
| OAuth2、webhook HMAC、带签名的主体转发,以及 HTTP 的限流、改写与审计字段是保留字段 | 继续保留,明确为无效果或拒绝 | R2 |
| 签名 URL 在过期之前可以重放 | 接受 | R2 |
| 不能按证书序列号撤销,也没有对已启用的客户端 CA 代际的确认 | 保留 CRL 与绑定撤销;是否增加确认在 L11 中决定 | R2、L11 |
| 下发的配置没有持久化:NSD 中断期间冷启动没有期望状态 | 接受,与 G12 一致 | R2 |
一个跨 realm 的密钥冲突会在整台网关范围内撤掉受影响的对端(nsgw:crates/nsgw-control/src/realm_config_snapshot.rs,merged_gateway_wg_snapshot) | 接受这种 fail-closed 的波及范围并给出诊断信号(所有者,2026-10-08) | R2 |
| 修改本地下限需要重启;Egress 的连接期限(10 s)与流数上限(1,024)是常量 | 保留并写入文档 | R2 |
| realm 快照每次轮询都重新编译每个 realm;控制面变更频率没有实测的规模包络 | 与报文基准一起测量;补救措施在协议工作里 | R2、协议选定 |
| 信封 v1 是硬性关卡;未知 API 是错误,而未知事件是跳过 | 兼容性 fixture | R0 |
| 三种传输各不相同:按流的重放缓存、每次调用一次 Noise 握手、QUIC 没有入口计量 | 写明支持的姿态;不降级到明文 | R0 |
| 就绪与心跳并不表明某项策略已被应用 | 定义验收信号;已应用的修订随 L11 到来 | R0、R2 |
| 公网边缘只构造发布端后端,自定义域名的归属由 NSD 证明 | 保持这个边界;R4 的 DNS 供数不得绕过归属 | R2、R4 |
对其他仓库的请求
Section titled “对其他仓库的请求”在需要它们的阶段开始之前与负责人达成一致;请求落地之前,依赖它的功能不能通过自己的门。
| 阶段 | nsshared | nsplane | nsd | ns | deploy |
|---|---|---|---|---|---|
| R0 | 无;商定网关的 v1 契约与 L8、L11 之间的版本边界 | 钉住版本、feature 与加密库的兼容性 | 无;记录所依据的提交 | 后续阶段用来测试的节点版本 | 钉住版本的 NSD,连同它自己的数据库与池,在本地用容器起;构建凭据 |
| R1 | 无:L10 与发布规则已在 v0.6.0 交付。信箱的导出与恢复不在 nsshared:信箱状态只由 relay 读写、不在成员之间复制,由 nsgw-rendezvous 自行定义(nsshared 维护计划,2026-10-10) | 无 | 无 | 持久化发布修订的 relay 与登记表客户端 | 独立的 relay 端点与存储;恢复与分区演练 |
| R2 | 无 | 无;nsplane 的测量工具若已具备就使用 | 无;中继资格与快照方面的限制记录在案,留给 R3 | 运行声明的节点矩阵 | 在支持的部署模式下,引擎与 TUN 加上网关自有的 nft 与 TPROXY;带主机工具的运行时镜像;特权测试任务 |
| 协议选定 | L11 | 网关的适配器所需的效果与截止屏障 | 发布边界、持久的属主 fence、全双工投递、结果存储;按接收者选定协议 | 在契约冻结之前评审共用模型 | 经实际部署的代理验证全部三种载体;fence 接管与恢复演练 |
| R3 | L8 | IPv6 验证;只有选定的路径需要时才提 NAT64 的需求;只有 Egress 与 Exit 挪进引擎过滤时才提按标签的流规则;这几项都需要先解除 0.11 的冻结 | M6 工作包、中继角色语义、账户 relay 作为独立的参与方,接收名单并上报用量,发往网关的中继字节只计一次费(G21)、快照解耦、没有 connector 之后 tunnel 取值的含义、不再签发 Exit 承载令牌 | 新的消费方;M6 的寻址、名单、载体;删除 Exit 承载的客户端和节点侧会话 /client 剩下的使用方(connector 客户端已经没有了) | IPv6 出口、账户 relay 作为独立的部署单元、上线控制、排空与回滚 |
| R4 | L9 | 无 | 若 L9 需要,名字的投影 | 名字申请与发现 | 权威 DNS 服务 |
设计文档需要相应修改,随引起它的阶段一起跟踪:R0 时改迁移计划 §1(G1)、§3.2、D4 与新架构设计页(没有 connector,也没有 Exit 承载,G14 与 G20)、§3.2、M6 一行与 D1(账户 relay 独立成进程,G21)、§3.2(WireGuard 后端、不进入新线的两种中继)、D6(G11)与 §6.1(G10);R4 时改 nsgw 各页。
| # | 问题 | 状态 | 决定或建议 |
|---|---|---|---|
| G1 | 在哪里重构 | 已定,2026-10-06 | 在 nsgw 仓库开 next 新线,使用独立的部署与运行时身份,并维护一份前移清单;main 只收修复,通过 R3 的门之后归档。nsshared 就是这样做的(nsshared:docs/handoff-next-takeover.md) |
| G2 | 从 main 还是从 feat/cascaded-relay 移植 | 已定,2026-10-06 | 从 main;移植每个受影响的子系统之前,必须先对该分支上与级联无关的改动做一次限定范围的审查。对上文所述分支顶端的第一次审查,把其中 213 个提交归为级联专属、74 个归为与级联无关、26 个归为混合,并把后两类归并为 21 项:12 项在移植前先应用,2 项变成测试,2 项落在已退役的路径上,5 项需要所有者表态(G15、G16)。最明确的一项是计费修复:关停时 GatewayUsageReporter::final_flush(nsgw:crates/nsgw-server/src/gateway_usage_reporter.rs)的两个分支会漏掉最后一个用量窗口。分支有变动时重做这项审查 |
| G3 | 网关数据面 | 已定,2026-10-05 | WireGuard 只在 nsplane 引擎上。新线没有内核 WireGuard 后端、BoringTun 与 v1 中继。内核后端只能以 L5 的形式回来 |
| G4 | 网关与 NSD 的契约 | 已定,2026-10-06 | 针对今天 NSD 的 v1 兼容契约是网关的一个临时 crate nsgw-contract-v1,不放进 nsshared。它只有一个使用者(ns 与 nsd 各自维护一份拷贝),R3 时删除,而 nsshared 自己的规则是不带 v1 兼容代码。这是对放置规则的一个例外,只限这一份契约;它仍然守库 crate 应有的纪律:只带序列化依赖、没有 I/O、字节向量取自 NSD。M6 的载荷 profile(L8)与控制资源协议(L11)在 nsshared。代价不变:新线永远不会在 v1 契约上进生产(G8),网关的契约会有三种形态(v1 经两条通道、v1 载荷经新协议、M6 载荷),其中两种会被删除 |
| G5 | 网关上的策略求值 | 已定,2026-10-06 | 按产品身份做的准入在网关里决定,由 nsgw-policy 完成,它移植自 v1 的策略引擎。它长期服务公网入口:访问者不是节点,没有报文过滤能看到他;认出访问者、区分“未知”与“已验证的匿名”、展开组、选择虚拟拨号端口、为计量指明命中的主体,这些无论如何都是网关的工作。R3 之前,同一个求值器也服务 Egress 与对端准入,它们的规则今天是按主体写的。nsplane 于 2026-10-06 决定不带业务语义(nsplane:docs/decisions/2026-10-06-business-agnostic-scope.md):它按网段、端口、协议与不透明的标签过滤报文与流,不实现主体语义,所以 L2 按原样在那边被拒绝。R3 时 Egress 与 Exit 是否改用引擎的过滤、规则按节点写,属于 R3 的设计。与今天行为的对等性在网关里对照 v1 引擎生成的向量来测试 |
| G6 | CI 与镜像构建如何取得 nsplane 与 nsshared | 已定,2026-10-06 | 不可变的 git 解析结果与提交入库的 lockfile,对 nsshared 的私有远端使用只读的构建凭据且不留在镜像层中;只有明确要求离线构建时才 vendoring |
| G7 | nft 源地址转换与 TPROXY 是否算数据面 | 已定,2026-10-05 | 不算。它们是网关的主机集成,R2 中留在 nsgw、仍在内核 |
| G8 | R2 的完成条件 | 已定,2026-10-05 | 一套隔离的 DEV 环境;R3 之前不与旧网关混池。R0 至 R2 不需要改 NSD |
| G9 | 控制协议 | 方向已定,2026-10-05 | 作为重构的一部分重新设计。建议的设计见控制协议,等待 G11 至 G13 |
| G10 | R3 是否必须更换内核的 NAT 与 TPROXY | 未决 | 建议:不必。保留经过验证的内核后端,在 R3 的设计中选定 NAT64 路径,只有具备对等性证据时才更换后端 |
| G11 | 控制协议何时变更 | 未决 | 建议:在 R2 之后、身份切换之前,只对新网关,作为 NSD 工作包中第一个获准的切片。这修订了迁移决定 D6。若被否决,则随 M6 变更 |
| G12 | 新协议的中断与重启策略 | 未决 | 建议的默认值:控制连接丢失后,账户侧角色停止接纳新连接,已接纳的流量在一个最长 300 s 的签名租约内排空,重启后只有在基线到达之后才服务。这是可用性上的改变,不是细节:今天 NSD 不可达时网关继续用 realm 桶里的状态服务(24 小时后只是标记为降级),而按这个默认值,NSD 中断五分钟就会让每台网关的公网入口、Exit 与 Egress 停止。NSD 成为硬依赖,它的可用性目标必须与之匹配。更长的离线服务需要可信的本地投影与密钥缓存 |
| G13 | 新协议的切换保证 | 未决 | 建议的默认值:投递可以合并为”最新的完整状态加持久的撤销屏障”,机器之间切换时允许有界的拒绝与重试。执行每一个中间修订需要日志;零间隙切换需要参与方协议 |
| G14 | 是否移植 Exit 的 WSS 承载(/exit-wss/v1) | 已定,2026-10-06 | 不移植。按设计,网关是一个有身份键的节点,WSS 是 relay 的一种载体,节点侧只有一个 relay 客户端(迁移决定 D1 与 D4)。R3 起,UDP 被阻断的节点像访问其他对端一样,经 relay v2 的 WSS 载体到达网关,Exit 只是这条隧道里的流量。R2 中 Exit 与 Egress 一样需要 UDP 可用;R2 在隔离环境中验收,线上节点在 R3 之前仍由旧网关及其承载服务。这去掉了 L6 的裸报文一半与 nsplane-core 里的准入钩子。节点侧的承载客户端与 NSD 为它签发的令牌随 M6 切换删除,那是 ns 与 nsd 的工作 |
| G15 | 级联分支上的直连探测端点(/relay-probe/v1)是否进入新线 | 已定,2026-10-06 | 不移植。它只存在于 feat/cascaded-relay,main 没有这个端点,它的授权来自已退役的 mac1 中继的探测投影。它的五个 relay_probe_* 配置键与指标都不进入新线。以后如果需要这项测量,它应是一个新的带租约的投影,另行设计 |
| G16 | realm 快照中每个签名事件的大小上限 | 已定,2026-10-06 | 从 R0 起采纳:一个事件连同帧开销超过 1 MiB 时拒绝整份快照,且发生在任何状态被替换之前。main 没有这项检查。钉住提交上的 NSD 已经把单个事件限制在 896 KiB(nsd:crates/nsd-api/src/envelope.rs,MAX_CONFIG_STREAM_EVENT_BYTES),低于这个上限。R0 向 NSD 负责人确认这条限制覆盖每一条快照路径,并把结果同兼容性 fixture 一起记录 |
| G17 | 网关的 v1 契约在 main 之外还取什么(细化 G4) | 已定,2026-10-06 | 以 main 为准,另从 ns-shared 的分支取心跳响应的 features 字段、消息解码器的分配保护,以及保留契约的字节向量。不取:路径测量字段与探测授权。该分支没有改写 main 上任何合法的载荷或签名上下文。配置流上出现超限事件时断开流,与分支上共用的接收端一致,而不是像 main 那样跳过:被跳过的事件可能正是一条撤销 |
| G18 | 由哪个 crate 得出数据面准入的 WireGuard 对端集合 | 已定,2026-10-08 | Exit 与 Egress 各自给出它们授权的对端(带类型的准入),由 nsgw-server 的装配把它们同普通的准入对端合并成最终集合,交给 nsgw-plane;nsgw-plane 既不读 realm 也不读策略状态。“被授权为 Exit 来源的对端不能借别的角色越过 Exit 的新鲜度边界”这条规则属于这次合并。这就是今天代码的形状(nsgw-wss 的 acl_gate.rs,GatewayExitSourcePeers、WgPeerGateReport;runtime/full.rs,WgConfigGateContext),不新增同层依赖。没有选的两种:由 realm 视图携带这份集合,那会让视图依赖服务的生效状态;由 nsgw-routing 负责整道门,那需要它依赖 Exit 与 Egress。R3 有了 nsgw-node 之后,这份集合的归属再定一次 |
| G19 | 引擎上的对账,以及计费所用的计数器 | 已定,2026-10-08 | 引擎一次只改一个对端(nsplane:crates/nsplane/src/handle.rs,EngineHandle::add_or_update_peer、remove_peer),不能原子地替换整份集合。对账是向最新期望状态的串行收敛:先收紧再放开,读回引擎的实际状态之后才把期望状态记为已生效,部分失败后整体重试。不向 nsplane 要事务式接口,也不宣称任何变更是原子的。计费用 PeerStats 里线上的那一对计数器(rx、tx;nsplane:crates/nsplane-core/src/types.rs),它和今天计费用的内核计数器一样包含握手与保活;第一个检查点承载流量之前,用一段确定的流量同内核计数器对比确认。载荷的那一对(data_rx、data_tx)不用于计费:那会改变向用户收费的口径 |
| G20 | 是否移植公网入口的 WSS connector(/relay) | 已定,2026-10-06 | 不移植。连同 G14,Exit、Egress 与公网入口在节点一侧只剩一条路:引擎上的 WireGuard,R3 起网关是一个节点,UDP 被阻断的对端走 relay v2 的 WSS 载体。这去掉了 nsgw-wss、需求 L6、除 admin 令牌之外的所有 WSS 令牌角色,以及 connector 的各项对等性问题。接受的代价:UDP 被阻断的发布端上的 TCP 服务,此后是 WebSocket 里的 WireGuard,丢包时比原来的流代理差;公网服务 tunnel 字段的 ws、wss 与 prefer_wss 在 M6 契约里要么有新的含义、要么废弃;设计文档原本为公网入口保留流代理(D4),要相应修改。R2 中,要求 connector 的路由不暴露;R3 之前线上仍由旧网关服务。节点侧的 节点自 2026-08 起已经没有 connector 客户端;它的节点侧会话 /client(本地代理和一种 Egress 情形仍在拨)随 M6 切换删除,那是 ns 与 nsd 的工作 |
| G21 | R3 起账户 relay 在哪里运行 | 已定,2026-10-07 | 独立成进程:带名单准入的 nsgw-relayd。nsgw-server 不内嵌 relay,像其他节点一样接入一台 relay。这修订了迁移决定 D1,原先是两个二进制共用 relay 核心。一个仓库、两个二进制、三种进程:网关(nsgw-server,带 Envoy)、账户 relay(nsgw-relayd,名单来自 NSD)、公共 relay(nsgw-relayd,开放准入,带登记表、信箱与目录)。理由:只有一种 relay 要开发、测试和运维;网关在这一侧只是一个节点;relay 与网关各自故障、各自升级;暴露面最广的 WebSocket 监听不在持有网关密钥以及 nft、TPROXY 权限的进程里;两者可以部署在不同位置;NSD 不必再区分带中继与不带中继的网关。接受的代价:UDP 被阻断的节点到网关多一跳;relay 进程要同 NSD 建立会话,用来取名单,并上报它自己计量的用量;网关要以客户端身份实现 relay v2 的节点一侧,它在节点的 overlay crate 里有,但不在任何库里;多一个部署单元。两者不再通过配置合回一个进程:nsgw-server 的任何构建都不链接 nsgw-relay。R0 与 R2 不受影响;R1 让 nsgw-relayd 的准入方式可选 |
与迁移计划的差异
Section titled “与迁移计划的差异”| 迁移计划 | 本计划 | 依据 |
|---|---|---|
§1:新旧实现并存于 main,没有长期分支 | next 新线;旧线服务到切换 | G1 |
§3.2:nsgw-wg 的 kernel 与 userspace 保留 | 都不移植;只用 nsplane 引擎 | G3 |
| §3.2:mac1 中继与不透明 WSS 中继在 M6 切换、M9 删除 | 不进入新线 | G3 |
§3.2 与 D4:/relay connector 路径在 M0 删除,terminate 保留给公网入口 | /relay 才是公网入口的 connector,/client 是节点侧会话,与 §3.2 相反。两者都不移植:不为公网入口保留流代理 | G20 |
| §3.2:依赖换成 v2 协议 crate | v1 控制契约是网关的一个临时 crate,M6 契约进入 nsshared(L8);按产品身份做的准入在网关里求值(nsgw-policy) | G4、G5 |
§3.2:nsgw-control 保留,去掉中继表 | 另外增加一个期望状态视图(nsgw-realm),以及一个新的控制资源协议 | G9 |
| §3.2 没有给出 Exit 的 WSS 承载的去向 | 不移植;R3 起由 relay v2 的 WSS 载体覆盖 | G14 |
§3.2、M6 与 D1:nsgw-relay 由独立 relay 与 nsgw-server 共用,账户网关按名单准入 | 账户 relay 是带名单准入的 nsgw-relayd,独立成进程;nsgw-server 不链接 relay,作为节点接入 | G21 |
设计文档:独立 relay 二进制名为 nsgw-quick-relay | nsgw-relayd:它提供 relay v2 与会合服务,不只服务 Quick | 命名(所有者,2026-10-06) |
| D6:M6 工作包之前不做 NSD 的工作 | 把面向新网关的控制协议作为该工作包中更早的一个切片 | G11,未决 |
| §4:M2 在 DEV 与 v1 并存;M6 是账户侧的第一次变更 | M6 之前增加 R2 阶段,在隔离环境中验证 | G8 |
| §6.1:NAT64 的选择留到 M6 | 不变:在 R3 的设计中选定 | G10 |
需要保留的行为
Section titled “需要保留的行为”各阶段引用到的、关于今天网关的事实。
- 信任引导是网关自己定义的。 一个 HTTPS origin,经 WebPKI,再取一份发现文档,其中有按顺序排列的端点、Noise
公钥、QUIC 的 SPKI pin 与信封签名者 pin(
nsgw:crates/nsgw-control/src/discovery.rs,DiscoveryDocument、ControlTrustV1;spki_quic.rs)。它不在 v1control里,后者的引导方式是更早的、不兼容的那一种。 - 后端与链路模式正交。
ServiceBackendKind为NsnBacked、GatewayEgress或GatewayExit,ACL 的终决点随 后端移动:发布端后端由网关按主体判定,之后发布端只判服务白名单;Egress 与 Exit 只在网关判定 (nsiod-docs:00-system-architecture.md,Service backend model)。 terminate有两层意思。 作为链路模式,它是/client会话。作为能力,NSD 用它给节点在网关上分配 Exit 与 Egress 的客户端入口(ns-shared:crates/control/src/types.rs,WssLegRole)。发布端在/relay上认证用的relay角色,在新网关里没有校验方(G20)。- 对端在进入设备之前先过一道门。
nsgw:crates/nsgw-wss/src/acl_gate.rs(filter_wg_config)按策略以及随路由 下发的开关nsn_service_terminate_admission_enabled去掉 terminate 主体的对端,并始终保留发布端对端与 Egress 来源对端,所以关闭 terminate 不会切断公网入口、Egress 或 Exit。 - 本地下限约束 NSD 能投影什么。
gateway_services.toml(nsgw:crates/nsgw-server/src/gateway_services.rs,GatewayServicesFile):文件缺失视为空,文件非法则停止启动; Egress 的目标取投影与下限的交集。 - 网关有自己的状态:机器密钥与 WireGuard 密钥,登记时分配并持久化的隧道地址
(
nsgw:crates/nsgw-server/src/gateway_tunnel_ip.rs,Store),以及用量上报器的落盘队列。 - 过期策略不止一种。 配置流上 ACL 抓取失败即刻拒绝,阈值只用来为持续失败发审计事件(
nsgw:crates/nsgw-wss/src/acl_gate.rs,apply_fetch_failed_threshold);realm 轮询失败则保留旧的桶;公网入口在没有打开 相应开关时继续用旧状态;Exit 另有自己的新鲜度检查 (nsiod-docs:operations/05-acl-convergence-observability.md,Staleness budgets)。 - 可观测性是一份约定。
GET /admin/v1/peers、按握手状态分类的nsgw_wg_peers(KEEPALIVE_GRACE_SECS= 180)、 字节计数、就绪与排空、审计环以及 admin 的权限范围,是运维与测试今天读取的东西 (nsiod-docs:data-plane/06-node-data-plane.md,NSGW: WireGuard observability)。
| 风险 | 说明 |
|---|---|
| 一次变更在报文、主机与计费三个边界上只生效了一部分 | 当 nft 投影、ACL 代际、Egress 套接字或用量化身发生变化时,某个对端或承载可能还有排队的任务。没有任何一个调用能让这种更新成为原子操作。缓解:精确的截止约定,以及在 R2 的门上提供不利时序与重启的证据 |
| 区域恢复还原出一条看似合理的记录,却丢了冲突、发布或删除的边界 | 在 10^7 条记录的规模下,快照与日志重放、压缩、过期与分区带来很长的恢复窗口。缓解:在 R1 生产完成之前具备确认与恢复的不变量,以及按资源预算验证的恢复测试;R3 对账户 relay 再做一遍 |
| R3 中某一方先于其他方变更 | 节点寻址、中继角色的投影、tunnel 取值的含义、IPv6 主机路径与用量归属必须一致;隔离的 IPv4 R2 证明不了这一点。缓解:明确的版本与就绪边界,旧线归档之前做一次排空与回滚演练 |
| NSD 的发布边界 | 新协议的质量取决于 NSD 发布的状态。那些边构建边写状态的投影路径必须产出一份完整、可重复、不可变的发布,否则协议只是为一份残缺的状态做了认证。这是控制面工作中最大的单项风险 |
| 网关自己持有的代码变多 | R0 至 R2 没有任何阶段等待其他仓库的开发:v1 契约、策略求值器、不提供 WebSocket 服务都已定(G4、G5、G14、G20)。代价是这些代码的正确性要由网关自己保证:nsgw-contract-v1 到 R3 为止,靠取自 NSD 的字节向量检查;nsgw-policy 长期存在,靠 v1 引擎生成的向量检查。网关之外剩下的是 R3 时 nsd、ns 与 nsshared 的工作 |
| NSD 成为硬依赖 | 按建议的中断策略(G12),NSD 中断几分钟就会让每台网关的账户侧角色停止,而今天它们会继续服务。在 NSD 的可用性与之匹配之前,新协议不能在生产中选定 |
| 为删除而建的工作 | 按建议的 G4,v1 契约 crate、两个旧式适配器,以及(如果 G11 被接受)一份临时的载荷登记表,都只存在到 R3。它们换来的是在 NSD 改动之前用真实节点做验证;阶段计划把它们放在边缘,使删除时只动一个 crate |
| 引擎在网关规模下的表现,且新线没有退路 | 内核 WireGuard 是今天的生产默认。网关在 R2 第一个检查点承载流量之前实测挂很多对端的引擎,在 R2 完成时实测完整路径(L4);nsplane 自 2026-10-08 起冻结在 0.11.x,所以结果不理想时记录并报给所有者,优化需求要等冻结解除,L5 是最后手段 |
| R3 之前新线没有中继 | 新网关不能替换旧网关,而且在 R3 之前,UDP 被阻断的节点无法使用它的 Egress |
| 两条线并存 | R0 至 R2 期间 main 上对保留子系统的修复需要前移;旧线上的级联工作持续越久,代价越大 |
| 测试随文件移动 | 网关的测试大多是内联的;移植时拆分文件容易漏掉覆盖,所以每次移植对比前后的测试数量 |