跳转到内容

NS · 应用通道

状态:部分实现。 confirm 模式与 Quick 配对(kind = "pairing")已在 ns 实现(crates/ns/src/app_channel,2026-10-04):Offer / Answer / ACCEPT / COMPLETE 经 overlay 的会合信箱客户端走 /rendezvous/v1,派生 K_ch 与 WireGuard PSK;通道从不自己安装对端。pake / peer 模式与文件发送等应用尚未实现。应用通道是 ns 内部的通用层:Quick 配对、文件发送以及之后的语音、聊天等应用都运行在它上面。通道的两端都是 ns;relay 只看到会合协议的不透明消息,nsplane 只看到对端与端口。本页取代会合协议原 §9 的逐应用映射、Quick 协议 v2 原 §10.2 的配对载荷,以及文件发送原 §4.2 与 §5.1 的会合与密钥协商。协议引用(§n)除非另有说明均指本页。

目标推论
新应用只声明参数应用给出一份 profile(§3);会合、引入、身份交换、PSK、对端安装与隧道入口全部复用
一套引入流程,三种鉴权人工核对节点 ID(confirm)、一次性口令 PAKE(pake)、已有对端(peer),覆盖配对、票据与已配对设备之间的应用
应用之间互相隔离kind 进入每个签名与每次密钥派生,一个应用的消息不能在另一个应用里重放
relay 不感知通道只使用会合协议的信箱;不新增 relay 操作、报文类型或白名单
数据只走隧道信箱只承载引入所需的小消息;应用数据始终在 WireGuard 隧道内(会合协议 §10)

非目标:多于两方的会话、离线投递、按名字或身份查找对方、隧道内应用协议本身(各应用自己定义)。

层组件职责
应用ns:pairing、transfer,之后的语音、聊天;第三方应用经 ns 的应用接口声明 profile;命令、确认提示与界面;隧道内的应用协议
应用通道ns本页:profile、引入、身份交换、密钥派生、对端安装、隧道内入口
会合relay 的 /rendezvous/v1;消息类型在 nsshared信箱、短码、接纳、队列、上限,与应用无关
数据面nsplane安装与移除对端、隧道内连接、按来源命名空间的 ACL 与 pinhole

通道只在 ns 中实现,不进入 nsshared:两端都是 ns,第三方应用也经 ns 的应用接口使用它,不自行实现本页的协议。

AppProfile {
kind [a-z0-9-]{1,32}(会合协议 §5.1)
auth confirm | pake | peer
lifetime permanent | session | none
ttl 信箱 TTL 的默认值与上限,≤ 900 s
app_port 隧道内 TCP 端口;null 表示没有隧道内阶段
}
auth信箱用法适用场景lifetime
confirmadmission: owner,candidates 1–4双方核对对方节点 ID 后建立长期关系permanent
pakeadmission: first,candidates: 1;或在已有对端的隧道内运行(§5.2)持有一次性口令即授权一次会话session
peer不使用信箱两端已是 ready 对端none

内置 profile:

应用kindauthlifetimettlapp_port
Quick 配对pairingconfirmpermanent默认 600 s,最多 900 snull
文件发送(relay 票据)transferpakesession600 s47901
文件发送(已有对端、对端票据)transferpeer;对端票据另在隧道内运行 pakenone—47901
语音、聊天待定peer 或 pakenone 或 session—待定

其他组合不允许:confirm 总是建立长期对端,pake 从不写入允许名单,peer 从不安装对端。

所有消息都是规范 CBOR 定长数组(Quick 协议 v2 §12.1),放在信箱的 intro 与 data 里;解码器在任何签名、MAC 或 AEAD 校验之前拒绝不规范的输入、多余字节、未知版本与未知类型。

