NS · Quick 协议 v2
状态:节点侧已实现,relay 侧未实现。 ns 自 2026-10-04 起只实现本页(分支
refactor/nsplane,计划20261003-2145-quick-v2-on-engine;协议类型来自nsshared-proto),v1 的令牌、报文与状态已删除(§14)。relay 侧的 v2 服务端(登记表、NSR2、/rendezvous/v1)是 nsgw 的迁移里程碑 M2,尚未开始;在此之前 ns 的 Quick 只能对接显式指定的 v2 relay(开发与测试用 nsshared 的nsshared-mock-server),nsgw-quick-relay仍运行 v1。本页取代 v1,建立在新架构之上。协议引用(§n)除非另有说明均指本页。
1. 目标、非目标与分层
Section titled “1. 目标、非目标与分层”| 目标 | 推论 |
|---|---|
| 地址只由身份派生,全局唯一 | 没有网络、范围或 network_id;同一个身份在所有来源(Quick、NSD、应用)里地址相同 |
| 一份签名记录说明”这个身份当前用哪把 WireGuard 键” | NodeRecord(§3)同时用于 relay 注册、登记表、DNS TXT、隧道内更新与应用通道 |
| 离线的对端能自己追上密钥变化 | 登记表(§7)按节点 ID 查到最新记录;不需要手动导入,也不需要预约切换 |
| 配对只是应用通道上的一个应用 | 没有配对令牌与 INTRO;应用通道的 confirm 引入交换签名记录并派生 PSK(§10.2) |
| 不经会合也能互相放行 | 双方各自按节点 ID 放行对方,经登记表取得记录(§10.3) |
| 没有承重的单点 | 已配置的对端在没有 relay 与登记表时仍能直连通信 |
非目标:对端之间的中转、子网、服务发布、出口(这些是新架构 §7 的功能模块);按名字或前缀查找节点;身份 I 丢失后的恢复。
本页按层组织。§2–§9 是所有对端来源共用的节点协议:账户模式的节点同样使用 §2 的派生、§3 的记录、§7 的登记表、§8 的 relay 报文与 §9 的控制通道。§10 是 Quick 来源本身,§11 是名字。各层在实现中的位置:
| 层 | 本页 | 实现 |
|---|---|---|
| 身份、派生、记录 | §2–§4 | nsshared 的 nsshared-proto(取代 quick-wire) |
| 数据面不变量、IPv4 翻译 | §5、§6 | nsplane 引擎的 PacketFilter 链 |
| 登记表、relay 报文 | §7、§8 | 消息类型在 nsshared-proto;ns 的 RelayClient 与 relay |
| 隧道内控制通道 | §9 | nsplane 引擎的过滤器(Handled)+ ns 的路径阶梯 |
| Quick 来源 | §10 | ns overlay 的 quick PeerSource;配对经 ns 的应用通道 |
1.1 规模约束
Section titled “1.1 规模约束”| 数量 | 预算 |
|---|---|
| 累计身份 | ≤ 10⁹ |
| 同时在线节点 | ≤ 10⁸ |
| 每个节点已配置的对端 | 默认 ≤ 10⁴,硬上限 10⁵ |
| 每个 relay 保存的登记记录 | 默认 ≤ 10⁷(§7.4) |
| 准备期限 | 5 s:从本地安装对端到 WireGuard 路径可用并完成本地发布 |
超出预算是带具体原因的容量错误,从不静默降级。
2. 身份与派生
Section titled “2. 身份与派生”2.1 两把密钥
Section titled “2.1 两把密钥”每个节点有一把长期的 Ed25519 身份键 I 与一把可轮换的 X25519 WireGuard 键 K。I 签名记录;K 认证隧道。所有派生只以 I 为输入,轮换 K 不改变地址与名字。一个身份同一时刻只运行在一台设备上;同一个 I 出现两把不同的 K 按冲突处理(§3.3)。
2.2 派生
Section titled “2.2 派生”所有派生都是带密钥的 BLAKE2s,密钥是下表的 ASCII 标签(域分隔,不是机密)。标签发布后不再改,格式变化时换代号。
| 标签 | 输入 | 输出 | 结果 |
|---|---|---|---|
ns-node-id-v2 | I | 12 字节 | 节点 ID:小写、无填充 RFC 4648 base32,20 字符;原始 12 字节同时是 relay slot(§8.1) |
ns-node-addr-v2 | I | 15 字节,右移 1 位得到 119 位 g | 地址组:addr(I, m) = fd ‖ g ‖ m |
ns-alias4-v2 | I ‖ kind_u8 ‖ i_u8 | 3 字节,取高 22 位 | 本地 IPv4 别名的第 i 个候选(§6.2) |
g = BLAKE2s(out = 15, key = "ns-node-addr-v2")(I) >> 1N6(I) = fd ‖ g ‖ 0 原生 IPv6 服务地址(新架构中的 node6)N4(I) = fd ‖ g ‖ 1 IPv4 服务地址;发往它的报文在接收端翻译为 IPv4(§6)group = N6(I)/127- 每个节点一个
/127地址组。两个地址让 IPv4 与 IPv6 的映射无需逐流状态即可双向还原:IPv4 套接字在报文上总是N4(self),原生 IPv6 套接字总是N6(self);远端只监听 IPv4 的服务经N4直接可达,不需要端口桥(§6.1)。 - 碰撞:
B个身份中任意两个地址组相同的概率约为B²/2¹²⁰,B = 10⁹时为 7.5 × 10⁻¹⁹。唯一性是概率性的:节点从不安装与自己或已有对端地址组相同的对端,碰撞时拒绝并报告冲突的完整身份(address_conflict)。安全身份始终是完整的I,从不是它的哈希。 - 发布的 LAN 前缀
lan6由新架构 §4.1 定义,属于子网路由功能,不在本页。
2.3 示例
Section titled “2.3 示例”演示身份由种子 SHA-256("ns-demo-node-v2") 生成,WireGuard 键由 SHA-256("ns-demo-wg-v2") 生成:
I = x9wqBt4ZKdMxi43oc78ScDDD7gAH8g4G6g_FQFZ0Wvs (base64url)K = NZkMHL1XEJhrcG68XIzeUZktuR/U1aR2e6u2dcRMzDk= (base64,与 wg 工具一致)节点 ID = 5kemem6slxrgyd63rcqaN6 = fda6:72ac:58cb:cc8d:1e39:e507:bda:5052N4 = fda6:72ac:58cb:cc8d:1e39:e507:bda:50533. 节点记录
Section titled “3. 节点记录”3.1 格式
Section titled “3.1 格式”NodeRecord = [ ver u8 = 2 I bytes32 Ed25519 身份公钥 K bytes32 X25519 WireGuard 公钥 gen u64 K 的代际,每次轮换加一,从 1 开始 issued_at u64 Unix 秒;同一个 I 严格递增 expires u64 Unix 秒,不含;只限制登记表与 DNS 的服务期限(§3.2) relays text[] 0..4 个 relay 标识(§8.6),节点持有活动 slot 的 relay,home 在前 sig bytes64 Ed25519_I("ns/node-record/v2" ‖ CBOR(前七个字段))]- 规范 CBOR 定长数组(§12.1);解码器在验签之前拒绝非规范编码、未知版本与多余字节。最大 600 字节。
- 记录不含直连端点:端点变化频繁且暴露位置,只在隧道内交换(§9)。
issued_at由节点取max(now, 上一次 issued_at + 1)并持久化,墙钟回拨不会产生更小的值。expires - issued_at不超过 7 天,默认 7 天;剩余不足一半时,或relays变化时,节点用相同的gen与K重签一条新记录。
3.2 有效性
Section titled “3.2 有效性”记录在以下条件下有效:编码规范、ver = 2、签名在 I 下校验通过、expires > issued_at、relays 每项符合 §8.6。
expires 不是密钥有效期。节点安装或保留对端时不检查它;K 一直可用,直到出现更高的 gen。只有两处检查 expires:relay 不为已过期的记录注册或应答(§7、§8),客户端丢弃已过期的登记表或 DNS 应答。
3.3 顺序与冲突
Section titled “3.3 顺序与冲突”每个保存记录的一方(节点为每个对端、relay 为每个身份)按 I 保存:committed_gen、该代际的当前记录 R,以及冲突证据 E(可为空)。收到有效的候选记录时:
| 候选 | 处理 |
|---|---|
gen < committed_gen | 拒绝 |
gen > committed_gen | 接受:R = 候选,E 清空,committed_gen = gen |
gen == committed_gen,K 与 R 相同 | 候选在下述顺序上更大时替换 R(issued_at 更大即只替换记录字节:relays、expires);否则幂等 |
gen == committed_gen,K 不同 | 冲突:锁存,该代际被禁用(节点移除这个 WireGuard 对端,relay 拒绝注册与应答),直到出现更高的 gen;报告 record_conflict |
状态是”该代际收到过的全部记录”的确定性摘要。同一代际的记录按 (issued_at, 规范编码字节) 比较大小:R 是其中最大的一条;E 是 K 与 R 不同的记录里最大的一条,没有则为空。E 非空即冲突锁存,并保留了引起冲突的那条记录供审计。因此以任何顺序收到同一批记录,得到的状态都相同,两份状态可以确定性地合并(§7.6)。
更高的 gen 就是对旧 K 的撤销,没有单独的撤销记录。被拒绝的输入从不改变状态。冲突意味着同一个 I 在两台设备上使用(克隆或泄露),用户需要在真正的设备上执行 key rotate。
4. K 轮换
Section titled “4. K 轮换”轮换是一次立即切换,由 ns quick key rotate 触发;K 没有有效期,因此没有自动续期、预约时刻或时钟要求。
- 生成
K',签一条gen + 1的记录R',与私钥一起原子持久化为”下一把密钥”。 - 经每个已建立的会话向对端发送隧道内
RECORD(R')(§9),最多等待 5 s 的 ACK;没有 ACK 不推迟切换。 - 原子地把引擎私钥换成
K'并提交代际,然后以R'与新的 incarnation 在每个 relay 重新注册(§8.3),并向登记表发布R'(§7.2)。
收到 R' 的对端立即换用新的公钥,第一次新握手完成后即恢复通信;切换前后几秒内的报文可能丢失,Quick 不承诺无损轮换。错过 R' 的对端按 §7.3 的重新同步规则从登记表取得它。重启时若存在已持久化但未提交的下一把密钥,从第 3 步继续。
gen 是 u64,从不回绕;用尽时只能换新身份。
5. 数据面不变量
Section titled “5. 数据面不变量”每个节点一个 WireGuard 引擎、一把活动的 K。每个对端恰好以其 N6/128 与 N4/128 为 AllowedIPs(仅限本次会话的应用对端只有 N6),从不更宽。引擎的过滤器链在每个平台、每种呈现方式(TUN、用户态协议栈)下执行:
- 入站目的地:解密后,目的地址必须是
N6(self)或N4(self),否则丢弃并计数;在翻译与任何本地投递之前检查。 - 不转发:来自对端的报文从不发往另一个对端或物理接口;翻译器的改写(§6)是唯一允许的改写。
- 出站源地址:进入隧道的报文以
N6(self)或N4(self)为源地址,否则在加密前拒绝。 - 拒绝路由:已移除对端的地址与别名保留拒绝路由与 DNS 墓碑,流量在本地失败而不经默认路由泄漏。
账户模式的子网、服务与出口功能按新架构 §7 放宽 1 与 2,且只对授予它们的来源生效;Quick 来源从不放宽。
6. 节点本地 IPv4
Section titled “6. 节点本地 IPv4”6.1 模型
Section titled “6.1 模型”IPv4 只存在于节点内部,报文上只有 fd00::/8 地址:
| 本地 IPv4 | 报文地址 | 含义 |
|---|---|---|
self4(默认 100.64.0.1) | N4(self) | 本节点 IPv4 套接字在报文上的样子 |
alias6(b) | N6(b) | 对端 b 的原生 IPv6 服务 |
alias4(b) | N4(b) | 对端 b 只监听 IPv4 的服务 |
出站(本机 → 隧道) IPv4 dst alias6(b) | alias4(b) → N6(b) | N4(b);IPv4 src self4 → N4(self)入站(隧道 → 本机) IPv6 dst N4(self) → self4(总是翻译);N6(self) 原样投递 IPv6 src N6(a) | N4(a) → alias6(a) | alias4(a)关闭 IPv4 的节点只安装 N6,不分配别名。
6.2 别名表
Section titled “6.2 别名表”- 地址池默认
100.64.0.0/10,可配置。候选i(0..15)为pool.base + (H22(I, kind, i) mod pool.size),kind为 6 或 4;跳过self4、表中已有的地址与被主机路由、接口地址或防火墙规则覆盖的地址;16 个都不可用时报告冲突。 - 每个别名一条以
self4为首选源的/32主机路由,从不安装覆盖整个地址池的路由。运行期间监视主机路由,新出现冲突的别名被暂停(拒绝路由、DNS 墓碑)并报告,从不静默移动。 - 别名一经分配就保留;移除对端后成为墓碑,只会为同一个
(I, kind)复用。 - 别名、路由、WireGuard 对端、翻译条目与名字作为一个整体发布:先发布映射,最后发布名字;移除时先撤销名字并加拒绝路由。
6.3 翻译
Section titled “6.3 翻译”无流表的 RFC 7915 翻译,显式地址映射(RFC 7757):TCP / UDP 校验和增量更新(RFC 1624),零校验和的 IPv4 UDP 完整计算;IPv4 分片翻译为带 Fragment 头的 IPv6,只有零校验和的分片 UDP 需要重组(60 s、有字节预算);ICMP 错误连同内嵌报文头一起翻译并限速;范围限于单播 TCP、UDP 与 ICMP echo / 错误,不提供应用层网关。
MTU:隧道接口 1420,IPv4 侧宣告 1400;经 relay 的内层上限默认 1376,即 16 × floor((外层路径 MTU − 外层 IP 头 − 8 − 29 − 32) / 16) 在 1500 字节 IPv6 外层下的值(relay 头 29 字节,§8.1;WireGuard 数据开销 32 字节),翻译后的 IPv4 再减 20。引擎在加密前执行所选路径的上限,生成限速的 ICMPv6 Packet Too Big(经翻译成为 IPv4 Fragmentation Needed),从不宣告低于 1280 的 IPv6 MTU,换路径时重新验证。翻译、分片与 MTU 在 nsplane 的翻译过滤器里实现一次,TUN 与用户态协议栈共用。
7. 登记表
Section titled “7. 登记表”登记表让任何节点只凭节点 ID 取得对方的当前记录。它由 relay 提供,按节点 ID 分片,客户端自己选择分片,relay 之间不需要新的协议。
7.1 登记域与分片
Section titled “7.1 登记域与分片”- 登记域是节点使用的 relay 目录(会合协议 §5.4)中宣告
registry能力的条目;节点只配置了一个 relay 时,登记域就是它。 - 某个节点 ID 的分片 relay 由最高随机权重(HRW)选出:对登记域中每个条目
e计算w = BLAKE2s(out = 8, key = "ns-registry-shard-v2")(node_id ‖ e.id_be16),按w降序(相同时按id升序)排列,取前两个。 - 目录版本变化会移动少量分片。发布方在目录变化后重新发布;查找方先问前两个,都没有时再问第三、第四个,以覆盖一次目录变化。
- 分片 relay 若是 relay 区域,记录在区域成员之间按 v1 区域标记的单调合并规则复制(v1 §12.8),任一成员都能应答。
7.2 操作
Section titled “7.2 操作”登记表操作位于 relay 的会合服务 /rendezvous/v1 上(会合协议 §5.2),只在 relay 宣告 registry 能力时提供,不需要参与方 hello:
| 请求 | 载荷 | 结果 |
|---|---|---|
publish | {record, dns, proof} | published {expires} |
lookup_node | {node_id} | record {record},或 record {record: null} |
dns是公网 DNS 的发布档位,取"none"(默认)、"address"或"record"(§11.2);省略时按"none"处理。proof = Ed25519_I("ns/registry-publish/v2" ‖ challenge.nonce ‖ authority ‖ SHA-256(record) ‖ dns_u8),dns_u8依次为 0、1、2。只有身份持有者能发布,转手重放一条公开的记录不会改变dns选择。- relay 校验记录有效且未过期、
proof正确、BLAKE2s-96(I) 落在本 relay 负责的分片内(宽松检查:本 relay 在当前目录中是该 ID 的前四个之一),再按 §3.3 与已存记录比较。被拒绝时返回conflict(冲突锁存或代际更低)或invalid_message。 dns = "none"时记录只供精确查找;"address"时登记域的权威 DNS 只发布 AAAA;"record"时发布 AAAA 与 TXT(§11.2)。从高档改回低档时,权威 DNS 立即撤回多出的记录,但已经被缓存或存档的内容无法收回(§11.3)。- 档位按记录的修订
rev = (gen, issued_at)跟踪:relay 为每个身份保存档位修订pub_rev与在该修订上见过的档位集合。一次被接受的发布,其记录的rev大于pub_rev时,pub_rev = rev、集合 ={dns};等于时把dns加入集合;小于时不改变档位(记录本身仍按 §3.3 处理)。集合只含一个档位时它就是生效档位;同一修订上出现两个以上档位即发布冲突,在出现更大的修订之前,权威 DNS 不输出该身份的任何记录。记录冲突锁存(§3.3)期间同样不输出。 - 因此节点改变档位时必须先重签一条
issued_at更大的记录(§3.1),从不用同一条记录换档位重发;较旧的记录永远不能改变档位。代价是每次改档位多签一条记录。 lookup_node只接受完整的 12 字节节点 ID,只返回未过期的记录;冲突锁存中的 ID 返回conflict。没有枚举、前缀或模糊查询。查得到不等于可以通信:是否安装对端由每个节点的来源决定。- 记录在
expires之后 10 s 内删除。
7.3 何时发布与查找
Section titled “7.3 何时发布与查找”| 节点动作 | 时机 |
|---|---|
| 发布 | 首次启动、每次重签记录(§3.1)、轮换(§4)、目录版本变化;失败时有界退避(5 s 翻倍至 5 min,±25 %) |
| 按 ID 放行时查找 | 放行时立即查找,查不到时按同样的退避重试(§10.3) |
| 重新同步 | 已配置的对端连续 3 次握手发起没有应答(约 15 s)且仍有待发流量时查找一次;查到更高 gen 或新的 relays 时立即安装并重试握手;仍未恢复时最多每 5 min 再查一次 |
| 收到陌生公钥的握手 | 不查找;引擎丢弃未配置公钥的握手 |
7.4 上限
Section titled “7.4 上限”| 上限 | 默认值 |
|---|---|
| 每个 relay 保存的记录 | 10⁷;满时拒绝新身份(capacity),不驱逐未过期的记录 |
publish | 每源 IP 每分钟 12 次;每个 I 每 60 s 1 次;全局每秒 1 000 次 |
lookup_node | 每源 IP 每分钟 60 次;全局每秒 5 000 次 |
7.5 relay 能看到什么
Section titled “7.5 relay 能看到什么”分片 relay 能看到发布者的完整记录(身份、WireGuard 公钥、home relay)与源地址,以及谁按哪个节点 ID 查找了谁。它不能伪造记录,也不能把一个节点 ID 指向别的键(客户端校验 BLAKE2s-96(I) = 节点 ID 与签名);它可以拒绝应答或返回已被取代但仍未过期的旧记录,后者会在对端握手失败后的下一次重新同步中被更新的记录取代。
7.6 保存、恢复与合并
Section titled “7.6 保存、恢复与合并”relay 的登记状态,以及节点为每个对端保存的 §3.3 状态,可以导出为规范 CBOR(§12.1),在重启后恢复,并在区域成员之间合并:
RecordState = [ver = 1, I bytes32, R record_bytes | null, E record_bytes | null]RegistryState = [ver = 1, RecordState, pub_gen u64, pub_issued_at u64, tiers u8]record_bytes是记录原样的规范编码(§3.1)。tiers是档位集合的位图:bit 0 为none,bit 1 为address,bit 2 为record;没有被接受过的发布时pub_gen = pub_issued_at = tiers = 0。- 恢复时逐项校验,任何一项不符就拒绝整份状态:每条记录按 §3.2 有效且属于
I;E非空时R非空、E.gen == R.gen、E.K != R.K,且E在 §3.3 的顺序上小于R;R为空时E为空、tiers = 0;tiers只用低三位,tiers = 0当且仅当pub_gen = pub_issued_at = 0。 - 合并同一个
I的两份状态:RecordState取两份{R, E}的并集,按 §3.3 重新计算committed_gen、R与E;档位取pub_rev较大的一方,相等时tiers按位或。合并满足交换律、结合律与幂等,成员以任何顺序、任何次数交换状态都会收敛到同一结果。 - 过期:
R.expires + 10 s已过的状态在合并与导出之前视为空(§7.2 的删除)。 - 成员之间如何传输这些状态(复制帧、快照、owner 栅栏)属于 relay 的区域协议,不在本页。
8. relay 报文 v2
Section titled “8. relay 报文 v2”relay 在 WireGuard 之下转发密文、为节点反射 UDP 端点,不持有覆盖网私钥,不是对端。这是所有对端来源共用的唯一 relay 协议(新架构 §6)。
8.1 报文头
Section titled “8.1 报文头”一个 UDP 报文或一个完整的二进制 WebSocket 消息恰好承载一个 relay 报文,最大 2048 字节;非 DATA 类型最大 1200 字节。
| 偏移 | 宽度 | 字段 |
|---|---|---|
| 0 | 4 | ASCII 魔数 NSR2 |
| 4 | 1 | type |
| 5 | 12 | src slot:发送方的节点 ID 原始字节 |
| 17 | 12 | dst slot |
| 29 | 余下 | body |
全零 slot 表示 relay;节点 ID 恰为全零的身份不能注册。relay 保存完整的 I,拒绝不同身份的 slot 碰撞。
8.2 类型
Section titled “8.2 类型”| 类型 | 名称 | 方向 | src / dst | 报文体 |
|---|---|---|---|---|
| 1 | REGISTER | 节点 → relay | 非零 / 零 | [record, incarnation u64, ts u64, node_ephemeral bytes32, relay_name text, sig bytes64] |
| 2 | REGISTERED | relay → 节点 | 零 / 非零 | [incarnation, relay_ephemeral bytes32, presence_ttl_s u32, observed_addr, ack_seq u64, capabilities text[], mac bytes32] |
| 3 | PRESENCE | 节点 → relay | 非零 / 零 | [incarnation, seq u64, mac] |
| 4 | REFLECT | 节点 → relay | 非零 / 零 | [incarnation, seq, nonce bytes16, mac] |
| 5 | REFLECTED | relay → 节点 | 零 / 非零 | [incarnation, seq, nonce bytes16, observed_addr, mac] |
| 6 | DATA | 节点 → relay → 节点 | 非零 / 非零 | 原样的 WireGuard 报文,1..2019 字节,不是 CBOR |
| 7 | STATUS | relay → 节点 | 零 / 非零 | [type_in u8, destination bytes12, status u8] |
| 8 | REVOKE | 节点 → relay | 非零 / 零 | [incarnation, seq, reason u8, mac] |
- 除 DATA 外,报文体是一个规范定长数组。
observed_addr为[ip bytes4|bytes16, port u16],WSS 上为null。capabilities是会合协议 §3.1 的标记,只在ack_seq = 0的暂定应答中非空。 - STATUS:1 无目的地、2 被限速、3 过大、4 代际或 incarnation 陈旧、5 不支持的类型、6 发送方未注册或源不匹配。STATUS 不证明投递,从不回复 STATUS。REVOKE 的 reason:0 轮换、1 关闭。
- v1 的 INTRO / INTRO_ACK 已删除:准入全部在会合信箱或登记表之上完成,relay 只转发 DATA。
8.3 注册
Section titled “8.3 注册”签名与密钥:
sig = Ed25519_I("ns/relay-register/v2" ‖ CBOR([record, incarnation, ts, node_ephemeral, relay_name]))reg_hash = SHA-256(CBOR(六字段 REGISTER 报文体))sk = HKDF-SHA256(ikm = X25519(临时私钥, 对方临时公钥), info = "ns/relay-session/v2" ‖ src_slot ‖ incarnation_be64 ‖ node_ephemeral ‖ relay_ephemeral ‖ reg_hash)mac(3、4、8) = HMAC-SHA256(sk, "ns/relay-ctl/v2" ‖ CBOR([magic, type, src, dst, 去掉 mac 的报文体数组]))mac(2、5) = HMAC-SHA256(sk, "ns/relay-reply/v2" ‖ CBOR([magic, type, src, dst, 去掉 mac 的报文体数组]))规则(与 v1 相同的部分只列结论):
- 在任何密码学运算之前检查长度、魔数、方向、schema、预算、
src = 节点 ID(record.I)、relay_name等于本 relay 的公布标识,以及|now - ts| ≤ 120 s。然后校验记录有效且未过期、REGISTER 签名,并按 §3.3 与本 relay 保存的标记比较;冲突锁存与更低代际被拒绝。拒绝全零的ss。 - 暂定注册以确切的已签名 REGISTER 与源为键,最长 10 s;相同的重传得到相同的暂定 REGISTERED,不延长寿命;其他源不能移动已有的源。
- 节点校验暂定 REGISTERED 后用
sk发送seq为正的 PRESENCE;relay 先持久提交标记(committed_gen、K、冲突、incarnation 高水位)、源、incarnation 与sk,再以相同ack_seq的 REGISTERED 确认。未确认的 slot 不能转发;持久化失败时拒绝服务。 - 同一
gen内新会话需要更大的 incarnation;更高的gen可以重置 incarnation。incarnation 按 relay 与代际持久化,跨载体递增。 seq每个会话从 1 递增;接收方接受高于高水位的已认证值,允许空缺。只有被接受的节点控制报文刷新存活。presence 在”最后控制时间 + TTL”与record.expires中较早者过期;TTL 30..3600 s,默认 90 s;PRESENCE 间隔不超过min(TTL/3, 25 s)。- NAT 重绑定、载体迁移与记录更新(重签、轮换)都通过新的 incarnation 重新注册完成。
- REFLECT 只在已确认的 UDP 会话上,每个 PRESENCE 间隔最多一次。
UDP 启动在 0、1、2 s 重传同一个 REGISTER,3 s 内没有暂定应答时允许回退到 WSS;多地址 authority 每轮最多并行 3 个地址、只确认一个。完整的选择、回退与区域规则沿用 v1 §12.5,只把 KeyBinding 换成记录、(network_id, I) 换成 I。
8.4 转发
Section titled “8.4 转发”- DATA 要求 src 活动且来自确切的源、dst 在本成员活动或在同区域其他成员有合格的 presence。没有范围检查:任意两个已注册节点之间都可以转发,是否接受由接收端的 WireGuard 与来源决定。
- relay 只读 DATA 的第一个字节以识别握手发起报文,计入按目的地的握手发起预算。
- 接收方先检查 dst 是自己的节点 ID、src 是已安装对端的节点 ID,否则在解密前丢弃;然后把剥离报文头的 WireGuard 报文交给引擎。直连路径上不使用 relay 报文头。
8.5 载体、预算与区域
Section titled “8.5 载体、预算与区域”- WSS:
wss://host:port/ns/v2,子协议ns-relay-v2;一个二进制消息一个报文,不压缩,文本或超大消息关闭连接;TLS 按 relay 标识的主机名校验。 - 预算沿用 v1 §5.2.1 的生产默认值,删除 INTRO / INTRO_ACK 一行,37 字节报文头改为 29 字节。
- relay 区域(2..16 个成员、一跳 mesh、标记单调合并、确定性 presence)沿用 v1 §12.8,只做同样的替换:slot 为 12 字节,标记以
I为键,KeyBinding 标记换成 §3.3 的记录标记。
8.6 relay 标识
Section titled “8.6 relay 标识”relay 标识为 host:port:host 是小写 DNS 名字、IPv4 字面量或带方括号的 IPv6 字面量,port 为 1..65535,整体不超过 255 字节,语法同 v1 §12.5.1 的 authority。v1 的短 ID 删除:目录项(会合协议 §5.4)的 host:port 就是标识,ips 只是连接提示。已签名的标识从不被改写。
9. 隧道内控制通道
Section titled “9. 隧道内控制通道”两个对端之间在隧道内用 UDP,从 N6(self):47900 到 N6(peer):47900。引擎在入站检查(§5)之后、翻译与本地投递之前消费这些报文(过滤器返回 Handled),本地协议栈从不看到这个端口。它对合并对端表中的每个对端生效,与来源无关。
一个报文是一个最多 1200 字节的规范数组 [ver u8 = 2, type u8, body]。WireGuard 会话认证发送方。
| 类型 | 名称 | 报文体 |
|---|---|---|
| 0 | ACK | [type_in u8, hash bytes16],hash 为被确认报文字节的 16 字节无密钥 BLAKE2s |
| 1 | ENDPOINTS | [revision u64, candidates, relays];candidates 最多 8 个 [ip, port, kind](0 本地接口、1 relay 反射、2 配置);relays 最多 4 个 relay 标识,发送方在其上持有活动 slot |
| 2 | RECORD | [record],发送方自己的 NodeRecord |
| 3 | PROBE_REQUEST | [nonce bytes32] |
| 4 | PROBE_REPLY | [nonce bytes32] |
-
需确认的类型 1、2:会话建立时与内容变化时发送,在 1、2、4 s 重传直到收到哈希匹配的 ACK;每个对端最多 4 条未确认。格式错误或被拒绝的记录不得到 ACK。
-
RECORD:
record.I必须等于该对端的I,然后按 §3.3 处理;更高的gen立即替换该对端的 WireGuard 公钥(§4)。 -
ENDPOINTS:
revision按本节点持久、严格递增,跨会话、载体与轮换;接收方持久化已接受的最大版本与快照哈希。更低版本忽略;同版本同字节幂等确认;同版本不同字节报告endpoint_state_conflict且不确认;更大的版本原子替换两个列表。候选与 relay 的排序、截断与”每个对端发送同一子集”的规则沿用 v1 §12.7。 -
探测与路径选择沿用 v1 §12.7:每个候选每 250 ms 最多一个请求、1 s 期限、30 s 一轮;只有来自确切尝试元组、回显活动 nonce、在当前会话下解密的回复晋升直连;非保活的出站流量 15 s 无应答即判定直连丢失,回退到双方公共的 relay;home relay 优先。这些规则即是新架构 §6 中统一路径阶梯的直连与 relay 两级,WSS 是 relay 的另一种载体。
-
对端反射候选。 节点收到一个
PROBE_REQUEST,它在某个已安装对端的当前会话下解密、并且直接到达本节点的 UDP 载体(不经 relay,不经 WSS),而它的外层源(ip, port)不在该对端的候选中时,节点可以把这个源记为该对端的对端反射候选:- 只能从这样的
PROBE_REQUEST学习。握手、cookie 回复、PROBE_REPLY、健康检查回复、DATA以及任何经 relay 到达的报文都不得产生对端反射候选;观测到的源从不移动 relay slot。 - 不得对外发布:不出现在本节点自己的
ENDPOINTS中,不持久化,也不提供给任何其他对端。 - 与发布的候选同样探测:同一套
PROBE_REQUEST/PROBE_REPLY关联、每个候选 250 ms 节奏、1 s 期限与 30 s 一轮,以及 v1 §12.6 的每对端与每节点尝试、请求上限;只有来自它确切元组的关联回复才能晋升。学到时加入进行中的一轮;没有进行中的一轮且 30 s 间隔允许时开始一轮;从不缩短间隔,也从不抢占已确认的直连路径。 - 每个对端最多 2 个,新的替换最旧的未确认者;每个对端每 250 ms 最多学习 1 个,全节点每秒最多 64 个,超出的丢弃且无任何效果。
- 最后一次学习后 30 s 过期,除非它是选定的直连端点;对端的
K被替换或对端被移除时全部丢弃;一次新的已认证握手(包括例行换密钥)只丢弃未被选定的对端反射候选;路径从某个对端反射候选降级时只丢弃该候选。接受新的ENDPOINTS只替换发布的候选列表,既不删除也不恢复对端反射候选。 - 一旦被选定,它与任何直连端点遵守相同规则:只有来自它确切元组的已认证入站流量或被接受的健康检查回复保持它存活,15 s 丢失规则与 relay 回退不变。
安全考虑。 外层源未经认证:路径上的攻击者可以从别的源重放截获的请求,NAT 也可能改写它。因此学到的候选只是提示,在一次关联的
PROBE_REPLY证明当前会话下经该确切元组与对端往返之前不获得任何信任,这与发布的候选所需的证明相同。发往它的探测有界且大小相同(v1 §12.6),并且只有请求通过认证的已安装对端才能触发,所以节点不能被用来向第三方放大流量:每个已认证请求最多产生一个大小相同的回复,外加现有上限内的探测。从不发布这个地址,避免一个对端的 NAT 映射泄露给其他对端。 - 只能从这样的
10. Quick 来源
Section titled “10. Quick 来源”10.1 状态
Section titled “10.1 状态”Quick 来源是一个没有服务器的 PeerSource(新架构 §5.1),它的全部状态都在本地:
Allow { I, added_by ∈ {pairing, id}, psk: bytes32 | null, added_at }Removed { I, removed_at }PeerRec { I, record, committed_gen, conflicted } // §3.3Nickname { label, I }对每个 I:在 Allow 中、不在 Removed 中、且有可接受的记录时,来源向合并对端表输出一个对端 {I, K, AllowedIPs = N6/128 + N4/128, psk, relays, keepalive 25 s}。授权是双向的:只有双方都输出了对方,WireGuard 握手才能完成。Quick 来源内的对端互相不可见,A–B 与 A–C 不让 B 到达 C。
10.2 配对(应用通道 kind = "pairing")
Section titled “10.2 配对(应用通道 kind = "pairing")”配对是应用通道上的一个应用,profile 为 {kind: "pairing", auth: confirm, lifetime: permanent, ttl: 默认 600 s、最多 900 s, app_port: null}。消息格式、校验、密钥派生与安全性质见应用通道 §4、§5.1 与 §8;本节只列 Quick 来源特有的部分:
- 短码。
invite打印 Quick 模式 §4 的短码(relay 短码 + 8 位数字)。 - 确认。 加入方显示邀请方的节点 ID 与
name并要求确认,join --peer <节点ID>匹配时自动确认、不匹配时失败;邀请方对每个加入方同样要求确认,invite --recipient <节点ID>匹配时自动接纳。 - 安装。 写入
Allow {added_by: pairing, psk}与对方记录,并输出对端(§10.1)。已存在的对端被重新配对时只替换 PSK 与记录,保留别名与昵称。 - 准备。 从安装起 5 s 内 WireGuard 路径可用并完成本地发布,否则报告
prepare_timed_out,保留对端供正常重连。 - 移除记录。 对方在本机
Removed中时,在显示确认之前拒绝(§10.4)。
10.3 按节点 ID 放行
Section titled “10.3 按节点 ID 放行”ns quick allow add <节点ID>写入 Allow {added_by: id, psk: null},然后按 §7.3 查找对方的记录;查到后校验 BLAKE2s-96(record.I) = 节点 ID、记录有效、地址组不冲突,再输出对端。对方也按 ID 放行了本机(或此前已配对)时即可连接。状态依次为 waiting_for_record、waiting_for_peer(已安装,尚未完成握手)与 connected。这条路径不需要会合,也没有 PSK;只在双方能经可信渠道得到对方节点 ID 时使用。
10.4 移除与恢复
Section titled “10.4 移除与恢复”ns quick peer remove PEER 写入 Removed,删除 Allow 与 WireGuard 对端,别名成为墓碑并加拒绝路由,撤销名字。被移除的身份不能经配对或按 ID 放行重新加入,直到 ns quick allow restore PEER 删除移除记录;之后需要重新配对或放行。
11. 名字
Section titled “11. 名字”11.1 本地
Section titled “11.1 本地”本地 resolver(新架构 §5.4.3,属于呈现层)为每个 Quick 对端应答:
| 名字 | AAAA | A |
|---|---|---|
<节点ID>.n.ns.io | N6 | alias6 |
<节点ID>.v4.n.ns.io | N4 | alias4 |
<昵称>.n.ns.io | N6 | alias6 |
<昵称>.v4.n.ns.io | N4 | alias4 |
昵称是本地的:小写 [a-z0-9-],1–63 字节,首尾不是连字符,v4 保留;只在本机唯一,从不发布。配对时默认取对方 name 规范化后加 - 与节点 ID 前 4 个字符,冲突时不分配默认昵称并报告。PTR 指向昵称(有时)或节点 ID 名字。名字不可用时一切照常,命令处处接受节点 ID、昵称与地址。
11.2 公网
Section titled “11.2 公网”公网发布由节点在登记表 publish 中选择(§7.2),分三档,默认不发布:
| 档位 | 权威 DNS 发布的内容 | 用途 |
|---|---|---|
none(默认) | 无;记录只在登记表中供按节点 ID 精确查找 | 绝大多数节点 |
address | <节点ID>.n.ns.io. AAAA N6 | 需要在没有本地 resolver 的环境(浏览器、容器、其他工具)用名字访问,又不想公开记录 |
record | AAAA,加上完整记录的 TXT | 公开服务的节点:希望任何人只凭 DNS 就能拿到可验证的记录 |
record 档的示例:
5kemem6slxrgyd63rcqa.n.ns.io. AAAA fda6:72ac:58cb:cc8d:1e39:e507:bda:50525kemem6slxrgyd63rcqa.n.ns.io. TXT "v=ns2 r=iAJYIMfcKgbeGSnTMYuN6HO_EnAww-4AB_IOBuoPxUBWdFr7WCA1mQwcvVcQmGtwbrxcjN5RmS25H9TVpHZ7q7Z1xEzMOQMaaVW5ABppXvOAgXgYaGtnLWExLnJlbGF5Lm5zLmlvOjUxODIyWECkn2HAyOlMQSYLsKZwSQXVg3BGBKW08g9tslS3ReFfQodZ6hsaJEyCKYY6XCmiAkWY8mXZYSrHunEBmpQfdccP"TXT 的 r 是完整 NodeRecord 的无填充 base64url;超过 255 字节时拆成多个字符串,客户端拼接。上例是 §2.3 演示身份 gen = 3、issued_at = 1767225600、expires = 1767830400、relays = ["hkg-a1.relay.ns.io:51822"] 的记录(174 字节,TXT 240 字节)。客户端校验:记录有效且未过期、BLAKE2s-96(I) 等于查询的标签、N6(I) 等于 AAAA;任一不符就丢弃应答。因此 record 档的应答不需要信任 resolver 与权威服务器,也不依赖 DNSSEC。
address 档的 AAAA 未经认证:没有记录,客户端无法核对 N6(I)。它只是名字到地址的便利,从不用于发现对端或决定信任:
- ns 从不使用公网 AAAA;对端的地址总是由本机按
I计算,本地 resolver 优先。 - 被篡改的 AAAA 最多把流量导向另一个已安装、已认证的对端的
N6,或导向不存在的地址而被丢弃:每个对端的AllowedIPs由本机按其I计算,隧道之外到不了任何人。需要确认对方身份的应用应在其上使用 TLS。 - 发现一个未知节点(取得
K与 relay)只走登记表的lookup_node(§7.3)。
两档都不发布 .v4 视图与 A 记录。
11.3 公开的隐私影响
Section titled “11.3 公开的隐私影响”公开的内容会被 resolver 缓存、被 passive DNS 数据库与公共存档长期保存;改回低档或停止发布只能阻止新的数据,已公开的内容无法收回。
| 公开内容 | address | record | 可被推断出的信息 |
|---|---|---|---|
节点 ID 与 N6 | 是 | 是 | 一个长期不变的标识;只凭节点 ID 算不出 N6,发布后两者被关联 |
I | 否 | 是 | 跨场合关联同一台设备(配对、文件发送与命令输出同样出现节点 ID) |
K | 否 | 是 | 任何人都能构造 mac1 合法的握手发往该节点,迫使它做 DH 后丢弃(只能靠 relay 的每目的地握手预算与 WireGuard cookie 缓解) |
gen、issued_at、expires | 否 | 是 | 轮换时刻与大致在线时段 |
relays | 否 | 是 | 大致地区与网络迁移 |
不公开的内容:直连端点与 IP(记录本身就不含端点,§3.1)、私钥、PSK、对端与通信关系、账户、本地昵称。
规则:
- 默认
none。CLI 在选择address或record时提示上表与”无法收回”,record需要显式确认。 - 注重隐私的用户不发布,或者为公开服务单独使用一个身份。
- 节点按固定周期重签记录(例如每天固定时刻),不随在线状态重签,减少
issued_at透露的活动信息;relays变化引起的重签不可避免。 - 权威 DNS 不使用可遍历的 NSEC(不启用 DNSSEC,或使用在线签名的最小否定应答),不保留或只短期保留查询日志,按来源限速,TTL 取短值。
- 证书只签通配符
*.n.ns.io,不为单个节点名签证书:后者会进入公开的证书透明度日志,使节点 ID 可被枚举。
12. 编码与常量
Section titled “12. 编码与常量”12.1 编码
Section titled “12.1 编码”所有记录、报文体与应用通道消息都是 RFC 8949 §4.2.1 确定性 CBOR 定长数组:只用确定长度,整数最短编码,密钥与哈希为字节串,文本为有效 UTF-8 的文本串,缺省值为 null,不用浮点、标签或映射。解码器在任何签名或 MAC 校验之前拒绝不规范的输入与多余字节。CBOR 外的多字节整数为大端序。
12.2 签名与 MAC 上下文
Section titled “12.2 签名与 MAC 上下文”签名为 RFC 8032 pure Ed25519,严格校验(拒绝非规范 S 与小阶点);被签名的消息是 ASCII 上下文紧接规范编码,没有分隔符。
| 上下文 | 签名者 | 用途 |
|---|---|---|
ns/node-record/v2 | I | NodeRecord(§3) |
ns/registry-publish/v2 | I | 登记表发布证明(§7.2) |
ns/relay-register/v2 | I | REGISTER(§8.3) |
ns/relay-session/v2 | — | relay 会话密钥 HKDF info |
ns/relay-ctl/v2、ns/relay-reply/v2 | sk | relay 控制与应答 MAC |
ns/channel-*/v1 | I 或 — | 应用通道的签名与派生(应用通道 §10),配对使用其中的 confirm 部分 |
12.3 常量
Section titled “12.3 常量”| 项目 | 值 |
|---|---|
| NodeRecord 版本 / 最大长度 | 2 / 600 字节 |
| 记录服务期限 | expires - issued_at ≤ 604800 s;剩余不足一半时重签 |
| 节点 ID | 12 字节;base32 20 字符 |
| 地址组 | fd ‖ g(119 位) ‖ m,/127 |
| relay 报文头 / 最大报文 / 控制报文 / DATA 体 | 29 / 2048 / 1200 / 1..2019 字节 |
REGISTER ts 窗口 | ±120 s |
| presence TTL | 30..3600 s,默认 90 s |
| 隧道内控制端口 / 最大报文 | UDP 47900 / 1200 字节 |
| ENDPOINTS 候选 / relay | 8 / 4 |
| 登记分片副本 | 2;查找最多问 4 个 |
| 配对信箱 TTL | 默认 600 s,最多 900 s |
| 准备期限 | 5 s |
| 经 relay 的内层 MTU 上限 | 1376 字节 |
| 对端反射候选 | 每个对端 2 个;30 s 生命周期;每对端每 250 ms ≤ 1 个,每节点每秒 ≤ 64 个 |
13. 命令变化
Section titled “13. 命令变化”相对 Quick 模式 §2:
| 保留或新增 | 删除 |
|---|---|
up、down、status、invite、join、peers、peer show | name | remove;allow add <节点ID>(新增)、allow list | restore;key rotate(公开) | policy、pending、upgrade;隐藏的 token、pair、peer binding import、key binding export、aliases、recovery、recover |
invite --recipient 与 join --peer 接受节点 ID 或完整身份键。status 增加本机记录的 gen、expires 与登记表发布状态;peers 为每个对端显示 added_by 与是否有 PSK。
13.1 实现与本页的差异(2026-10-13)
Section titled “13.1 实现与本页的差异(2026-10-13)”- 没有内置的 v2 relay 目录,
ns quick up需要显式--relay host:port(§8.6 的标识),join也接受--relay。 - 配置里的端口桥被忽略并告警:对端经
N4访问 IPv4 服务,不再需要桥。 n.ns.io由节点守护进程的 DNS 权威应答(Quick 运行时加入系统 resolver zone,独立 resolver 已删除);<名字>.v4.n.ns.io回答N4/alias4。- TUN MTU 默认 1420(接受 1280–1420);经 relay 的内层 MTU 固定 1376;Windows 上
quick0暂为 65535,等 nsplane MT-3。 - 公开 JSON 为 schema v3,错误带
details.cause。 - Quick 身份与 WireGuard 键由设备密钥链派生(
quick/identity/v<N>,每个记录代际一把 WireGuard 键,2026-10-12);v1 Quick 存储的迁移已删除,不再读取旧身份。 - Quick 是节点守护进程上的一个 membership,与账户模式的 NSD membership 同时运行(Quick 对端的
alias4来自引擎的同一张别名表,跨来源默认拒绝):ns quick up经守护进程的私有 IPC 把 Quick 加到运行中的守护进程,没有时先只带 Quick 启动一个;ns quick down只摘掉 Quick;独立 worker 已删除(2026-10-12)。policy、pending、upgrade与全部隐藏命令删除,按未知命令拒绝。
14. 与 v1 的差异与迁移
Section titled “14. 与 v1 的差异与迁移”| 项目 | v1 | v2 |
|---|---|---|
| 范围 | 每条记录带 network_id,固定为 DEFAULT_NETWORK_ID | 没有范围 |
| 地址 | BLAKE2s(ver ‖ network_id ‖ I) 的地址组 | BLAKE2s(I) 的地址组(§2.2) |
| 键声明 | KeyBinding(not_before/not_after)+ Revocation | NodeRecord,gen 排序,expires 只管服务期限(§3) |
| 轮换 | 预约 cutover、时钟规则、自动续期、离线手动导入 | 立即切换,登记表重新同步(§4、§7.3) |
| 配对 | PairingToken(kn1)+ INTRO / INTRO_ACK + /quick/v1 | 应用通道 confirm(§10.2);按 ID 放行(§10.3) |
| PSK | 由令牌秘密派生,共同持有者可算出 | 由应用通道的临时 DH 派生;按 ID 放行时没有 |
| 准入策略 | auto / confirm / recipient-only + 待批准队列 | 配对时总是人工或按指定节点 ID 确认 |
| relay 报文 | KNR1,37 字节头,16 字节 slot,含 INTRO | NSR2,29 字节头,节点 ID 作 slot,无 INTRO(§8) |
| relay 标识 | authority 或短 ID | 只有 host:port(§8.6) |
| 登记表 | 无 | 客户端 HRW 分片(§7) |
| 隧道内控制 | 只给 Quick;类型 1 兼容 + 类型 5 有序端点;NAME、REVOCATION | 所有来源共用;ENDPOINTS 与 RECORD(§9) |
| 名字 | quick.internal、网络区域、便利区域、命名层 | n.ns.io,本地昵称(§11) |
| 线上常量 | keynet/* | ns/* |
迁移。 v2 运行时发现 v1 状态时,先备份整个状态目录,只保留身份键 I,丢弃其余状态(令牌、对端、允许与移除记录、别名、绑定),生成新的 K 与 gen = 1 的记录,并在 status 中报告”v1 对端需要重新配对”。地址随派生改变,旧的对端关系无法沿用。relay 端丢弃 v1 标记库,以空的 v2 标记库与登记表启动。