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)除非另有说明均指本页。
1. 目标与非目标
Section titled “1. 目标与非目标”| 目标 | 推论 |
|---|---|
| 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/v2WSS 载体共用监听但路径分开。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。
3.1 能力宣告
Section titled “3.1 能力宣告”relay 用一组能力标记说明自己提供哪些服务(新架构 §6)。同一组标记出现在三处:relay 目录的每一项(§5.4)、/rendezvous/v1 的 challenge,以及 relay 报文握手的应答;节点侧的 relay 客户端据此选择载体,而不是逐个试探。
| 标记 | 含义 |
|---|---|
udp | relay UDP 载体 |
wss | relay 报文的 /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或握手应答中的宣告为准。
4. 参与方鉴权
Section titled “4. 参与方鉴权”除 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。
5.1 模型
Section titled “5.1 模型”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}5.2 请求
Section titled “5.2 请求”| 请求 | 载荷 | 发送方 | 结果 |
|---|---|---|---|
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} | 创建者或 peer | sent {seq} |
recv | {id, after, wait_ms} | 创建者、候选或 peer | mailbox {stage, waiting, peer, messages: [{seq, data}]} |
close | {id} | 创建者、候选或 peer | ok |
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 最多挂起这么久再应答;任何变化(新消息、接纳、驱逐、关闭、过期)立即唤醒。
5.3 阶段规则
Section titled “5.3 阶段规则”| 事件 | 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应答,客户端换成员重试。
5.4 relay 目录
Section titled “5.4 relay 目录”目录给每个 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 |
| 每个信箱被拒的 actor | 16 |
reject 已接纳对端 | 每个信箱 3 次 |
intro / data | 4 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 才会转到区域的下一个成员。
8. relay 能看到什么
Section titled “8. relay 能看到什么”- 能看到:
kind、数字码、各请求的时间与源地址、每个信箱的临时 actor 公钥、载荷的大小与条数。 - 看不到:应用没有放进明文载荷的设备身份(
pake通道的身份只在加密的 HELLO 里;confirm通道的 Offer / Answer 明文携带双方的签名记录,应用通道 §8)、PAKE 口令或派生密钥、PSK、应用数据,以及载荷的任何语义。端点必须对需要保密的载荷做端到端加密与认证;relay 只保证顺序、上限与唯一接纳。 - 通信 relay(与会合 relay 可能不同)看到的是已注册的记录、源地址、presence 与加密报文的大小和 slot 关系(Quick 协议 v2 §8)。
信箱之上的端点协议由 ns 的应用通道统一定义,relay 不感知:
| 通道鉴权 | 信箱用法 | 首批应用 |
|---|---|---|
confirm | open {admission: owner, candidates: 1–4, intro: Offer};加入方 join {intro: Answer};创建方 admit 一个、reject 其余,send ACCEPT,加入方 send COMPLETE 后双方 close | Quick 配对(kind = "pairing") |
pake | open {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 的上限)。
11. 与 /quick/v1 的差异
Section titled “11. 与 /quick/v1 的差异”| 项目 | /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 poll | recv 长轮询(≤ 20 s) |
| 字节串编码 | JSON 数组 | base64url 字符串 |
| 能力宣告 | 无 | challenge.capabilities 与目录项的 capabilities(§3.1) |
| relay 目录 | authority(*.sslip.io 主机名) | host、ips、port、capabilities(§5.4) |
12. 已定事项
Section titled “12. 已定事项”| 事项 | 决定(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 只定义本页的消息类型 |