跳转到内容

NS · Quick 协议

v1,已被取代: ns 自 2026-10-04 起只实现 Quick 协议 v2,不兼容本页。本页仍对 nsgw-quick-relay 当前部署的 v1 relay 与会合服务具有规范性,直到 relay v2 服务端(迁移里程碑 M2)上线;ns 的 v2 客户端不与它互通。

状态:对已实现的 Quick 模式具有规范性。 这是 NS Quick 模式 的协议:一个两两互联的 WireGuard 覆盖网络。每个节点的覆盖地址由其身份键派生;IPv4 只是节点本地的别名,在 WireGuard 边界做翻译;两个节点交换配对令牌后即可连接。不需要账户、登记表或目录。relay 位于 WireGuard 之下,从不成为对端,也不持有覆盖地址。不做子网共享:覆盖网络中只有节点。第 12 节对报文字节与 relay 成员间协议具有规范性。

所有安装都运行同一个公开的默认上下文(§2.7)。报文常量、DNS 区域与载体路径保留实现中的 keynet 拼写(quick.internal、/keynet/v1、keynet/key-binding/v1 这类签名与 KDF 上下文,以及 crate quick-wire、quick-runtime、quick-pairing)。面向用户的命令、/quick/v1 配对会合、relay 选择、平台与运行规则见 Quick 模式。

目标推论
交换过配对令牌的任意两个节点可以直接连接,中间不需要任何服务地址由身份键在本地派生;授权 = 持有令牌 + 接收方的允许名单
全球规模每个节点在每个网络中一个 IPv6 /128;唯一性是概率性的,并配合本地冲突检测,因此不存在全局索引
旧应用需要 IPv4IPv4 是节点本地别名,在 WireGuard 边界做无逐流状态的翻译;仅支持 IPv4 的服务由显式桥接覆盖
人们更习惯用名字每个节点都有不可变的身份名与本地昵称;将来可选的命名层可提供唯一标签
没有承重的单点已配置的对端在没有 relay 或命名层时仍能通信;只有直连失败时才需要 relay

非目标:对端之间的中转(a 与 b、c 分别配对,并不让 b 经 a 到达 c)、全局唯一的 IPv4、子网共享、私有区域的 DNSSEC、在没有命名层时按名字查找节点、服务发布与出口节点(后续版本)。

数量预算说明
累计身份 I≤ 10⁹包含已退役的身份
累计地址组 B = 不同的 (network_id, I)≤ 10¹⁰决定碰撞预算(§3.3)
同时在线节点≤ 10⁸决定 relay 预算
每个节点已配置的对端默认 ≤ 10⁴,硬上限 10⁵带路由与别名的已安装 WireGuard 对端
每个节点未完成的配对令牌≤ 10³有界配置
每个网络的可发现集合(仅命名层)≤ 10⁵从不整体推送
准备期限5 s 设计参数每个端点各自计时:从本地准入并发送 INTRO_ACK(签发方)或校验 INTRO_ACK(加入方)起,到 WireGuard 路径可用并完成本地发布为止;等待接受另有上限(§7.5、§13.4)

超出预算是带具体拒绝原因的容量事件,从不静默降级。

每个节点安装有一把长期的 Ed25519 身份键 I,以及一把可轮换的 X25519 WireGuard 键 K。I 为每条记录签名;K 认证隧道。地址与名字由 I 派生,因此轮换 K 不会改变它们。

X25519 是 Diffie–Hellman 密钥,在 WireGuard 中没有签名操作;要用它签名需要 XEdDSA 之类的额外方案。单独使用一把 Ed25519 密钥是标准做法。

I 按网络声明自己当前使用哪把 K:

KeyBinding {
ver u8 = 1
network_id bytes32 the network public key (§2.6)
I bytes32 Ed25519 public key
K bytes32 X25519 public key
gen u64 monotonic per I; one K per gen
not_before u64 Unix seconds
not_after u64 Unix seconds, exclusive
sig bytes64 Ed25519 over the deterministic encoding of the fields above
}

规则:

  • 编码为确定性 CBOR(RFC 8949 §4.2.1)定长数组:原始 32 字节密钥、最短形式整数、无浮点、无可选字段、无未知字段。非规范编码在验签之前即被拒绝。
  • 签名上下文为固定字符串 keynet/key-binding/v1。
  • 接收方只有在用其已为该网络接受的 I 校验通过绑定后,才为 K 安装 WireGuard 对端(§7.4)。作为 AllowedIPs 安装的两个 /128 地址由 (network_id, I) 在本地重新计算(§3.2),从不取自记录本身。
  • 有效的绑定证明 I 声称使用 K;WireGuard 握手证明持有 K。两者都不证明该节点受欢迎:是否接受由 §7.4 的令牌与允许名单逻辑决定。
  • 接收方按 (network_id, I) 持久化已提交的最高 gen 与撤销高水位。更低的 gen 被拒绝;两个 gen 相同而 K 不同的绑定会让两者都失效,直到所有者发布无歧义的绑定。
  • 节点对每个 (I, network_id, gen) 恰好签发一个 KeyBinding。它在为 I 配置网络(创建自有网络或记录已加入的网络)或安排轮换的同一次原子存储写入中签名并持久化该绑定。之后的每次使用(令牌的 binding、INTRO 的 binding_recipient、INTRO_ACK 的 binding_issuer、relay REGISTER 的 binding 以及当前绑定导出)都携带这些完全相同的字节。其他操作从不签发 KeyBinding。
  • 没有轮换计划的代际,其绑定使用 not_before = T - 240 与 not_after = T + 2592000,其中 T 为签发时的墙钟。240 秒偏移让绑定在 §2.3 允许的最大两两时钟差内对所有参与方都处于有效期。时钟不可靠、T < 240 或溢出时签发失败,返回 clock_unreliable,且不改变任何状态。已安排轮换的代际,其绑定对每个已配置网络(无论自有还是加入)都使用该计划的 cutover 与 not_after;若该区间已不再有效,在该代际中配置网络会以 binding_required 失败。
  • 若存储中有已配置的网络却没有持久化的当前绑定,会在下一次写入中、任何使用之前,按上述规则补签一次。已持久化的绑定在其代际内从不重签、改期或替换。只读操作从不签发绑定。
  • 签发 KeyBinding 从不推进持久化的轮换时钟下限(§2.3)。

2.3 K 的轮换:单一活动密钥,固定切换

Section titled “2.3 K 的轮换:单一活动密钥,固定切换”

一个 WireGuard 接口只能把某个 /128 绑定到一个对端,入站报文按源地址与该对端匹配。两把密钥无法在同一接口上共享一个地址,因此轮换是一次切换:

  1. 为设备分配一个单调递增的 gen+1。在发布任何内容之前,使用当前接口持久写入下一把私有 K,以及每个已配置网络(自有或加入)的签名绑定。第 2 步检查的旧绑定就是持久化的当前绑定。共享一把活动 K 的设备不能只轮换其中一个网络。
  2. 安排 cutover = binding.not_before,至少在接受安排后 300 秒。默认值为 now + 300。新绑定默认 not_after = cutover + 2592000,在切换时必须仍有至少 600 秒有效期。对普通的计划轮换,旧绑定必须有效到 cutover + 240;否则选择更早的有效计划,或报告 rotation_window_invalid。所有计算都使用带溢出检查的 u64 运算。从不在本地延长已签名的有效区间。
  3. 每个未来绑定都先经过规范性、签名、范围与冲突检查,再暂存。持久暂存既不安装它,也不推进 committed_gen。BINDING ACK 只确认已持久暂存或已有效安装;缺少 ACK 从不推迟切换,也从不续期绑定。
  4. 当本地 now >= cutover 时,重新检查有效期与标记,并原子地替换两条 AllowedIPs、活动的私钥 / 公钥 K 与持久的代际状态。v1 中旧数据密钥的宽限期严格为零。新的握手在 K_gen+1 下建立会话;墙钟回拨不能恢复旧 K。
  5. 在切换时通过旧代际提交一条签名的 Revocation,并经新的可用会话分发。在每个旧 relay 会话上尽力发送原因为 0 的 REVOKE,然后用当前绑定、新的临时密钥以及(可表示时)持久化的更大 incarnation 发送 REGISTER。更高的代际可以重置已用尽的 incarnation。从不等待 REVOKE 的确认,也从不经未确认的 slot 发送新 K 的流量。

运行时自动续期设备密钥。设 E 为节点在所有已配置网络中当前绑定的最早 not_after。当运行时在运行、没有已安排的轮换、且 now >= E - 604800 时:若 E >= now + 540,运行时安排一次普通轮换,cutover = E - 240;否则安排一次迟到轮换,cutover = now + 300。迟到轮换(无论自动还是通过 key rotate 请求)只在 E < now + 540 时允许;它豁免”旧绑定须有效到 cutover + 240”与”旧绑定须仍有效”两项要求,其余对旧绑定的检查不变。计划根据持久化的当前绑定原子地计算并写入。用户已安排的轮换会抑制自动安排;已有轮换时执行 key rotate 报告 rotation_pending。自动安排要求时钟可用:墙钟不落后于持久化的轮换时钟下限,且在之前 300 秒的单调时间内,墙钟相对单调时钟的跳变不超过 120 秒;运行时启动也会开启这 300 秒的观察期。大于 120 秒的跳变按相邻两次单调轮询样本之间测量;每个样本低于 120 秒的缓慢漂移不算跳变,300 秒的稳定窗口仍要求期间没有检测到跳变。两个 clock_unreliable 原因同时存在时(墙钟落后于下限,且稳定窗口尚未结束),下一次尝试等到两者中较晚的最早允许时间。失败的尝试不改变状态并会重试。错过已发布绑定的对端使用 §2.3.1。迟到轮换之后,不再持有该节点当前绑定的对端同样使用 §2.3.1。

每个参与方的时钟预期与 UTC 相差不超过 120 秒,因此两个时钟最多相差 240 秒。这是可用性假设,而不是有效期宽限:令牌、绑定与 relay 的过期判断保持精确比较。relay REGISTER 的检查仍是 §12.5 中单独的、含端点的 ±120 秒比较,并不意味着运行边界内的任意两个时钟都能注册。时钟不确定或检测到跳变时报告 clock_unreliable,并在时钟可用之前阻止新的计划轮换;已提交的代际从不倒退。定时等待使用单调时钟,安装时则重新检查墙钟有效期。重启后使用持久化的绝对计划,而不是新的提前量。

迟到的 BINDING 满足 cutover <= now < binding.not_after 且 §12.3 允许时立即安装。过期、已撤销、陈旧或冲突的输入被拒绝,不改变状态。只有当该 (network_id, I) 没有其他可接受的当前绑定时才报告 binding_required;拒绝不会顶替一个可接受的现有绑定。代际、撤销与冲突锁存仍由 §12.3 规定。同一代际中冲突的未来 K 会阻止安排该代际;只有更高且无歧义的代际才能解决。已泄露的旧密钥即使会因此失去当前通道,也立即被禁用。切换时的报文可能丢失;Quick 不承诺无损轮换。

断开前已持久化下一个绑定的已配置对端,会在计划的切换时刻应用它。只有当该 (network_id, I) 没有可接受的当前绑定时,对端才报告 binding_required 并保持断开;它不会据此推断自己的允许名单条目已被移除。被拒绝的输入不改变状态,也不顶替其他可接受的当前绑定(§12.3)。v1 不定义隧道外的自动绑定投递。

完成轮换的节点可以在切换后导出其当前签名的八字段 KeyBinding。用户通过带外方式传递这个公开文件,并为一个已准入、未被移除的完整 (network_id, I) 对端导入它(§13.2)。导入执行 §12.3 的全部标记与冲突检查,不能创建准入或清除移除记录,保留 PSK、别名、地址与名字,原子地提交更高的当前绑定,然后尝试正常建立 WireGuard。相同的绑定导入是幂等的。已提交代际与密钥的当前绑定遵循 §12.3 的同代际规则。未来绑定会被这个当前绑定接口拒绝。如果双方都错过了对方的轮换,双方都导入对方的当前绑定。已知对端从不被静默地用新令牌重新配对;有意的重新配对仍可按 §§7.4–7.6 进行,产生新的准入与 PSK 生命周期。

参数v1 规则
时钟运行边界每个参与方时钟预期与 UTC 相差 120 s 以内;两两之间可能相差 240 s。这只是可用性假设。
计划提前量自接受安排起至少 300 s;默认 now + 300。
切换时刻恰好为 binding.not_before。
新绑定的生命周期默认 not_after = cutover + 2592000;切换时至少还剩 600 s。
旧绑定的生命周期普通计划要求旧 not_after >= cutover + 240;迟到轮换(E < now + 540)不要求。
旧数据密钥宽限严格为 0 s。
缺少 ACK只是接收状态;从不推迟切换或续期绑定。
自动续期now >= E - 604800 时安排;E >= now + 540 时 cutover = E - 240,否则为迟到轮换,cutover = now + 300。
迟到轮换只在 E < now + 540 时;豁免旧绑定的有效性与 old not_after >= cutover + 240。
续期时钟墙钟不早于轮换时钟下限,且 300 s 内相对单调时钟没有超过 120 s 的跳变。

持久状态包括:下一把密钥的分配、未来绑定的暂存、已安排的切换,以及原子的当前代际安装。重启后从持久化的绝对状态继续。无效、过期、已撤销或冲突的输入从不推进它,已提交的代际状态从不回滚。

被盗的 I 无法证明谁才是合法所有者。丢失或被盗会改变地址,并需要新的身份。恢复根、恢复声明、其经认证的分发以及恢复命令都推迟到 v1 报文规范之外。将来的设计需要在丢失之前登记恢复根,但 v1 既不定义其记录,也不定义分发路径。新的配对可以按普通策略接纳新身份;其令牌既不携带也不证明与旧身份的恢复关系,移除旧身份是单独的显式操作。身份丢失后,Quick 不提供”地址不变、更换密钥”。

节点安装对端时:

  • I 相同、network_id 相同、K 相同:幂等(重装、重试、从备份恢复);
  • I 相同、K 不同:绑定更新,由 §2.2 与 §2.3 处理;
  • I 不同、地址组相同:地址碰撞(§3.4);新对端被拒绝并报告。

节点从不静默地重新生成自己的身份。

一个身份同一时刻只运行在一台设备上。两台使用同一 I 的活动设备会争用同样的 relay slot(§12.5 规定每个 slot 只有一个活动源,只有更大的 incarnation 才能替换它),并向每个对端呈现相同的地址;Quick 不支持这种情况,运行时也不检测它。每台设备使用自己的身份,并共享网络(§2.6)。

网络是创建网络时在本地生成的一对 Ed25519 密钥。network_id 是网络公钥,32 字节,每条记录与每个令牌都携带它。私钥是所有权凭据:只在运行命名层(§7.7)时需要,其余时候闲置。创建时选择的显示名是签名元数据与本地便利输入,不属于不可变的规范网络区域(§7.1);两个网络可以同名。

这个通用模型就是报文模型:每条记录与每个令牌都携带 32 字节的 network_id,§12 的编解码接受任意范围。v1 不允许节点创建、拥有或选择网络;每个安装恰好参与一个公开范围,即 §2.7 的默认上下文,其显示名为空。按设备划分的个人网络与用户创建的网络已从产品中移除,作为推迟的选项保留(§11)。

network_id 提供四样东西:标签唯一性的范围、允许名单与所有权的分组、网络之间地址字面量的隔离(§3.2),以及记录的重放保护。它不提供数据面隔离:Quick 是两两互联且从不转发的,因此无论网络成员关系如何,同一节点的两个对端都无法互相到达。

v1 安装使用一个公开的默认上下文代替网络:

DEFAULT_NETWORK_ID = SHA-256("keynet-default-context-v1") # ASCII, no trailing newline
= 953ead48b53d57f072706b6a5d400037c8baf02f1f516cef60a83b17b6902c02
  • 是标识,不是权威。 这个值是报文范围。它不是任何私钥对应的公钥,没有共享的签名秘密,也没有网络所有者权威。绑定、令牌、relay 注册与撤销保留各自的设备签名、代际、过期与重放规则;每个对端仍需显式准入,A–B 的配对加上 B–C 的配对不会让 A 访问 C。
  • 每个安装一个范围。 运行时状态中没有自有网络,恰好有一个已加入的范围,即默认上下文,其显示名为空。创建、选择或显示网络的功能不可用;命令、帮助、配置、状态、错误与 JSON 都不出现这个概念(§13)。这个值不可配置:改变它会改变每个地址、relay slot 与配对范围。
  • 派生。 这个常量是地址派生(§3.2)、relay slot id(§12.4)与配对 PSK(§7.4、§12.2)的 network_id 输入。因此每个身份恰好拥有一个地址组。
  • 令牌。 签发的令牌保持十二字段布局,network_id = DEFAULT_NETWORK_ID,net_name = ""。导入方在修改状态之前拒绝任何其他 network_id 的令牌(§12.3);net_name 既不是权威,也不用于显示。
  • Relay。 relay 接受任意 32 字节范围,由范围与身份派生 slot,只在相同范围之间转发(§12.5)。共享 relay 可达性不构成授权。
  • 名字。 没有规范的网络区域。名字为 <nickname>.quick.internal、<identity-label>.id.quick.internal 及其 .v4 对应名(§7.1)。命名层需要一把并不存在的范围签名密钥,因此被禁用(§7.7)。
  • 早期状态。 带有按设备划分网络的安装在启动时被拒绝。显式执行 ns quick upgrade 会备份该状态,保留身份键 I,安装默认上下文,并要求每个对端重新配对;由于范围改变,地址也会改变。

Quick 地址直接位于 fd00::/8 之下。没有产品级前缀:产品级 /48 只有在有登记表保证其内部唯一性时才有意义,而 Quick 没有登记表。这遵循 RFC 4193 的模型,即 ULA 的唯一性是概率性的。代价见 §7.8:没有聚合路由,没有单一的反向区域,无法按粗粒度前缀过滤。

只有经过一个已认证 WireGuard 对端、且其 AllowedIPs 包含该地址时,地址才可信(§4);前缀本身不授权任何东西。

每个 (network_id, I) 对拥有一个地址组,由两个只在最后一位(模式)上不同的 /128 地址组成:

| 8 bits: fd | 119 bits: g | 1 bit: mode |
g = BLAKE2s(out = 15 bytes, key = "keynet-addr-v1")(ver_u8 || network_id[32] || I[32]) >> 1
addr(I, net, m) = fd || g || m
N6(I, net) = addr(I, net, 0) native IPv6 service address
N4(I, net) = addr(I, net, 1) IPv4 service address (packets to it are translated, §6)
group(addr) = addr with the last bit cleared

ver 是地址格式版本。K 不是输入,因此轮换 K 不改变地址。network_id 是输入,因此同一身份在不同范围中有不同地址。这并不能让身份在不同范围之间不可关联:看到同一个公开 I 的人仍然可以关联。在 v1 规范中输入是默认上下文(§2.7),因此每个身份恰好拥有一个地址组。

对 B 个使用均匀 119 位哈希的地址组,任意两个发生碰撞的概率约为 B²/2¹²⁰:

累计地址组 B碰撞概率
10⁹7.5 × 10⁻¹⁹
10¹⁰(预算,§1.1)7.5 × 10⁻¹⁷

在 v1 规范中 B 等于身份数,因为每个身份在默认上下文中只有一个地址组。

因此唯一性是一个概率性约定:Quick 不承诺世界上任意两个身份都不会派生出同一个地址组,它承诺的是节点从不安装两个地址组相同的对端。找到任意一对碰撞约需 2⁶⁰ 的计算量;针对选定受害者则需 2¹¹⁹。两者都不能替代认证:安全身份是完整的 I,从不是它的哈希。

节点在安装对端之前检查:对端的地址组不同于自己在该网络中的地址组,也不同于每个已配置对端的地址组。发生碰撞时拒绝安装,并在报告中给出冲突的身份;不重新分配任何东西,也不重新生成密钥。节点加入新网络时执行同样的检查(自己的新地址组对比所有现有地址组)。由于对端是逐个安装的且检查在本地进行,不需要任何协调。

  • 可派生、自证明。 任何为某网络接受了 I 的节点都能算出两个地址,并把它们安装为 AllowedIPs。随后 WireGuard 会丢弃内层源地址不是该对端两个地址之一的任何解密报文。
  • 跨设备、路径、relay 与 K 轮换保持稳定。 地址只随 I 或 network_id 改变。
  • 无需登记。 没有分配、租约、回收或复用。

每个节点运行一个 WireGuard 接口,带一把活动的 K。节点所属的每个网络向该接口添加自己的两个 /128 地址。每个已配置的对端恰好以其两个 /128 地址作为 AllowedIPs;任何对端都不会得到更宽的前缀:

[Interface]
Address = N6(self, net) /128, N4(self, net) /128 # per network
[Peer] # peer b in network net
PublicKey = K_b (verified through KeyBinding)
PresharedKey = PSK(a, b) # derived from the pairing token, §7.4
AllowedIPs = N6(b, net)/128, N4(b, net)/128

AllowedIPs 只保护源端:WireGuard 把解密后报文的源地址与发送它的对端匹配,然后把报文交给主机。它不检查目的地址,而开启了转发的主机会把发往第三个对端的报文路由回接口。因此 Quick 在每个平台上、WireGuard 与主机协议栈之间的报文路径中强制以下不变量:

  1. 入站目的地。 解密后,目的地址必须是发送方对端所在网络的 N6(self, net) 或 N4(self, net)。其他报文一律丢弃并计数。这项检查在翻译与主机协议栈之前进行;它也覆盖从其他接口到达、源或目的为 Quick 地址的报文。
  2. 不转发。 来自对端的报文从不进入主机的转发路径,既不发往另一个对端,也不发往物理接口。翻译器自身的内部交接(§6)是唯一允许的改写。
  3. 出站源地址。 进入隧道的报文以 N6(self, net) 或 N4(self, net) 为源地址;其他源地址在加密前被拒绝。
  4. 拒绝路由。 已移除对端与已退役本地地址组的地址保留显式的拒绝路由(以及 DNS 墓碑),使发往它们的流量在本地失败,而不是经默认路由泄漏出去。

添加或移除对端只改变该对端的条目;地址从不改变。

relay 承载加密的 WireGuard 报文与配对介绍。它不持有覆盖网络私钥,也没有覆盖地址或名字,不能在端点处授权对端。它认证注册,并执行传输准入与各项上限。

功能机制
载体UDP:37 字节寻址头加原样的 WireGuard DATA 体;回退时每个二进制 TLS WebSocket 消息承载一个完全相同的报文(§12.5)。relay 标识与两种载体的目标遵循 §12.5.1。
会合令牌的 home relay authority 可以是区域(§12.8);节点在任一成员注册。签名的 REGISTER 授权一个新的临时会话;REGISTERED 与一次已确认的 PRESENCE 建立本地 slot。slot 的 16 字节 id 由 (network_id,I) 派生;同时保留完整的所有者元组。
介绍加入方先在签发方的 relay 确认自己的 slot,再发送 INTRO。INTRO_ACK 返回到那里。两种介绍也都可以按 §12.5 使用已知的直连 UDP 端点。
转发检查活动的发送源,查找显式的目的地,计入预算,原样转发完整报文;不对所有目标做 mac1 扫描。
反射只返回活动节点自己被观察到的 UDP 端点。WebSocket 源不是 UDP 候选。
路径选择适配器负责直连 / relay 路径,维护一个稳定的虚拟 WireGuard 端点,并按路径执行 MTU(§6.6)。
端点交换在已建立的 WireGuard 会话内交换原子的、有序的候选 / relay 快照。relay 路径可以引导这一过程;反射不保证能穿越所有 NAT。静默会话在晋升直连之前使用相关联的认证探测(§12.7)。
扩展relay authority 由签发方选择;没有全局的身份到 relay 目录。一个 authority 可以有 2..16 个成员,在区域内一跳转发,不做区域间中转(§12.8)。无论成员多少,节点对每个 authority 只选一个逻辑会话:包括它在 §12.7 ORDERED_ENDPOINTS 中公布的每个 relay,以及它需要的每个对端 relay,总数受配置上限限制(默认 8 个 relay 会话);超出后,其余对端只走直连并被报告。有序快照最多仍只容纳四个 relay 名字。

