跳转到内容

NS · 会合协议(/rendezvous/v1)

状态:消息类型与节点侧客户端已实现,relay 侧未实现。 消息类型在 nsshared-proto v0.2.0 起实现(nsshared_proto::rendezvous,含独立生成的测试向量),ns 的 overlay 自 2026-10-04 起实现信箱客户端与登记表客户端(Quick 配对与按节点 ID 放行在用);relay 侧只有 nsshared 的单实例 mock(nsshared-mock-rendezvous),生产实现是 nsgw 的迁移里程碑 M2。本页定义 relay 上与应用无关的会合服务 /rendezvous/v1:信箱、登记表与 relay 目录。它取代 Quick 模式 §3 的 /quick/v1,也取代从未部署的草案 /quick/v2(2026-10-03 改名:服务与 Quick 无关,语音、聊天等应用同样使用它)。不兼容 /quick/v1:ns 与 relay 在同一个版本中切换,/quick/v1 随之删除。Quick 配对、文件发送与之后的应用都运行在 ns 的应用通道上,信箱只是它的会合手段(§9)。消息类型在 nsshared 中定义,relay 与 ns 共用。协议引用(§n)除非另有说明均指本页。

目标推论
relay 与应用无关relay 只提供短码、唯一接纳、有界的双向不透明消息队列与过期;它不解析载荷,不知道配对、令牌或文件
新应用不需要改 relay应用只定义一个 kind 标签和两端的消息;配对与文件发送是第一批应用
信箱不需要长期身份连接用每个信箱新生成的参与方密钥鉴权;设备身份键 I 只在应用选择时出现在载荷里(配对携带签名记录,文件发送把它放进加密的 hello)
沿用 v1 的运维性质相同的 TLS、挑战、私有持久化、区域 owner 转发、限速与绝对过期规则

非目标:大流量数据(应用数据始终走 WireGuard 隧道,见 §10)、离线投递、多于两方的会话、按名字或身份查找。

层组件职责
会合服务relay(/rendezvous/v1)本页 §3–§8:信箱、短码、接纳、队列、上限、登记表与目录;与应用无关
会合客户端与应用通道ns连接与重连、参与方密钥、区域成员轮转、长轮询;应用通道:profile、引入、密钥派生、对端安装、按 kind 分发
数据面机制nsplane安装与移除会话对端、隧道内连接(netstack)、按来源命名空间的 ACL 与 pinhole、4↔6 翻译;不感知会合与 kind
命令与界面ns、第三方应用ns quick invite / join、ns send / receive、第三方应用的界面;确认提示、输出格式
  • 在 relay 现有 TLS 监听上的 WebSocket:wss://{authority}/rendezvous/v1,子协议 ns-rendezvous-v1,与 relay 报文的 /ns/v2 WSS 载体共用监听但路径分开。TLS 校验规则与 v1 相同(Quick 模式 §3.2)。
  • 每个 WebSocket 文本消息承载一条 JSON 消息,最大 16 KiB,在解析前检查。字节串一律编码为无填充的 base64url 字符串。
  • 请求格式为 {"operation": <名称>, "value": <载荷>},响应格式为 {"result": <名称>, "value": <载荷>};名称使用 snake_case,未知字段一律拒绝。一个连接上的请求按顺序处理,每个请求恰有一个响应。
  • 连接建立后,broker 先发送 challenge {nonce, authority, capabilities}。nonce 为不全为零的 32 字节随机值;authority 必须等于客户端期望的 relay 或区域身份,不一致时客户端断开;capabilities 见 §3.1。

relay 用一组能力标记说明自己提供哪些服务(新架构 §6)。同一组标记出现在三处:relay 目录的每一项(§5.4)、/rendezvous/v1 的 challenge,以及 relay 报文握手的应答;节点侧的 relay 客户端据此选择载体,而不是逐个试探。

标记含义
udprelay UDP 载体
wssrelay 报文的 /ns/v2 WSS 载体
mailbox/rendezvous/v1 会合信箱(本页)
region该 authority 是一个 relay 区域,由多个成员提供服务
registry登记表(Quick 协议 v2 §7),publish 与 lookup_node
  • 标记是小写 ASCII,[a-z0-9.-]{1,32},最多 16 个;未知标记一律忽略,从不因此拒绝连接。
  • 标记词表定义在 nsshared,relay 与节点共用一份。
  • 能力只说明 relay 提供什么,从不授权任何操作;鉴权与上限仍按各服务自己的规则执行。目录中的能力是提示,以连接后 challenge 或握手应答中的宣告为准。

除 directory 与登记表的 publish、lookup(Quick 协议 v2 §7.2)外,每个请求都以某个参与方的身份执行。连接在发出其他请求之前先发送 hello:

hello {
actor 32 字节 Ed25519 公钥
signature Ed25519_actor( "ns-rendezvous-actor-v1\0" || nonce || authority )
}
  • broker 用严格模式的 Ed25519 校验签名,之后该连接上的请求都属于这个参与方。一个连接只能 hello 一次。
  • 参与方密钥对每个信箱新生成,信箱结束即丢弃。relay 从不把它当作设备身份;设备身份 I 只在应用通道的载荷中出现,并由对端校验(§9)。
  • 断线后,客户端在任一应答的成员上建立新连接、对新挑战签名并发送同一个 actor,即可继续操作原信箱。不需要单独的 resume。