// confirm:信箱 intro
Offer = [ver = 1, kind text, record_o, e_o bytes32, authority text, expires u64, name text|null, sig_o]
sig_o = Ed25519_Io("ns/channel-offer/v1" ‖ CBOR(前七个字段))
Answer = [ver = 1, kind text, record_p, e_p bytes32, mailbox_id bytes16, offer_hash bytes32, name text|null, sig_p]
sig_p = Ed25519_Ip("ns/channel-answer/v1" ‖ CBOR(前七个字段))
// pake:信箱 intro
Share = [ver = 1, kind text, Y bytes] // CPace 公开消息
// 两种模式:信箱 data
Message = [ver = 1, type u8, body]
1 ACCEPT [answer_hash bytes32, sig] // confirm;sig = Ed25519_Io("ns/channel-accept/v1" ‖ offer_hash ‖ answer_hash)
2 COMPLETE []
3 HELLO [ciphertext bytes] // pake;AEAD(k_hello_dir, nonce = 0, Hello)
4 APP [payload bytes] // 应用自己的小消息,通道建立后才允许
Hello = [ver = 1, record, endpoint text|null, not_after u64, sig]
sig = Ed25519_I("ns/channel-hello/v1" ‖ kind ‖ th ‖ CBOR(前四个字段))
offer_hash = SHA-256(CBOR(Offer)),answer_hash = SHA-256(CBOR(Answer))
th = SHA-256(CBOR(Share_o) ‖ CBOR(Share_p) ‖ mailbox_id)
  • record 是发送方当前的签名 NodeRecord(Quick 协议 v2 §3);接收方按其有效性与 §3.3 的代际 / 冲突规则处理。
  • 每个消息中的 kind 必须等于信箱的 kind 与本地 profile 的 kind,否则丢弃并中止通道。
  • name 是对方给出的显示名,只用于界面与默认昵称,从不参与授权。
  • APP 受信箱的 8 KiB data 上限约束;大于此的数据走隧道。
confirm: ikm = X25519(e_o, e_p)(全零结果被拒绝),t = offer_hash ‖ answer_hash
pake: ikm = K_pake(CPace,§5.2), t = th
K_ch = HKDF-SHA256(salt = 空, ikm, info = "ns/channel-key/v1" ‖ kind ‖ t)
psk = HKDF-Expand(K_ch, "ns/channel-psk/v1" ‖ I_o ‖ I_p, 32) // WireGuard PSK
k_hello_o = HKDF-Expand(K_ch, "ns/channel-hello-o/v1", 32) // 创建方发出的 HELLO
k_hello_p = HKDF-Expand(K_ch, "ns/channel-hello-p/v1", 32) // 加入方发出的 HELLO
K_confirm = HKDF-Expand(K_ch, "ns/channel-confirm/v1", 32) // 隧道内 OPEN 的 MAC(§7)