用户态套接字适配器添加 / 剥离报文头,并把 slot 映射到已配置的完整身份。直连 WireGuard 报文没有 relay 头。内核集成推迟。每个成员本地的 relay slot 只有一个活动源;NAT 或载体迁移需要一次新的已确认注册。分区期间,节点的选择并不意味着区域内会话的原子唯一性(§12.8)。

  • 注册。 I 对当前 KeyBinding、incarnation、时间戳、新的临时公钥与确切的 relay 名字签名。有界的暂定会话在该源返回一个用会话密钥认证的 PRESENCE 之前,不能路由流量,也不能替换活动源。REGISTERED 确认暂定建立以及之后的 PRESENCE。这证明持有 I 授权的临时密钥,而不是 K;K 由绑定授权。区域成员依据自己的持久下限提交,然后发布 MARKS 与 PRESENT(§12.8);它从不等待其他成员。
  • 新鲜度。 应用 §12.3 的代际 / 冲突 / 撤销规则,以及 §12.5 跨载体的 incarnation 顺序。拒绝为零的 slot id 与完整所有者碰撞。路由过期或 REVOKE 之后保留安全标记;被截获或未确认的请求不能移动源。
  • 源绑定。 已确认的源是确切的 UDP IP/端口元组或 WebSocket 连接。控制报文必须使用它。DATA、介绍、观测与入站报文从不改变它;迁移需要新的签名 incarnation 与确认。
  • 对端授权。 relay 访问不授予任何对端权限。端点校验令牌证明 / 绑定或 WireGuard 认证、允许名单与 PSK。适配器在解密 DATA 之前检查本地目的地与已安装的源对端。slot 头不是密码学身份。
  • 预算。 在密码学运算之前,按源 IP 与全局限制 REGISTER 工作量。限制暂定 / 活动 slot、保留的标记、每个连接的 slot、排队的字节 / 时长与回复。按发送方、目的地与全局限制控制报文 / INTRO 与 DATA 的报文数 / 字节数;另外按目的地单独计量握手发起报文。不缓冲离线投递。容量耗尽时拒绝新工作,而不是提前驱逐安全标记。
  • DATA 与 cookie。 DATA 不额外加 MAC;UDP 源伪造与路径上注入仍是可用性风险。WireGuard 端到端认证并拒绝重放。端点生成的 cookie 原样转发。relay 自行生成 cookie 推迟:relay 与端点的秘密相互独立,不能互换。
  • 回复。 REGISTERED / REFLECTED 经会话认证并相关联。STATUS 只是建议性的,不能证明投递、存活、撤销或配对被拒。未确认的 UDP 回复不能大于触发它的报文;输出队列有界。
  • 信任。 UDP 向 relay 认证节点,但不向节点认证 relay 公布的名字。路径上的主动攻击者可以冒充或干扰它。WSS 校验正常的服务器证书。两者都不授予 relay 端点秘密或权威。

所有字节单位均为二进制单位。报文与字节令牌桶按单调流逝时间补充,以标明的突发量为容量。报文在分配、密码学运算或入队之前必须满足每个适用的桶;多范围的行对每个范围分别计费。“按源”指完整的 IP 地址,而不是 UDP 端口,同一源的多个别名不会放大其额度。两种节点载体在每个成员上共享服务级全局预算。在区域中,全局范围限制的是每个成员的物理工作量,而不是整个区域的合计配额。

预算的适用分阶段:观测到的源 IP 与服务级全局的认证前准入范围,在其保护的工作之前计费(全局节点控制入口范围可在 MAC 校验前计费一次);节点控制的发送方范围只在规范 schema、确切的绑定源、当前会话 / incarnation、MAC 与序号检查都通过之后计费。转发从不要求 relay 校验仅端点可见的 MAC 或 WireGuard 载荷。格式错误的 relay 报文与未认证的身份声明从不消耗已认证发送方或转发范围;每个适用的入口、转发或回复范围在后续受保护工作之前,对包括报文头在内的完整原始报文恰好计数一次。

对本地投递的 DATA 或 INTRO / INTRO_ACK:先完成 relay 层解码,以及活动且源确切的发送方、活动的目的地与同一上下文的检查,再对每个适用的发送方、目的地、转发全局与握手发起子集范围各计费一次。对同一区域内的远端目的地,准入分两个阶段(§12.8 规则 3):入口校验发送方与同上下文的合格 presence,然后在 mesh 入队之前计费发送方及其转发全局范围;出口校验声明的完整源所有者及其 slot 派生、活动目的地的完整所有者与上下文,然后在输出入队之前计费目的地、握手发起子集及其转发全局范围。排队的输出绑定到那个确切的已提交目的地会话,会话退役时丢弃。只有入口计费节点回复预算并发出相关联的 STATUS。

资源默认值、突发或硬上限
REGISTER 校验尝试每 IP 4/s,突发 8;全局 1000/s,突发 2000;在签名或 DH 运算之前
并发密码学工作32 个执行中,128 个排队,最长排队 1 s
暂定注册全局 4096;每源 IP 16;每个完整所有者 2;每个 WSS 连接 8
活动 slot全局 10000;每个 WSS 连接 8;不为接纳新身份而驱逐
保留的完整所有者标记1000000 个不同的 (network_id, I) 条目;编码后的逻辑标记数据最多 512 MiB
标记存储磁盘预算4 GiB,含日志与压缩余量;在达到上限之前停止新的提交
WSS 连接全局 2048;每源 IP 16;最多 256 个并发的未确认连接
新的 TLS / WSS 握手每源 IP 2/s,突发 4;全局 100/s,突发 200;在昂贵工作之前
节点控制报文发送方 16 报文/s,突发 32,且 32 KiB/s,突发 64 KiB;全局 20000 报文/s,突发 40000,且 32 MiB/s,突发 64 MiB
转发的 INTRO / INTRO_ACK发送方与目的地各 4 报文/s,突发 8,且 8 KiB/s,突发 16 KiB;全局 2000 报文/s,突发 4000,且 4 MiB/s,突发 8 MiB
转发的 DATA发送方与目的地各 2000 报文/s,突发 4000,且 2 MiB/s,突发 4 MiB;全局 100000 报文/s,突发 200000,且 128 MiB/s,突发 256 MiB
DATA 中的握手发起子集目的地 20 报文/s,突发 40;同时计入每个 DATA 桶
回复(含 STATUS 与 REGISTERED)每源 IP 16 报文/s,突发 32,且 32 KiB/s,突发 64 KiB;全局 20000 报文/s,突发 40000,且 32 MiB/s,突发 64 MiB
排队输出每个已提交目的地会话 64 个报文、128 KiB;全局 8192 个报文、16 MiB;最长 1 s
WSS 入站组帧每个连接一条最多 2048 字节的消息,已知控制报文上限 1200;全局合计 4 MiB
认证前限速状态表最多 65536 个 IP 条目,空闲保留 120 s;表满时拒绝未见过的源,而不是清除活跃的桶

在每个适用的入口、转发与回复关口,对包括 37 字节报文头在内的完整 Quick 报文计数一次。TLS / WebSocket 的分配与工作上限另外适用。注册桶对重复报文同样计费。没有已提交会话的回复使用按确切源的有界暂定 / 源队列,受同样的 64 报文 / 128 KiB 上限与全局上限约束。从不为离线目的地排队。耗尽时通常丢弃工作;只有在 §12.5 允许时才发送合格的 STATUS 2。不定义新的过载码、回执或减载列表。

mesh 数据输出另有每对 64 帧、128 KiB、最长排队 1 s 的上限(§12.8 规则 7)。mesh 数据与节点输出共享现有的进程级 8192 报文 / 16 MiB 输出上限;出口输出还遵守已提交目的地的上限。mesh 控制使用单独的预留:每对 256 帧、1 MiB,每进程 16 MiB,入口、关联与传输缓冲区都有界。被阻塞的数据入队或存储工作不能饿死它。§12.6 记录这些同时成立的上限。

以上是可配置的生产默认值,不是吞吐量承诺。覆盖值必须显式、有限、非零,并带检查的计量;把容量降到已保留的安全状态以下属于无效配置。报告生效值。节点控制与 DATA 的容量相互独立,而 CPU 与节点输出上限是共享的;mesh 控制预留如上所述单独存在。

在完整所有者键下持久化 committed_gen、committed_K、conflicted、revoked_through 以及当前代际的 incarnation_high_water。在正向确认授权转发之前,先预留标记容量并原子提交这些标记。在提交标记之后、回复之前崩溃,要求使用更大的 incarnation,从不允许复用已提交的值。重启不恢复任何活动 / 暂定会话、sk、排队输出或控制序号窗口。在接受注册之前加载并校验标记。预期的存储缺失、损坏、持久写入失败或相对本地持久状态的回滚,一律拒绝服务(fail closed);有意的首次运行初始化是另一回事。

当前代际的 incarnation 标记在 presence 过期、REVOKE、关闭与载体变化后依然保留。只有在持久检查点保留了完整的最新所有者下限之后,压缩才能删除被取代的日志版本。更高的代际可以回收已过时的、特定于代际的 incarnation 细节,但任何 TTL、LRU、绑定过期或看似不活跃都不能删除完整所有者的安全下限。

区域成员按 §12.8 规则 2 的单调合并复制保留所有者的完整并集。撤销跨代际独立合并;特定于代际的密钥、冲突与 incarnation 状态跟随更高的代际,同一代际时按确定性方式处理冲突。在改变路由或发布之前持久化变更后的合并结果,并据此重新评估本地会话与待确认项。本地注册从不等待成员达成一致。每个诚实的成员都保持”未确认 REGISTER”不变量,但分区时没有全区域的收敛期限:陈旧的本地会话与延迟的退役可能同时存在,持有所需临时密钥的所有者发出的陈旧确认可能造成持久冲突。仅凭相同的当前标记从不让确切的活动会话退役。每个成员现有的所有者与字节上限作用于完整并集,而不是区域容量的叠加。

达到容量时,在持久化健康的前提下,现有的安全会话继续;新身份以容量耗尽被拒绝,准入就绪度降低;已有所有者的更高代际更新在放得下时使用预留空间。恢复手段仅限于在标记完好的前提下扩容、压缩冗余版本,或为新身份单独配置一个 relay 服务。对同一个公布的 relay 身份,不存在安全的清除标记操作。丢失了无法恢复的下限的服务保持关闭,直到恢复完好的存储,或退役其名字 / 配置。

收到的区域下限无法持久化时,同步与准入失败,依赖未应用状态的路由与确认被抑制(§12.8 规则 8)。没有先前存储的新成员保持关闭注册,直到选定的一个完好对等成员提供完整的持久标记快照(§12.8 规则 6);引导过程不豁免存储失败后的恢复规则。

REGISTER、REGISTERED、PRESENCE、REFLECT、REFLECTED、INTRO、INTRO_ACK、DATA、STATUS 与 REVOKE 使用 §12.5。每个 UDP 报文或完整的二进制 WebSocket 消息包含一个 relay 报文;节点载体没有按长度分帧的字节流形式。成员间传输使用 §12.8 的单独分帧。

隧道内控制通道是隧道内的 UDP 端口 47900,只能在 N6(self) 上访问,在两个对端之间承载 §12.7 的记录:有序的端点 / 活动 relay 快照、带切换时刻的 KeyBinding 更新、撤销、可选的名字记录,以及经认证的直连路径探测。它只在 WireGuard 建立后存在,并由该会话认证。可转移的绑定、撤销与命名记录带有自己的签名;探测与端点记录只由会话认证。relay 控制与对端准入相互独立。

relay 能看到已注册的身份 / 绑定 / 临时公钥、源地址、presence、介绍报文的内容与时间,以及加密报文的大小与 slot 关系。INTRO / INTRO_ACK 在这一层经过认证但没有加密,因此 relay 还能看到每个加入方使用了哪个 token_id,并统计通过同一个令牌被接纳的身份数。令牌秘密与端到端私钥从不发给 relay。

隧道内的端点候选是加密的,但观测到的 UDP 地址是已知的,流量变化也可能暴露直连路径是否成功。relay 可以误报观测、注入未认证的流量并拒绝服务;它不能伪造由端点认证的记录,也不能解密 WireGuard 载荷。这些元数据与可用性方面的局限同样适用于经过认证的恶意 WSS relay。

区域成员看到的元数据与单个 relay 相同,presence 表在区域内复制身份到成员的映射。成员相互信任对方的发送方准入、presence 与标记声明。被攻破的授权成员可以捏造这些声明,为它持有 relay 秘密的会话合成 relay 会话控制报文,并传播虚假的高水位、撤销或冲突状态。这可能造成持续的拒绝服务,而不只是暂时的路由丢失;mesh 声明不携带能针对恶意成员独立验证的持有证据。该成员仍然无法派生节点身份或端到端私钥,无法伪造由端点认证的配对或隧道内记录,无法在端点处授权对端,也无法解密 WireGuard 载荷。所有 v1 安装共享 DEFAULT_NETWORK_ID;上下文相同既不是对端授权,也不是租户边界。信任边界是仅由运营方掌握的成员资格与区域 CA(§12.8)。

IPv4 只存在于节点内部。报文上的两个服务地址(§3.2)让 IPv4 与 IPv6 之间的映射无需逐流状态即可双向还原:

本地 IPv4报文地址含义
self4N4(self, net)本节点的 IPv4 套接字在报文上的样子
alias6(b)N6(b, net)对端 b 的原生 IPv6 服务,供本地 IPv4 客户端访问
alias4(b)N4(b, net)对端 b 的 IPv4 服务,供本地 IPv4 客户端访问

本节点的原生 IPv6 套接字使用 N6(self),从不使用别名。IPv4 套接字在报文上总是表现为 N4(self)。由于每个本地套接字族恰好对应一个报文身份,而每个报文地址恰好映射到一个本地地址,双向翻译都是地址的纯函数。如果每个节点只有一个地址,就既无法区分入站报文是发给仅 IPv4 服务还是 IPv6 服务,也无法在报文上区分端口相同的 IPv4 套接字与 IPV6_V6ONLY 套接字;有了两个地址,这两个问题都不存在。

代价是每个对端两条 AllowedIPs,以及每个对端最多两个别名。关闭 IPv4 的节点只保留 N6。

每个节点一张表,持久化:

键别名报文地址来源
self100.64.0.1/32(可配置)N4(self, net)固定
(net, I_b, 6)100.64.3.7/32N6(b, net)哈希
(net, I_b, 4)100.64.3.8/32N4(b, net)哈希
(net, I_c, 6)100.64.9.200/32N6(c, net)用户

地址池默认是 100.64.0.0/10(RFC 6598),与 self4 一样可配置。已把该地址池用于其他用途的主机(运营商级 NAT 接口、安装了 100.64.0.0/10 的其他覆盖网络)必须换用其他地址池;软件会报告确切的冲突路由,并拒绝在被遮蔽的范围内分配。

allocate_alias(net, I, kind):
for i in 0..16:
candidate = POOL.base + (H22(net, I, kind, i) mod POOL.size) # deterministic sequence
if candidate == self4 or candidate is in the table: continue
if conflicts_with_host(candidate): continue
return candidate
fail("no free alias in the pool; see reported conflicts")
  • conflicts_with_host 查询所有相关的路由表与策略规则、本地接口地址与防火墙规则。它忽略默认路由与 Quick 自己的路由;任何其他覆盖候选地址的路由都会导致拒绝,包括更宽的路由,例如局域网上的 /24。
  • 只要其中任何一类无法观测,检查就处于未决状态:受影响的别名保持暂停,发布不会完成。配对按现有语义结束(prepare_timed_out),并以现有 schema 给出明确的 Conflict 报告:kind 为无法观测的类别,address 为该别名,描述长度有界。主机观测命令有时限,且从不在持有存储锁时启动。
  • 确定性序列让每台设备上的第一个候选相同,但跨设备的稳定性只是尽力而为:有 1000 个对端时,至少一对第一候选发生碰撞的概率约为 11 %。可移植的别名快照在 v1 中推迟(§6.5)。
  • 每个别名获得一条以 self4 为首选源的 /32 主机路由。从不安装覆盖整个地址池的路由;正因如此,地址池被部分遮蔽也能容忍。
  • 别名表、路由、WireGuard 对端、翻译条目与 DNS 作为一个带版本的整体发布:先发布映射与路径的两个方向,最后发布 DNS;移除时先撤销 DNS 并添加拒绝路由,再移除其余部分。
  • 别名一经分配就保留。移除对端时,其别名作为带拒绝路由的墓碑保留,不会被自动重新分配。应用缓存地址的时间比任何 DNS TTL 都长,复用别名会把它们的流量送到另一个身份。手动回收是显式的、有文档的操作。
  • 显式的重新准入只能复用 (network_id, I, kind) 相同(即地址族相同)的墓碑;这个别名从不提供给其他身份。显式移除仍然优先:必须先清除移除记录,身份再通过普通准入。重新准入让匹配的条目经历 tombstone → suspended → active:先保留拒绝路由与 DNS 墓碑,只有当 §6.3 的常规主机观测表明发布能够成功时才激活。source:user 的别名原样复用,而不是派生新的哈希候选。
  • 接口启用期间持续监视主机路由的变化,而不只在启动时检查。新出现冲突的别名会被暂停(拒绝路由、DNS 墓碑)并报告;从不静默地移动它。

用户可以设置地址池中任何不是 self4、不在表中、且未被主机路由覆盖的地址;拒绝时给出原因。别名只是本地昵称:修改别名不会改变对端那边的任何东西。别名的导出 / 导入在 v1 中推迟:没有定义签名快照格式、状态、版本、签名上下文或冲突解决协议。CLI 动词仍然可见,但返回 feature_deferred,不读写任何快照(§13.2)。§6.4 的规则仍是绝对的:曾为另一个完整 (network_id, I, kind) 发布过的地址,从不被自动重新分配。

翻译器位于主机 IPv4 侧与隧道之间。它不持有流表;它持有别名表、PMTU 数据与有界的分片重组状态。

outbound (host → tunnel)
IPv4 dst alias6(b) | alias4(b) → N6(b) | N4(b) by full address
IPv4 src self4 → N4(self)
inbound (tunnel → host)
IPv6 dst N4(self) → self4 always translated
IPv6 dst N6(self) → delivered natively never translated
IPv6 src N6(a) | N4(a) → alias6(a) | alias4(a)

规则遵循 RFC 7915,并使用显式地址映射(RFC 7757):

  • 分片。 IPv4 分片翻译时带 IPv6 Fragment 头(RFC 7915 §4.1)。只有校验和为零的分片 IPv4 UDP 报文才需要重组,时限 60 s,并有字节预算;超出预算的部分丢弃并计数。
  • 校验和。 TCP 与 UDP 校验和按增量更新(RFC 1624)。IPv4 UDP 的零校验和需完整计算。ICMPv6 校验和按伪首部计算;只有在每个变化字段都已知时才允许增量更新。
  • ICMP。 错误报文连同其内嵌的报文头一起翻译(RFC 7915 §4.4 与 §5.4);内嵌报文头的地址遵循同样的映射,其跳数限制不再递减。错误报文有速率限制,并在投递前按别名表校验。
  • MTU。 WireGuard 接口 MTU 为 1420;IPv4 侧宣告 1400。默认的经 relay 内层上限为 1376(普通 IPv4 因翻译扩展 20 字节,为 1356)。在加密前执行选定路径的上限;生成经过校验、有速率限制的 ICMPv6 Packet Too Big,并按现有的分片 / ICMP 规则翻译为 IPv4 Fragmentation Needed。反向翻译加 20,且从不宣告低于 1280 的 IPv6 MTU。UDP relay 在 IP/UDP 之上增加 37 字节,另有 WireGuard 32 字节的数据开销与填充。使用 16 字节填充时的安全上限是 16*floor((outer_path_mtu-outer_ip_header_bytes-8-37-32)/16),并进一步受 1376 与实际封装能力限制。外层的选项 / 扩展头也要计入。2048 字节的编解码上限不是路径 MTU。切换路径时缓存并重新验证 MTU;不要把更大的直连报文静默地经 relay 发送。如果实际封装承载不了 1280 的内层 IPv6 MTU,就选择另一个可行的载体或报告失败;不定义对加密报文的应用层分片。WSS 允许 TCP 对消息字节分段,但使用同样的经 relay 内层上限。外层 ICMP 错误由适配器处理。
  • 范围。 单播 TCP、UDP,以及 ICMP echo 与错误报文。在载荷中嵌入自身地址的应用看到的是本地别名,这对对端没有意义;Quick 不提供应用层网关。

只在 IPv4 self4:p 上监听的服务,任何对端都能通过 N4(self):p 访问,其他节点上的本地 IPv4 客户端则通过 alias4 访问。其他节点上的原生 IPv6 客户端通过 .v4 名字视图(§7.3)访问它,该视图解析到 N4。

如果还要让这类服务能通过普通名字访问,节点要运行显式的端口桥:在 N6(self):p 上真实监听,接受连接并代理到 IPv4 后端。端口桥按服务配置,在 IPv6 侧独占其端口,并维护自己的关联状态,生命周期有界(TCP 按状态机;UDP 关联空闲 300 s 后过期;ICMP echo 为 60 s)。配置变更只对新连接生效。后端看到的是端口桥的源地址;需要客户端身份的访问决策由端口桥依据经认证的对端做出。

节点协议栈翻译器与 §4 检查运行的位置
用户态 WireGuard + Linux TUN 设备在同一进程内,位于 TUN 设备与 WireGuard 引擎之间:host stack ↔ TUN ↔ classifier + translator ↔ WireGuard ↔ transport adapter ↔ direct or relay
没有 TUN 设备的用户态网络栈(代理或 SDK 模式)v1 推迟。将来的形态在协议栈内部做分类与翻译,只承载接入该协议栈的应用;对主机不透明。
内核 WireGuard推迟。需要内核侧的翻译钩子,以及一个添加与剥离 relay 头的本地代理(§5.1、§11)

在 v1 规范(§2.7)中,区域就是 quick.internal 本身:name = <nickname>.quick.internal,v4 name = <nickname>.v4.quick.internal,identity = <identity-label>.id.quick.internal 与 <identity-label>.id.v4.quick.internal。没有网络标签,也没有由显示名派生的便利区域。下面的全密钥区域适用于显式的网络范围,而 v1 不会创建这种范围。

network_label = "n-" + base32(network_id) # full 32-byte network key; 54 characters
ZONE = <network_label>.quick.internal
name = <label>.<ZONE> # canonical plain view
v4 name = <label>.v4.<ZONE> # canonical IPv4-service view
identity = <base32(H96(I))>.id.<ZONE> # immutable, always available

Base32 使用小写、不带填充的 RFC 4648;52 字符网络编码末尾未使用的四位为零。每个安装都为每个已配置网络提供全密钥规范区域。完整网络密钥相等是规范区域唯一的身份判断;不增加任何哈希或令牌字段。H96(I) 与身份标签沿用 §12.4 的构造。id 与 v4 是保留标签。

net_name 仍是签名的显示元数据,而不是经认证的分配;现有 PairingToken 的字段与字节不变。Network.display_name / net_name 必须是有效的 UTF-8,长度 0..=255 字节。空值是有效的报文 / 显示元数据,不产生便利区域。首次导入可以确立本地显示元数据,但之后拼写不同的令牌从不重命名已有网络,并会报告这一差异。本地便利区域可以使用按 §12.4 严格规范化的显示标签以及可配置的父域。空标签或保留标签只抑制该便利别名。规范化后相互冲突的显示标签会为每个冲突网络抑制有歧义的、不带后缀的便利区域;导入顺序从不决定胜者。经过校验后允许显式指定互不相同的便利标签。整个 n- 加 52 个 base32 字符的模式在规范父域下被保留,即使对未知网络也是如此,因此便利区域不能遮蔽规范区域。