Mailbox {
id 16 字节随机值,由 broker 生成
kind 应用标签,[a-z0-9-]{1,32};由通信双方约定,relay 不设白名单
owner 创建者的 actor
admission first | owner
candidates 1–4,同时等待接纳的候选上限;admission = first 时为 1
expires Unix 秒;now < expires <= now + 900
digits 8 位数字;只在 open 阶段可被查找
intro 创建者的介绍载荷,≤ 4 KiB,可为空
waiting [{actor, intro}] 未接纳的候选
peer 已接纳的对端 actor,或无
rejects 已执行的 reject 次数
queues owner → peer、peer → owner 两个有序队列
stage open | admitted | closing
}
请求载荷发送方结果
directory—任意(不需要 hello)directory(relay 目录 v2,§5.4)
publish{record, dns, proof}任意(不需要 hello;proof 由记录中的 I 签名)published {expires};仅在宣告 registry 时(Quick 协议 v2 §7.2)
lookup_node{node_id}任意(不需要 hello)record {record};仅在宣告 registry 时
hello{actor, signature}任意ok
open{kind, ttl, admission, candidates, intro}创建者opened {id, digits, expires}
lookup数字码任意found {id, kind, expires, intro};返回创建者的介绍载荷
join{id, intro}加入者ok;admission = first 时同时被接纳
admit{id, actor}创建者ok;该候选成为 peer
reject{id, actor}创建者ok;移除候选,或驱逐已接纳的对端(§5.3)
send{id, data}创建者或 peersent {seq}
recv{id, after, wait_ms}创建者、候选或 peermailbox {stage, waiting, peer, messages: [{seq, data}]}
close{id}创建者、候选或 peerok
  • ttl 为 1–900 s。intro 与 data 是不透明字节串:intro ≤ 4 KiB,data ≤ 8 KiB。
  • recv 只把 waiting 返回给创建者;候选与 peer 看到的 waiting 为空。messages 只包含发给请求方、seq > after 的消息;after 同时确认此前的消息,broker 删除已确认的消息。
  • wait_ms 为 0–20 000。没有新消息且阶段未变时,broker 最多挂起这么久再应答;任何变化(新消息、接纳、驱逐、关闭、过期)立即唤醒。
事件broker 的处理
open阶段 open;分配不冲突的 8 位数字(冲突由 broker 重试)
join(admission = owner)加入 waiting;已满时返回 capacity。创建者自己不能加入
join(admission = first)直接成为 peer;阶段变为 admitted,数字码立即从查找中移除
admit候选成为 peer;阶段变为 admitted,移除数字码与其余候选
reject 候选移除该候选;记录被拒的 actor,直到信箱过期都不能再次加入
reject 已接纳的对端清空两个队列,peer 置空,阶段回到 open 并恢复同一数字码;rejects 加一,达到 3 次时信箱关闭
send只允许在 admitted 阶段;seq 在每个方向从 1 开始递增
创建者或 peer close阶段变为 closing:拒绝新的 send,对方仍可读完已排队的消息并看到 closing;对方确认最后一条消息、对方也 close,或 30 s 后移除
候选 close只移除它自己
创建者或 peer 180 s 无活动移除信箱(每 10 s 检查一次)
绝对过期10 s 内移除;生命周期从不超过 15 分钟,重启也不会延长
  • 查找、候选与被拒的猜测都不能延长信箱,也不会让其他信箱失效。
  • 信箱状态(包括未确认的消息)保存在私有存储中,与 relay 的永久安全标记分开;丢失它从不影响数据载体。
  • 在区域中,每个 owner epoch 只有一个会合 owner;其他成员经区域控制链路把请求转发给它,与 v1 相同。挂起中的 recv 由 owner 持有,计入 §6 的并发上限:消息、接纳与关闭都在 owner 上发生,由它直接唤醒等待者,入口成员不需要轮询。owner 栅栏变化时立即以 unavailable 应答,客户端换成员重试。

目录给每个 relay 同时列出连接地址与证书名字,客户端不需要先解析公网 DNS(新架构 §5.5):

Directory { version u64 >= 1, relays: RelayEntry[] } // 最多 576 条
RelayEntry {
id u16 != 0
code 两个字母
host TLS 名字(SNI 与证书校验),例如 hkg-a1.relay.ns.io
ips 1–4 个 IP 字面量(IPv4 / IPv6)
port u16
capabilities text[](§3.1)
}
  • relay 身份(challenge.authority、应用通道 Offer 中的 authority)为 host:port。
  • 客户端连接 ips 中的地址与 port,用 host 做 SNI 与证书校验;公网 DNS 可用时也可以解析 host 以跟随 IP 变更,解析失败就用目录里的 IP。证书由 relay.ns.io 的 DNS-01 签发。
  • id、code、host 各自唯一;id 及其 code 永久不变,更新可以增加条目、修改 ips 或 capabilities,从不删除 id、重新分配短码或复用已退役的短码。任何变更都需要更高的版本;同一版本必须完全相同。短码字母、未知前缀刷新等规则同 Quick 模式 §4。
  • 内置目录随 v2 换成 *.relay.ns.io 名字,不再使用 *.sslip.io。
