NS · 新架构设计(统一数据面)
状态:设计页,部分已实现。 本页写于 2026-10-02(当时
ns为分支fix/pma-rust-baseline),基于即将合并的 nsplane 数据面给出 NS 的下一代架构。截至 2026-10-12:单一引擎(§3 第 1 层,M4 / M5)、身份派生地址与本地 IPv4 别名(§4)、多来源并存(§5.2:多个 NSD 的多个 Network 与 Quick 作为平等的 membership 同时运行)已在 ns 分支refactor/nsplane实现;relay v2 服务端、登记权威、n.ns.io名字与功能模块化尚未。实现时的补充决定见 §10。本页的目标:把账户模式、Quick 模式与应用模式中重复实现的部分抽到一套底层,上层只按需要开关功能。现有页面(账户模式、Quick 模式、应用模式)描述的仍是当前实现;本页描述目标。
1. 四个前提
Section titled “1. 四个前提”| # | 前提 | 推论 |
|---|---|---|
| 1 | 每个节点一个全局唯一的内部地址,IPv6,由节点自己从身份键派生,经 relay 做全局登记;IPv4 通过 6↔4 转换到达 | 不再有按网络分配地址的逻辑;v4 与 v6 解耦,调度与分配只看 v6 |
| 2 | 一个节点可同时连接多个 NSD;Quick 模式是一个”没有服务器的特殊 NSD”;Quick、账户、应用三者互通 | 控制面变成可插拔的对端来源,底层只看合并后的对端表 |
| 3 | 无 TUN、有 TUN、WS 三种工作方式下底层行为一致,上层按需启用或禁用 | 呈现方式(TUN / 用户态协议栈 / 本地代理)与传输(UDP / relay / WSS)正交,都由同一个引擎提供 |
| 4 | 应用代理、反向代理、内网穿透等与底层逻辑无关,按需开关 | 它们是建在核心 API 上的功能模块,不感知模式 |
2. 现状:重复在哪里
Section titled “2. 现状:重复在哪里”按 ns 仓库 crates/*/src 的代码量:
| 重复项 | 账户模式 | Quick 模式 |
|---|---|---|
| WireGuard 数据面 | tunnel-wg(18.5k 行):UserSpace 送入 netstack,Tun 经 nat 送入内核 | quick-runtime(31.6k 行):只有 TUN;自带报文解析、分片、校验和与翻译 |
| IPv4 ↔ IPv6 翻译 | nat::subnet_route(LAN IPv4 ↔ ULA fd<T><R>::/48)、nat::packet_nat(服务 DNAT / SNAT) | quick-runtime::translate(本地 IPv4 别名 ↔ 身份派生的 IPv6) |
| relay 与路径选择 | tunnel-wg::relay + p2p + link_ladder + tunnel-ws:NSGW wg-relay、wss pump、NSD 下发的候选端点 | quick-runtime::relay_client + transport:Quick relay slot、ORDERED_ENDPOINTS、认证探测 |
| 地址计划 | NSD 分配 IPv4:Node 100.64.0.0/11 + 扩展段,Service VIP 100.96.0.0/15,网关 100.127.0.0/16;子网 ULA | fd + BLAKE2s(network_id, identity) 的 /128 地址对 |
| 对端表与名字 | NSD 投影的 WgConfig;Short DNS v2 | 本地配对记录;<昵称>.quick.internal |
| 呈现方式 | tun / dns-vip / proxy(SOCKS5、HTTP、PAC) | 只有 TUN |
| 控制面 | Envelope v1 + 配置流,一次只能有一个 active NSD | 配对令牌 + relay 目录;与账户模式按状态目录互斥 |
盘点时(2026-10-02)两套数据面都基于 crates.io boringtun 0.7 的 noise 层;2026-10-03 起账户模式、2026-10-04 起 Quick 模式都已换到 nsplane 引擎上,旧数据面 crate 已删除(迁移计划 §4.1)。合计约 6 万行里,真正只属于某一种模式的是控制面协议与配对;报文处理、翻译、路径与 relay 是重复实现。
3. 目标分层
Section titled “3. 目标分层”┌─────────────────────────────────────────────────────────────────┐│ 4. 功能模块(开关) 服务发布 · 反向代理 / 公网入口 · 子网路由 · 出口 ││ · 本地代理 · 应用(send、第三方) │├─────────────────────────────────────────────────────────────────┤│ 3. 呈现方式(按节点选择) TUN · 用户态协议栈 · DNS-VIP · SOCKS5/HTTP/PAC │├─────────────────────────────────────────────────────────────────┤│ 2. 覆盖网核心(一套) 身份 · 地址派生 · 全局登记 · 对端来源 · 合并对端表 ││ · 策略 · 名字 · 路径阶梯 │├─────────────────────────────────────────────────────────────────┤│ 1. nsplane 引擎(每节点一个) WireGuard · 传输 · 路径策略 · 过滤链 ││ · netstack · TUN · ACL · 翻译 │└─────────────────────────────────────────────────────────────────┘| 层 | 归属 | 内容 | 对应现有代码 |
|---|---|---|---|
| 1 引擎 | nsplane | nsplane-core(无 I/O)、nsplane(tokio 驱动)、nsplane-packet、nsplane-tun、nsplane-netstack、nsplane-acl;多传输、PathPolicy、PacketFilter 链;本页要求再加一个翻译过滤器(§4.3) | tunnel-wg 与 quick-runtime 的报文路径、netstack、nat 的翻译部分、acl |
| 2 核心 | ns overlay | 身份键、地址派生、登记客户端、PeerSource trait 与合并对端表、跨来源策略、名字表、链路阶梯规则 | addressing、link_ladder、quick-wire::address、short_dns、dns/ |
| 3 呈现 | ns edge | 把合并后的覆盖网呈现给本机:TUN 设备、netstack 监听、DNS 服务器、dns-vip 环回监听、SOCKS5 / HTTP / PAC | engine/startup_tun、startup_proxy、startup_dns、socks5_proxy、http_proxy、pac |
| 4 功能 | ns features/* | 每个功能一个模块,只依赖核心与呈现层的 API | services.toml 发布、subnet/、exit_*、公网入口、应用会话 |
每一层只依赖下一层。模式这个词只在第 2 层出现,并且只是”对端从哪里来”。
DNS 不在任何一层的依赖里。 名字(§5.4)只是便利:地址由身份派生、对端由来源下发、会合由 relay 登记表完成,没有一步需要解析名字;§5.5 逐项列出没有 DNS 时的路径。
4. 地址:一个节点一个 IPv6
Section titled “4. 地址:一个节点一个 IPv6”4.1 派生
Section titled “4.1 派生”g = BLAKE2s-120("ns-node-addr-v2", I) >> 1 // 119 位,与网络无关node6(I) = fd ‖ g ‖ 0 // 原生 IPv6 服务地址,即 Quick v2 的 N6node4(I) = fd ‖ g ‖ 1 // IPv4 服务地址,即 Quick v2 的 N4每个节点一个 /127 地址组,规范见 Quick 协议 v2 §2。下文的 node6 都指地址组中的原生地址;node4 让远端只监听 IPv4 的服务无需端口桥即可访问(§4.3)。
- 只由身份键
I派生,与 NSD、Network、Quick 配对无关:同一台设备在所有来源里是同一个地址。这是前提 2 能成立的基础:两个来源给出同一个对端时,合并对端表无需做地址冲突处理。 - 现有 Quick 地址是
fd‖ hash(network_id, identity),同一设备在不同网络里地址不同;账户模式的地址由 NSD 分配。两者都被替换。 - 119 位哈希空间内碰撞概率可忽略(10⁹ 个身份约 7.5 × 10⁻¹⁹);节点安装对端时仍做显式冲突拒绝。
- 子网与服务不再占用节点地址:节点发布的 LAN 使用 IPv4 内嵌前缀
lan6(I, id) = fd ‖ BLAKE2s-72("ns-lan-addr-v2", I, subnet_id) ‖ 0x0000 ‖ IPv4(/96,RFC 6052 风格),由节点自己派生,NSD 只做授权。
所有派生都用带密钥的 BLAKE2s,密钥是一段固定的域分隔标签(不是机密),保证同一把公钥在不同用途下的哈希互不相关。标签一旦发布就不再改;输入或输出格式变化时换代号:
| 标签 | 输入 | 输出 | 用途 | 取代 |
|---|---|---|---|---|
ns-node-id-v2 | I | 12 字节 → base32,20 字符 | 节点 ID(名字标签、命令参数) | keynet-id-v1 |
ns-node-addr-v2 | I | 15 字节右移 1 位 → fd ‖ g ‖ m,/127 | node6、node4 | keynet-addr-v1(曾混入 network_id) |
ns-lan-addr-v2 | I ‖ subnet_id | 9 字节 → fd ‖ … ‖ 0x0000 ‖ IPv4,/96 | 发布的 LAN 前缀 | NSD 分配的 fd<T><R>::/48 |
ns/node-record/v2 | 签名上下文 ‖ 记录前七个字段的规范 CBOR | Ed25519 签名 | NodeRecord 的 sig:登记表、TXT、relay 注册与隧道内更新共用(§5.4.1) | KeyBinding 的 keynet/key-binding/v1 |
ns-alias4-v2 | I ‖ kind ‖ i | 22 位 → 100.64.0.0/10 内的别名 | 本机 IPv4 别名的候选序列(§4.3) | NSD 分配的 Node IPv4 |
现有协议常量(keynet/... 上下文、/keynet/v1、keynet-relay-v1 等)是 v1 的线上值,随 Quick 协议 v2 一次性换成 ns/* 拼写,不保留兼容。
参照:Yggdrasil(200::/7)与 Mycelium(400::/7)由公钥派生 IPv6,无需分配;ZeroTier 6PLANE / RFC 4193 由网络 ID 与节点 ID 派生。本设计取”由身份派生、无需分配”,不取它们的路由方式。
4.2 全局登记
Section titled “4.2 全局登记”地址由节点自己算出,登记只负责可查找与可拒绝。登记处是 relay;NSD 不是登记处,只在需要时同步 relay 的记录。
| 角色 | 记录 | 规则 |
|---|---|---|
relay(nsgw-quick-relay,唯一登记权威) | NodeRecord:I、当前 WireGuard 公钥 K、代际 gen、issued_at、服务期限 expires、home relay;由 I 签名(Quick 协议 v2 §3)。不含直连端点 | 所有节点都发布到登记表,账户模式的节点也一样;记录以 I 为键,按节点 ID 由客户端 HRW 分片到登记域中的两个 relay(v2 §7);node6 不是输入,任何一方都由 I 重算 |
| NSD(账户) | 按节点 ID 查找 relay 登记表中的 NodeRecord,另加 Device / Node / Network 归属 | 只在投影该设备、或跨 Network 调度时查找;校验签名后使用,不改写、不签发 |
NSD 不再分配任何覆盖网地址。网关同样是一个有身份键的节点,覆盖地址由其身份派生;网关侧只处理 IPv6(wg0、ACL、路由、出口、Egress 与公网入口后端都按 node6),原来的 100.127.0.0/16 随之废弃(见迁移计划 §6)。
查找。 登记表只支持按节点 ID 精确查找,不提供枚举、前缀或模糊查询;节点 ID 有 96 位,无法从地址或名字反推,不知道 ID 就查不到。查找按源 IP 与全局限速。relay 不知道配对关系,也不据此过滤:查得到不等于可以通信,是否接纳对方由两端按各自的来源决定(已配对、NSD 授权或应用会合准入)。
登记记录同时是公网 DNS 的发布源:节点在发布时选择档位(publish 请求中的 dns:none 默认、address、record,由身份对 relay 挑战签名的 proof 认证,不进入签名记录本身)。address 只发布 <节点 ID>.n.ns.io 的 AAAA;record 把校验通过的 NodeRecord 原样发布为 AAAA 与 TXT(§5.4),登记处既不改写也不另造记录;none 的记录只在 relay 内供精确查找。隐私影响见 Quick 协议 v2 §11.3。
故意冲突的处理。 因为地址不是声明而是由 I 重算,“抢占某个地址”只有找到 BLAKE2s 的第二原像一条路,不可行;因此冲突全部收敛到身份层面:
| 情形 | 处理 |
|---|---|
登记的 node6 与 H(I) 不一致 | 直接拒绝,不入库,计入该源 IP 的限速 |
同一个 I 从两处同时登记(设备被克隆、密钥泄露) | 代际单调:gen 更高、签名有效的记录胜出;更低代际拒绝;同代际不同 K 锁存冲突,直到出现更高代际(v2 §3.3)。两端都能在 ns status 看到”另一处登记”,由用户轮换密钥(key rotate)或移除设备 |
区域复制中两个成员持有同一 I 的不同记录 | 按同一顺序规则单调合并;同代际不同 K 在全区域锁存冲突,lookup 返回 conflict |
| 大量伪造身份填满登记表 | 发布需要对 relay 挑战签名,并按源 IP、身份与全局限速;记录最多 7 天过期;只支持按节点 ID 精确查找,不可枚举 |
| NSD 同步到与自己账户记录不一致的键 | 以 relay 上代际更高且签名有效的绑定为准,NSD 重新投影;NSD 从不覆盖 relay 记录 |
4.3 IPv4 是本地视图
Section titled “4.3 IPv4 是本地视图”应用只认 IPv4 时,节点在本地给每个对端铸造一个 IPv4 别名,在本机的翻译过滤器里做无状态 4↔6 转换,再进入隧道。远端只看到 IPv6。
| 项 | 规则 |
|---|---|
| 别名空间 | 100.64.0.0/10(CGNAT),节点本地分配,不再由 NSD 分配 |
| 稳定性 | 别名按 I 确定性映射,碰撞时顺延;持久化,重启不变;每个对端最多两个(alias6 → node6、alias4 → node4) |
| 翻译 | 内嵌:alias6 ↔ node6、alias4 ↔ node4、self4 ↔ node4(self);对 LAN 前缀:lan4 ↔ lan6(直接取低 32 位);TCP / UDP / ICMP 校验和增量更新、分片按 §6 处理 |
| 位置 | nsplane 引擎的 PacketFilter 链,加密前与解密后各一次;TUN 与 netstack 共用 |
| 服务 VIP | 保留 dns-vip 的 127.127.0.0/16 环回 VIP;它只是呈现层的一种别名 |
这等于把 quick-runtime::translate 与 nat::subnet_route 合成一个翻译器,并把 nat::packet_nat 的 DNAT 收缩为”服务发布”功能模块里的端口映射。参照 Tailscale 4via6(子网路由把 IPv4 内嵌到 IPv6)与 464XLAT。
两个节点看同一个对端的 IPv4 别名可能不同,这与现在 Quick 的 alias4 一致;名字(§5.4)解析到 node6,IPv4 别名只在本机 DNS 的 A 记录里出现。
5. 对端来源:Quick 是一个特殊的 NSD
Section titled “5. 对端来源:Quick 是一个特殊的 NSD”5.1 trait
Section titled “5.1 trait”trait PeerSource { fn id(&self) -> SourceId; // nsd:<uuid> | quick | app:<session> fn peers(&self) -> watch::Receiver<Vec<Peer>>; // 身份、WG 键、node6、允许前缀、relay、端点提示 fn policy(&self) -> watch::Receiver<Policy>; // 本来源内的访问策略 fn names(&self) -> watch::Receiver<Vec<Name>>; // 本来源的名字记录 fn registry(&self) -> &dyn Registry; // 登记与查找(§4.2)}| 来源 | 对端来自 | 策略来自 | 现有代码 |
|---|---|---|---|
nsd:<uuid> | NSD 配置流(wg_config 等事件) | acl_config、node_l3_config.v1 | control、acl_runtime |
quick | 本地允许名单:信箱配对或按节点 ID 放行,记录来自配对或 relay 登记表(Quick 协议 v2 §10) | 配对或放行即授权(A–B),本地 allow / remove 记录 | quick-runtime 的持久化、quick-pairing |
app:<session> | 应用会合(应用模式)产生的仅限本次会话的对端 | 只放行应用端口 | 新 |
Quick 来源没有服务器,它的”配置流”是本地配对记录的变化;relay 只是登记处与中继。从第 1、3、4 层看,三种来源完全一样。
5.2 多来源并存
Section titled “5.2 多来源并存”- 一个节点同时持有多个来源:若干个
nsd:*、一个quick、任意个app:*。现在的”single active NSD”与”账户 / Quick 互斥”取消。(2026-10-12 已实现:来源按 membership 划分为nsd:<uuid>/<network>与quick,每个 membership 是引擎上的一个本地身份。) - 合并对端表以
I为键。同一设备出现在多个来源时只有一条 WireGuard 对端(同一个node6、同一把 WG 键),其允许前缀与策略取各来源的并集。 - 每个来源独立的控制连接、心跳与重连;一个 NSD 掉线只影响它贡献的对端与策略。
参照:ZeroTier 一个节点同时加入多个网络,且有无控制器的 ad-hoc 网络;Nebula 可配置多个 lighthouse。Tailscale、Netbird 都只支持一个控制面,这正是要避开的限制。
5.3 跨来源互通
Section titled “5.3 跨来源互通”| 流 | 默认 | 放行方式 |
|---|---|---|
| 同一来源内 | 按该来源策略 | — |
| 不同来源的对端之间 | 拒绝 | 按需放行,见下 |
| 应用对端 ↔ 任何对端 | 拒绝 | 不放行;应用对端只与创建它的会话通信 |
跨来源放行不是全局开关,而是逐对处理,依据是检测到的重叠与对端的信任情况:
- 检测:合并对端表发现同一个
I出现在多个来源(例如既是 Quick 配对对端又是某 NSD 的 Node)时,它只是一条对端,来源只影响各自的允许前缀与策略;不存在跨来源问题。 - 信任:两个不同的
I分属不同来源时,发起方节点看对方的信任等级:由本机用户亲自确认过指纹的 Quick 对端、由已登录账户批准的 NSD Node,都算”已信任”;仅由应用会合产生的对端不算。 - 放行:只有双方都”已信任”且本机用户显式放行(
ns policy allow <peer> --via <来源>)、或 NSD 下发含对方node6的 Grant 时,才在两者之间放行;放行是有向的,另一端需要各自放行。 - 撤销:任一来源移除该对端、或记录代际冲突导致信任状态改变时,相关放行自动失效。
策略评估在 nsplane ACL 过滤器里统一进行,输入是合并后的规则集;来源只是规则的命名空间。
5.4 名字:一个后缀 n.ns.io
Section titled “5.4 名字:一个后缀 n.ns.io”节点就是节点,名字不区分来源。所有名字都在我们拥有的域 n.ns.io 下,取代现在的 ns.internal(Short DNS v2)与 quick.internal。名字是便利层:本节的一切都可以缺席,功能不变(§5.5)。
5.4.1 节点 ID 名字:自证,不分配
Section titled “5.4.1 节点 ID 名字:自证,不分配”节点 ID 与 node6 都由身份公钥 I 派生(节点 ID = base32(BLAKE2s-96(I)),node6 = fd ‖ (BLAKE2s-120(I) >> 1) ‖ 0),没有分配这一步。节点用身份私钥签一条 NodeRecord(Quick 协议 v2 §3),发布到登记表(§4.2);账户模式的节点同样发布,NSD 只查找。默认不进入公网 DNS;节点选择 address 档时只发布 AAAA,选择 record 档时校验通过的记录原样发布:
<节点 ID>.n.ns.io. AAAA <node6><节点 ID>.n.ns.io. TXT "v=ns2 r=<base64url(NodeRecord 的规范 CBOR)>"| 谁 | 何时 | 校验 |
|---|---|---|
| relay | 发布前 | 记录有效且未过期;proof 由同一个 I 对本次挑战签名;gen 与冲突状态按 v2 §3.3;通过限速 |
| 客户端 | 使用时 | 记录签名有效且未过期;BLAKE2s-96(I) 等于查询的标签;node6(I) 等于 AAAA;任何一条不符就丢弃应答 |
示例。 下面是 Quick 协议 v2 §2.3 的演示节点(身份种子 SHA-256("ns-demo-node-v2"),WireGuard 种子 SHA-256("ns-demo-wg-v2")),节点 ID、地址与签名都由这两把演示键算出:
; I(Ed25519,base64url): x9wqBt4ZKdMxi43oc78ScDDD7gAH8g4G6g_FQFZ0Wvs; K(X25519,base64) : NZkMHL1XEJhrcG68XIzeUZktuR/U1aR2e6u2dcRMzDk=; 节点 ID = base32(BLAKE2s-96(key="ns-node-id-v2", I)) = 5kemem6slxrgyd63rcqa; node6 = fd ‖ (BLAKE2s-120(key="ns-node-addr-v2", I) >> 1) ‖ 0 = fda6:72ac:58cb:cc8d:1e39:e507:bda:5052; 记录:gen=3 issued_at=1767225600 expires=1767830400 relays=["hkg-a1.relay.ns.io:51822"]
$ORIGIN n.ns.io.$TTL 300
5kemem6slxrgyd63rcqa AAAA fda6:72ac:58cb:cc8d:1e39:e507:bda:50525kemem6slxrgyd63rcqa TXT "v=ns2 r=iAJYIMfcKgbeGSnTMYuN6HO_EnAww-4AB_IOBuoPxUBWdFr7WCA1mQwcvVcQmGtwbrxcjN5RmS25H9TVpHZ7q7Z1xEzMOQMaaVW5ABppXvOAgXgYaGtnLWExLnJlbGF5Lm5zLmlvOjUxODIyWECkn2HAyOlMQSYLsKZwSQXVg3BGBKW08g9tslS3ReFfQodZ6hsaJEyCKYY6XCmiAkWY8mXZYSrHunEBmpQfdccP"
; 分配的短名只是别名(§5.4.2)alice CNAME 5kemem6slxrgyd63rcqa| 字段 | 含义 |
|---|---|
v=ns2 | TXT 格式版本 |
r | 完整 NodeRecord([2, I, K, gen, issued_at, expires, relays, sig])的规范 CBOR,base64url 无填充;超过 255 字节时拆成多个字符串,客户端拼接 |
记录 174 字节,整条 TXT 240 字节。TXT、登记表 lookup、relay 注册与隧道内 RECORD 用的是同一份字节,只有一套校验。客户端拿到应答后的校验:
- 解码
r,记录签名在其中的I下有效,expires未过; base32(BLAKE2s-96("ns-node-id-v2", I))必须等于查询的标签5kemem6slxrgyd63rcqa;node6(I)必须等于 AAAA 的fda6:72ac:…:5052;- 本地若已有该节点更高的
gen,忽略这条。
四步都过,才把记录中的 K 当作该节点的 WireGuard 公钥、把 AAAA 当作它的地址。短名 alice 沿 CNAME 到达同一组记录,校验相同。
因此 record 档的公网 DNS、中间的 resolver 乃至权威服务器本身都不需要被信任,被篡改的应答会被客户端发现,不依赖 DNSSEC;客户端只凭一个节点 ID 名字就能拿到建立会话所需的全部公开材料,登记表与 DNS 是同一份数据的两种读法。address 档的 AAAA 未经认证,只是名字到地址的便利;发现未知节点只走登记表(Quick 协议 v2 §11.2)。
5.4.2 短名:分配的别名
Section titled “5.4.2 短名:分配的别名”人类可读的短名无法从公钥派生,只能先到先得地分配,由分配方维护”短名 → 节点 ID”的绑定,申请同样由 k 签名:
alice.n.ns.io. CNAME <节点 ID>.n.ns.io.| 分配方 | 范围 | 规则 |
|---|---|---|
| NSD | 账户用户,账户内唯一 | 由 Short DNS v2 投影,账户管理员可改名 |
| relay | Quick 用户 | 有期限、需续期、每身份数量有上限,防止囤积;过期后释放 |
短名只是 CNAME 别名,客户端沿着它拿到的仍是节点 ID 名字的记录(address 档只有 AAAA,record 档为自证的 AAAA + TXT),不引入额外信任。本地昵称(Quick 配对时起的名字)仍只在本机,不发布。
5.4.3 本地 resolver 与公网 DNS 的分工
Section titled “5.4.3 本地 resolver 与公网 DNS 的分工”| 名字 | 本地 resolver(节点运行时) | ns.io 公网权威 DNS |
|---|---|---|
<节点 ID>.n.ns.io | AAAA + A(本机 IPv4 别名)+ TXT | 按节点选择的档位:无、AAAA,或 AAAA + TXT |
| 分配的短名 | 同上(沿 CNAME) | CNAME |
| 本地昵称、各 NSD 的 Short DNS 短名 | AAAA + A | 不解析 |
服务名(Short DNS canonical 名、<service>.<gateway>.svc) | 发布端 node6 或网关地址,由 nsd:* 投影 | 不解析 |
- 本地 resolver 优先:它多出 IPv4 别名、本地昵称和服务名;公网 DNS 是没装本地 resolver 时的兜底。
node6在fd::/8内,本机有fd00::/8 → TUN的路由,任何协议都能用。部分 resolver 的重绑定防护会过滤指向 ULA 的公网应答,所以公网 DNS 只能是兜底。 - 短名冲突:不同来源在本机把同一个短名指向不同节点时,按来源加入顺序先到先得,后者只能用节点 ID 名字访问;
ns status报告冲突。 - 证书:只签通配符
*.n.ns.io的公网 CA 证书(DNS-01 由 zone 运营方完成),浏览器与客户端零告警;不为单个节点名签证书,否则节点 ID 会进入公开的证书透明度日志而可被枚举。账户模式的公网入口继续用 NSD 管理的证书。 - 短名输入:把
n.ns.io配进系统搜索后缀(Linuxsearch、macOS resolversearch、Windows NRPT 后缀),用户直接输alice即可。 - 不做:泛解析到
127.0.0.1。它只对带 Host / SNI 的 HTTP、TLS 有用,并且会被重绑定防护丢弃;dns-vip 的127.127.0.0/16VIP 由本机临时铸造,公网 DNS 无法对应。
5.4.4 谁能写 n.ns.io
Section titled “5.4.4 谁能写 n.ns.io”n.ns.io 是 ns.io 运营方的公网 zone,只有运营方的 relay 与托管 NSD 能向它发布记录。自建 NSD 用自己的域名(例如 n.example.com),机制完全相同:zone 名是 NSD 与 relay 的配置项,节点侧的本地 resolver 按来源知道各自的 zone。公网 zone 的发布接口只接受已校验的 NodeRecord 与签名的短名申请,不接受任何手工记录。
5.5 没有 DNS 时
Section titled “5.5 没有 DNS 时”DNS 可能以三种方式缺席:公网 n.ns.io 不可达或应答被重绑定防护过滤;本地 resolver 没装上(无 root、Windows NRPT 失败、端口被占);操作系统的 DNS 整体不可用。每一项功能都有不经 DNS 的路径,DNS 只是在它之上省掉手输地址的步骤:
| 功能 | 不经 DNS 的路径 | DNS 提供的便利 |
|---|---|---|
| 本机地址 | node6 由身份键本地算出 | — |
| 认识对端 | 来源下发:NSD 配置流、Quick 配对记录、应用会合;每条对端都带 node6、WG 键、relay 与端点提示 | — |
| 查找一个未知节点 | 用节点 ID 向其分片 relay 的登记表发 lookup_node(Quick 协议 v2 §7,只支持精确查找);答案是同一条签名的 NodeRecord | <节点 ID>.n.ns.io 的 AAAA + TXT 是同一份记录的 DNS 读法 |
| 配对与应用会合 | 短码 / 票据 → relay 会合信箱;TransferHello 自带 relay 与端点 | — |
| 用名字访问对端 | ns 命令处处接受节点 ID、node6 与本地昵称;ns peers / ns status 列出名字 → 地址,可直接复制;proxy 呈现方式(SOCKS5 / HTTP / PAC)在进程内按合并对端表解析名字,不经系统 DNS | 在浏览器、ssh 里直接输名字 |
| 到达 relay 与 NSD | relay 目录每项同时带 IP 字面量与 TLS 名字,见下;NSD 投影的网关端点已是 IP;节点缓存上次成功的 relay / NSD 地址 | 首次 ns up --server <URL> 解析 NSD 主机名 |
| 服务发布、子网、出口 | 按 node6 / 前缀 / 网关地址路由与授权 | 服务名、.svc 名 |
| 证书 | NS 自身用 WireGuard 身份,不用 TLS 名字 | 应用层 HTTPS 的公网证书 |
规则:
- 失败要可见,不要静默降级:本地 resolver 装不上时节点照常运行,
ns status与ns doctor报告”名字不可用,请用节点 ID 或地址”,不阻塞up。 - 不以 DNS 做安全判断:授权、准入、路径选择只看身份键、
node6与来源策略;名字解析结果从不进入策略。 - 公网 DNS 永远是镜像:它只发布登记表已有的记录,登记表不可达时 DNS 也会过期;反过来 DNS 不可达不影响登记与查找。
- TLS 名字与解析分离:到 relay / NSD 的 TLS 校验用目录或配置里的名字做 SNI 与证书匹配,连接目标可以是 IP 字面量,两者解耦。
relay 目录。 现在的内置目录用 *.sslip.io 主机名(例如 hkg-a1.43.159.53.106.sslip.io:51822):IP 写在名字里,却要先查公网 DNS、还依赖第三方 sslip.io 在线才能取出来,证书也签给第三方域名。新目录每项自带连接地址与证书名字:
KT id=1 host=hkg-a1.relay.ns.io ip=43.159.53.106 port=51822KM id=2 host=hkg-a2.relay.ns.io ip=124.156.159.181 port=51822SG id=3 host=sin-a1.relay.ns.io ip=43.133.56.69 port=51822客户端直接连 ip:port,TLS 用 host 做 SNI 与证书校验;证书由 relay.ns.io 的 DNS-01 签发。公网 DNS 可用时也可以解析 host 以跟随 IP 变更,解析失败就用目录里的 IP。relay 的身份仍由协议层的 relay 身份键证明,证书只保护传输。
6. 呈现方式与传输正交
Section titled “6. 呈现方式与传输正交”| 呈现方式 \ 传输 | UDP 直连 | relay UDP | relay WSS 载体 |
|---|---|---|---|
| TUN(内核协议栈) | ✓ | ✓ | ✓(relay 提供时) |
| 用户态协议栈(无 TUN,免 root) | ✓ | ✓ | ✓(relay 提供时) |
| DNS-VIP(环回 VIP + L4) | ✓ | ✓ | ✓(relay 提供时) |
| SOCKS5 / HTTP / PAC | ✓ | ✓ | ✓(relay 提供时) |
- 传输是 nsplane 引擎的
Transport实现,每个对端的路径由PathPolicy选择;现在的link_ladder(直连 → wg-relay → wss)与 Quick 的 ORDERED_ENDPOINTS 合成一个阶梯。WSS 不再是一种”模式”,而是 relay 的一种载体。 - 一个 relay 客户端:NSGW 账户 relay 与 Quick relay 在节点侧收敛为同一个
RelayClient抽象——登记、slot、反射探测、UDP 载体、WSS 载体。relay 在握手时宣告能力(udp、wss、mailbox、region、registry;标记词表与规则见 会合协议 §3.1,目录项同样携带),客户端按能力与可达性选载体:UDP 可达则用 UDP;UDP 被阻断且 relay 宣告wss则切到 WSS;relay 不提供 WSS 时就是不可达,ns status如实报告”relay 不提供 WSS 载体”,由用户换 relay。不同来源只是给同一个客户端配置不同的 relay 列表与凭据。 - 呈现是本机如何消费合并后的覆盖网,按节点选择、可多选(例如 TUN + SOCKS5)。netstack 与 TUN 是同一个引擎的两个
PacketSink。 - 现有
tunnel-ws的”纯 WebSocket 流代理,绕过 WireGuard”(终结模式)不保留:节点侧不再有逐路由的 terminate 回退,NSGW 的公网入口与出口也不再经它连接节点。出口、Egress 与公网入口在节点一侧只有一条路,即到达作为对端的网关或发布端的 WireGuard;UDP 被阻断时一律由 relay 的 WSS 载体承担(2026-10-06 决定,见 NSGW 新架构设计 的 G14 与 G20)。代价是:UDP 被阻断的发布端上的 TCP 服务不再走流代理,而是 WebSocket 里的 WireGuard。
分片与 MTU:引擎统一在翻译后处理 IPv6 分片与 ICMPv6 Packet Too Big(现 quick-runtime::fragment / mtu 的职责),TUN 与 netstack 共享同一组限制。
7. 功能模块
Section titled “7. 功能模块”| 模块 | 作用 | 依赖的核心 API | 开关 |
|---|---|---|---|
| 服务发布 | services.toml 把本机端口发布为可被策略引用的服务;端口映射 | 名字表、策略、netstack / TUN 监听 | 按节点 |
| 反向代理 / 公网入口 | 把服务暴露到公网:经 NSGW 的 HTTPS / L4 入口(frp 的 server 在 NSGW);只对 nsd:* 来源可用,因为需要 NSD 授权与 NSGW 证书 | 来源为 nsd:* 的投影、WSS 载体 | 按服务 |
| 内网穿透 | 不经 NSD 的点对点穿透:应用会合 + 仅限本次会话的对端 + 本地端口转发 | 应用会话、翻译、策略 | 按应用 |
| 子网路由 | 发布 LAN 为 lan6 前缀,做网关 | 翻译过滤器、来源策略 | 按节点 |
| 出口 | 选择 NSGW Exit 承载默认路由;只对 nsd:* 来源可用 | 来源 nsd:*、PathPolicy | 按节点 |
| 本地代理 | SOCKS5 / HTTP / PAC 监听 | 呈现层 | 按节点 |
| 应用 | ns send 等内置应用与第三方应用 | 应用会话接口(应用模式) | 按应用 |
模块之间不互相依赖;禁用一个模块只是不启动它,不改变引擎或核心的行为。参照 Tailscale 的 serve / funnel(反向代理作为可选功能)与 frp / rathole(服务端 - 客户端反向代理,本设计把服务端放在 NSGW)。
8. 参照项目
Section titled “8. 参照项目”| 项目 | 借鉴 | 不采用 |
|---|---|---|
| Tailscale(BSD-3) | 用户态 netstack 与 tsnet 的”无 TUN 也能用”;4via6 子网 IPv4 内嵌;MagicDNS 分区;serve / funnel 作为可选功能 | 单一控制面;控制面分配地址 |
| ZeroTier(BSL) | 一个节点同时加入多个网络;无控制器的 ad-hoc 网络(对应 Quick);6PLANE 派生地址 | L2 虚拟交换与其路由 |
| Yggdrasil(LGPL)/ Mycelium(Apache-2) | 公钥派生 IPv6,无需分配与冲突协调 | 全网路由(NS 只有两跳:直连或经 relay) |
| Netbird(BSD-3) | 管理面 / 信令 / relay 三分;Rosenpass 后量子选项留作参考 | 单一管理面 |
| Nebula(MIT) | 多 lighthouse;证书内嵌地址与组 | 自有的非 WireGuard 协议 |
| firezone connlib(Apache-2) | sans-I/O 内核 + 单一 I/O 驱动(nsplane 已采用) | — |
| frp / rathole(Apache-2) | 反向代理的 server / client 分工、按服务配置 | 独立的隧道协议(这里复用 WireGuard 与 NSGW) |
只借鉴设计,不复制代码。
9. 迁移阶段
Section titled “9. 迁移阶段”与 nsplane 计划的阶段对齐;每一阶段都可独立交付,ns 现有功能在对应阶段前不变。落到 ns 与 nsgw 两个仓库的里程碑、crate 去向与两次不兼容切换见迁移计划。
| 阶段 | 交付 | 验证 |
|---|---|---|
| A | nsplane 第 1–4 阶段:引擎、多传输、netstack、ACL(在 nsplane 仓库进行) | nsplane 自身门禁、与内核 WireGuard 的 e2e |
| B | 翻译过滤器进入 nsplane(合并 quick-runtime::translate 与 nat::subnet_route);Quick 协议 v2 的 relay 报文、登记表与 NodeRecord 在 relay 与 NSD 落地 | 翻译向量测试;登记冲突测试 |
| C | ns 覆盖网核心:PeerSource、合并对端表、地址派生 v2;nsd:* 来源在引擎上运行(替代 tunnel-wg) | 账户模式回归;多 NSD 并存 |
| D | quick 来源在引擎上运行(替代 quick-runtime 数据面,保留配对与持久化);跨来源策略 | Quick 回归;Quick ↔ 账户互通用例 |
| E | 呈现层统一:Quick 获得 netstack、DNS-VIP、本地代理;WSS 成为 Transport | 三种呈现 × 四种传输矩阵 |
| F | 功能模块化:服务发布、公网入口、子网、出口、应用拆成独立模块与开关 | 禁用任一模块的回归 |
| G | 应用模式:ns send、第三方应用接口 | 文件发送 设计用例 |
10. 需要决定的事项
Section titled “10. 需要决定的事项”已决定(2026-10-02):NSD 分配的 IPv4 Node 地址废弃,不做过渡;登记权威只在 relay,NSD 按需同步,故意冲突按 §4.2 处理;跨来源默认拒绝,按检测与信任逐对放行(§5.3);WSS 按 relay 能力决定,节点侧收敛为一个 relay 客户端(§6);所有节点只向 relay 登记,登记表只按节点 ID 精确查找、不按配对关系过滤,是否发布到公网 DNS 由节点在提交时选择(§4.2);relay 能力标记三处统一(§6)。
所有节点名字统一在 n.ns.io 下,不区分来源,公网权威 DNS 按 relay 登记表回答节点 ID 名字的 AAAA(§5.4);出口与公网入口只允许 nsd:* 来源(§7);tunnel-wg 与 quick-runtime 两套数据面都合并进 nsplane(§9 阶段 C、D),nsplane 计划的第 6 阶段需随之改为两者都迁移。
Quick 协议按本页重做为 Quick 协议 v2(2026-10-02):每个节点一个 /127 地址组(node6 与 node4);一份 NodeRecord 取代 KeyBinding 与 registry.v1,K 没有有效期,只按 gen 排序;登记表按节点 ID 由客户端分片;配对只是信箱应用,PSK 只来自信箱配对;删除离线配对令牌。
ns 实现时的补充决定(ADR 2026-10-11-node-memberships,2026-10-11,同日修订):(1) 来源即 membership(一个 NSD 的一个 Network、Quick、应用会话),全部平等,没有主 NSD,不按排序解决冲突;(2) 节点决定:NSD 与 Quick 配对只提议规则、对端、路由、名字与出口,生效配置按“受管配置 → 所有者决定 → 提议”的顺序由节点决定,每条策略在流的两端各自执行,节点从不在 membership 之间转发;(3) NSD 只知道自己的域,节点不向任何 NSD 上报它还连着什么,Node L3 ACK 只是回执;(4) 每个 membership 的身份 I_m = derive(R, 路径) 由一条设备密钥链(口令 → KEK → 随机根密钥 R)按 NSD 规范 URL 加盐派生,运营方无法通过比对密钥关联设备——这把 §4 的“一个节点一个身份与地址”细化为“一个节点一条密钥链,每个 membership 一个身份与一组 N6 / N4”,§4.2 的登记也按 membership 身份进行;(5) 本地 IPv4 冲突用节点本地的虚拟 IPv4 网段映射到各 membership 独立的 IPv6 前缀解决(等 nsplane 的前缀映射);(6) 不与旧 ns 兼容:不迁移状态与身份,每台设备重新登记。
本页没有未决事项。实现阶段的细节在各阶段的任务里决定。