便利父域由小写 ASCII DNS 标签组成,不计末尾的点最多 253 个表示字节;本地输入会去掉一个末尾的点。不隐式进行 Unicode 音译或 IDNA 转换。修改便利父域需要重启节点。安装或报告便利区域之前,校验其完整的区域名以及每个生成的正向名字:<label>.<zone>、<label>.v4.<zone>、<identity-label>.id.<zone> 与 <identity-label>.id.v4.<zone>。每个都必须在不计末尾点的 253 个表示字节以内。任何一个超出上限时,抑制整个便利区域并报告;其生成的名字变化时重新校验。规范区域不受影响。始终提供规范的 quick.internal 父域。跨设备引用使用规范的身份 FQDN;规范区域的失败从不回落公网 DNS。

没有命名层时,标签是本地昵称:用户把它分配给一个对端(db → I_b),它只在本节点内唯一,从不发布。有命名层(§7.7)时,标签在网络内唯一,由一个 I 拥有。标签按 §12.4 规范化,取值为小写 [a-z0-9-],长度 1–63 字节,不以连字符开头或结尾。

对通过令牌导入的对端:先规范化 issuer_name,再追加 - 与 12 字节 H96 摘要的前四个小写十六进制数字。只截断规范化后的前缀部分,以保留完整后缀,并使昵称最多 63 字节。规范化结果为空或为保留标签时,使用不可变的身份标签。若得到的默认昵称与该网络中已有的昵称冲突,则不给新来者分配默认昵称:它保留身份标签,并报告冲突以便用户改名。通过 INTRO 接纳的对端不携带设备名,因此在显式改名(§13.2)之前使用身份标签。

每个命名空间所有者在 TCP 与 UDP 127.0.0.54:53 上运行一个存根解析器。它根据本地对端表应答规范区域与无歧义的便利区域,并把其他问题经保存的上游图转发出去(§7.8)。能由已配置网络的显示标签与配置的父域派生、但被抑制的便利区域,对其下的每个名字在本地给出权威的 NXDOMAIN,从不转发到上游,并保持报告状态。父域下不匹配任何可派生区域的名字,以及显示名为空的网络,保持正常的上游行为。

查询应答
b.<ZONE> AAAAN6(b, net)
b.<ZONE> Aalias6(b)(若存在)
b.v4.<ZONE> AAAAN4(b, net)
b.v4.<ZONE> Aalias4(b)(若存在)
<base32>.id.<ZONE> AAAA任意已配置身份的 N6
<base32>.id.<ZONE> A该身份的 alias6(若已分配)
<base32>.id.v4.<ZONE> AAAA任意已配置身份的 N4
<base32>.id.v4.<ZONE> A该身份的 alias4(若已分配)
N6 或 alias6 的 PTR按下列优先级选出的一个规范普通视图目标
N4 或 alias4 的 PTR同一个选定标签,在规范区域之前紧接 .v4

对一个活动的已配置 (network_id, I),恰好选出一个 PTR 目标:首先是有效的、当前的、无冲突且未释放的最高版本命名层标签;否则是唯一的、未被其他身份的有效命名标签遮蔽的本地昵称,若有多个则取字节序最小者;否则是不可变的身份名。命名层的分配在正向冲突中同样胜出,会抑制并报告指向其他身份的本地昵称。同一版本相互冲突的 NameRecord 不予应答,也不能提供 PTR。命名层本身仍然推迟。

模式 4 的身份回退名为 <identity-label>.id.v4.<ZONE>;模式 6 为 <identity-label>.id.<ZONE>。因此每个反向目标正向解析后都落在同一个服务族。返回一条 PTR,TTL 60 秒。已暂停、已移除或已成为墓碑的映射没有正向 PTR;自有的墓碑返回权威的否定应答。未知的私有正向名字返回 NXDOMAIN,存在但没有所请求记录类型的名字返回 NODATA。不属于自己的反向问题只发往保存的上游解析器。别名、对端、昵称与 NameRecord 的变化或过期会清除受影响的肯定与否定缓存条目。当多个网络让 self4 的范围产生歧义时,self4 没有正向 PTR。

在同一个视图内,A 与 AAAA 始终指向同一个服务族,因此结果不依赖操作系统的地址选择策略。否定应答遵循 RFC 2308。TTL 为 60 秒。解析器接受一条 EDNS 版本 0 的 OPT 记录,宣告 1232 字节的 UDP 载荷;其他 EDNS 版本收到 BADVERS,不带 OPT 的响应上限为 512 字节。

配对令牌由一个节点签发,通过带外方式(聊天、二维码、DNS TXT)交给另一个节点。持有它即被授权连接签发方。

PairingToken {
ver u8 = 1
network_id bytes32 network public key
net_name text display name of the network
issuer bytes32 I of the issuer
binding KeyBinding of the issuer (current K)
issuer_name text optional display name of the issuing device, used for the default nickname (§7.2)
relay text required canonical relay identifier (§12.5.1)
endpoint text optional canonical direct authority, or null (§12.5.1)
secret bytes32 random; proves token possession and seeds the pair PSK
not_after u64 expiry, Unix seconds
recipient bytes32 optional: I of the only identity allowed to use this token
sig bytes64 Ed25519 by the issuer over the deterministic encoding above
}
text form: "kn1" + base64url(CBOR(PairingToken)) typically 550–700 bytes, at most 1024 (§12.3)

规则:

  • 除非写明 recipient,令牌在 not_after 之前可重复使用。可复用令牌用于接入多台设备;限定接收方的令牌用于已知的对端。
  • 始终执行允许名单:签发方只接受该网络允许名单上的身份发起的连接。有效令牌在首次使用时把出示它的身份加入允许名单;撤销令牌会停止后续添加,但不移除已添加的身份;从允许名单中移除一个身份会阻断它,与令牌无关。签发方可以要求对来自可复用令牌的添加进行交互确认。
  • 配对 PSK 为 PSK(a, b) = HKDF(secret, "keynet/psk/v1" || network_id || I_issuer || I_recipient),32 字节,在双方都安装为 WireGuard 预共享密钥。同一可复用令牌的两个接收方派生出不同的 PSK,但每个令牌持有者都知道 secret,而接收方的 I 是公开的,因此任一共同持有者都能算出其他接收方的 PSK。所以共同接收方之间的隔离依赖 WireGuard 的静态密钥,而不是 PSK;PSK 只针对从未持有该令牌的一方增加第二层、抗量子的保护。若这层保护也必须对其他接收方成立,就签发限定接收方的令牌,每个对端一个秘密。secret 必须按凭据对待。
  • 令牌是持有者凭据。只分享给应当能够连接的节点;只要已知对端的 I,就优先使用限定接收方的令牌。INTRO_ACK 证明知道 secret 与活动 nonce,而不是持有签发方的密钥:位于路径上的可复用令牌共同持有者(包括 relay 运营方)可以伪造一次接受;伪造最终以 prepare_timed_out 结束,因为 WireGuard 握手需要签发方的 K(§7.6)。
  • 内嵌的 binding 是签发方在该范围持久化的当前绑定。令牌实际的 not_after 取请求的过期时间与 binding.not_after 中较小者。
  • 在 v1 规范中,network_id 为 DEFAULT_NETWORK_ID,net_name 为空字符串;导入方在修改状态之前拒绝任何其他 network_id 的令牌(§2.7、§12.3)。

流程为 导入 → 介绍 → 准备 → 发布。接受可能需要等待人工或离线的签发方;准备另有 5 s 的期限。该操作由运行时持有,独立于任何附着的 CLI 或应用视图(§13.4)。

  1. 导入。 接收方严格解码令牌,校验其签名、接收方限制与不含端点的过期时间。它校验内嵌的签发方绑定、身份 / 网络是否匹配、当前有效性以及代际 / 撤销规则,然后执行地址组冲突检查(§3.4)与本地资源检查。校验失败时什么也不安装。成功时派生 PSK(issuer, self)、签发方的两个地址与所需的别名,并把签发方安装为对端,附带翻译条目、路由与一条候选传输路径。在对端可用之前,名字保持不发布。运行时分配一个本地 pairing_id。

  2. 介绍。 接收方严格按 §12.2 与 §12.5 构造 INTRO 及其 MAC,以 token_id 指明令牌,并使用运行时生成、与本次活动尝试关联的 32 字节 nonce。它发往签发方可达的直连端点或 home relay。经 relay 时,接收方先完成 REGISTER / REGISTERED,并用一个经会话认证的 PRESENCE 与对应的正向 REGISTERED 确认自己的 slot,使 INTRO_ACK 有一个活动的返回目的地。运行时以有界退避加抖动重试(默认 1 s,翻倍至 60 s,±25 %),直到令牌过期或本地取消。CLI 的观察预算不会停止重试。

  3. 接受。 签发方按 token_id 查找令牌,用该令牌的 secret 校验 MAC,校验绑定与接收方 / 网络限制,执行显式移除记录,并应用配置的策略(§13.3)。confirm 策略下的可复用令牌会创建一个有界、会过期的本地待批准项。批准会在准入前重新校验请求;匹配的限定接收方令牌不需要额外的人工批准,但要通过同样的有效性与移除检查。签发方安装已接纳的对端,并发送 §12 定义的正向 INTRO_ACK。加入方在校验其 MAC、范围、当前绑定与回显的活动 nonce 之后,才将其视为接受。v1 不定义否定确认,因此沉默无法区分拒绝与不可达。

  4. 准备。 加入方在第一个校验通过、相匹配的 INTRO_ACK 时开始 5 s 期限;签发方在安装对端并发送确认时开始相应的本地期限。重传不重置任一期限。WireGuard 握手建立一条可用的直连或经 relay 的路径。端点候选在会话内交换,之后打洞可能改善路径。relay 路径可用时,直连成功不是前提。

  5. 发布。 本地转发 / 翻译状态与 WireGuard 路径都就绪后,每个端点把该对端的名字标记为可应答,并放行挂起的查询。发布包含在准备期限之内。如果准备超时,报告 prepare_timed_out,同时保留已接纳的对端供正常重连;不发布不可用的名字,也不静默撤销准入。

接受之前的过期或取消会停止后续 INTRO 尝试,并退役未被接受的本地安装状态,在适用时保留别名墓碑与拒绝路由行为。这种清理从不移除一个独立接纳的对端。准入之后,令牌过期不会移除对端。

已配置的对端不重复令牌准入。在已有可接受的当前绑定时,它们的重连与密钥更新遵循普通的 WireGuard 与 KeyBinding 规则。因当前绑定未知而失联的对端使用 §2.3.1 的手动导入路径;v1 不定义自动的离线恢复,也从不静默重置其准入或 PSK。本地 pairing_id 报告配对结果;签发方的 pending_id 是另一个本地对象,v1 的配对消息不传输它。空闲节点的密钥更新由 §2.3 的自动续期发布,因此保持可达的已接纳对端无需用户操作即可保持当前绑定。

结果用户或应用看到什么
令牌无效、导入时已过期或绑定到其他身份导入失败并给出原因;什么也不安装。
地址组碰撞导入失败并给出冲突的完整身份;什么也不安装。
签发方要求确认签发方看到一个本地 pending_id。加入方保持 waiting_for_acceptance,可查询自己单独的本地 pairing_id。
签发方拒绝请求或按策略拒绝签发方记录本地结果,不发送 INTRO_ACK。通过 v1 报文,加入方无法把这与沉默区分开。
CLI 接受等待结束前没有有效的 INTRO_ACKCLI 返回 waiting_for_acceptance、本地 pairing_id 与观察到的传输证据。运行时在接受生命周期内继续。
签发方沉默或不可达沉默报告为未确认的接受。具体的本地传输错误可以单独报告;它不能证明签发方的意图或该身份整体不可用。
relay 不可用且没有可用的直连路径报告观察到的 relay / 路径失败。直连的已配置对端不受影响;挂起的运行时操作一直重试,直到过期或取消。
令牌在接受前过期停止 INTRO 重试,把操作标记为过期,只退役其未被接受的安装状态。已接纳的对端保持接纳。
用户取消未被接受的配对停止该操作的重试并退役其未被接受的状态;不改变无关的或已接纳的对端。
准入 / 确认校验通过后,准备或发布超过 5 s报告 prepare_timed_out,保留已接纳的对端供正常重连。重复消息不延长期限。
节点在接受前或准备期间重启未被接受的操作按 §13.4 恢复,直到令牌过期或取消;准备期间重启报告 prepare_timed_out,并保留已接纳的对端。
对端离线或待批准期间轮换了 K安装或继续之前,要求符合代际规则的当前有效绑定;不接受陈旧绑定来满足定时器。
离线切换后,已配置对端没有可接受的当前绑定只有当该 (network_id, I) 没有其他可接受的当前绑定时才报告 binding_required;被拒绝的输入从不改变状态;保留准入与 PSK;只通过 §2.3.1 或已有的有效 BINDING 安装经认证的当前绑定。
别名地址池耗尽或被遮蔽报告确切的本地冲突。只得到 IPv6 的结果需要用户显式选择;—yes 不会静默改变所请求的服务族行为。
本地的待批准、配对、对端或令牌容量耗尽以具体的容量错误拒绝新操作;不静默驱逐已有的同意、移除记录或已配置对端。

签发方本地的拒绝不是经远端认证的拒绝结果。relay STATUS 只描述 relay 的处理(§12.5)。令牌校验成功、确认、传输投递与可用对端发布是不同的事件,不能混为一谈地报告。

命名层在 v1 中被禁用:默认上下文没有范围签名密钥,因此任何 NameRecord 都无法被会签,客户端既不发布也不安装经网络授权的名字(§2.7)。它将来针对显式网络范围的约定,是让网络所有者在规范区域下添加唯一标签。这是唯一会出现中心方的地方,它按网络划分,§2–§7.6 的任何内容都不依赖它。

  • 记录。 NameRecord {ver, network_id, I, label, op ∈ {claim, rename, release}, version, not_after, sig_owner, sig_network}。所有者 I 对内容签名;网络私钥的持有者会签以分配该标签,唯一性就是这样决定的。
  • 分发。 记录在对端之间经隧道内控制通道传递;当所有者运行目录服务时,也可从该服务按需获取。目录为网络标记为可发现的身份回答”标签 → I、当前 KeyBinding、home relay”,并把名字变成一次配对:解析一个可发现的标签会以一个可复用的短期配对令牌启动 §7.5 流程,这个令牌由可发现节点自己签发、预先存放在目录中,并在其保持可发现期间持续刷新。目录从不代表节点签名;它也做不到,因为令牌由签发方的 I 签名,并作为配对 PSK 的种子。通过目录请求并签发限定接收方令牌的交互推迟(§11)。
  • 一致性。 网络密钥为分配定序。同一个完整 (network_id, I) 拥有的所有记录共享一条单调的记录流与一个版本高水位;label 只标识该流条目影响的资源。更低的版本被拒绝。同一版本且规范记录字节完全相同时幂等;同一版本但规范记录字节不同时,无论标签为何,都视为该所有者流的冲突,两条冲突记录都不予应答。只有严格更大的有效记录才推进高水位。被释放的标签进入 24 h 的墓碑期。没有目录时,对端只能从已连接的对端获知记录;这是最终一致性,并如实说明。命名层在 v1 中仍然推迟。
  • 信任。 网络密钥持有者可以分配或拒绝标签,也可以隐藏记录;它不能伪造记录,因为需要所有者的签名;它也不能把地址绑定到密钥,因为地址由 I 派生。

只有网络区域与别名地址池的反向区域路由到本地解析器。其余一切仍由系统解析器处理,包括主机原有的任何 split DNS 配置。

操作系统机制
带 systemd-resolved 的 Linux命名空间私有的解析器配置只把 Quick 区域与自有反向区域路由到不限定作用域的 127.0.0.54 服务器,保留现有的按链路 split DNS,从不安装 ~.;准确的环回投递与无关 split DNS 保持不变,二者都需要后续的验收证据
不带 systemd-resolved 的 Linux命名空间私有的转发器在安装其私有 resolv.conf 视图之前捕获完整的上游图,并以事务方式恢复;接管、转发环路防护与崩溃恢复需要后续的验收证据
macOS在 /private/etc/resolver 下按区域写入指向本地解析器的 split resolver 文件,只在 root 拥有且受保护的目录中创建,关闭时撤回(Quick 模式 §6.1)
iOSv1 推迟;将来的适配器可使用 NEDNSSettings.matchDomains
Windows一条带标记的 NRPT 规则把 Quick 区域指向 127.0.0.54;与之冲突的有效域策略会被报告,不会被覆盖(Quick 模式 §6.1)
Androidv1 推迟;将来的 VpnService 适配器必须考虑无法按域名路由 DNS 的限制

Quick IPv6 地址的反向查找没有可供路由的公共前缀。能把 d.f.ip6.arpa 路由到本地解析器的平台会这样做;解析器应答已配置的身份,并把每个不属于自己的问题原样经保存的上游图转发。不能路由它的平台不提供 Quick 的 PTR。

多个用户同时透明使用,需要各自独立的网络与挂载命名空间。每个命名空间有一个运行时 / 资源所有者,带经所有者认证的私有 IPC,受影响的应用运行在该用户的网络 / 解析器命名空间内。在一个共享的主机网络命名空间中,最多允许一个透明的 Quick 所有者。第二个所有者会在接管端口或修改任何路由 / 解析器之前收到 resource_owner_conflict。这是产品约束,不代表能在整台主机上按用户透明地提供任意 DNS。

资源所有者与绑定
Quick DNS在自有网络命名空间中的 TCP 与 UDP 127.0.0.54:53;修改前先预留;从不使用 127.0.0.53
本地控制现有的、经所有者认证的 Unix IPC;不提供 DNS、公开监听或新的未认证 API
TUN 与路由由一个命名空间所有者执行 §§4 与 6.3–6.4,包括别名路由
解析器配置命名空间私有的挂载视图,以日志记录原始内容、归属与事务标识
缓存解析器实例 / UID 加完整的 network_id;没有共享的私有应答缓存
生命周期锁绑定命名空间身份与 UID 的独占锁,从不仅凭 PID 文件推断

接管之前,捕获显式的上游服务器地址、路由信息、search / options 与 split DNS 行为。如果无法忠实地表示这个图,以 resolver_configuration_invalid 失败。在接管之前解析上游名字,或使用显式 IP。任何上游都不能是 Quick 自己,也不能是会把问题路由回 Quick 解析器的存根。非私有问题使用底层套接字发往保存的上游,从不发往当前系统解析器,也从不递归调用私有的 resolved 实例。未知的 Quick 反向问题最多经过一次域路由层。

systemd-resolved 适配器使用私有实例的全局 127.0.0.54 服务器,以及仅用于路由的已配置区域与反向区域,同时保留现有的按链路服务器 / 域。它不把环回 DNS 的作用域限定到 TUN。回退适配器安装一个私有的 resolv.conf 视图。两者都只在已安装的配置仍与其事务一致时才恢复;外部的并发修改会产生 resolver_restore_conflict。崩溃恢复使用同一份归属日志,从不把自己的解析器记为上游。

私有区域的失败从不回落公网 DNS。解析器不监听覆盖地址与非环回地址,并拒绝对端的查询。仅靠套接字绑定并不能实现用户隔离。自带 DNS-over-HTTPS 客户端的应用会绕过 split DNS。这些 Linux 机制是规范性目标,其两项验收仍然必需;本文不声称已经执行或验证过这些系统 API。

每个节点:

Identity { I, scopes[] }
Context { network_id = DEFAULT_NETWORK_ID, display_name = "" } # v1 profile: the only scope, joined, never owned
Network { network_id = network_pub, network_priv (owner only), net_name, canonical_zone, local_zones[], personal: bool } # generic model only; not created in v1
KeyBinding { ver, network_id, I, K, gen, not_before, not_after, sig }
PairingToken { ver, network_id, net_name, issuer, binding, issuer_name, relay, endpoint, secret, not_after, recipient, sig }
Peer { network_id, I, binding, addresses = derived, psk, source ∈ {token issued, token imported, directory}, generation, paths }
Allowlist { network_id, I, added_by ∈ {token id, user, directory}, added_at }
AliasEntry { network_id, I, kind ∈ {6, 4}, alias, source ∈ {hash, user}, created_at, state ∈ {active, suspended, tombstone} }
Bridge { network_id, port, proto, backend, budgets }
Nickname { network_id, label, target I }
Pairing { pairing_id, token id, network_id, I, state, acceptance expiry, preparation deadline }
Pending { pending_id, network_id, I, token id, first_received, expiry, state }
PendingRotation { generation, cutover, bindings_by_network, staged_peers, state }
EndpointPublication { network_id, local I, revision, snapshot, snapshot_hash }
EndpointHighWater { network_id, peer I, revision, snapshot_hash }
ResolverOwner { namespace_identity, uid, lock, resolver_transaction, saved_upstream_graph }
RelayConfig { advertised_name, host, port, trust }

命名层,每个网络(可选):

NameRecord { ver, network_id, I, label, op, version, not_after, sig_owner, sig_network }
Directory { network_id, I, binding, relays, deposited tokens, discoverable }

Relay:

Slot { network_id, I, slot_id, binding, K, gen, incarnation, source, sk, seq_high_water, presence_ttl_s, last_control, registration_hash, budgets }
PendingRegistration { owner, slot_id, signed_register, source, node_ephemeral, relay_ephemeral, sk, created_at, deadline }
RelayMarks { network_id, I, committed_gen, committed_K, conflicted, revoked_through, incarnation_high_water }
RelayStore { marks, encoded_mark_bytes, journal_bytes, capacity, readiness }
RelayService { advertised_name, udp_listener, wss_listener, member_local_slot_and_mark_store }
RegionMember { region_name, member_id, boot_epoch, region_ca, certificate, members, mesh_listener, store, durable_revision, presence_revision, channels, synchronization, admission }
PresenceEntry { member_id, boot_epoch, network_id, I, slot_id, gen, incarnation, carrier, revision, eligibility, grace_deadline }
MeshChannel { peer_member_id, boot_epoch, channel_kind, conn_gen, state, consumed_forward_id, progress_deadline, reconnect_backoff }
MeshQueue { peer_member_id, data_frames, data_bytes, oldest_data_deadline, control_frames, control_bytes, snapshot_buffer, process_output_usage }
MissCorrelation { peer_member_id, conn_gen, forward_id, original_header, input_length, exact_source, committed_session_identity, reply_deadline }

source 是 UDP IP/端口元组或 WebSocket 连接标识。以上是运行时数据模型的示意,不是额外的报文记录。

PresenceEntry.slot_id 就是 PRESENT 与 GONE 中的 slot 字段。MeshChannel.conn_gen 是该通道的 HELLO 代际;MissCorrelation.conn_gen 特指 MISS 中以 data_conn_gen 携带的数据通道代际。关联还限定在当前经认证的一对成员 boot epoch 之内。RegionMember.presence_revision 提供 SNAPSHOT 的 base_revision;RegionMember.durable_revision 提供 SNAPSHOT 的 marks_revision。运行时字段名不会给节点记录增加字段。

待定状态有界,且不能路由流量;丢弃 Slot 或 PendingRegistration 不会抹去 RelayMarks。

在区域中,每个成员的 RelayStore 保留合并后的完整所有者并集;会话、暂定密钥与排队输出保持本地且易失。presence 缓存为每个来源保留来源 epoch、一个已接受的高水位版本与截断状态;在 GONE 时推进该版本,可以防止较旧的 PRESENT 恢复已移除的会话,而无需按所有者保存删除历史表。快照暂存与 mesh 队列分别有界;MissCorrelation 把出口失败关联到入口上确切的活动发送方会话(§12.8)。

a 在网络 net 中运行 ssh b.<ZONE>,b 是已配置的对端。解析器返回 AAAA N6(b) 与 A alias6(b);ssh 用哪一个取决于平台(§7.3)。共有四种地址族组合,都只是纯粹的地址映射。

