跳转到内容

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每个节点一个全局唯一的内部地址,IPv6,由节点自己从身份键派生,经 relay 做全局登记;IPv4 通过 6↔4 转换到达不再有按网络分配地址的逻辑;v4 与 v6 解耦,调度与分配只看 v6
2一个节点可同时连接多个 NSD;Quick 模式是一个”没有服务器的特殊 NSD”;Quick、账户、应用三者互通控制面变成可插拔的对端来源,底层只看合并后的对端表
3无 TUN、有 TUN、WS 三种工作方式下底层行为一致,上层按需启用或禁用呈现方式(TUN / 用户态协议栈 / 本地代理)与传输(UDP / relay / WSS)正交,都由同一个引擎提供
4应用代理、反向代理、内网穿透等与底层逻辑无关,按需开关它们是建在核心 API 上的功能模块,不感知模式

按 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;子网 ULAfd + 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 是重复实现。

┌─────────────────────────────────────────────────────────────────┐
│ 4. 功能模块(开关) 服务发布 · 反向代理 / 公网入口 · 子网路由 · 出口 │
│ · 本地代理 · 应用(send、第三方) │
├─────────────────────────────────────────────────────────────────┤
│ 3. 呈现方式(按节点选择) TUN · 用户态协议栈 · DNS-VIP · SOCKS5/HTTP/PAC │
├─────────────────────────────────────────────────────────────────┤
│ 2. 覆盖网核心(一套) 身份 · 地址派生 · 全局登记 · 对端来源 · 合并对端表 │
│ · 策略 · 名字 · 路径阶梯 │
├─────────────────────────────────────────────────────────────────┤
│ 1. nsplane 引擎(每节点一个) WireGuard · 传输 · 路径策略 · 过滤链 │
│ · netstack · TUN · ACL · 翻译 │
└─────────────────────────────────────────────────────────────────┘
层归属内容对应现有代码
1 引擎nsplanensplane-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 / PACengine/startup_tun、startup_proxy、startup_dns、socks5_proxy、http_proxy、pac
4 功能ns features/*每个功能一个模块,只依赖核心与呈现层的 APIservices.toml 发布、subnet/、exit_*、公网入口、应用会话

每一层只依赖下一层。模式这个词只在第 2 层出现,并且只是”对端从哪里来”。

DNS 不在任何一层的依赖里。 名字(§5.4)只是便利:地址由身份派生、对端由来源下发、会合由 relay 登记表完成,没有一步需要解析名字;§5.5 逐项列出没有 DNS 时的路径。

g = BLAKE2s-120("ns-node-addr-v2", I) >> 1 // 119 位,与网络无关
node6(I) = fd ‖ g ‖ 0 // 原生 IPv6 服务地址,即 Quick v2 的 N6
node4(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-v2I12 字节 → base32,20 字符节点 ID(名字标签、命令参数)keynet-id-v1
ns-node-addr-v2I15 字节右移 1 位 → fd ‖ g ‖ m,/127node6、node4keynet-addr-v1(曾混入 network_id)
ns-lan-addr-v2I ‖ subnet_id9 字节 → fd ‖ … ‖ 0x0000 ‖ IPv4,/96发布的 LAN 前缀NSD 分配的 fd<T><R>::/48
ns/node-record/v2签名上下文 ‖ 记录前七个字段的规范 CBOREd25519 签名NodeRecord 的 sig:登记表、TXT、relay 注册与隧道内更新共用(§5.4.1)KeyBinding 的 keynet/key-binding/v1
ns-alias4-v2I ‖ kind ‖ i22 位 → 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 派生。本设计取”由身份派生、无需分配”,不取它们的路由方式。

地址由节点自己算出,登记只负责可查找与可拒绝。登记处是 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 记录

应用只认 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”
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.v1control、acl_runtime
quick本地允许名单:信箱配对或按节点 ID 放行,记录来自配对或 relay 登记表(Quick 协议 v2 §10)配对或放行即授权(A–B),本地 allow / remove 记录quick-runtime 的持久化、quick-pairing
app:<session>应用会合(应用模式)产生的仅限本次会话的对端只放行应用端口新

Quick 来源没有服务器,它的”配置流”是本地配对记录的变化;relay 只是登记处与中继。从第 1、3、4 层看,三种来源完全一样。

  • 一个节点同时持有多个来源:若干个 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 都只支持一个控制面,这正是要避开的限制。

流默认放行方式
同一来源内按该来源策略—
不同来源的对端之间拒绝按需放行,见下
应用对端 ↔ 任何对端拒绝不放行;应用对端只与创建它的会话通信

跨来源放行不是全局开关,而是逐对处理,依据是检测到的重叠与对端的信任情况:

  1. 检测:合并对端表发现同一个 I 出现在多个来源(例如既是 Quick 配对对端又是某 NSD 的 Node)时,它只是一条对端,来源只影响各自的允许前缀与策略;不存在跨来源问题。
  2. 信任:两个不同的 I 分属不同来源时,发起方节点看对方的信任等级:由本机用户亲自确认过指纹的 Quick 对端、由已登录账户批准的 NSD Node,都算”已信任”;仅由应用会合产生的对端不算。
  3. 放行:只有双方都”已信任”且本机用户显式放行(ns policy allow <peer> --via <来源>)、或 NSD 下发含对方 node6 的 Grant 时,才在两者之间放行;放行是有向的,另一端需要各自放行。
  4. 撤销:任一来源移除该对端、或记录代际冲突导致信任状态改变时,相关放行自动失效。

策略评估在 nsplane ACL 过滤器里统一进行,输入是合并后的规则集;来源只是规则的命名空间。

节点就是节点,名字不区分来源。所有名字都在我们拥有的域 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:5052
5kemem6slxrgyd63rcqa TXT "v=ns2 r=iAJYIMfcKgbeGSnTMYuN6HO_EnAww-4AB_IOBuoPxUBWdFr7WCA1mQwcvVcQmGtwbrxcjN5RmS25H9TVpHZ7q7Z1xEzMOQMaaVW5ABppXvOAgXgYaGtnLWExLnJlbGF5Lm5zLmlvOjUxODIyWECkn2HAyOlMQSYLsKZwSQXVg3BGBKW08g9tslS3ReFfQodZ6hsaJEyCKYY6XCmiAkWY8mXZYSrHunEBmpQfdccP"
; 分配的短名只是别名(§5.4.2)
alice CNAME 5kemem6slxrgyd63rcqa
字段含义
v=ns2TXT 格式版本
r完整 NodeRecord([2, I, K, gen, issued_at, expires, relays, sig])的规范 CBOR,base64url 无填充;超过 255 字节时拆成多个字符串,客户端拼接

记录 174 字节,整条 TXT 240 字节。TXT、登记表 lookup、relay 注册与隧道内 RECORD 用的是同一份字节,只有一套校验。客户端拿到应答后的校验:

  1. 解码 r,记录签名在其中的 I 下有效,expires 未过;
  2. base32(BLAKE2s-96("ns-node-id-v2", I)) 必须等于查询的标签 5kemem6slxrgyd63rcqa;
  3. node6(I) 必须等于 AAAA 的 fda6:72ac:…:5052;
  4. 本地若已有该节点更高的 gen,忽略这条。

四步都过,才把记录中的 K 当作该节点的 WireGuard 公钥、把 AAAA 当作它的地址。短名 alice 沿 CNAME 到达同一组记录,校验相同。

因此 record 档的公网 DNS、中间的 resolver 乃至权威服务器本身都不需要被信任,被篡改的应答会被客户端发现,不依赖 DNSSEC;客户端只凭一个节点 ID 名字就能拿到建立会话所需的全部公开材料,登记表与 DNS 是同一份数据的两种读法。address 档的 AAAA 未经认证,只是名字到地址的便利;发现未知节点只走登记表(Quick 协议 v2 §11.2)。

人类可读的短名无法从公钥派生,只能先到先得地分配,由分配方维护”短名 → 节点 ID”的绑定,申请同样由 k 签名:

alice.n.ns.io. CNAME <节点 ID>.n.ns.io.
分配方范围规则
NSD账户用户,账户内唯一由 Short DNS v2 投影,账户管理员可改名
relayQuick 用户有期限、需续期、每身份数量有上限,防止囤积;过期后释放

短名只是 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.ioAAAA + 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 配进系统搜索后缀(Linux search、macOS resolver search、Windows NRPT 后缀),用户直接输 alice 即可。
  • 不做:泛解析到 127.0.0.1。它只对带 Host / SNI 的 HTTP、TLS 有用,并且会被重绑定防护丢弃;dns-vip 的 127.127.0.0/16 VIP 由本机临时铸造,公网 DNS 无法对应。

n.ns.io 是 ns.io 运营方的公网 zone,只有运营方的 relay 与托管 NSD 能向它发布记录。自建 NSD 用自己的域名(例如 n.example.com),机制完全相同:zone 名是 NSD 与 relay 的配置项,节点侧的本地 resolver 按来源知道各自的 zone。公网 zone 的发布接口只接受已校验的 NodeRecord 与签名的短名申请,不接受任何手工记录。

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 与 NSDrelay 目录每项同时带 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=51822
KM id=2 host=hkg-a2.relay.ns.io ip=124.156.159.181 port=51822
SG 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 身份键证明,证书只保护传输。

呈现方式 \ 传输UDP 直连relay UDPrelay 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 共享同一组限制。

模块作用依赖的核心 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)。

项目借鉴不采用
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)

只借鉴设计,不复制代码。

与 nsplane 计划的阶段对齐;每一阶段都可独立交付,ns 现有功能在对应阶段前不变。落到 ns 与 nsgw 两个仓库的里程碑、crate 去向与两次不兼容切换见迁移计划。

阶段交付验证
Ansplane 第 1–4 阶段:引擎、多传输、netstack、ACL(在 nsplane 仓库进行)nsplane 自身门禁、与内核 WireGuard 的 e2e
B翻译过滤器进入 nsplane(合并 quick-runtime::translate 与 nat::subnet_route);Quick 协议 v2 的 relay 报文、登记表与 NodeRecord 在 relay 与 NSD 落地翻译向量测试;登记冲突测试
Cns 覆盖网核心:PeerSource、合并对端表、地址派生 v2;nsd:* 来源在引擎上运行(替代 tunnel-wg)账户模式回归;多 NSD 并存
Dquick 来源在引擎上运行(替代 quick-runtime 数据面,保留配对与持久化);跨来源策略Quick 回归;Quick ↔ 账户互通用例
E呈现层统一:Quick 获得 netstack、DNS-VIP、本地代理;WSS 成为 Transport三种呈现 × 四种传输矩阵
F功能模块化:服务发布、公网入口、子网、出口、应用拆成独立模块与开关禁用任一模块的回归
G应用模式:ns send、第三方应用接口文件发送 设计用例

已决定(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 兼容:不迁移状态与身份,每台设备重新登记。

本页没有未决事项。实现阶段的细节在各阶段的任务里决定。