上限值
保存的信箱10 000
每个 actor 的有效信箱3
每个信箱的候选4
每个信箱被拒的 actor16
reject 已接纳对端每个信箱 3 次
intro / data4 KiB / 8 KiB
每个方向未确认的消息16 条、64 KiB
每个信箱累计消息256 条、512 KiB
全局排队消息字节64 MiB
挂起的 recv每个连接 1 个;全局 4 096 个
消息大小16 KiB
查找每个源 IP 每分钟 12 次,全局每秒 100 次
创建每个源 IP 每分钟 30 次,全局每秒 100 次
每个连接的请求每秒 10 次(挂起的 recv 只在发出时计一次)

保存信箱的上限并不代表并发能力;relay 的连接与组帧内存预算同样适用。所有上限都是 relay 配置项,表中为生产默认值。

error 的值为以下之一:invalid_message、invalid_actor、invalid_code、unknown_relay、invalid_directory、unknown_mailbox、not_permitted、wrong_stage、capacity、queue_full、rate_limited、conflict、randomness、storage、unavailable。

错误中从不包含数字码、载荷或其他不可信输入。relay 返回的错误是最终结果;只有传输故障与 unavailable 才会转到区域的下一个成员。

  • 能看到:kind、数字码、各请求的时间与源地址、每个信箱的临时 actor 公钥、载荷的大小与条数。
  • 看不到:应用没有放进明文载荷的设备身份(pake 通道的身份只在加密的 HELLO 里;confirm 通道的 Offer / Answer 明文携带双方的签名记录,应用通道 §8)、PAKE 口令或派生密钥、PSK、应用数据,以及载荷的任何语义。端点必须对需要保密的载荷做端到端加密与认证;relay 只保证顺序、上限与唯一接纳。
  • 通信 relay(与会合 relay 可能不同)看到的是已注册的记录、源地址、presence 与加密报文的大小和 slot 关系(Quick 协议 v2 §8)。

信箱之上的端点协议由 ns 的应用通道统一定义,relay 不感知:

通道鉴权信箱用法首批应用
confirmopen {admission: owner, candidates: 1–4, intro: Offer};加入方 join {intro: Answer};创建方 admit 一个、reject 其余,send ACCEPT,加入方 send COMPLETE 后双方 closeQuick 配对(kind = "pairing")
pakeopen {admission: first, candidates: 1, intro: Share};加入方 join {intro: Share} 立即被接纳;双方 send 加密的 HELLO;猜错口令时创建方 reject,第 3 次后信箱关闭文件发送(kind = "transfer")

新应用只需要在 ns 中声明一份 profile(kind、鉴权方式、对端生命周期、隧道内端口,应用通道 §9)。relay 接受任何合法的 kind,不登记也不审批,不需要任何改动。

10. 为什么不在 relay 上提供数据流

Section titled “10. 为什么不在 relay 上提供数据流”

两端一旦有了 WireGuard 对端(已配对的对端,或文件发送的仅限本次传输的对端),隧道就是通用的数据通道:路径选择(直连或经 relay)、加密、直连探测与 relay 转发预算都已具备,应用在隧道内按端口区分。relay 上另设字节流会引入第二套加密与鉴权,绕过节点的入站过滤,还需要另一套转发预算。因此 /rendezvous/v1 只承载会合所需的小消息(§6 的上限)。

项目/quick/v1/rendezvous/v1
路径与子协议/quick/v1、ns-quick-v1/rendezvous/v1、ns-rendezvous-v1
鉴权每个请求携带设备身份签名的 Proof每个连接一次 hello,临时 actor 密钥
语义invite / join、select、单次 deliver、complete与应用无关的 open、join、admit、reject、send、recv、close
消息邀请方单向一个 SealedToken双向有序队列,确认后删除
获取状态每 0.7 s pollrecv 长轮询(≤ 20 s)
字节串编码JSON 数组base64url 字符串
能力宣告无challenge.capabilities 与目录项的 capabilities(§3.1)
relay 目录authority(*.sslip.io 主机名)host、ips、port、capabilities(§5.4)
事项决定(2026-10-02)
kind 白名单relay 不设白名单。kind 由通信双方约定,relay 只校验格式并用于统计
区域中挂起的 recv由会合 owner 持有(§5.3),入口成员只转发请求与应答
名字(2026-10-03)草案 /quick/v2、ns-quick-v2、ns-quick-actor-v2 改为 /rendezvous/v1、ns-rendezvous-v1、ns-rendezvous-actor-v1:服务与 Quick 无关,版本号随新名字从 1 开始。草案从未部署,没有兼容层
应用协议的位置(2026-10-03)配对与文件发送的载荷不再逐个定义在本页,统一由 ns 的应用通道定义;nsshared 只定义本页的消息类型