a 上的客户端b 上的服务报文翻译
IPv6 套接字IPv6 服务,普通名字N6(a):p → N6(b):22 及其返回无
IPv4 套接字(self4)IPv6 服务,普通名字N4(a):p → N6(b):22;回复 N6(b):22 → N4(a):p只在 a:alias6(b) ↔ N6(b)、self4 ↔ N4(a)
IPv6 套接字仅 IPv4 服务,.v4 名字N6(a):p → N4(b):22;回复 N4(b):22 → N6(a):p只在 b:N4(b) ↔ self4、N6(a) ↔ alias6(a)
IPv4 套接字仅 IPv4 服务,.v4 名字N4(a):p → N4(b):22;回复 N4(b):22 → N4(a):p两端:各自的 alias4,以及 self4 ↔ N4(self)

其他情形:

  • 同一端口,两个地址族。 a 上绑定同一端口的 IPv4 套接字与 IPV6_V6ONLY 套接字在报文上分别表现为 N4(a):p 与 N6(a):p;b 分别回复到各自的地址。
  • 用普通名字访问带端口桥的仅 IPv4 服务。 N6(a):p → N6(b):22 到达端口桥的监听;端口桥向 self4:22 建立后端连接。
  • 本地短路。 发往本节点自己的别名或地址的流量在本地投递,从不进入隧道。
  • Relay。 适配器选择经 relay 的路径时,加密报文带上 37 字节的 relay 头传输;地址、会话与翻译不受影响。

两端各自保留自己的别名:a 称 b 为 100.64.3.7,b 称 a 为 100.64.9.1,报文上只出现 fd00::/8 地址。

场景处理
主机加入了与某个别名重叠的局域网运行期间检测到;该别名被暂停,带拒绝路由与 DNS 墓碑,并报告,只有在请求时才重新分配
导入时地址组碰撞导入失败并给出冲突的身份;身份保持不变
别名地址池耗尽或被完全遮蔽给出指明冲突路由的具体错误;IPv6 继续工作;由运维人员配置其他地址池
TCP 会话期间对端改名会话不受影响;地址从不改变
K 轮换全设备固定切换(§2.3);切换时的报文可能丢失;已暂存绑定的对端按计划切换;只有当该 (network_id, I) 没有可接受的当前绑定时,对端才报告 binding_required 并使用手动导入绑定;被拒绝的输入从不改变状态,也不顶替其他可接受的当前绑定
I 丢失或被盗创建新身份并按普通方式配对,然后显式移除旧身份。恢复根 / 声明与连续性证明推迟;令牌不携带恢复关系(§2.4)
令牌泄露撤销令牌(不再向允许名单添加);移除经它接纳的身份;改为签发限定接收方的令牌
解析器不可用已配置对端的裸地址继续可用;名字无法解析;其他区域由系统解析器提供
home relay 不可用直连路径继续;经 relay 的对端变得不可达并被报告;经该 relay 的新配对等待。节点在有序快照中公布活动的 relay(§12.7);收不到快照的对端需要一条现有路径,或显式的本地端点 / 绑定恢复,而不是推断出来的 relay 目录
直连路径丢失适配器把对端移到经 relay 的路径;地址与会话不变
relay 保留标记容量耗尽在持久化健康时让现有的安全会话继续,拒绝新身份,报告准入就绪度下降,只能通过保持完好的扩容 / 压缩或另行命名的服务恢复(§5.2.2)
端口桥后端停机前端连接关闭或重置;UDP 关联超时;通过本地 API 报告
命名层目录不可用已有名字从本地表解析;可发现名字的配对与标签变更等待;其他一切不受影响

决策及被否定的替代方案:

#决策被否定的替代方案
1两个服务地址,无状态翻译,按端口显式桥接(§6)一个地址,两端都做有状态 NAT46
2地址由 (network_id, I) 派生;network_id 是网络公钥,保持轻量(§2.6)只由 I 派生地址;没有网络概念
3fd00::/8,119 位哈希,本地冲突检测(§3)产品级 /48,79 位哈希,强制登记表
4单一活动 K,固定切换(§2.3)轮换期间两个传输实例重叠
5通过令牌配对:导入 → 介绍 → 准备 → 发布(§7.5)经控制面完成”发现→授权→准备→发布”的事务
6命名层可选,按网络划分(§7.7)注册、名字与授权都依赖强制的控制面
7除非限定接收方,令牌在过期前可复用;始终执行允许名单(§7.4)一次性令牌;任何令牌持有者都被开放接受
8v1 范围只有节点到节点的 L3v1 就提供服务、子网与出口节点
9relay 在 UDP 上以 37 字节寻址头承载原始 WireGuard 报文,回退时在 TLS WebSocket 消息中承载相同的报文(§5、§12.5)只用按长度分帧的流作为载体;为发现目的地对所有 slot 做无界的 mac1 扫描
10INTRO 以 token_id 指明令牌;隧道内控制通道有固定的规范(§12.7),承载端点候选、relay 公布、绑定更新、撤销与名字记录每个 INTRO 都尝试所有未完成令牌的秘密;控制通道留给实现者自定
11缺少当前 K 的离线对端使用手动的当前 KeyBinding 导出 / 导入,不改变准入或 PSK(§2.3.1)自动的隧道外恢复、无限期使用旧密钥或静默重新配对
12轮换使用 120 s 的时钟运行边界、最少且默认 300 s 的提前量、cutover == not_before、默认 30 天的新生命周期、至少剩余 600 s、普通计划下旧绑定有效到 cutover + 240 s,旧数据密钥宽限为零(§2.3)未签名的独立计划、按 ACK 推迟切换或活动数据密钥重叠
13类型 5 ORDERED_ENDPOINTS 携带持久的完整快照版本;旧的类型 1 仅作兼容,不能覆盖有序状态(§12.7)按到达顺序、时间戳,或不兼容地扩展类型 1
14类型 6 与 7 以新的 32 字节 nonce 提供经认证的直连探测请求 / 回复关联(§12.7)依据通用 ACK、cookie 或任意已认证流量晋升路径
15显式范围的规范网络区域使用完整网络密钥;v1 默认上下文直接使用 quick.internal;由显示名派生的区域只是本地便利(§7.1、§2.7)把未签名的显示 / 配置文本当作共享的规范命名空间
16PTR 依次使用有效的命名层标签、唯一的本地昵称、不可变的身份名,并为模式 4 提供显式的身份视图(§7.3)按到达顺序或多目标的 PTR,以及跨地址族的反向目标
17Linux 透明归属为每个网络命名空间一个运行时,DNS 在 127.0.0.54:53,保存上游图并事务式恢复(§7.8.1)多个用户守护进程在一个命名空间中竞争,或把环回 DNS 作用域限定到 TUN
18relay 标识使用规范的 authority / 短 ID 语法,UDP 与 WSS 共用一个公开 authority;短 ID 需要显式的受信映射(§12.5.1)隐藏的 WSS 覆盖、宽松的 URL 语法或隐式登记表
19生产 relay 预算有限,而完整所有者的安全下限永久保留,容量满时准入拒绝服务(§5.2.1–§5.2.2)按 TTL / LRU 删除下限,或丢失标记后重新开放 relay
20本地 JSON schema、稳定的错误词汇与有界的 Linux 扫码输入是固定的本地契约(§13.2、§13.5)每个命令各自的临时 JSON、原始库错误,或无界 / 隐式的摄像头选择
21运行时在绑定过期前 7 天以普通轮换续期 K,错过普通窗口时使用迟到轮换(§2.3)只允许手动轮换、同代际重签,或更长的绑定生命周期
22v1 运行一个公开的默认上下文:每个安装的 network_id 都是 keynet-default-context-v1 的固定 SHA-256,不创建、选择或显示网络,令牌携带该常量与空的 net_name,命名层禁用(§2.7)按设备划分的个人网络,外加用户可选择的其他网络
23一个公布的 authority 内的 relay 区域,成员之间一跳 mesh 转发;全互联的 2..16 个成员(§12.8)多跳 relay 路由、区域间中转,或每个 authority 只有一台机器
24每个成员本地提交后,持久标记与确定性 presence 异步收敛(§12.8 规则 1–2)成员同步达成一致,或全区域原子准入
25只用 TCP,控制与数据连接分开,双向 TLS 1.3,区域 CA,以成员列表作为允许名单(§12.8)单独的报文隧道,或按证书配置信任
26每轮注册最多三个并行的地址候选,恰好一个确认目标,每轮使用新的材料(§12.5)确认多个候选,或在一个 incarnation 内切换选定目标

以下是有界的配置参数,而非决策:每个节点的 relay 会话数与路径迟滞(§5.1)、INTRO 退避与直连 INTRO 预算(§7.5、§12.6)、准备期限(§1.1、§13.4)、令牌过期默认值、别名地址池与 self4(§6.2)、端口桥预算(§6.7)、命名层查找上限(§7.7),以及便利区域父域(§7.1)。§12.6 在互操作或安全需要时固定生产默认值与校验下限。

推迟:默认上下文之外用户可见的网络范围(§2.7);别名导出 / 导入与任何签名的别名快照格式(§6.5);内核 WireGuard(§6.8);无 TUN 的 SDK / 代理模式;移动端 / FFI;无损密钥轮换;按网络不可关联的身份;应用层网关;服务 / 子网 / 出口扩展;众所周知的公共 relay 列表;relay 减载列表;带显式可互操作端点准入规范的、由 relay 生成的 WireGuard cookie;经目录交换限定接收方的令牌(§7.7);恢复根 / 声明及其命令与经认证的分发(§2.4)。v1 中不存在别名快照的报文格式。直连路径可用时仍偏好保留经 relay 路径的选项推迟;§12.7 的路径选择不变。

节点维护自己的纯协议实现,只把共享的互操作样例当作数据使用。relay 使用共享的协议 crate,作为独立的二进制 / 服务 nsgw-quick-relay 运行;nsgw-server 不受影响,UDP 与 WSS 共享 relay crate 的 slot / 标记表。

本节固定 §2、§5 与 §7 留给实现者的每一个 v1 字节。由于 §11 推迟了身份恢复,这里不定义恢复记录。本节具有规范性;与本节不一致的实现即使与前文的叙述一致也是错误的。整数均为无符号;CBOR 内的多字节整数遵循 CBOR;CBOR 外的多字节整数为大端序。

每条记录都是定长的 CBOR 数组(RFC 8949 §4.2.1 确定性编码):只用确定长度,整数使用最短编码,密钥与哈希用字节串,名字用文本串,缺省的可选字段用 null(0xf6),不用浮点、标签或映射。解码器在校验任何签名或 MAC 之前,拒绝不定长度、非最短整数、错误的主类型、错误的数组长度、数组之后的多余字节,以及任何不是有效 UTF-8 的文本。

签名使用 Ed25519(RFC 8032 §5.1 的 “pure” 变体,不预哈希,不用 Ed25519ctx)。被签名的消息是一个 ASCII 上下文字符串,紧接未签名数组的规范编码,中间没有分隔符,也没有长度前缀:

记录上下文未签名数组已签名数组
KeyBindingkeynet/key-binding/v1[ver, network_id, I, K, gen, not_before, not_after]上述七个字段后接 sig(八个字段)
PairingTokenkeynet/pairing-token/v1§7.4 的十一个字段按顺序排列,binding 嵌套为其已签名的八字段数组,issuer_name、endpoint 与 recipient 分别为文本、文本与字节串,或 null上述十一个字段后接 sig(十二个字段)
NameRecord,所有者keynet/name-record/v1[ver, network_id, I, label, op, version, not_after]七个字段后接 sig_owner
NameRecord,网络keynet/name-record-network/v1包含 sig_owner 的八字段数组八个字段后接 sig_network(九个字段)
Relay REGISTERkeynet/relay-register/v1[binding, incarnation, ts, node_ephemeral, relay_name]上述五个字段后接 sig(六个字段)
Revocationkeynet/revocation/v1[ver, network_id, I, revoked_through, issued_at]上述五个字段后接 sig(六个字段)

MAC 是对一个 ASCII 上下文紧接下表规范数组计算的 HMAC-SHA256。INTRO / INTRO_ACK 使用令牌秘密;relay 控制 / 回复使用 sk。

MAC上下文覆盖的数组
INTROkeynet/intro/v1[network_id, I_recipient, binding_recipient, token_id, nonce]
INTRO_ACKkeynet/intro-ack/v1[network_id, I_issuer, binding_issuer, nonce](nonce 由 INTRO 回显)
Relay 控制keynet/relay-ctl/v1[magic_bytes4,type,src,dst,unsigned_body_array],类型 3、4、10,密钥 sk(§12.5)
Relay 回复keynet/relay-reply/v1[magic_bytes4,type,src,dst,unsigned_body_array],类型 2、5,密钥 sk(§12.5)

INTRO / INTRO_ACK 的 nonce 为 32 字节,由 INTRO 发送方从密码学随机源生成,并由 INTRO_ACK 回显;relay 反射 nonce 另为 16 字节。配对 PSK 为 HKDF-SHA256(RFC 5869),不带 salt(零长度 salt,RFC 将其映射为 HashLen 个零字节),输入密钥材料为 secret,info 为 "keynet/psk/v1" || network_id || I_issuer || I_recipient,输出 32 字节。签发方与接收方始终按此顺序,因此双方派生出相同的密钥。

relay sk 为 HKDF-SHA256,不带 salt,IKM 为临时 X25519 得到的 ss,info 为 ASCII “keynet/relay-session/v1” || src_slot || incarnation_be64 || node_ephemeral || relay_ephemeral || reg_hash,输出 32 字节。reg_hash 是规范的已签名六字段 REGISTER 的 SHA256。拒绝全零的 ss。§12.5 固定所有报文头 / 嵌套数组的记录内容;配对 PSK 与介绍 MAC 不变。

  • KeyBinding 的签名校验通过即为密码学有效;not_before <= now < not_after 时为当前有效。安装需要两者都满足。not_before >= not_after 视为格式错误而拒绝。
  • 节点按 (network_id, I) 保存 committed_gen(已安装的最高 gen)与可选的 revoked_through gen。gen < committed_gen 或 gen <= revoked_through 的绑定被拒绝。gen == committed_gen 且 K 不同的绑定会锁存冲突:该代际被禁用,直到出现 gen 严格更高的有效绑定。被拒绝的过期、已撤销、陈旧或冲突输入从不改变状态;拒绝本身既不设置连通状态,也不顶替可接受的当前绑定。上述同代际冲突锁存规则不变。只有当该 (network_id, I) 没有其他可接受的当前绑定时才报告 binding_required。
  • 对 gen == committed_gen 且为已提交 K 的候选,在规范性、签名、范围、撤销与冲突锁存检查之后:不是当前有效的候选被拒绝,不改变状态也不锁存;字节完全相同的候选幂等;当前有效的候选在已存绑定不是当前有效、或候选的 not_after 更大时替换已存绑定。其他情况幂等。替换只改变已存的绑定字节;committed_gen、K、撤销与冲突标记、准入、PSK、别名、地址与名字都不变。每个被接受的区间都由 I 签名,因此这不是本地延长。同一代际而 K 不同仍是冲突。
  • 为已接纳的对端导入令牌,不会提交同一 (gen, K) 的绑定替换。同一 (gen, K) 的 BINDING 记录若字节完全相同,或按上一条规则被保留,则作为有效的当前安装予以确认。不是当前有效的候选不会得到 ACK,也不会锁存冲突。
  • 未来的 KeyBinding 在通过规范性、签名与范围校验后可以暂存,但不会成为当前绑定,也不推进 committed_gen。每个身份保留一个下一代际,为每个使用设备共享 K 的已配置网络各有一个绑定。BINDING 控制记录要求 cutover == binding.not_before。持久暂存可以被确认;安装仍需要当前有效,并通过每一项代际 / 撤销 / 冲突检查。迟到的当前绑定立即安装。过期或已撤销的输入从不推进状态。
  • NameRecord 对每个 (network_id, I) 使用一条单调的流与一个版本高水位。label 标识条目影响的资源,而不是单独的版本流。同一版本且规范记录字节完全相同时幂等;同一版本但规范字节不同时,对整个所有者流构成冲突,两条记录都不予应答。只有严格更大的有效版本才推进高水位。op 为 0 claim、1 rename、2 release。命名层在 v1 中仍然推迟。
  • PairingToken 在以下条件下被接受:其签名在 issuer 下校验通过,内嵌绑定校验通过且指向相同的 network_id 与 I,now < not_after,且 recipient 为 null 或等于导入节点的 I。v1 规范的实现在任何状态变更之前还要求 network_id == DEFAULT_NETWORK_ID(§2.7、§12.6);通用编解码器仍会为一致性测试与诊断解码并校验其他范围。文本形式为 kn1 后接不带填充的 RFC 4648 base64url;解码器在分配内存之前拒绝填充字符、空白、字母表外的字符、末尾未使用位不为零的情况,以及任何长于 1024 字节的文本。
  • token_id 是规范的已签名 PairingToken 数组(十二字段的 CBOR 字节,而非文本形式)的 SHA-256。签发方按 token_id 保存未完成的令牌;指明未知、已撤销或已过期令牌的 INTRO 被丢弃,不予回复。
  • 签名在 I 下校验通过且 ver 为 1 的 Revocation 记录,把该 (network_id, I) 的 revoked_through 提升为当前值与记录中 revoked_through 的较大者;它从不降低该值,issued_at 仅供参考。gen <= revoked_through 的绑定按上述规则被拒绝。
  • 规范网络标签:ASCII n- 后接完整 32 字节 network_id 的小写、不带填充的 RFC 4648 base32 编码。编码后的密钥为 52 个字符,末尾四个未使用位为零,因此标签恰好 54 字节。规范区域在其后追加 .quick.internal。完整的 n- 加 52 字符模式在该父域下不得用作本地便利标签。
  • 身份标签:以 keynet-id-v1 为密钥、对 I 计算 12 字节摘要的 BLAKE2s;这 96 位编码为 20 个小写、不带填充的 RFC 4648 base32 字符;最后一个字符的四个未使用位为零。
  • 别名候选:对 (network_id, I, kind)(kind 为 6 或 4)的第 i 个候选(0 ≤ i ≤ 15),H22 是以 keynet-alias-v1 为密钥、对 network_id || I || kind_u8 || i_u8 计算的 BLAKE2s 的 3 字节大端摘要的高 22 位;候选地址为 pool.base + (H22 mod pool.size),其中 pool.size 是配置的地址池中的地址数,必须非零。该序列不去重,也不做主机路由过滤;这些是 §6.3 中调用方的步骤。
  • 由显示名规范化标签:保留小写 ASCII 字母与数字;其他字符的每个最长连续段变为一个连字符;去掉首尾的连字符;结果截断到 63 字节后再次去掉末尾的连字符;空结果或保留标签 id 与 v4 被拒绝。校验已规范化的标签时,检查完全相同的字母表、长度与保留规则。
  • 默认昵称:规范化 issuer_name,为 - 加 12 字节 H96 摘要的前四个小写十六进制数字预留五个字节,只把规范化前缀截断到最多 58 字节,再追加完整后缀。规范化结果为空或为保留标签时,改用不可变的身份标签。与该网络中已有的昵称冲突时,不给新来者分配默认昵称;它保留身份标签,并报告冲突以便用户改名。
  • DNS 父域输入:由单个点分隔的、1–63 字节的小写 ASCII 标签,不计末尾的点最多 253 个表示字节。本地输入去掉一个末尾的点。拒绝原始 Unicode,不隐式进行 IDNA 转换。安装或报告便利区域之前,按同样的 253 字节表示上限校验完整区域以及每个生成的普通、.v4、身份与身份 .v4 名字(§7.1);生成的名字变化时重新校验。任何超长的名字都会抑制整个便利区域并被报告;规范区域不受影响。
  • slot id:以 “keynet-relay-slot-v1” 为密钥、对 network_id || I(完整的两个 32 字节输入)计算 16 字节摘要的 BLAKE2s。它标识一个 relay slot,不是秘密,也不是覆盖地址。全零值保留给 relay;派生 id 为零的节点不能按本规范注册。relay 在哈希之外保留并比较完整的 (network_id, I) 所有者,拒绝不同所有者的碰撞,且不改变现有的 slot 或其安全标记。不要重新映射碰撞或为零的摘要,不要截短所有者检查,也不要静默地重新生成身份。

恰好一个 UDP 报文或一个完整的二进制 WebSocket 消息包含一个 relay 报文。含报文头的最大长度为 2048 字节;非 DATA 类型另有 1200 字节上限。在解析报文体或分配内存之前,拒绝超限或被截断的传输消息。这些是编解码上限,不是路径 MTU。

偏移宽度字段
04ASCII 魔数 KNR1
41type u8
516src slot id
2116dst slot id
37余下部分body

KNR1 标识本版本。没有长度或代际字段。短于 37 字节或魔数不同的输入无效。slot id 为零表示 relay。类型 1、3、4、10 要求 src 非零、dst 为零;类型 2、5、9 要求 src 为零、dst 非零;类型 6、7、8 要求两者都非零。

除 DATA 外,每个报文体恰好是一个 §12.1 规范定长数组,没有多余字节。在认证之前检查所有字节宽度、无符号范围、嵌套结构与 UTF-8。binding 指完整的已签名八字段 KeyBinding。不要规范化收到的字节。

类型名称方向报文体
1REGISTER节点到 relay[binding, incarnation u64, ts u64, node_ephemeral bytes32, relay_name text, sig bytes64]
2REGISTEREDrelay 到节点[incarnation u64, relay_ephemeral bytes32, presence_ttl_s u32, observed_addr, ack_seq u64, mac bytes32]
3PRESENCE节点到 relay[incarnation u64, seq u64, mac bytes32]
4REFLECT节点到 relay[incarnation u64, seq u64, nonce bytes16, mac bytes32]
5REFLECTEDrelay 到节点[incarnation u64, seq u64, nonce bytes16, observed_addr, mac bytes32]
6INTRO经 relay 或直连[network_id bytes32, I_recipient bytes32, binding_recipient, token_id bytes32, nonce bytes32, mac bytes32]
7INTRO_ACK经 relay 或直连[network_id bytes32, I_issuer bytes32, binding_issuer, nonce bytes32, mac bytes32]
8DATA节点经 relay 到节点一个原样的 WireGuard 报文,1..2011 字节,对本编解码器不透明,不是 CBOR;relay 只读取其第一个字节,用于识别握手发起报文(WireGuard 类型 1),以计入按目的地的握手发起桶
9STATUSrelay 到节点[type_in u8, destination bytes16, status u8]
10REVOKE节点到 relay[incarnation u64, seq u64, reason u8, mac bytes32]

relay_name 为 1..255 个 UTF-8 字节,与公布的 relay 标识逐字节一致。编解码器中不做大小写折叠或 URI 规范化。令牌中的 host:port 同时选定两种载体;短 ID 通过显式的本地配置解析,而不是新的登记表。解析不改变已签名的 relay_name。

observed_addr 为 [ip bytes4 或 bytes16, port u16,取值 1..65535]。IPv4 使用四字节,从不使用映射的 IPv6。WSS 上的 REGISTERED 使用 null;UDP 的 REGISTERED 与 REFLECTED 使用地址。拒绝来自无法表示其 zone 的限定作用域 IPv6 源的 UDP 注册。观测只是需要做可达性测试的建议候选,不是对端断言,也不保证 NAT 行为。

REGISTERED 的 ack_seq=0 表示暂定;正的 ack_seq 确认 PRESENCE 与活动 slot。TTL 为 30..3600 秒,默认 90。REFLECTED 回显其请求的 seq 与 nonce。REVOKE 的 reason 为 0 轮换或 1 关闭;其他值视为格式错误。STATUS 报文体中的 destination 回显输入报文头的 dst,STATUS 报文头的 dst 为输入报文头的 src(因此发给未注册源的 STATUS 6 携带该源声称的 slot);status 为 1 无目的地、2 被限速、3 过大、4 代际 / incarnation 陈旧、5 不支持的类型、6 发送方未注册 / 源不匹配。其他值视为格式错误。STATUS 不表示成功 / 投递,也不表示配对被拒。