o 指信箱创建方,p 指被接纳的加入方。AEAD 为 ChaCha20-Poly1305;每个 HELLO 密钥只加密一条消息,因此 nonce 固定为 0。临时密钥 e_o、e_p、CPace 秘密与 K_ch 在通道结束后丢弃;psk 只交给对端安装(§6)。

  1. 创建。 创建方 open {kind, admission: owner, candidates, ttl, intro: Offer};authority 是所拨 relay 的标识,expires = now + ttl。用户分享短码(relay 短码 + 8 位数字,会合协议 §5.4)。
  2. 查看。 加入方 lookup 得到 Offer 并校验:签名与记录有效、record_o.I 与签名者一致、authority 等于所拨 relay、now < expires、对方不在本机移除记录中、地址组不与自己或已有对端冲突。界面显示对方的节点 ID 与 name,用户确认;应用可以提供”指定节点 ID 时自动确认、不匹配时失败”的参数。
  3. 加入。 加入方 join {id, intro: Answer}。
  4. 接纳。 创建方从 recv 的 waiting 读取每个 Answer,做与第 2 步相同的校验,并检查 mailbox_id 与 offer_hash。用户 admit 一个、reject 其余(同样可以按指定节点 ID 自动接纳),随后发送 ACCEPT。
  5. 安装。 创建方发送 ACCEPT 后、加入方校验 ACCEPT 后,各自派生 psk 并按 lifetime 安装对端(§6)。
  6. 结束。 加入方就绪后发送 COMPLETE 并 close;创建方收到 COMPLETE 后 close,或在自己就绪 30 s 后关闭。
  1. 创建。 创建方生成一次性口令,open {kind, admission: first, candidates: 1, ttl, intro: Share_o}。口令与短码一起组成应用的票据(例如文件发送 §3)。
  2. 加入。 加入方 lookup 得到 Share_o,join {id, intro: Share_p},立即被接纳。
  3. 派生。 双方运行 CPace:sid = mailbox_id,CI = "ns/channel-cpace/v1" ‖ kind ‖ authority,口令为票据中的口令,得到 K_pake,再按 §4.1 派生 K_ch。
  4. Hello。 双方各 send 一条 HELLO。接收方解密、校验签名、记录(有效性与代际 / 冲突规则)、not_after > now 与本机移除记录。
  5. 猜错口令。 创建方解不开对方的 HELLO 时 reject 该对端;信箱回到 open 并保留同一短码,第 3 次 reject 后关闭(会合协议 §5.3)。加入方解不开时直接 close。
  6. 安装与结束。 双方的 HELLO 都校验通过后,各自派生 psk,按 lifetime 安装对端(§6),然后 close 信箱。

在隧道内运行 pake。 两端已是对端、但应用仍要求持有口令(文件发送的对端票据)时,不使用信箱:Share 与 HELLO 作为隧道内 OPEN 之后的头几帧交换(§7),sid 为 OPEN 中的 channel_id,authority 为空串。身份已由 WireGuard 认证,HELLO 只用于确认口令并绑定双方身份;不安装新对端。

两端已是 ready 对端(已配对,或同一账户内由 NSD 投影)。不使用会合与信箱:发起方直接在隧道内连接对方的 app_port 并发送 OPEN(§7),身份就是承载这条流的 WireGuard 对端。是否允许由对端来源命名空间的 pinhole 规则决定(nsplane)。

lifetime写入AllowedIPs入站过滤生命周期
permanentQuick 来源的 Allow {I, added_by: kind, psk} 与对方记录(Quick 协议 v2 §10.1)N6/128 + N4/128发往本机的任意流量直到移除
session仅限本次会话的对端:不进允许名单,不分配别名或名字,不留下移除记录只有 N6/128只接受 app_port 上来自 N6(peer) 的 TCP 及其返回流,加上隧道内控制端口 47900直到会话完成、失败或过期;自创建起 ≤ ttl,加上应用报告有进展的时间,最长 24 h
none不安装;使用已有对端—对端来源命名空间允许的 app_port pinhole,随会话关闭随会话
  • 已存在的长期对端被同一身份重新 confirm 时,只替换 PSK 与记录,保留别名与昵称。
  • 从安装起 5 s 内 WireGuard 路径可用并完成本地发布,否则报告 prepare_timed_out;长期对端保留供正常重连,会话对端删除。
  • 仅限本次会话的对端计入每个网络 1 024 个对端的上限,另有每个节点默认最多 8 个并发的单独上限;超过任一上限时,通道在会合之前以容量错误失败。
  • 两台设备之后经 confirm 建立长期关系时,先删除双方之间的会话对端;会话 PSK 从不复用。
  • 崩溃遗留的会话对端在节点下次启动时删除。

有 app_port 的应用,在隧道内的 TCP 流上使用带长度前缀的帧 u32 length ‖ u8 type ‖ payload,每帧最大 1 MiB。类型 0 保留给通道,其余由应用定义:

OPEN = [ver = 1, kind text, channel_id bytes16, mac bytes32 | null]
mac = HMAC-SHA256(K_confirm, "ns/channel-open/v1" ‖ channel_id ‖ I_sender ‖ I_receiver)
  • 连接方先发 OPEN,被连接方校验后回一个 OPEN(I_sender 与 I_receiver 对调),之后才交换应用帧。
  • 经信箱建立的通道:channel_id = mailbox_id,mac 必须存在;MAC 中的两个身份必须与承载这条流的 WireGuard 对端一致。这样隧道证明对端身份,MAC 证明对端就是完成引入的那一方。
  • peer 通道:channel_id 为发起方生成的 16 字节随机值,mac 为 null;需要口令时随后运行隧道内 pake(§5.2)。
  • kind 不匹配、MAC 错误或第一帧不是 OPEN 时立即关闭连接。
  • relay 看到临时参与方密钥、kind、短码、消息大小与时间。confirm 的 Offer / Answer 明文携带双方的签名记录与临时 DH 公钥;pake 的身份只出现在加密的 HELLO 中。relay 看不到口令、K_ch、PSK 或应用数据。
  • relay 不能伪造 Offer、Answer、ACCEPT 或 HELLO,也不能伪造”已接纳”:加入方只认创建方签名的 ACCEPT 或能解开的 HELLO。
  • confirm 中 relay 用自己的身份替换整份 Offer 或 Answer 时,用户核对的节点 ID 会不一致,这是唯一需要人工的一步;节点 ID 有 96 位,无法碰撞出相同的值。
  • pake 中知道短码的 relay 运营方,每次尝试最多在线猜一次口令;第 3 次失败后信箱关闭。口令熵由应用决定(文件发送约 33 位)。
  • kind 进入签名、CPace 的 CI 与 K_ch 的派生:为一个应用产生的消息在另一个应用里无法通过校验。
  • 信箱过期、被拒绝或取消时什么也不安装。
  1. 选择 kind,以及 §3 中允许的 auth 与 lifetime 组合。
  2. 有隧道内阶段时分配 app_port,并定义 OPEN 之后的应用帧。
  3. 需要会合时定义票据或短码的界面;口令长度满足自己的安全需求。
  4. 在 ns 的应用接口中登记 profile 与命令。relay、nsshared、nsplane 都不需要改动。
项目值
消息版本1
签名上下文ns/channel-offer/v1、ns/channel-answer/v1、ns/channel-accept/v1、ns/channel-hello/v1
派生标签ns/channel-key/v1、ns/channel-psk/v1、ns/channel-hello-o/v1、ns/channel-hello-p/v1、ns/channel-confirm/v1、ns/channel-open/v1
CPaceCI 前缀 ns/channel-cpace/v1;sid 为信箱 id 或隧道内 channel_id
AEADChaCha20-Poly1305,nonce 0,每个密钥一条消息
隧道内帧u32 length ‖ u8 type ‖ payload,最大 1 MiB;类型 0 为 OPEN
会话对端每个节点默认最多 8 个并发;最长 24 h

签名为 RFC 8032 pure Ed25519,严格校验;被签名的消息是 ASCII 上下文紧接后续字节,没有分隔符(同 Quick 协议 v2 §12.2)。

原设计应用通道
配对 Invite / Join(ver = 2)confirm 的 Offer / Answer(ver = 1),增加 kind 字段
ns/pairing-invite/v2、ns/pairing-join/v2、ns/pairing-accept/v2ns/channel-offer/v1、ns/channel-answer/v1、ns/channel-accept/v1
ns/pairing-psk/v2ns/channel-key/v1 + ns/channel-psk/v1
文件发送 TransferHellopake 的 HELLO
ns-transfer/psk/v1、ns-transfer/confirm/v1ns/channel-psk/v1、ns/channel-confirm/v1 + 隧道内 OPEN
ns-transfer/cpace/v1ns/channel-cpace/v1
  • 第三方应用能否声明 session 对端,以及可以使用哪些 app_port(应用模式 §5)。
  • 账户模式下没有现成对端时,是否允许 pake 经 Quick relay 建立会话对端。
  • 语音、聊天的 kind、端口与是否需要 pake。