签名与密钥 / MAC 的记录内容:

  • REGISTER 由 binding.I 以普通 Ed25519 对 ASCII “keynet/relay-register/v1” || CBOR([binding,incarnation,ts,node_ephemeral,relay_name]) 签名。嵌套的 binding 包含其签名。
  • R 是规范的六字段已签名 REGISTER 报文体;reg_hash=SHA256(R)。ss 是本地临时私钥与对方临时公钥的 X25519。拒绝全零的 ss。新的尝试使用新的临时私钥;完全相同的重传复用该尝试。
  • sk 为 HKDF-SHA256,不带 salt,IKM 为 ss,info 为 ASCII “keynet/relay-session/v1” || src_slot || incarnation_be64 || node_ephemeral || relay_ephemeral || reg_hash,输出 32 字节。src_slot 即 REGISTER.src。拼接的字段都是定宽的;incarnation 为八字节大端。
  • 节点控制报文 3、4、10 使用 HMAC-SHA256(sk, ASCII “keynet/relay-ctl/v1” || CBOR([magic_bytes4,type,src,dst,U]))。relay 回复 2、5 使用 “keynet/relay-reply/v1” 与同样的五字段数组。U 是去掉末尾 mac 的嵌套报文体数组,不展开,也不包成字节串。magic_bytes4 为 KNR1。在常量时间 MAC 校验之前先校验规范性 / schema。
  • INTRO / INTRO_ACK 保留其 §12.2 基于令牌秘密的 MAC;relay 报文头不进入这些原像。签发方按 INTRO 的 token_id 选择秘密(§12.3)。认证报文体之后,端点要求 dst 等于本地 slot、本地网络匹配、src 等于由报文体中发送方 / 网络派生出的 slot,且 binding 指向该发送方 / 网络。报文头不能授权或替换身份。

§5.2.1 中分阶段的预算适用与计量规则适用于下面的注册、控制、转发与回复。

注册与控制:

  1. 在密码学运算之前,检查长度、魔数、方向、规范 schema、预算、派生出的 REGISTER.src、relay_name,以及不溢出的、含端点的 abs(now-ts)<=120 秒。在建立暂定状态之前,校验 binding 的签名 / 当前有效性与 REGISTER 签名。应用 §12.3 的标记;同代际不同 K 的候选可以为确认持有而暂存,但从不安装路由或清除冲突。无效输入从不提交状态。嵌套的 binding 是节点在该网络持久化的当前绑定(§2.2)。没有当前绑定时节点不注册,也不为注册另签绑定。本地 (network_id, I) 没有可接受的当前绑定时,节点推迟 relay 注册:不发出 UDP REGISTER 或 WSS REGISTER,也不打开 WSS 连接,直连路径不受影响。现有的 relay 诊断在没有持久化绑定时报告 binding_required,在已持久化的绑定不是当前有效时报告 binding_invalid;成功恢复后清除。推迟期间,节点以有界指数退避定期重新评估绑定是否可用。绑定状态变化会立即唤醒这次推迟的检查,不必等待退避。获得可接受的当前绑定后,节点恰好产生一次初始注册转换;普通的载体重传只在这次转换之后才适用。重复观察到同一状态不会产生新的转换。
  2. 暂定记录以 relay 服务、完整所有者、确切的已签名 REGISTER 与确切的源为键。为它分配一个新的 relay 临时密钥,最长生命周期 10 秒。来自同一源的完全相同的重传,会重发同一个暂定 REGISTERED,但不延长其生命周期。其他源可以获得一个单独有界、临时密钥不同的挑战;它不能仅凭先到达就移动现有的暂定 / 活动源或占住活动 slot。
  3. 节点校验暂定 REGISTERED 的 MAC、当前尝试、源 / 连接、TTL 与观测,然后用 sk 发送序号为正的 PRESENCE。relay 校验其 MAC 与确切的暂定源,并在提交之前重新检查待定期限、时间戳、绑定的当前有效性与当前标记。未确认的 slot 不能转发。
  4. 先预检 / 预留持久标记容量,然后在正向确认授权转发之前,原子提交绑定 / 标记、源、incarnation、sk 与 seq。只有在持久提交之后,才退役旧会话并发送确认该 seq 的 REGISTERED。节点只有在收到相匹配的正向确认后才使用该 slot。确认丢失时,再发一个新 seq 的 PRESENCE,即可从已激活的会话得到新的确认;在提交标记之后崩溃则可能需要更大的 incarnation。
  5. 更低或已撤销的代际失败。经持有确认的、有效的同代际不同 K 声明会锁存 §12.3 冲突,并禁用该代际,直到出现更高的可接受代际;它从不选择最新的 K。在没有冲突的代际 / K 内,新会话需要更大的 incarnation。更高的代际可以重置 incarnation。代际与 incarnation 相同时,只有在该会话存活期间、且 REGISTER / 源完全相同时才幂等;它不能重建已过期或已撤销的会话。
  6. 拒绝不同完整所有者的 slot 碰撞,且不改变现有者。按 §12.3 与 §5.2.2 持久化代际 / 撤销 / 冲突 / incarnation 下限;完整所有者的下限永久保留,包括过期、REVOKE、关闭与载体变化之后。节点跨载体按 relay / 代际持久化递增的 incarnation。在接受注册之前加载并校验标记;预期状态缺失、损坏、写入失败或检测到回滚时拒绝服务(fail closed)。限制标记存储,宁可拒绝新身份也不丢弃安全下限。incarnation 用尽需要更高的有效代际;代际用尽从不回绕,需要通过普通配对使用新身份。
  7. 节点 seq 从 1 开始,每次新的控制发送加一。接收方接受任何高于高水位的已认证 seq,允许丢失 / 乱序造成的空缺。更低或相等的值被丢弃,不回复、不刷新 TTL、不改变状态。无效的 MAC 从不推进标记。seq 用尽需要新的已确认 incarnation。
  8. 只有来自确切活动源、被接受的节点控制报文才刷新 TTL。数据、介绍、重复的 REGISTER / 控制、STATUS 与回复都不刷新。过期时刻为”最后控制时间加 TTL”与 binding.not_after 中较早者(不含端点)。REVOKE 移除该会话并保留标记;它不撤销令牌或对端允许名单。
  9. PRESENCE 先确认建立,之后间隔最多 min(TTL/3,25 秒)。每个被接受的请求都会收到 ack_seq 相匹配的 REGISTERED。节点只接受针对当前会话中未完成请求的回复;重放不刷新存活。连续两个间隔没有新的、已认证且相匹配的响应时开始恢复,即按规则 11 以更大的 incarnation 从 UDP 载体开始重新注册;这并不是对签发方拒绝 / 离线的判断。
  10. REFLECT 要求活动的 UDP 源与递增的已认证 seq。只接受针对当前会话未完成 seq / nonce 的 REFLECTED。不支持 WSS 上的 REFLECT,可能返回 STATUS 5,也不产生 UDP 候选。节点在 UDP relay 会话激活时发送 REFLECT,之后每个 PRESENCE 间隔 min(TTL/3, 25 秒) 最多发送一次,下限 10 秒,只在已确认的 UDP 会话上发送。最多 8 个未完成的 REFLECT 关联;超出时驱逐最旧的一个,对被驱逐请求的迟到回复予以忽略。PRESENCE 关联不受影响。
  11. NAT 重绑定 / 新连接 / 载体迁移需要新的签名临时密钥与更大的 incarnation,然后确认。替换处于暂定状态期间,保留原本有效的旧源。排队的输出绑定到其已提交的会话;退役时丢弃该会话排队的发送,而不是把它们转向新源。

转发与错误:

对 DATA / INTRO / INTRO_ACK,要求 src 活动且源确切,并且 dst 在本成员上活动,或在同一区域的其他成员上有合格的 presence。完整的源与目的地所有者必须属于同一上下文。应用 §5.2.1 与 §12.8 规则 3 的入口与出口准入阶段,并原样转发字节。查找使用 slot id 与保存的完整所有者。校验介绍报文体 / 嵌套绑定的结构,但端到端的 MAC / 签名校验留给端点。经 mesh 收到的帧从不再次转发;只有入口成员向节点发送 STATUS,出口失败按 §12.8 规则 3 由 MISS 关联。

接收方先检查本地 dst 与 src 已安装的完整对端映射,再把剥离后的 DATA 交给其稳定的 WireGuard 端点。WireGuard 校验不透明的报文体、当前密钥与重放窗口;报文头既不认证发送方,也不认证代际。直连 WireGuard 使用原始报文。只有直连的 INTRO / INTRO_ACK 使用 KNR1,并且即使来自未知源也要做完整的边界 / 令牌 / 报文体 / 报文头检查。节点在任何 MAC 计算之前,按源地址与全局为直连 INTRO 与 INTRO_ACK 的处理计算预算(§12.6),超出预算时静默丢弃。不要把任意带报文头的直连 DATA 当作 relay 流量。端点 cookie 原样转发;不要用伪造的对端 slot 生成 relay cookie。

UDP 上格式错误的已知类型静默处理。KNR1 下的未知类型可以收到 STATUS 5。STATUS 1-5 要求一个已注册且匹配的源。STATUS 6 可以应答未注册 / 不匹配的源,但 REGISTER / STATUS 除外;从不以 STATUS 回复 STATUS。所有 STATUS 回复都必须不大于触发它的报文,并在源 / 全局回复预算之内。暂定的 REGISTERED 不能大于其 REGISTER 请求。其他情况静默丢弃。未认证的 STATUS 从不移动 / 删除会话,也不确认存活。

解码器不等待更多字节,也不消费拼接的记录。非 DATA 恰好在其规范数组处结束;DATA 消费有界的余下部分。把切片交给解码器之前,保留 UDP 截断检测与有界的 WebSocket 组帧。

WebSocket 载体:

在 UDP 的 host/port 上使用 wss://host:port/keynet/v1 与子协议 keynet-relay-v1。校验正常的 TLS 证书;UDP 失败不豁免校验。短标识通过显式的本地 host/port/trust 配置解析。

一个二进制消息包含一个报文。允许 WebSocket 分片,组帧总量上限 2048,类型已知时执行 1200 的控制上限。不使用压缩扩展、文本、拼接记录或长度前缀。文本 / 超大 / 魔数错误 / 格式错误的已知类型消息会关闭连接。标准 WebSocket 控制帧不刷新 Quick 的 presence。

连接打开后 10 秒内至少确认一次注册。额外的 slot 拥有独立的会话,并受配置的每连接 / 全局上限约束。slot 各自过期;既没有活动 slot、也没有未过期暂定注册的连接会被关闭。关闭会退役其 slot,但不抹去标记。UDP / WSS 的 slot 可以通过同一张表转发。

默认的 UDP 启动在 0、1、2 秒重传同一个 REGISTER;3 秒内没有可用的暂定响应时允许 WSS 回退。在 10 秒的尝试期限内未能确认同样允许回退。在替换一个正常工作的源之前,先确认回退。60 秒后以有界退避与迟滞重试 UDP;间隔可配置。只有已确认的更新 incarnation 才改变活动源;失败的探测保留正常工作的 WSS。

多地址 authority 的节点选择:

  1. 每个已签名 authority 一个逻辑 relay 会话,每个会话同一时刻一轮 UDP。一轮最多使用 3 个去重后的地址,取自现有的解析结果(最多 8 个地址,公平轮转,用尽或恢复后刷新;截断会被报告,不承诺从一次解析就能探测大型区域的所有成员)。
  2. 每一轮之前,运行时持久地分配新的 incarnation 与新的节点临时密钥。本轮的每个地址都按现有的 0、1、2 s 重传计划收到字节完全相同的 REGISTER 报文。
  3. 每个暂定 REGISTERED 回复都按其确切的尝试源地址、当前轮次、incarnation、MAC、TTL 与观测校验,每个地址分别派生候选密钥;选定的 relay 临时密钥固定下来,使之后的暂定回复不能替换已选定的挑战。
  4. 在 PRESENCE 入队之前原子地选出第一个有效候选;PRESENCE 只发往该候选(重试时使用新的序号),会话只在该候选相关联的正向 REGISTERED 到达时才激活。一次确认意味着一个目标,而不是只发送一个报文。落选候选排队中的 REGISTER 与 PRESENCE 被取消,其迟到回复被忽略,运行时从不以落选候选的密钥发出确认。一旦尝试确认,在同一个 incarnation 下从不切换胜者;选定候选失败则结束本轮,下一轮以新的材料轮转地址。
  5. 活动会话保留其确切的源地址或连接、密钥、incarnation 与控制关联;DATA、保活与 REFLECT 使用该源,从不使用第一个解析出的地址。成员丢失后的重新注册是一个 incarnation 更大的新轮次(§12.5 的迁移规则),而不是发往第一个 DNS 地址的控制报文。
  6. WSS 回退保持现有的触发条件(3 s 内没有可用的 UDP 暂定回复)。开始回退会结束失败的 UDP 轮次,并分配新的 incarnation 与临时密钥;来自过时尝试的迟到确认被取消。选定的 UDP 候选保持现有的 10 s 确认期限。WSS 会话绑定到其确切的连接,按原 authority 的主机名与信任校验;新的 UDP 探测期间保留健康的活动 WSS 会话,只在确认之后才替换。
  7. 三个地址可能到达同一个成员(别名)。成员本地每个所有者 2 个暂定注册的上限仍然适用,被拒绝即是普通的失败候选;不会为并行尝试提高该上限。
  8. “第一个可用回复”选出的是采样中响应最快的成员,并不保证是地理上最近或全局延迟最低的成员。“每个 authority 一个会话”是节点侧的选择规则:分区中的成员可能保留同一节点的旧会话,直到其 TTL 过期或标记收敛。

PairingToken 的 relay、类型 5 ORDERED_ENDPOINTS 中的 relay 公布,以及 REGISTER 的 relay_name,使用同一种规范标识:

relay-identifier = authority / short-id
endpoint-hint = authority
authority = ipv4 ":" port / "[" ipv6 "]:" port / dns-name ":" port
short-id = lower-alpha *(lower-alpha / digit / "-")
port = decimal integer 1..65535, no leading zero

短 ID 为 1–63 个 ASCII 字节,不以连字符结尾,不含点或冒号。端点不能是短 ID。endpoint = null 是唯一的缺省表示;空字符串无效。relay 是必需且非空的。scheme、路径、用户信息、查询、片段、隐式端口、SRV 查找、百分号转义与首尾空白都无效。

IPv4 恰好有四个取值 0..255 的十进制字节,除 0 外不带前导零;看起来像数字但无效的点分文本不会转而当作 DNS。IPv6 在 authority 中用方括号括起,使用小写十六进制,各组不带前导零,压缩最长的、至少两组的零段,长度相同时取最左侧。zone ID、映射的 IPv4 与点分结尾都无效。未指定地址、组播与限定作用域 / 链路本地的目的地无效;环回与私有单播地址允许用于显式的本地配置与隔离测试。DNS 名字由 1–63 字节的小写 ASCII 标签组成,取值为字母 / 数字 / 连字符,首尾不是连字符,以单个点分隔,不带末尾的点。原始 Unicode 无效;ASCII 的 xn-- 标签按字面处理,不隐式做 IDNA。全数字的点分文本保留给 IPv4 解析。整个字段(含方括号与端口)不超过现有的 255 字节上限。

本地输入可以在签名之前把 ASCII 的 DNS / IPv6 文本转为小写、规范化有效地址,并去掉一个 DNS 末尾的点。收到的已签名文本必须已经是规范形式,否则被拒绝而不是规范化。解析从不替换已签名的标识。示例:relay.example:51822、192.0.2.10:51822、[2001:db8::10]:51822 与 relay-a 是有效的 relay 标识。wss://relay.example:51822/keynet/v1、Relay.Example:051822、2001:db8::10:51822、[fe80::1%eth0]:51822、192.000.2.10:51822 以及作为端点的 relay-a 都无效。

每个 relay 服务有一个 advertised_name;UDP 与 WSS 共享每个成员本地保留的标记,区域标记按 §12.8 收敛。一个区域恰好有一个规范的公布标识,在每个成员上配置得完全相同。节点签署的就是这个名字;已签名的 relay 名字从不被改写为地址。即使 DNS 解析到同一台主机,不同的已签名名字也会被拒绝。成员 id 从不出现在令牌或端点公布中;mesh 地址从不是节点可见的记录。

公布的标识UDP 目标WSS 目标与证书名
DNS authority relay.example:51822该主机的 DNS 地址,UDP 端口 51822wss://relay.example:51822/keynet/v1;relay.example 的证书;普通的基于主机名的 TLS 选择
IPv4 或带方括号的 IPv6 authority字面 IP 与端口相同的字面 host/port URI;证书按该 IP 地址校验
短 ID relay-a显式本地映射中的规范 host 与端口相同的 host/port,固定的路径 / 子协议;证书按映射的 host 及其显式信任配置校验

短 ID 映射是受信的本地配置 {identifier, host, port, trust}。其 identifier 等于该服务的公布名字;host/port 遵循 authority 语法;trust 为系统根证书或显式配置的私有证书库。每个使用该 ID 的节点都要有意识地安装这个映射。未知 ID 报告 relay_config_missing;不意味着任何登记表或猜测。已签名的 relay 文本不携带证书、映射记录或 WSS URI。

两种载体暴露同一个公开主机与数字端口,每个成员都在该公开端口上终结 UDP 与 WSS。同一个公布标识背后的多台终结机器使用 §12.8 的区域规范:每个成员原子提交本地 slot 与保留的标记,异步合并区域标记,并做确定性的 presence 收敛;这不是一个原子的全区域命名空间。分区期间,陈旧的本地会话与延迟的退役可能在那里规定的限度内同时存在。同一成员上的 UDP 与 WSS 共享其本地核心,迁移保持 §12.5 的新 incarnation 与正向确认规则。不同的公布标识仍是不同的 authority 与会话,不迁移共享的 slot。隐藏的 WSS URL 覆盖无效。WSS 严格使用路径 /keynet/v1、子协议 keynet-relay-v1,不允许重定向、文本、压缩或拼接。其观测为 null;经 WSS 的 REFLECT 不产生 UDP 候选。本地映射变化会退役针对该映射的尝试,并要求新的已确认会话,但不重置同一公布名字 / 代际下持久化的 incarnation。

对 DNS authority,其主机在已签名的端口上发布每个成员的地址;节点的解析与采样仍受 §12.5 约束。短 ID 保持上述显式映射:映射到 DNS 主机时提供多个地址,映射到字面地址时只提供一个。字面 IP authority 只有一个地址,客户端不会发现成员并做故障转移。位于该字面地址之前的 anycast 或负载均衡不在本规范范围内,且必须保持载体与会话的亲和性。解析成员地址从不改变原 authority 的 WSS 主机名与信任校验。节点也可以在配置中为区域显式列出成员,并按成员游标轮换(Quick 模式 §5.4);这不改变已签名的区域标识与报文。

项目值
地址格式版本 ver1
DEFAULT_NETWORK_ID,v1 规范的范围ASCII keynet-default-context-v1 的 SHA-256:953ead48b53d57f072706b6a5d400037c8baf02f1f516cef60a83b17b6902c02;不可配置
KeyBinding、PairingToken、NameRecord、INTRO、Revocation 与隧道内控制记录的版本1
token_id32 字节,规范已签名 PairingToken 的 SHA-256
令牌文本形式最大长度1024 字节
issuer_name、net_name、relay、endpoint 文本字段各自最多 255 字节,有效 UTF-8
标签规范化后 1-63 字节
INTRO / INTRO_ACK nonce32 字节
secret、PSK、MAC32 字节
relay 报文最大长度2048 字节,含报文头
relay 报文头37 字节:魔数 KNR1、type、src、dst
DATA 报文体最大长度2011 字节;非空,对 relay 编解码器不透明
控制报文最大长度,类型 1-7、9、101200 字节,含报文头
slot id16 字节;零值保留给 relay
relay 临时公钥、ss、sk、reg_hash32 字节;拒绝全零的 ss
relay REGISTER 的 relay_name1-255 个 UTF-8 字节;与公布名字完全一致
relay 反射 nonce16 字节
REGISTER tsUnix 秒 u64;含端点的 +/-120 秒窗口;比较不溢出
presence_ttl_su32,取值 30..3600;默认 90 秒
PRESENCE 间隔最多 min(presence_ttl_s/3,25 秒);每个被接受的请求都有一个相匹配的已认证 REGISTERED 回复
暂定注册生命周期最多 10 秒;重复的 REGISTER 不延长它
presence 过期不含端点,另受 KeyBinding not_after 约束;只有被接受的节点控制报文才刷新它
relay incarnationu64;新会话在同一代际内跨载体严格递增;从不回绕
relay 控制序号u64,每个会话从 1 开始;发送方递增,接收方允许空缺并拒绝更低 / 相等的值;从不回绕
relay 回复关联REGISTERED ack_seq=0 为暂定,正的 ack_seq 与 PRESENCE 匹配;REFLECTED 与当前会话的 seq 和 nonce 匹配
保留的 relay 标记代际 / 撤销 / 冲突标记遵循 §12.3;当前代际的 incarnation 标记在 slot 过期与 REVOKE 后保留
默认的经 relay 内层 MTU 上限1376 字节,另受实际路径 MTU 约束(§6.6)
每个节点的 relay 会话默认 8;可配置(§5.1)
INTRO 重试退避初始 1 s,翻倍至 60 s,±25 % 抖动;可配置(§7.5)
直连 INTRO / INTRO_ACK 预算每个源地址每秒 4 个,突发 8;每个节点每秒 200 个;可配置
隧道内控制端口N6(self) 上的 UDP 47900(§12.7)
隧道内控制报文最大长度UDP 载荷 1200 字节
ENDPOINTS 候选与 relay最多 8 个候选与 4 个 relay 名字;每个 relay 名字 1–255 字节;序列化后的报文仍不超过 1200 字节
有序端点版本u64,从 1 开始,按 (local I, network_id) 跨会话、载体与 K 轮换持久且严格递增;从不回绕
隧道内重传1 s、2 s、4 s,之后停止,直到下一次会话开始;每个对端最多 4 条未确认的记录
探测 nonce32 字节,新的密码学随机值;与 INTRO 的 bytes32 和 REFLECT 的 bytes16 nonce 相互独立
探测请求期限1 s(不含端点),另受轮次期限约束;每个候选只有一个未完成请求
打洞轮次每个候选每 250 ms 最多一次探测;按单调时间最多 30 s;新的一轮要求上一轮已结束且 now >= previous_round_start + 30 s
进行中的候选探测每个对端最多 8 个,每个节点最多 4096 个
生成的探测请求每个节点 256/s,突发 256
探测回复每个已认证对端 32/s,突发 32;每个节点 256/s,突发 256
时钟运行边界每个参与方的时钟与 UTC 相差 120 s 以内;仅为可用性假设
轮换计划提前量至少 300 s;默认 cutover now + 300;cutover == binding.not_before
新绑定生命周期默认在 cutover 之后 2592000 s;cutover 时至少剩余 600 s
旧绑定有效期普通计划要求 not_after >= cutover + 240 s
旧数据密钥宽限v1 中为 0 s
自动续期提前量在最早的当前绑定 not_after 之前 604800 s
续期时钟稳定性300 s 内墙钟跳变不超过 120 s
续期重试间隔自动尝试失败后 60 s
当前绑定导入最大长度512 字节,恰好一个规范的已签名八字段 KeyBinding 数组
规范网络标签54 字节:n- 加完整 network_id 的 52 个小写、不带填充的 base32 字符
规范区域父域固定为 quick.internal
便利父域与生成的名字可配置的小写 ASCII DNS 父域;父域、完整区域以及每个生成的普通、.v4、身份与身份 .v4 名字,不计末尾点最多 253 个表示字节。在安装 / 报告之前以及生成的名字变化时校验;任何超长名字只抑制并报告该便利区域(§§7.1、12.4)
relay 标识 / 端点文本规范的 §12.5.1 语法;整个已签名字段最多 255 字节
扫码超时默认 30 s;接受范围 1..300 s,单调时间
扫码采集最大 1920x1080,每秒处理最多 10 帧,缓冲 / 解码的图像数据最多 32 MiB
扫码解出的令牌交给普通的严格令牌解码器之前最多 1024 字节

relay 生产默认值具有规范性,只能按 §5.2.1 的非零 / 保留状态规则配置:

资源默认值、突发或硬上限
REGISTER 校验尝试每 IP 4/s,突发 8;全局 1000/s,突发 2000
并发密码学工作32 个执行中,128 个排队,最长排队 1 s
暂定注册全局 4096;每源 IP 16;每个完整所有者 2;每个 WSS 连接 8
活动 slot全局 10000;每个 WSS 连接 8
保留的完整所有者标记1000000 个所有者,编码后的逻辑标记数据最多 512 MiB
标记存储磁盘4 GiB,含日志与压缩余量
WSS 连接全局 2048;每源 IP 16;最多 256 个未确认
新的 TLS / WSS 握手每源 IP 2/s,突发 4;全局 100/s,突发 200
节点控制报文发送方 16 报文/s,突发 32,且 32 KiB/s,突发 64 KiB;全局 20000 报文/s,突发 40000,且 32 MiB/s,突发 64 MiB
转发的 INTRO / INTRO_ACK发送方与目的地各 4 报文/s,突发 8,且 8 KiB/s,突发 16 KiB;全局 2000 报文/s,突发 4000,且 4 MiB/s,突发 8 MiB
转发的 DATA发送方与目的地各 2000 报文/s,突发 4000,且 2 MiB/s,突发 4 MiB;全局 100000 报文/s,突发 200000,且 128 MiB/s,突发 256 MiB
DATA 中的握手发起子集目的地 20 报文/s,突发 40,另计入所有 DATA 桶
回复每源 IP 16 报文/s,突发 32,且 32 KiB/s,突发 64 KiB;全局 20000 报文/s,突发 40000,且 32 MiB/s,突发 64 MiB
排队输出每个已提交目的地 64 个报文、128 KiB;全局 8192 个报文、16 MiB;最长 1 s
WSS 入站组帧每个连接一条最多 2048 字节的消息,已知控制报文上限 1200;全局合计 4 MiB
认证前限速状态65536 个 IP 条目,空闲保留 120 s;表满时拒绝未见过的源

成员间默认值(§12.8)在现有节点上限之外另行适用。上面的全局资源范围按成员计;保留所有者的上限作用于每个成员上的完整区域并集。

项目值
每个区域的成员2..16;全互联,每对无序成员两条 TLS / TCP 连接
mesh 帧最大长度完整编码的 CBOR 数组 4096 字节,不含 4 字节长度前缀;转发的节点报文最多 2048 字节
SNAPSHOT 分块presence 与标记条目合计最多 256 个,另受编码后的帧大小约束;从不拆分条目
presence 缓存宽限2 * presence_ttl_s,默认值下为 180 s;从不授权经不可用通道投递
mesh PING每 5 s 一次;3 次无应答则关闭两条连接
数据停滞有未完成数据时 15 s 内接收进度没有推进;关闭数据连接
TCP 存活在支持的平台上,每 5 s 发送保活探测,3 次失败;未确认写入超时 15 s
连接建立期限连接、TLS 与 HELLO 共用 10 s
mesh 重连退避每个失败的通道初始 1 s,翻倍至 60 s,带 25% 抖动;健康 30 s 后重置
每对成员的 mesh 数据输出64 帧、128 KiB;最长排队 1 s
进程数据与节点输出mesh 数据与节点输出合计 8192 个报文、16 MiB;出口处仍适用现有的已提交目的地上限
mesh 控制输出预留与数据和节点输出分开:每对 256 帧、1 MiB,每进程 16 MiB
活动来源 presence全区域 160000 条记录,每个成员 10000 条;快照暂存与事件版本分别有界
每个成员的保留所有者并集现有上限:1000000 个所有者,512 MiB 编码后的逻辑标记,4 GiB 磁盘(含日志与压缩余量);不是区域容量的叠加
MISS 回复窗口自入口准入起按单调时间 15 s;在 now >= reply_deadline 时过期,另受确切发送方会话与数据代际约束
MISS 关联每对成员最多 4096 个条目、2 MiB;每进程最多 16384 个条目、8 MiB;同时成立;不为接纳新的转发而驱逐活动条目
mesh 帧输入组帧每个通道一个前缀 / 报文体组帧,最多 4100 字节;每进程最多 128 KiB;每个通道最多一个已完成但未消费的输入帧,计入同一上限
mesh 传输缓冲保留的 TLS 与套接字缓冲合计,每个通道每个方向最多 256 KiB,每进程最多 16 MiB,与应用帧和输出队列分开计量
presence 快照暂存每个来源一个传入快照;每个来源最多 10000 条记录、4 MiB,每进程最多 150000 条记录、64 MiB;与已发布的 presence 分开计费
快照期间推迟的变更每个发出的对端快照最多 4096 个待处理的 presence 或标记变更、1 MiB;每进程最多 16 MiB;计入所有保留的载荷与队列 / 索引分配
快照标记批次每个传入快照最多 256 条所有者记录、64 KiB 暂存的编码标记记录;每进程最多 1 MiB;保留的并集与所有快照磁盘版本仍受现有的逻辑 / 磁盘上限约束
presence 版本跟踪每个配置的来源(最多 16 个)一个已接受的版本与截断状态;没有无界的删除历史表
mesh 状态响应最多 65536 个 UTF-8 JSON 字节,含完整报文体;一个本地成员与最多 15 个对等成员条目

成员地址从不进入 ORDERED_ENDPOINTS;隧道内记录始终是端到端的。

隧道内控制通道(§5.3)在隧道内从 N6(self) 端口 47900 到 N6(peer) 端口 47900,每个 UDP 报文承载一条记录。适配器在 WireGuard 与 TUN 设备之间、§4 入站检查之后、翻译之前消费这些报文,因此主机协议栈从不看到这个端口。一个报文是一个最多 1200 字节的规范数组 [ver u8 = 1, type u8, body]。WireGuard 会话认证发送方,因此 ACK、两种端点形式以及探测请求 / 回复不另带签名;BINDING、REVOCATION 与 NAME 携带嵌套记录的签名,可以被保存。未知类型、错误的 ver 或格式错误的报文体会被忽略,不予回复。

类型名称报文体
0ACK[type_in u8, hash bytes16];hash 是对被确认报文字节计算的、16 字节摘要且不带密钥的 BLAKE2s
1ENDPOINTS[candidates, relays];candidates 是最多 8 个 [ip bytes4 or bytes16, port u16 in 1..65535, kind u8] 条目的数组,kind 为 0 本地接口、1 由 relay 反射(§12.5 REFLECTED)、2 配置;relays 是最多 4 个 relay 名字(文本,1–255 字节,按令牌中的写法)的数组,发送方在这些 relay 上持有活动 slot
2BINDING[binding (signed KeyBinding), cutover u64],cutover == binding.not_before;接收方按 §§2.3 与 12.3 暂存或安装
3REVOCATION[revocation],§12.2 的六字段已签名记录;接收方提升 revoked_through(§12.3)
4NAME[name_record],九字段的已签名 NameRecord(§12.2);仅命名层使用(§7.7)
5ORDERED_ENDPOINTS[revision u64, candidates, relays];candidates 是最多 8 个 [ip bytes4 or bytes16, port u16 in 1..65535, kind u8] 条目的数组,kind 为 0 本地接口、1 反射、2 配置;relays 是最多 4 个规范的 §12.5.1 relay 标识文本串的数组,每个 1–255 字节;完整的三字段报文体是一个原子快照
6PROBE_REQUEST[nonce bytes32];新的、密码学随机的直连路径探测 nonce
7PROBE_REPLY[nonce bytes32];原样回显请求的 nonce

规则:

  • 需确认的状态记录。 节点在会话建立时以及候选或活动 relay 集合变化时发送类型 5;下一个绑定一经发布即发送 BINDING;某个代际被撤销时发送 REVOCATION;只有在推迟的命名层需要时才发送 NAME。类型 1–5 使用现有的 ACK / 重试规则:在 1 s、2 s 与 4 s 重传,直到收到哈希匹配的 ACK 后停止;每个对端最多 4 条未确认的记录。未知或格式错误的记录不会得到 ACK。类型 5 版本相同且字节完全相同的重传会被确认;版本相同而内容不同的冲突不会。类型 6 与 7 从不得到通用 ACK,从不进入该重试队列,也不计入四条记录的上限。
  • 有序快照状态。 发送方为每个 (local I, network_id) 维护一个持久、严格递增的类型 5 版本,跨会话、载体、候选变化、relay 变化与 K 轮换。它从 1 开始,在发送变化后的快照之前持久推进,只在字节完全相同的重传时复用。重启后重发持久化的最新版本与快照。取消被取代快照的排队重试,接收方则继续拒绝已经发出的陈旧副本。恢复出的状态不确定时,停止发布,直到恢复完好的持久状态。版本用尽对该身份的有序发布是终局性的,报告 endpoint_revision_exhausted;它从不回绕,也不强制轮换身份。v1 中没有 epoch / 重置记录。
  • 有序接受。 接收方在已认证的完整发送方 / 网络下持久化已接受的最大版本与快照哈希。更低的版本被忽略。版本相同且规范报文完全相同时幂等地确认。版本相同而规范字节不同时报告 endpoint_state_conflict,保留最后接受的路径,且不给正向 ACK。更大的、格式正确的快照原子地替换两个列表(包括用空列表撤回),持久化版本 / 哈希,然后得到一个 type_in 为 5 的 ACK。只能公布实际活动、已正向确认的 relay slot。收到的列表不持久化。重启后,对于版本与哈希都等于已持久化的已接受值的类型 5 重发,接收方在每个运行时实例中只把它重新应用到内存中的路径状态一次,并确认它。更低的版本仍被忽略,版本相同而字节不同仍是冲突。
  • 规范快照选择。 8 个候选、4 个 relay、255 字节 relay 与 1200 字节报文的上限同时适用。发送前先编码。配置的 home relay 处于活动时排在最前,其余 relay 名字按规范字节序排列;候选按地址族宽度、原始 IP 字节、数字端口与 kind 排序。只取能完整放下的条目。relay 选择按上述顺序在去重后的条目上贪心进行:某个完整 relay 条目放不进剩余的 1200 字节报文预算时,省略并报告它,然后继续扫描后面可能放得下的完整条目,直到选满四个 relay 或输入用尽。省略计数基于完整的去重输入,包括达到选择上限之后的条目。在该版本下,为该网络中的每个对端持久化恰好同一个已发送子集;从不为不同接收方选择不同的子集。报告被省略的本地条目。没有分片,也没有部分列表合并。
  • 类型 1(兼容)。 类型 1 保留其原始报文体并仍可识别,但不能覆盖类型 5 的有序状态,也不能满足有序端点的接受条件。只提供类型 1 的对端保持在其已建立的路径上,并报告 control_version_unsupported;不声称支持安全撤回或有序兼容。新版对端使用类型 5,不会静默降级。
  • 探测传输。 即使应用处于静默,适配器也会生成类型 6 请求。它们是从 N6(self):47900 到 N6(peer):47900 的内层 IPv6 UDP 报文,由现有的 WireGuard 会话加密。一次直连尝试向一个候选发送原始 WireGuard 字节,从不发送 KNR1 DATA。解密后的探测报文要通过 §4 的每一项源 / 目的地 / 网络检查,并在进入 TUN 或翻译之前被消费。cookie、不能证明已建立对端的握手响应、任意数据、relay STATUS 与通用 ACK 都不是探测回复。
  • 探测关联。 每个候选保留 (network_id, 完整对端 I, 本地 WireGuard 会话标识, 候选 IP/端口, nonce, send_time, deadline),每个候选最多一个未完成请求。只有当类型 7 回复在该对端的当前会话下解密、回显活动 nonce、直接来自确切的尝试外层元组,并且早于 1 秒请求期限(不含端点)与轮次期限时,才为发现晋升接受它一次。对选定直连路径的健康挑战,是只发往选定端点的类型 6 请求,其 1 秒期限(不含端点)从实际载体发送时开始。当其回复在同一当前会话下解密、回显活动的挑战 nonce、早于该期限,且被挑战的端点仍是选定端点时,无论来自哪个直连外层源都接受一次;从不接受经 relay 的回复。nonce 证明对端收到了只发往选定端点的报文,回复到达证明了返回方向;外层源未经认证,端点相关或非对称 NAT 可能会转换它。这样的回复只刷新选定路径的存活:从不晋升,从不替换或改换选定端点,其外层源最多只是对端被观察到的回复目标。会话 / 密钥替换会使所有未完成的探测失效;选定端点变化会使未完成的健康挑战失效。不匹配的回复既不晋升路径也不刷新存活,被消费且不予回复。
  • 探测响应与上限。 响应方通过同一个对端会话,直接向已认证请求的观测外层源发送一个大小相同、有界的回复。经 relay 的请求没有合格的直连源,不产生反射回复。在加密之前计算预算;观测到的源从不移动 relay slot。遵守每个候选每 250 ms 一个请求、30 秒的轮次,以及 §12.6 的所有节点 / 对端上限。轮次内的候选变化会更新进行中轮次的集合,但不延长其期限:保留的候选保持其未完成请求,被撤回候选的回复不再能晋升,新增的候选在下一个请求时机被探测;每对端上限容纳不下的变化,会让先前的集合保持到本轮结束;新的一轮要求上一轮已结束且 now >= previous_round_start + 30 s。超出的工作被跳过,不干扰已建立的路径。
  • 路径选择。 第一个有效的、相关联的回复会晋升直连路径。在八个会话的节点上限内保留一个已确认的公共 relay 作为回退。只有当本地 home slot 处于活动,且双方都已接受对方包含该 home relay 的、相互合格的公共类型 5 ORDERED_ENDPOINTS 快照(双方各自接受的活动 relay 列表都包含同一个规范 relay 标识)时,才有资格回切到配置的 home relay。两个条件中后成立的那个,触发下一个调和周期选择 home。能证明接受的匹配 ACK 就足够:不等待 ACK 重试,也不增加回切定时器。在两个条件都成立之前保持当前 relay,包括第二个 relay;测试中的超时不具规范性。探测丢失不影响当前路径。非保活的出站 WireGuard 流量 15 s 无应答(按单调时间计)则直连路径丢失;只有来自选定端点确切外层元组的已认证入站流量,或一个被接受的健康回复(见”探测关联”)才会重置计时;其他已认证的入站流量,包括来自不同返回元组的 DATA,都不重置;静默期与探测丢失从不计入。随后对端回退到已确认的公共 relay。路径变化会重新验证 MTU,不改变别名 / PSK,也不推迟独立的准备结果。WSS 控制报文与 WSS 的 null 观测从不成为 UDP 候选。
  • relay 选择。 对端经 relay 的路径使用双方类型 5 快照中都存在的 relay。初始选择在签发方配置的 home relay 处于活动时优先选它;从已选定的 relay 回切遵循上面”路径选择”中的回切规则。没有公共 relay 时,在会话上限内注册到一个被公布的 relay,或报告只能直连。relay 名字遵循 §12.5.1,不携带授权。

区域是由 2..16 个全互联成员提供服务的一个规范公布标识。每个成员是一个 relay 进程,有一个 1..16 之间的稳定成员 id、自己的持久存储与完整的本地 relay 核心。HELLO 中的区域名就是该公布标识。mesh 端点与面向节点的公开监听分开配置,从不出现在令牌、ENDPOINTS、ORDERED_ENDPOINTS 或其他节点可见的记录中。成员可以位于不同的网络与司法辖区;站点之间的运营方链路就是唯一的一跳 mesh。转发从不跨区域,也从不超过一跳 mesh。16 个成员时,拓扑有 120 个无序对、全区域 240 条连接、每个成员 30 条连接。这是拓扑上限,不是经过验证的最大负载;容量声明由规则 8 约束。

传输:只用 TCP,每对无序成员两条连接。

  • 两条连接都是双向认证的 TLS 1.3。每个成员持有区域 CA 签发的证书,其 subject 在一个配置好的、无歧义的字段中写明成员 id;只有当对方证书链到区域 CA、处于有效期内、具有预期的密钥用途,且写明的成员 id 在配置的成员列表中时才被接受,因此成员列表就是允许名单。成员 id 较小的一方发起两条连接;连接、TLS 与 HELLO 共用 10 s 期限。
  • 每帧是 4 字节大端长度后接一个规范 CBOR 数组 [ver u8 = 1, type u8, body](§12.1 的规则)。长度计入完整的编码数组,不含前缀,最大 4096。
  • 控制连接承载 HELLO、SNAPSHOT、PRESENT、GONE、MARKS、MISS、PING 与 PONG。数据连接作为建立时的例外承载一次 HELLO,之后只承载 FORWARD。分开二者可以隔离背压:饱和的数据流从不延迟 presence 或标记。
  • 接受数据连接上的队头阻塞:丢失一个分段会让这对成员的每个转发报文延迟一次重传,节点侧的 WSS 回退本来就有这一性质。因此 TCP 丢包可能表现为抖动、停滞超时与重连,而不只是延迟。relay 自己从不重传转发的报文;丢包由端到端的 WireGuard 与应用处理。
  • mesh 端点可以是站点之间运营方链路上的私有地址。id 较小的发起方需要能独立于公开节点地址路由到每个 id 较大的监听;TLS 校验的是成员身份,而不是私有 IP 或公开的 WSS 证书名。TCP 流不施加 mesh MTU:完整 2048 字节 relay 上限的转发报文可以完整穿过 mesh,节点侧 1376 的经 relay 上限不变。
类型名称报文体
1HELLO[region_name text, member_id u8, boot_epoch u64, channel_kind u8, conn_gen u32],channel_kind 为 0 控制、1 数据;两条连接上双向的第一帧;区域名不同、成员 id 与证书或预期对方不符、成员 id 重复或通道类型未知时关闭连接
2SNAPSHOT[snapshot_id u64, base_revision u64, marks_revision u64, chunk_index u32, presence_entries array, mark_entries array, more bool];发送方在 base_revision 时自己的活动 presence,以及在本地持久 marks_revision 时其完整的保留标记并集;嵌套条目的 schema 见下文;按编码大小分块
3PRESENT[slot bytes16, network_id bytes32, I bytes32, gen u64, incarnation u64, carrier u8, boot_epoch u64, revision u64];在发送方上某个 slot 变为活动,或其代际或 incarnation 变化时发送
4GONE[slot bytes16, network_id bytes32, I bytes32, gen u64, incarnation u64, reason u8, boot_epoch u64, revision u64],reason 为 0 过期、1 撤销、2 被取代、3 关闭
5MARKS[network_id bytes32, I bytes32, committed_gen u64, committed_K bytes32, conflicted bool, revoked_through (null or u64), incarnation_high_water u64];每次本地标记提交与每次合并结果变化时发送,相同则不发
6FORWARD[forward_id u64, source_owner [network_id bytes32, I bytes32], datagram bytes];完整的原始 relay 报文,含报文头,最多 2048 字节;只用于 INTRO、INTRO_ACK 与 DATA;只在数据连接上
7MISS[data_conn_gen u32, forward_id u64, status u8];data_conn_gen 与 forward_id 标识在该认证对上收到的 FORWARD;status 1 表示本地没有合格的目的地,2 表示目的地准入或输出耗尽;只在控制连接上
8PING[nonce u64, data_conn_gen u32, consumed_forward_id u64],在控制连接上每 5 秒一次;三次无应答关闭两条连接
9PONG[nonce u64, data_conn_gen u32, consumed_forward_id u64]

所有帧的报文体都是按所示字段顺序排列的定长规范 CBOR 数组。presence_entries 与 mark_entries 是确定长度的记录数组,不是字节串、映射或展开的字段。每个 presence 条目恰好是八字段的 PRESENT 报文体 [slot bytes16, network_id bytes32, I bytes32, gen u64, incarnation u64, carrier u8, boot_epoch u64, revision u64];每个标记条目恰好是七字段的 MARKS 报文体 [network_id bytes32, I bytes32, committed_gen u64, committed_K bytes32, conflicted bool, revoked_through (null or u64), incarnation_high_water u64]。嵌套条目省略外层的版本 / 类型封装。空列表为 [],从不为 null。每块条目合计最多 256 个,且完整的编码帧仍须不超过 4096 字节。缺省的 revoked_through 显式为 null;其他字段都不能省略或用 null 代替。

PRESENT 的 carrier 为 0 表示 UDP、1 表示 WSS;其他值无效。这些值描述来源已提交的节点载体,并不选择 mesh 传输。HELLO 的 region_name 是 §12.5.1 的规范标识,1..255 个 UTF-8 字节,逐字节比较。member_id 为 1..16。快照中的 presence 条目带有发送方已认证的 boot epoch、一个不大于 base_revision 的非零版本,以及一个等于其完整所有者按 §12.4 派生结果的非零 slot。在整个快照中,每个完整所有者在两个条目列表中各自最多出现一次;同一所有者同时有 presence 与标记记录是允许且预期的。在发布暂存的 presence 之前,按规则 1 与 2 对照已持久合并的标记校验它;没有兼容的保留所有者标记的 presence 不合格。

对每一帧执行 §12.1 的规范编码、确切的字段数与类型、整数范围、枚举值以及允许的通道。在为报文体分配内存之前,拒绝为零或超过 4096 的长度前缀。分帧、规范 schema、版本、类型或通道违规会关闭该通道并使其同步失效;无效的完整 FORWARD 报文按规则 3 丢弃且不回复。任何嵌套的 mesh 记录都不会改变节点报文或隧道内记录。

规则:

  1. Presence。 每个来源只发布自己的活动会话,以 (member_id, boot_epoch, owner, gen, incarnation) 标识,并在一个 epoch 内以单调递增的版本对其 presence 变化排序。boot_epoch 在每次进程启动时重新生成,是进程身份而不是存储版本;同一进程内的重连保持不变,且对每个对方,两条连接都必须给出相同的 epoch。接收方只接受来自来源当前认证流的事件,丢弃旧流的事件。更高的代际无论 incarnation 如何都胜出;同一代际内更高的 incarnation 胜出;两者都受合并后的标记约束(PRESENT 从不覆盖更高的下限)。状态相同的情况(同一所有者、代际、兼容的密钥与 incarnation 在两个成员上都已确认)在上述检查之后,由成员 id 较小者保留会话来解决。这一决胜可能会退役节点实际正在使用的会话;它是可用性上的取舍,不代表节点的意图。落败的成员停止该会话的已认证确认与转发,丢弃其排队输出并发布 reason 2 的 GONE;节点按普通的漏掉 PRESENCE 规则(约两个间隔,默认 TTL 下为 50 s)以新的、更大的 incarnation 恢复。reason 2 的 GONE 只在 mesh 中使用:从不作为节点报文发送,也从不成为新的 STATUS 码;STATUS 仍是建议性的,不能触发经认证的退役。GONE 只移除确切的来源会话,并记录其版本,使更旧的 PRESENT 无法恢复它。boot_epoch 变化会立即使旧来源的路由失效,并要求新的完整快照。退役的 slot 不会关闭仍在为其他活动 slot 服务的 WSS 连接。

  2. 标记。 合并是单调的。revoked_through 为 null 或 u64,对存在的值取最大值合并,与 committed_gen 无关。更大的 committed_gen 连同其特定于代际的密钥、冲突与 incarnation 字段一起胜出;旧代际的 incarnation 从不带入新代际。同一代际时,incarnation 高水位取最大值,conflicted 取或,并在密钥不同时锁存;字典序较小的密钥只作为确定性的序列化代表保留,从不作为被授权的胜者。成员在应用路由变化或发布之前持久化合并后的状态,然后重新评估本地会话与待确认项:更低的代际、已撤销或冲突的代际,以及同一代际中更低的 incarnation 被退役;仅凭相同的当前标记从不退役确切的活动会话。成员按其当前持有的下限提交注册,之后发布 MARKS 与 PRESENT;它从不等待其他成员。重启时合并完好的持久下限,不恢复任何会话或会话密钥。本规则的局限:在诚实成员之间,未确认的 REGISTER 不改变任何保留标记或活动会话,即使在同步前被重放也一样,因为激活需要用节点临时私钥做的 PRESENCE 证明;每个成员执行自己的持久下限;全区域的下限在通信恢复后收敛,分区期间没有收敛上限;持有所需临时密钥的所有者可以在不知情的成员上确认陈旧状态,而同一代际下密钥不同的确认可能锁存一个持久冲突,需要更高的可接受代际才能解决。异步发布是有意的设计;它既不提供全区域的原子准入,也不保证每个陈旧状态的影响都只是暂时缺失路径。

  3. 转发。 入口成员执行 §12.5 的每一项发送方检查(活动的 src、确切的源、同一上下文、发送方与全局范围)。FORWARD 只接纳已校验的 INTRO、INTRO_ACK 与 DATA,携带其原始字节,从不携带成员本地的控制报文(REGISTER、PRESENCE、REVOKE、REFLECT、REFLECTED)。如果 dst 在本地活动,入口直接投递,并对每个适用范围计费一次;否则查找 presence 表,计费发送方范围及其转发全局范围,并经数据连接发送 FORWARD。forward_id 在发送方向的数据连接代际内唯一,并按发送顺序单调分配;入口保留一个有界、会过期的关联(原始报文头、输入长度、确切的源、已提交的会话标识)。出口成员检查 source_owner 能派生出报文的 src,且其上下文等于活动目的地所有者的上下文(source_owner 是经认证的成员断言,不是持有证据),按其活动 slot 记录校验目的地的完整所有者,在入队之前计费目的地范围、握手发起子集及其转发全局范围,并把排队的输出绑定到那个确切的已提交会话,使其在退役时被丢弃。每个成员的全局上限约束的是该成员上的物理工作,而不是全区域的合计配额。成员从不转发经 mesh 到达的报文,因此路径在构造上无环。出口未命中时返回 status 为 1 或 2 的 MISS;格式错误的 mesh 输入静默丢弃。入口对每个 MISS 最多发出一个相关联的 STATUS,且只在那个确切的发送方会话仍然活动时,使用原始输入长度以及源 IP 与全局回复预算;未知、过期或重复的 MISS 被忽略。

    MISS 关联以当前认证对的成员 id 与 boot epoch、数据连接的 conn_gen,以及发送方向的 forward_id 为键。出口在消费 FORWARD 时捕获该代际,并在 MISS 中原样携带,即使控制写入发生在数据重连之后也是如此。只有当代际等于入口当前的数据连接代际、且确切的关联仍然活动时才接受 MISS。代际为零、代际陈旧、id 未知、条目已过期或发送方会话已退役时,不产生任何节点回复。在尝试发出唯一的 STATUS 之前先消耗有效的关联;回复预算拒绝不会恢复它。数据丢失时,入口可以在同样的会话、输入大小与回复预算检查下,为每个仍然活动的关联最多产生一个 status 1 回复,然后移除它;之后的 MISS 不能产生第二个回复。在替代的数据连接可用之前,清除剩余的旧代际关联。只有在该替代代际上,转发 id 才能从 1 重新开始。

    这对成员的数据队列已满时,入口丢弃报文,并在回复预算内回答 STATUS 2;数据通道被宣告不可用,或连接丢失且出口没有应答时,按同样规则回答 STATUS 1。只有入口成员才会向节点发送 STATUS。

  4. 会话与反射按成员保持: 暂定注册、PRESENCE、会话密钥与 REFLECT 观测属于节点所连接的成员。WSS 输出仍是出口确切已提交连接上的一个有界二进制消息,WSS REFLECT 仍不受支持且观测为 null,直连 INTRO 与 INTRO_ACK 不受影响。

  5. 通道识别与存活。 HELLO 除区域、成员 id 与 boot epoch 外,还携带通道类型与连接代际;成员校验预期的对方、通道与 epoch,每种类型保留一个当前连接,并以确定的方式拒绝陈旧的重复连接。

    成员 id 较小的一方拥有每条连接两个方向的 conn_gen。它在自己的 boot epoch 内为每个对方与每种通道类型维护单独的 u32 计数器:第一次尝试使用 1,之后每次尝试(包括失败的尝试)消耗下一个值。HELLO 中零无效。发起方先发送 HELLO;接受方校验后以相同的通道类型与 conn_gen、自己的成员 id 与自己的 boot epoch 回复。只有两条 HELLO 都通过校验后,连接才成为当前连接。对同一个发起方 boot epoch 与通道类型,只接受大于此前所有已接受代际的代际;按普通无符号整数比较,从不使用序列号算术。相等或更小的候选被关闭,不替换当前连接。有效的更新连接替换并关闭该类型的先前连接,此后被替换连接的回调或帧都被忽略。

    一对成员由双方经认证的成员 id 与双方的 boot epoch 标识;双方的 epoch 值不必相同,但每一方在控制与数据连接上都必须报告自己相同的 epoch。先建立控制连接,再接受数据连接。数据 HELLO 必须与当前已建立的控制对的 epoch 一致。任一方 boot epoch 变化都会关闭先前的成员对,使其路由与关联失效,并要求在转发之前重新完成控制同步。不同的发起方 boot epoch 只能通过这种完整的控制对替换开启新的计数器命名空间,从不通过只替换数据连接实现。不要按数值比较 boot epoch。conn_gen 用尽时从不回绕:关闭受影响的成员对,并要求发起进程以新的 boot epoch 重启后再尝试;仅重连不会重置该计数器。

    数据连接的每个方向都有独立的 forward_id 序列,从 1 开始,按发送顺序严格递增。零保留表示”未消费任何 FORWARD”。在某个方向即将用尽 u64 之前,停止在该连接上的新发送,并以更大的 conn_gen 建立新的数据连接;不确定的发送与旧关联直接丢弃,不重放。PING 与 PONG 报告发送方在当前数据连接上的接收进度,而不是发送进度。没有当前数据连接时,报告 data_conn_gen = 0 与 consumed_forward_id = 0;有当前连接但没有消费过 FORWARD 时,报告其代际与零。“已消费”指完整的帧已通过校验,并且已被接纳进确切的目的地输出队列,或已按规则 3 处置;它不是投递确认。进度只能按接收顺序随已处理的帧推进,且不得超过该方向实际发送的最后一个 id。针对旧代际的报告不能推进当前进度水位,也不能重置 15 s 停滞计时器。PONG 只回显 PING 的 nonce,并给出自己的接收进度字段。PING nonce 是每条控制连接从 1 开始的 u64 计数器;最多三个未完成,只有一个匹配的、一次性的 PONG 能应答一个,用尽时需要新的控制连接。PING 从不要求数据通道回复。

    只有当控制连接与数据连接都在同一对已认证的成员 boot epoch 下正常时,成员才可转发。PING 与 PONG 在控制连接上每 5 s 一次,漏掉三次则关闭该成员对,它们报告当前数据连接代际与已完全消费的最高 forward_id。有未完成数据时,接收进度 15 s 没有推进只关闭停滞的数据连接,并把该成员对标记为不可转发;在平台支持时启用 TCP 保活(5 s 探测,三次失败)与 15 s 的未确认写入超时。数据连接恢复时重置其关联,从不重放不确定的发送。每个失败通道的重连退避从 1 s 开始,翻倍至 60 s,带 25% 抖动,健康运行 30 s 后重置。某对成员的数据连接断开期间,入口对发往该成员的转发以 STATUS 1 失败,presence 与标记继续在控制连接上传输。

  6. 快照与重新同步。 连接建立时以及任何失效之后,发送方以流的方式发送 SNAPSHOT,内容是自己当前的活动 presence 与完整的保留标记并集,按 4096 字节帧上限以编码大小分块,每块条目合计最多 256 个,条目不跨块拆分;chunk_index 从零开始连续,more = false 结束快照,即使快照为空也是如此。

    snapshot_id 是发送方拥有的 u64 计数器,从 1 开始,在发送方的 boot epoch 内对每个新快照严格递增,跨对方与重连;零无效,被放弃的 id 从不复用。每个来源同一时刻只暂存一个传入快照。其 snapshot_id、base_revision 与 marks_revision 在每一块中保持相同;chunk_index 从零开始逐一递增,不得有空缺、重复或回绕。意外的 id、截断点变化、重复或跳过的序号,或最后一块之后的数据,都会使同步失效。新的控制连接开始一个新快照,而不是延续旧快照。

    presence 版本为 u64,对空的新 boot epoch 从零开始,在每个本地 PRESENT 或 GONE 事件之前加一;事件版本零无效。只有该 epoch 初始的空 presence 的快照 base_revision 才可以为零。在一个来源 epoch 内,快照截断点不得早于接收方已接受的版本。完整快照之后,保留 base_revision 作为该来源的高水位版本;之后的事件版本必须严格更大,被接受的事件会推进它。由于经认证的控制流为该来源的所有事件排序,这个标量也能防止 GONE 之后被更旧的 PRESENT 恢复;不需要无界的按所有者删除历史。只有当经认证的来源 boot epoch 变化且新快照完成时才重置它。

    mesh 计数器都不回绕。在 presence 版本或 snapshot id 即将超出 u64 之前,停止发布,并以新的 boot epoch、完好的持久标记、不恢复任何会话的方式重启成员。如果快照在其 u32 块序号溢出之前无法完成,就明确地使同步失败,不发布部分 presence;重新开始同一个超大快照不算成功恢复。durable_revision 是本地持久化的 u64 计数器,在显式初始化的空存储上初始为零,并随每次变化的持久标记事务原子推进。它独立于 presence 版本,从不因进程重启而重置。持久版本用尽时,准入与同步一律关闭,直到通过显式支持的迁移恢复完好的存储;不回绕、不抹去下限,也不给出成功的新快照截断点。

    presence 视图冻结在 base_revision;后续的 presence 事件在最后一块之后按版本顺序到达,缓冲有界,缓冲溢出会重新开始同步。接收方在标记到达时就把它们以流的方式写入有界的持久存储,并在明确的表上限内暂存 presence,只有在完整接收且标记校验通过后才发布;完成时只替换该来源的 presence,并删除它不再列出的路由,从不删除标记。断开或无效的块会丢弃部分 presence,保留已合并的标记,并要求新的快照。与某成员的通道丢失时,该成员的条目自丢失起按单调时间保留两倍 presence_ttl_s 的宽限期(默认 180 s),但只要任一必需通道不可用,宽限期就从不授权投递;重连后要完成同步,缓存的路由才再次合格,唯一的例外是同一 epoch 内只恢复数据连接时,可以复用持续保持同步的控制状态。标记从不随 presence 一起被回收。重启时加载完好的标记,开始时没有会话、暂定密钥或输出。没有先前存储的新成员保持关闭注册,直到一个被显式选定的完好对方提供完整的持久标记快照;这是引导,而不是就注册达成同步一致。

    发送方在所捕获 presence 所依赖的所有持久标记提交之后,于一个串行化的本地快照截断点同时捕获 base_revision 与 marks_revision。marks_revision 是发送方持久化的本地持久版本,不是全区域版本,也不是接收方的存储版本。按截断点时的状态以流的方式发送每条保留的所有者记录,包括没有活动会话的所有者;从不因为某个截断点内的所有者在传输期间发生变化而省略它。对截断点使用一致的、不可变或事务性的视图。所有块携带相同的两个版本与 snapshot id。只有最后一块有 more = false;完全为空的截断点是一个条目数组为空的最后一块。

    对该对方的出站控制流,把截断点之后提交的每个 MARKS 变化与 presence 事件推迟到最后一个 SNAPSHOT 块之后。推迟的 MARKS 保持持久提交顺序,presence 保持来源版本顺序;某个 presence 事件所依赖的 MARKS 变化排在它之前。存活帧与 MISS 可以与快照块交错。规则 7 中 MARKS 的优先级只适用于在这一快照屏障下合格的消息,从不把截断点之后的 MARKS 移到最后一块之前。本地准入不等待快照传输:如果有界的推迟变更缓冲区满了,就关闭该同步并开始一个新快照,而不是阻塞本地提交或静默丢弃变更。

    接收方在把最后一块视为完成之前,持久合并快照的每一批标记。完成意味着已接收并持久化指定 marks_revision 下的完整并集,所有块连续且有效,并且已对照接收方合并后的下限检查了暂存的 presence。只有在这样完成之后,才持久化一条引导完成记录,写明选定的完好对方、其 boot epoch、snapshot id 与 marks_revision;并且要在开放注册之前完成。在这条记录持久化之前崩溃或断开,首次运行的成员保持关闭注册,并保留已合并的下限供下一次完整引导使用。重启时,缺少预期的引导完成记录,不会让部分存储变成已初始化的成员。没有保留所有者的存储仍可以从一个被显式选定的完好对方的有效空快照引导。

    引导完成记录确立的是选定对方的完整截断点,而不是与所有成员一致,也不代表知道之后的每一次提交。之后的 MARKS 按正常方式处理,并在任何依赖它的 presence 事件之前持久化;发布仍是异步的。无法持久化合并后的下限或完成记录时,引导失败,准入按规则 8 关闭。丢失或损坏了预期下限的成员,不能借首次运行引导绕过 §5.2.2 的恢复要求。

    每个被接受的输入只选择一个出口,最多入队一次,重连后从不重放数据队列。

  7. 队列。 每对成员的 mesh 数据输出上限为 64 帧、128 KiB,最长排队 1 s;mesh 数据与节点输出合计每进程不超过 8192 个排队报文与 16 MiB。mesh 控制输出不计入这两项进程数据 / 节点上限,而计入下面单独的控制预留。出口处仍适用 §5.2.1 的已提交目的地上限。

    控制输出有单独的有界预留(每对 256 帧、1 MiB,每进程 16 MiB),存活帧与 MARKS 优先;presence 事件保持快照截断点的顺序,溢出时关闭同步并强制新的快照,而不是静默丢失状态。入口、进行中的 MISS 关联以及 TLS 与套接字缓冲分别有界。数据入队从不在持有状态所有者或控制写入器时阻塞,存储工作从不阻塞存活调度。时长上限覆盖的是本地待发送的输出,而不是 TCP 内的投递时间。

    §12.6 的所有计数与字节上限同时适用。帧队列字节按完整编码帧加四字节前缀计费;保留的分配开销单独计量,而不是任其无界。关联与暂存的字节上限包括保留的键、索引与分配开销。在接纳远端转发之前预留一个关联及其存储;任一上限已满时,不入队该转发,只在现有回复规则下允许 STATUS 2。关联过期时静默移除条目;这不是投递成功或失败的证据。在按收到的长度分配之前,预留有界的帧组装空间。读侧压力只暂停该通道,不阻塞另一个通道或存活调度。超时或无效的部分帧会关闭该通道;不完整的帧适用现有的 15 s 无进展上限,只有收到更多字节才会刷新。

    配置并验证实际生效的传输缓冲上限,考虑平台对缓冲区的倍增与 TLS 缓冲;禁用无界的自动增长,或在无法执行实际上限时使配置失败。报告实际生效的上限。传输、帧组装、关联、已发布的 presence、快照暂存与输出预留是同时成立的独立账户。presence 或推迟变更溢出会使该同步失效,丢弃部分 presence,并以新的 snapshot id 重新开始;已持久化的标记保留。不为腾出空间而驱逐已发布的活动状态或持久下限。在接受下一批之前先写完持久标记批次;持久化或保留并集耗尽遵循规则 8。为出站截断点保留一个有界的不可变存储视图或有界的磁盘快照,而不是把完整并集克隆进内存。同步受背压时,已有所有者的更新容量与存活必须保持可用。

  8. 容量。 默认上限为全区域 160000 条活动来源 presence 记录(每个成员 10000 条),外加有界的快照暂存与事件版本。每个成员在 §12.6 的现有上限(1000000 个所有者,512 MiB 逻辑,4 GiB 磁盘)下保存完整的保留所有者并集,这些上限作用于每个成员上的并集,不是区域容量的叠加。成员在引导时校验其实际生效的上限与存储余量。如果收到的下限无法持久化,成员把其同步与准入标记为失败,并抑制依赖未应用状态的路由与确认;它从不驱逐下限,也从不报告同步成功。分区期间的独立准入不能保证合并后的并集放得下;这种容量失败会被明确报告。快照标记以有界的持久批次或通过增量持久存储应用,跳过未变化的合并结果;在作出任何容量声明之前,要在所支持的规模下对伴随并发本地提交的初始同步做基准测试;对这条路径,每收到一条标记就克隆并重写整个存储是不可接受的。

  9. 成员资格与证书。 双方都校验 TLS 1.3 证书链、有效期、密钥用途与成员 id 字段,并要求 HELLO 中的身份与证书及预期的对方一致。证书过期或在配置重载时被移除成员时,关闭现有连接;不支持重载的实现改为记录对所有接受方成员的滚动重启流程。用同一个允许 id 替换证书,不会在旧证书过期之前撤销它。紧急响应是在所有地方移除该 id,或轮换受信 CA;签发、轮换与过期监控是有文档的运营流程,CA 私钥保持离线。

  10. 运维。 /readyz 保持为本地安全准入就绪:仅有分区不会让健康的本地 relay 变为未就绪。mesh 降级在有界的 /status 中单独报告,包括区域名、成员 id、boot epoch、本地持久版本、每个对方的通道与同步状态、presence 数量、队列压力与证书过期时间。

    完整的 /status JSON 报文体最多 65536 个 UTF-8 字节,包括本地成员与最多 15 个对方。区域名是 1..255 字节的规范标识;成员 id 为 1..16 的整数,boot epoch 与持久版本为 u64 整数,计数与字节 / 时长计数器为非负 u64 整数,就绪与合格为布尔值,通道 / 同步状态为最多 32 个 ASCII 字节的固定枚举字符串,证书过期时间为 Unix 秒 u64,缺失时为 null。队列压力以当前值与上限计数器表示,而不是无界文本。不包含所有者列表、证书链、自由格式的错误或历史。在声称支持该拓扑之前,验证 16 个成员时编码响应的最大长度。序列化溢出返回有界的运维错误,从不返回被截断的成功响应;它不改变 /readyz,也不发出节点 STATUS。

面向用户的命令(up、down、status、invite、join、peers、peer、policy、pending、allow、upgrade)见 Quick 模式 §2;invite 与 join 通过 /quick/v1 会合驱动下面的操作(Quick 模式 §3)。本节规定运行时操作及其底层命令。

本节规定客户端运行时操作及其本地接口;移动端与 libns 接口按 §13.7 推迟。它不向 §12 的报文规范增加任何字段、消息或算法。运行时持有准入状态、重试与定时器;节点独立的协议层提供纯粹的原语,输入由调用方显式给出。

Quick 运行时运行在现有 ns 进程模型的守护进程中,通过经所有者认证的本地 IPC 控制。守护进程从不等待终端输入。透明的 Linux 资源按 §7.8.1 以网络命名空间为单位归属;多个用户需要各自独立的网络 / 解析器命名空间,而不是在一个主机命名空间中运行相互竞争的守护进程。

需要人工批准的签发方决策会创建一个本地待批准项,包含 pending_id、network_id、完整的请求方身份键、相匹配的 token_id、首次收到时间、过期时间与当前状态。CLI 在批准之前以小写十六进制显示完整的网络与身份键;身份名与昵称只是辅助标签。批准授权的是该网络中那个确切的身份,而不是之后由昵称解析出的任意对端。守护进程在应用决定之前重新校验该项。

加入方的配对有单独的本地 pairing_id。它标识加入方运行时上的一个操作,不是签发方的 pending_id。现有报文协议不暴露远端审批队列的标识。

交互确认只在 CLI 中进行,需要终端输入,并且确认文本要输出到终端;--yes 或 --json 会禁用它。--yes 只为本次调用的本地操作提供同意;它从不批准无关的待批准项,也从不绕过签发方策略。--json 只改变输出格式,从不授予同意。需要确认的操作在无法获得确认且没有 --yes 时,以 confirmation_required 失败。

底层命令族是 ns quick;本表中未列入 Quick 模式 §2.1 的命令不在帮助中显示。每个命令都支持 --json。变更类命令支持用 --yes 做本地确认。没有网络参数:每个命令都在默认上下文(§2.7)中执行。输出报告完整的网络 ID、规范区域与活动的本地便利区域。

命令作用
ns quick token issue [--recipient PEER] [--ttl DURATION] [--name TEXT] [--qr]签发一个令牌。--name 设置 issuer_name;--recipient 解析为完整的接收方身份。报告实际过期时间与 token_id。
ns quick token list列出令牌元数据与撤销状态,不含凭据。
ns quick token revoke TOKEN_ID停止通过该令牌向允许名单添加;已接纳的身份保持接纳。
ns quick pair (--stdin OR --file PATH OR --scan) [--scan-device PATH] [--scan-timeout SECONDS] [--wait SECONDS]读取恰好一个令牌,导入它,并创建或恢复一个本地配对操作。扫码选项只在 --scan 时有效。--wait 默认 120 秒;--wait 0 在本地提交成功后立即返回。
ns quick pair status PAIRING_ID查询操作状态、对端身份、当前路径信息与最近可观察到的错误。
ns quick pair cancel PAIRING_ID停止未被接受的配对的 INTRO 重试。准入之后,用 peer remove 移除对端。
ns quick policy set --join auto OR confirm OR recipient-only设置签发方的准入策略。
ns quick pending list列出签发方本地的待批准项,含完整身份键与过期时间。
ns quick pending approve PENDING_ID重新校验并批准那个确切的待批准请求。
ns quick pending deny PENDING_ID拒绝该请求,并按 §13.3 抑制其重试。
ns quick allow list列出已接纳的身份与显式移除记录。
ns quick allow remove PEER无论令牌如何都阻断该身份,并移除其活动的对端配置。
ns quick allow restore PEER显式清除一条移除记录。这本身不接纳该身份;有效的配对仍须满足当前策略,之后的重新准入只在 §6.4 下复用别名。
ns quick peers;ns quick peer show PEER列出对端,或显示一个对端的状态。
ns quick peer remove PEER执行 allow remove,并按 §§4 与 6.4 退役该对端的别名、路由与名字。
ns quick peer name PEER NICKNAME按 §§7.2 与 12.4 设置本地昵称。
ns quick peer alias PEER [--v6 ADDR] [--v4 ADDR]设置本地 IPv4 别名:--v6 选择用于 N6 服务的 alias6,--v4 选择用于 N4 服务的 alias4。两个值都是 IPv4 地址,按 §6.5 校验。
ns quick aliases export PATH;ns quick aliases import PATHv1 推迟。返回 feature_deferred、退出码 2 与 result:null,不读写任何快照。
ns quick bridge add PORT[/tcp OR /udp] --backend ADDR:PORT;ns quick bridge list;ns quick bridge remove PORT[/tcp OR /udp]管理 §6.7 的端口桥。省略协议表示 TCP。后端必须是本节点的 self4 地址;这些命令不开启到其他主机的转发。
ns quick key rotate [--at TIME]发布下一组全设备绑定,并按 §2.3 安排未来的切换;报告切换时刻、有效期与 ACK 状态。拒绝更早的计划、无效的新旧生命周期、不可靠的时钟或另一个待执行的轮换。当前绑定在 540 秒内过期或已经过期时,按 §2.3 安排迟到轮换。
ns quick key binding export PATH原样写出持久化的当前已签名八字段 KeyBinding 数组,不加封装、秘密或单独的切换时刻。导出从不签名也不改变状态;没有持久化的绑定时报告 binding_required,持久化的绑定不是当前有效时报告 binding_invalid,时钟不可靠时报告 clock_unreliable。
ns quick peer binding import PEER --file PATH读取最多 512 字节、恰好包含一个规范当前 KeyBinding 的文件,并为一个已准入、未被移除的对端原子地应用 §2.3.1。
ns quick recovery set-root PUBKEY;ns quick recovery set-root --threshold N PUBKEY...;ns quick recover --statement PATHv1 推迟。返回 feature_deferred、退出码 2 与 result:null,不读取秘密材料,也不改变状态。
ns quick upgrade [--yes]把早期按设备划分的网络状态转换到默认上下文(§2.7):运行时在运行时拒绝执行;备份旧状态且不覆盖已有备份;保留 I;安装默认上下文,并退役每个对端,使其都需要重新配对。

命令语法中的 OR 表示互斥的选项,而不是字面参数。

TOKEN_ID 是 token_id(§12.3)的完整小写十六进制形式,即规范已签名 PairingToken CBOR 字节的 SHA-256。它不是令牌字段;INTRO 以二进制携带它(§12.5)。PAIRING_ID 与 PENDING_ID 是各自本地运行时分配的不透明 ID;命令只接受对应类型的对象。

PEER 接受完整的身份键、20 字符的身份标签(节点 ID)、至少 8 个字符且恰好匹配一个已配置身份的节点 ID 前缀,或唯一解析到一个完整身份的昵称。无法解析的名字、有歧义的前缀或昵称、或更短的前缀都是错误。不涉及任何目录查找。

三种令牌输入来源互斥。输入读取是有界的:它接受一个最多 1024 字节的令牌,后面可以跟一个 LF 或 CRLF 传输结束符,并拒绝额外的数据。在严格的 §12.3 令牌解码之前,只去掉那个结束符。二维码扫描提供相同的令牌文本并做相同的校验。位置参数形式的令牌不属于本接口。

在 Linux 上,--scan-device PATH 与 --scan-timeout SECONDS 只在 --scan 时有效。超时默认 30 秒,接受 1..300 秒,在提交任何配对操作之前按单调时间测量。后端只打开用户可访问的 V4L2 采集设备。没有显式路径时,必须恰好有一个合格设备:零个报告 scan_unavailable,多个报告 scan_device_ambiguous。它从不提权,也不改变设备访问权限。

采集选择一个不超过 1920x1080 的受支持有界模式,每秒最多处理 10 帧,缓冲 / 解码最多 32 MiB。不支持的有界操作报告 scan_capture_failed。图像、载荷与帧转储既不保存也不记录。不需要 GUI 或终端。无头环境中没有摄像头时立即失败。同一帧中有多个不同的令牌候选时报告 scan_multiple_tokens;同一令牌重复出现只提交一次。非令牌的二维码在超时前被忽略。解出的 kn1 凭据进入普通的严格解码器,无效时报告其凭据错误且不提交。导入之前超时或取消不会创建配对状态。扫码载荷从不出现在错误或 JSON 中。

在无头环境中,--stdin、--file 与 --scan 仍然相互独立。--json 从不提供同意,并且与 --qr 不兼容;--yes 从不绕过签发方策略。摄像头测试可以使用受控的视频输入,而 Linux 命名空间验收不声称覆盖物理摄像头。

导入后,操作报告完整的签发方身份与身份标签。只有当 §7.2 / §12.4 的默认昵称有效且无冲突时才分配它;否则对端保留身份标签,并报告冲突以便用户改名。在批准中,昵称从不替代完整身份。

在应用策略之前,签发方校验 INTRO 的 MAC 与接收方绑定,检查网络与身份的一致性,执行令牌不含端点的过期时间与接收方限制,并应用代际 / 撤销规则。显式移除记录优先于每一种准入策略,包括限定接收方的令牌。

对已在该网络中接纳的身份,有效且相匹配的 INTRO 重试按幂等处理,不会创建新的待批准项。撤销令牌阻止新的添加,但不抹去已有的允许名单条目;过期不移除已接纳的对端。已配置的对端通过 WireGuard 重连,无需重复令牌准入。

对新身份,相匹配的令牌还必须未被撤销:

策略可复用令牌相匹配的限定接收方令牌
auto立即接纳。立即接纳。
confirm,所有安装的默认值创建待批准项;只在显式批准后接纳。立即接纳,因为签发时已指定接收方。
recipient-only不接纳,也不创建待批准项。立即接纳。

准入在发送 INTRO_ACK 之前安装对端。批准会重新检查令牌有效性、当前绑定与代际、移除记录、地址冲突以及安装所需的容量。已过期或无效的项不会因批准而复活。

按 network_id、token_id 与完整的请求方身份对待批准请求去重。重试不延长 first_received 或待批准过期时间。过期时间取 token.not_after 与 first_received 加 24 小时中较早者。被拒绝或已过期的待批准请求不会因同一令牌与身份的重试而重建;保留有界的抑制状态直到令牌过期。要再次请求需要新的令牌,显式的身份移除仍然优先。

每个节点对活动的签发方待批准 / 抑制条目有 1000 的硬上限,对未被接受的出站配对操作也有 1000 的硬上限。达到上限时拒绝新工作;从不静默驱逐待批准的同意或移除记录。§1.1 中已配置对端与令牌的上限同样适用。

拒绝对本地 IPC 客户端可见。v1 报文规范中没有经认证的签发方拒绝消息。沉默与 relay STATUS 都不能证明签发方批准或拒绝了请求。

配对有一个接受生命周期与一个准备期限。CLI 的等待是单独的观察预算,不拥有任何一个运行时生命周期。

接受之前,运行时以有界退避加抖动重试 INTRO,直到令牌不含端点的过期时间、显式的本地取消或本地可观察到的终局失败。它保留一个有界的活动尝试与 nonce 关联;重传既不创建重复的待批准请求,也不延长其过期时间。通过当前的报文规范,加入方无法观察到签发方本地的拒绝。

--wait 为正值时,CLI 最多等待这么多秒,直到收到有效的 INTRO_ACK。若确认在该预算内到达,CLI 再观察随后的准备结果,最多等到 5 s 的准备期限。若没有确认到达,它返回本地 pairing_id,并报告 waiting_for_acceptance 及最新的传输证据。--wait 0 在提交后立即返回。退出 CLI 不会取消操作。附着的等待在操作仍在准备时到达准备期限,返回 prepare_timed_out。本地提交之后,对任何输入来源,SIGINT 都会立即结束附着的 pair 调用而不取消运行时持有的操作:shell 看到退出码 130,且不写出 JSON 信封。§13.5.2 的退出码表只覆盖已完成的调用,导入前中断扫码仍报告 scan_cancelled。节点重启时,附着的 pair 调用以它观察到的 IPC 失败结束;它从不合成 expired 或 prepare_timed_out。重启后,用同一令牌执行 pair 会附着到恢复后的 pairing_id,或报告已接纳或终局状态。

在加入方,5 s 的准备期限只在校验第一个相匹配的 INTRO_ACK(包括其 MAC、身份、网络、回显的活动 nonce 与当前绑定)之后才开始。在签发方,准备期限从安装已接纳的对端并发送该确认时开始。重复的确认与重传不会重启这些期限。

WireGuard 路径可用且本地发布完成时,准备成功。可用的 relay 路径就足够;打洞独立继续,不会推迟成功。准备超时报告 prepare_timed_out。它不撤销准入,也不移除已安装的对端;之后的正常重连可能使该对端变得可用。

本地等待 / 准备预算使用单调的流逝时间测量。在校验与准入时检查令牌的绝对过期时间;重试与 CLI 重新连接都不会续期凭据。

节点重启不会结束配对操作。等待接受的操作保留其 pairing_id、令牌、nonce 与接受过期时间,并恢复 INTRO 重试,仍受令牌不含端点的过期时间约束,且从不获得比重启前持久记录的更多剩余生命周期。操作的每次持久更新都会记录剩余的接受预算,使其隐含的墙钟时刻从不早于已经观察到的本地时间。如果本地墙钟早于记录的时刻,运行时不发送 INTRO,为该操作报告 clock_unreliable,并让它保持等待,直到时钟可用或按单调时间测量的剩余预算用尽。接受定时器只在接受之前适用;它们从不改变已准入、准备中、已就绪或已超时的操作。

准备期限不跨重启保留。运行时停止时仍在进行的准备以 prepare_timed_out 结束;保留准入、PSK 与别名,之后的正常重连可能使对端就绪。

同一运行时内所有本地配对预算共享同一个单调时间原点。

默认输出人类可读的文本,可以本地化。使用 --json 时,每次调用(包括用法错误与运行时失败)都恰好向 stdout 写出一个 UTF-8 对象加 LF。必需的顶层键为 schema_version:2、operation:string、status:string、identity:Hex32|null、token_id:Hex32|null、pairing_id:LocalId|null、pending_id:LocalId|null、result:object|null、error:Error|null 与 transport:Transport|null。不适用的键为 null,从不省略。identity 是操作的主体;status 在 result 中以 self_identity 报告调用方。成功的变更使用 status:"ok";成功的查询也使用 status:"ok",即使嵌套的历史状态是失败的。失败的变更或查询使用 status:"failed"、result:null、非 null 的 error 以及对应的 §13.5.2 退出码。pair 的提交 / 附着状态与结果见下文。

Hex32 恰好是 64 个小写十六进制字符。U64 是一个 JSON 字符串,内容为 0..18446744073709551615 的规范无符号十进制数,除 "0" 外不带前导零;所有 Unix 秒、代际、版本、incarnation 与持久计数器都使用它。计数与端口是在其定义范围内的精确非负 JSON 整数。LocalId 是非空的不透明运行时字符串,从不被接受为其他类型的对象。IPv4 与 IPv6 字符串遵循 §12.5.1,作为地址值时 IPv6 不带方括号。列表即使为空也是数组,带键的列表按完整 network_id、完整身份、再按稳定的对象键排序。生产方只能增加稳定的字段;消费方容忍未知键,但删除字段、改变类型 / 含义或扩展枚举需要显式的兼容性决定。

在这些 schema 中,? 表示必需、但值可以为 null 的键。

对象必需字段与类型
Errorcode:string(取自 §13.5.3)、message:string、details:ErrorDetails
ErrorDetailsfield:string?、resource:string?、limit:U64?、conflicting_identity:Hex32?、conflicts:Conflict[];任何字符串都不回显凭据输入
Conflictkind:route/rule/address/firewall/alias/identity/owner、description:string、network_id:Hex32?、identity:Hex32?、address:string?
Transportcode:dns_failed/connect_failed/tls_failed/relay_unavailable/direct_unavailable/mtu_unusable/timeout?、path:Path?、observed_at:U64?;允许全部为 null
Pathkind:direct/relay_udp/relay_wss/none、endpoint:string?、relay:string?、inner_mtu:integer?、confirmed:boolean;none 的 endpoint / relay 为 null,confirmed 为 false
Context不输出:§2.7 的默认上下文没有 JSON 表示,没有 Network 对象或 networks 数组
Aliasnetwork_id:Hex32、identity:Hex32、kind:6/4、address:string、source:hash/user、state:active/suspended/tombstone
Peeridentity:Hex32、identity_label:string、nickname:string?、generation:U64、n6:string、n4:string?、aliases:Alias[]、admitted:boolean、connectivity:ready/offline/binding_required、path:Path、latest_error:Error?
TokenMetatoken_id:Hex32、network_id:Hex32、issuer:Hex32、recipient:Hex32?、issuer_name:string?、not_after:U64、revoked:boolean
Pairingpairing_id:LocalId、network_id:Hex32、identity:Hex32、identity_label:string、token_id:Hex32、state:waiting_for_acceptance/preparing/ready/prepare_timed_out/expired/cancelled/failed、acceptance_expiry:U64、preparation_remaining_ms:integer?、peer:Peer?、latest_error:Error?
Pendingpending_id:LocalId、network_id:Hex32、identity:Hex32、token_id:Hex32、first_received:U64、expiry:U64、state:pending/approved/denied/expired
AllowEntrynetwork_id:Hex32、identity:Hex32、state:admitted/removed、changed_at:U64、token_id:Hex32?
Bridgenetwork_id:Hex32、port:integer 1..65535、protocol:tcp/udp、backend:string(self4 authority)、state:active/stopped/failed、latest_error:Error?
Rotationgeneration:U64、cutover:U64、not_after:U64、state:scheduled/applied/failed、networks:Hex32[]、unacknowledged_peers:integer
KeyRenewalgeneration:U64、not_after:U64?、renew_at:U64?、state:not_due/due/scheduled/expired、late:boolean、rotation:Rotation?、latest_error:Error?

relay 注册按 §12.5 推迟时,直连路径不受影响。成功的 status 查询保持 status:"ok" 与顶层 error:null。现有的 relay 诊断在没有持久化绑定时报告 binding_required,在已持久化的绑定不是当前有效时报告 binding_invalid;成功恢复后清除。

直连的 Path.confirmed 表示经认证、相关联的可达性。Pairing 保留历史上的 prepare_timed_out 时,Peer 仍可能是 ready。

有已安排的轮换时,KeyRenewal.state 为 scheduled;否则 now >= not_after 时为 expired;否则 now >= renew_at 时为 due;否则为 not_due。当已安排的切换时刻加 240 秒超过 not_after,或没有已安排的轮换且 now + 540 > not_after 时,late 为 true。latest_error 描述运行中的运行时最近一次失败的自动尝试。状态为 expired、late 为 true,或状态为 due 且有最近错误时,人类可读的 status 输出向 stderr 写一条警告。

KeyRenewal.generation 是已提交的代际。not_after 即 E(§2.3),没有配置网络时为 null;renew_at 为 E - 604800,not_after 为 null 时它也为 null;not_after 为 null 时报告 not_due。rotation 是最近的轮换记录或 null。一次尝试成功、发现已有安排的轮换,或发现尚未到续期时间时,清除 latest_error;运行时未运行时它为 null。只要没有 --json,续期警告就写到 stderr,与 stdout 是否为终端无关。JSON 输出不带警告文本。

下面的 operation 字符串与每个结果字段都是必需的。适用的主体标识复制到顶层。集合类操作的单个标识保持为 null。所有成功的非 pair 操作以 0 退出。

命令 / operation成功时的 result 对象
up / up{running:boolean, self_identity:Hex32};一个就绪 / 提交快照,从不是无尽的 JSON 流
down / down{running:false, configuration_preserved:true}
status / status{running:boolean, self_identity:Hex32, peers:Peer[], counts:{peers:integer,tokens:integer,pending:integer,pairings:integer}, key_renewal:KeyRenewal}
token issue / token.issue{metadata:TokenMeta, token:string};唯一携带凭据的 JSON 结果;顶层 identity 为签发方
token list / token.list{tokens:TokenMeta[]}
token revoke / token.revoke{token_id:Hex32, revoked:true}
pair / pair{pairing:Pairing};wait 0 时使用 status:"submitted",以 0 退出;附着模式把顶层 status 设为当前的 Pairing.state,并使用 §13.5.2 的退出码映射
pair status / pair.status{pairing:Pairing};查询成功与否与嵌套状态无关
pair cancel / pair.cancel处于已取消状态的 {pairing:Pairing};已准入的配对报告 operation_not_cancellable
policy set / policy.set{join_policy:auto/confirm/recipient-only}
pending list / pending.list{pending:Pending[]}
pending approve / pending.approve{pending:Pending, peer:Peer};不声称准备已完成
pending deny / pending.deny处于已拒绝状态的 {pending:Pending}
allow list / allow.list{entries:AllowEntry[]}
allow remove / allow.remove{entry:AllowEntry, peer_removed:true}
allow restore / allow.restore{identity:Hex32, removal_cleared:true, admitted:false}
peers / peers{peers:Peer[]}
peer show / peer.show{peer:Peer}
peer remove / peer.remove{entry:AllowEntry, peer_removed:true}
peer name / peer.name{peer:Peer},带实际生效的规范化昵称
peer alias / peer.alias{peer:Peer, aliases:Alias[]}
aliases export / aliases.exportv1 没有成功结果;feature_deferred,退出码 2,result 为 null
aliases import / aliases.importv1 没有成功结果;feature_deferred,退出码 2,result 为 null
bridge add / bridge.add{bridge:Bridge}
bridge list / bridge.list{bridges:Bridge[]}
bridge remove / bridge.remove{port:integer, protocol:tcp/udp, removed:true}
key rotate / key.rotate{rotation:Rotation};顶层 identity 为本机
key binding export / key.binding.export{path:string, identity:Hex32, generation:U64, not_before:U64, not_after:U64}
peer binding import / peer.binding.import{peer:Peer, binding_installed:true};不声称已连通
recovery set-root / recovery.set-rootv1 没有成功结果;feature_deferred,退出码 2,result 为 null
recover / recoverv1 没有成功结果;feature_deferred,退出码 2,result 为 null
退出码含义
0操作 / 查询成功。pair wait 0 表示已提交;附着的 pair 表示已就绪。即使嵌套状态为待处理或失败,查询也算成功。
1本地 IPC、权限、存储、归属、时钟、容量或内部运行失败,带稳定的错误码。
2无效的参数 / 配置 / 选择器、不支持的组合、推迟的功能或需要确认。
3配对凭据无效、已过期或绑定到其他身份。
4附着的接受等待以未确认结束,或可观察到的本地拒绝 / 取消结束了操作。
5准备超时,或直接观察到的传输失败结束了调用。仅有沉默不代表签发方不可达。
6地址组冲突,并给出冲突的完整身份。
7别名地址池耗尽 / 被遮蔽 / 地址冲突,并给出本地详情。

附着的就绪返回 0。waiting_for_acceptance 在其观察期限到达时返回 4 与 acceptance_unconfirmed;prepare_timed_out 返回 5;已知的本地取消返回 4。接受之前过期返回 3 与 token_expired。查询这些状态时返回 0,嵌套的 latest_error 有值,顶层 error 为 null。无法选出操作的无效命令使用 operation:"invalid"。

退出码确切的错误码
1ipc_unavailable、ipc_unauthorized、permission_denied、storage_failed、storage_corrupt、resource_owner_conflict、resolver_unavailable、resolver_restore_conflict、clock_unreliable、binding_required、control_version_unsupported、endpoint_revision_exhausted、endpoint_state_conflict、peer_capacity、token_capacity、pending_capacity、pairing_capacity、relay_capacity、scan_unavailable、scan_permission_denied、scan_capture_failed、internal_error
2invalid_arguments、invalid_configuration、unsupported_option_combination、confirmation_required、peer_ambiguous、selector_invalid、object_not_found、object_type_mismatch、label_invalid、nickname_conflict、relay_identifier_invalid、endpoint_invalid、relay_config_missing、relay_endpoint_mismatch、resolver_configuration_invalid、rotation_window_invalid、rotation_pending、operation_not_cancellable、pending_not_actionable、binding_invalid、binding_stale、binding_conflict、bridge_backend_invalid、scan_device_ambiguous、scan_multiple_tokens、feature_deferred
3token_invalid、token_expired、token_recipient_mismatch、token_binding_invalid、token_binding_stale、token_binding_conflict
4acceptance_unconfirmed、operation_cancelled、local_denial、scan_timeout、scan_cancelled
5prepare_timed_out、transport_dns_failed、transport_connect_failed、transport_tls_failed、relay_unavailable、direct_unavailable、mtu_unusable
6address_group_conflict
7alias_pool_exhausted、alias_pool_shadowed、alias_address_conflict

令牌的语法、大小、解码与签名失败统一归为 token_invalid,不回显字节。传输证据单独分类。relay STATUS 可以作为一个已脱敏的观测,但不能产生 local_denial、撤销对端或证明全局中断。令牌内的陈旧绑定为 token_binding_stale;陈旧的当前绑定文件为 binding_stale。容量错误从不驱逐现有状态。存储争用耗尽时报告 resource_owner_conflict。意外失败使用已脱敏的 internal_error;不泄露库特有的错误码。

这些错误边界的触发定义与优先级:

  • 选择器与对象。 先校验选择器语法:selector_invalid 表示不受支持或格式错误的选择器,包括少于 8 个字符的节点 ID 前缀。匹配多个身份的对端名字使用 peer_ambiguous。语法有效但没有匹配对象的选择器使用 object_not_found,包括无法解析的身份名;属于其他对象类型的已知不透明本地 ID 改用 object_type_mismatch。先解析网络,再解析对端或其他限定范围的对象;歧义从不随意选一个匹配,也不变成未找到。
  • 绑定。 对当前绑定文件,binding_invalid 表示大小、规范结构、签名、完整所有者 / 范围或有效区间失败,包括未来或已过期的绑定。通过这些检查之后,binding_stale 表示 gen < committed_gen 或 gen <= revoked_through;它优先于 binding_conflict,后者表示在其他方面有效的同代际不同 K 冲突,或已被冲突锁存禁用的代际。这些错误描述被拒绝的输入,不改变状态;冲突检测 / 锁存仍按 §12.3。binding_required 则描述所选 (network_id, I) 没有任何可接受的当前绑定,从不替代更具体的输入错误。relay 注册推迟时(§12.5),没有持久化绑定报告 binding_required,已持久化但不是当前有效的绑定报告 binding_invalid;成功恢复后现有的 relay 诊断被清除。在令牌内部,于令牌语法 / 签名、过期(token_expired)与接收方(token_recipient_mismatch)检查之后,在相同的边界分别使用 token_binding_invalid、token_binding_stale 与 token_binding_conflict;令牌语法 / 签名失败为 token_invalid。
  • 扫码设备选择与采集。 打开设备之前先校验扫码参数 / 组合。scan_unavailable 表示没有合格设备,或显式选定的设备不存在 / 不可用;没有显式选择时,多个合格设备使用 scan_device_ambiguous。选定后,访问被拒绝使用 scan_permission_denied;其他采集 / 解码后端失败,或无法在采集上限内工作,使用 scan_capture_failed。选择先于访问,访问先于采集;使用这些具体的错误码,而不是通用的权限或传输失败。
  • 扫码结束。 导入前的显式取消使用 scan_cancelled;单调的采集期限到达而没有令牌使用 scan_timeout。在期限之前或恰好在期限时观察到的取消优先于超时;两者都不创建配对状态。在终止前处理的一帧中,多个不同的令牌候选在凭据校验之前使用 scan_multiple_tokens;单个候选进入普通的令牌校验并报告其凭据错误。非令牌的二维码与重复出现保持 §13.2 的行为。
  • 传输。 transport_dns_failed 表示观察到名字解析失败,transport_connect_failed 表示在目标解析之后观察到套接字 / 连接建立失败,transport_tls_failed 表示连接之后观察到 TLS 握手或证书校验失败;报告失败的阶段,不推断后续阶段的失败。relay_unavailable 表示尝试的 relay 路径无法获得或保持可用的已确认 relay 会话;direct_unavailable 表示尝试的直连路径无法建立或保持经认证的可达性。这些路径级错误码只在没有更具体的阶段失败时使用;mtu_unusable 标识被 §6.6 最小可用 MTU 拒绝的路径。prepare_timed_out 是 §13.4 准备期限的结果,而不是对哪种传输失败的猜测。只有当传输失败结束了调用 / 操作时,传输错误码才成为顶层错误并以 5 退出;否则它仍是带类型的 Transport 证据(DNS / 连接 / TLS 使用 dns_failed / connect_failed / tls_failed,传输期限使用 timeout)。成功的查询保持 status:"ok"、以 0 退出,顶层 error:null;附着的 pair 保持其规定的状态 / 退出结果。仅有签发方沉默仍只产生未确认的接受,从不产生传输失败或拒绝。

错误不回显凭据。JSON 不包含提示或进度;人类可读的进度与诊断使用 stderr。--json 从不隐含 --yes,并且与 --qr 不兼容;令牌签发只在其显式结果中返回凭据。

配对令牌包含持有者秘密。令牌签发在结果中返回新签发的令牌一次;--qr 在人类可读模式下额外显示同一凭据的二维码。签发报告 token_id、接收方限制与过期时间,并说明该令牌是持有者凭据。只要知道对端的完整身份,就优先签发限定接收方的令牌。

令牌列表、状态、待批准项、日志、错误消息与通知从不包含令牌、秘密、PSK 或私钥。JSON 签发结果与用户显式请求的令牌 / 二维码显示是凭据输出,不是日志。应用从不记录令牌输入。文档规定的 stdin、文件与扫码输入避免把凭据放进命令参数。

扫码图像、解出的载荷与帧转储从不持久化或记录。当前绑定导出只包含公开的已签名 KeyBinding;导入错误不回显文件字节。推迟的别名与恢复命令不读取其文件或秘密材料。

运行时只把凭据保存在受保护的本地凭据存储中,并通过现有的本地 IPC 授权边界限制访问。撤销停止新的准入;移除已接纳的身份是另一个操作。

移动端与 FFI 绑定推迟到 v1 之后。将来的接口使用同样的运行时操作与待批准队列,不改变 §12,但本规范不定义任何移动端 API 或通知行为。