跳转到内容

NS · Quick 模式

状态:v1,已被取代。 自 2026-10-04 起 ns 只实现 Quick 协议 v2(分支 refactor/nsplane),v1 的 quick-runtime、配对令牌、/quick/v1 会合与 quick.internal 名字已从 ns 删除;本页描述的是 ns main a05b6f55 与 nsgw main 3d31b5f 上的 v1 实现,其中 relay 侧(nsgw-quick-relay)至今仍是 v1,v2 服务端是迁移里程碑 M2。v2 的命令见 Quick 协议 v2 §13。

原文: Quick 模式是不需要账户的 NS 使用方式:两台设备用一次性短码配对,之后直接通过 WireGuard 通信。本页规定 ns quick 命令、relay 上的 /quick/v1 配对会合、relay 目录与短码、节点如何选择 relay、平台集成,以及运行与运维规则。底层协议(身份与地址、配对令牌、relay 报文、隧道内记录)见 Quick 协议,其不兼容的下一版见 Quick 协议 v2;本页不新增任何 relay 报文或隧道内记录。

方面Quick 模式
权威每台设备自己:身份键 I、准入策略、允许与移除记录。没有账户、NSD 或登记表。
配对一次性短码在 relay 上定位邀请;双方核对对方设备的完整身份指纹;随后邀请方通过加密投递把一个限定接收方的 PairingToken(Quick 协议 §7.4)交给加入方,加入方按普通配对流程(§7.5)导入。
配对范围只授权这一对设备。A–B 与 A–C 配对不让 B 与 C 互通。
数据路径两台设备之间的 WireGuard,能直连就直连,否则经 relay 转发;relay 只在 WireGuard 之下转发密文。
上下文所有安装都使用固定的公开默认上下文(Quick 协议 §2.7);用户不创建也不选择网络。
能力设备之间 L3:身份 IPv6 地址、节点本地 IPv4 别名、quick.internal 下的名字。不发布 Service、不做子网路由、不提供出口。
与账户模式的关系同一个 ns 二进制,按状态目录互斥。任一模式都不会停止或重置另一种;启动 Quick 模式前必须先断开账户模式。
命令作用
ns quick up [RELAY 选项] [--detach | --watch | --foreground]确保节点在运行。默认(--detach)启动后台 worker,本地就绪后返回(最多 30 s)。--watch 保持一个状态视图;中断视图不会停止节点。--foreground 在前台监管一个新 worker,供容器与服务管理器使用,收到 SIGTERM、中断或挂断时停止它;它从不接管已在运行的 worker。
ns quick down停止节点,保留身份、配对与配置(§6.4)。
ns quick status本机身份与节点 ID、生效的 relay 列表及其来源、已注册的 relay、已连接的对端。缺失的观测报告为未知,而不是零。
ns quick invite [RELAY 选项] [--recipient 指纹] [--ttl 时长] [--wait 秒] [--name 文本] [--qr]创建邀请并确认加入的设备(§3.5)。
ns quick join [CODE | --stdin] [--peer 指纹] [RELAY 选项] [--wait 秒]使用邀请加入(§3.5)。不给 CODE 也不用 --stdin 时,在隐藏输入提示中读取。
ns quick peers已配置的对端及其流量实际走的路径。
ns quick peer show | name | remove PEER [昵称]查看对端、设置本地昵称,或移除对端(持久的本地阻断)。
ns quick policy set --join auto|confirm|recipient-only可复用令牌的准入策略(Quick 协议 §13.3)。Quick 邀请总是使用限定接收方的令牌,任何策略都会接纳。
ns quick pending list | approve ID | deny ID签发方的待批准队列(Quick 协议 §13.3)。
ns quick allow list | remove PEER | restore PEER准入与移除记录。
ns quick upgrade [--yes] | --restore BACKUP --yes转换早期版本按设备划分的网络状态(§6.4)。

所有命令都接受 --json 与 --config FILE。

以下底层命令不在帮助中显示,供测试与自动化使用:token issue|list|revoke、pair(及其 status、cancel)、peer alias、peer binding import、key rotate、key binding export、aliases export|import、bridge add|list|remove、recovery set-root 与 recover。它们的行为见 Quick 协议 §13.2;aliases、recovery 与 recover 仍返回 feature_deferred。

up、invite、join(以及隐藏的 pair)接受:

选项含义
--relay RELAY选择并记住恰好一个 relay 或 relay 区域条目(§5)。重复给出或用逗号分隔的值会被拒绝。只写 DNS 主机名时使用端口 51822。
--reset-relays选择并记住内置列表。与 --relay 冲突。
--switch-relay允许为改变 relay 选择而重启正在运行的节点。它与 --yes 分开,--yes 从不授权重启。

PEER 接受完整的 64 字符身份键、20 字符节点 ID(Quick 协议 §12.4 的身份标签)、至少 8 个字符且唯一的节点 ID 前缀,或唯一的本地昵称。前缀或昵称有歧义时报错。批准与确认始终显示并绑定完整密钥;昵称或前缀只用于选择。

昵称由小写字母、数字和连字符组成,大写输入会转为小写。交互式配对成功后,invite 与 join 会提示设置昵称;跳过则保留身份标签。配对日期记录在本地,用于显示。

  • 首次启动需要 TUN 权限:Linux 需要 CAP_NET_ADMIN 与 iproute2(由拥有状态目录的登录用户使用 sudo);macOS 需要 root 才能创建 utun;Windows 需要管理员终端,并在 ns.exe 旁放置 wintun.dll。NS 从不弹出 UAC 提示或启动提权助手;需要启动节点的命令会退出并给出操作说明。
  • 节点运行后,同一用户可在普通终端通过经所有者认证的 IPC 控制它(status、peers、down、invite、join)。管理员权限不等于归属:从不接管或停止其他用户的节点。
  • 隐式配置是状态目录中的 quick.toml(Linux 上为 ~/.nsio),可以不存在。--config FILE 必须存在且仅所有者可访问(Unix 上 chmod 600;Windows 上使用私有 ACL);格式错误或不安全的文件是错误,从不静默回退。

本节描述现有实现。会合协议 将以与应用无关的 /rendezvous/v1 会合服务取代它,配对改为应用通道上的应用(Quick 协议 v2 §10.2),不保留 v1 兼容。

  • 邀请方:创建邀请,确认加入方,签发并封装凭据。
  • 加入方:查找邀请,确认邀请方,打开凭据并导入。
  • 会合服务(broker):relay 上的 /quick/v1 服务。它在有限时间内保存公开的身份证明与密文,不能批准对端、读取凭据或派生连接密钥。承担会合的 relay 与节点注册通信的 relay 是相互独立的角色。
  • 在 relay 现有 TLS 监听上的 WebSocket:wss://{authority}/quick/v1,子协议 ns-quick-v1,与 /keynet/v1 数据载体分开。
  • TLS 按所拨的 authority 校验;对已配置的 relay 使用该条目的 trust store,否则使用系统根证书。邀请从不安装 CA,也从不放宽校验。
  • 每个 WebSocket 消息承载一条 JSON 消息,最大 16 KiB,在解析前检查。客户端连接超时 8 s、每次响应超时 8 s,每 0.7 s 轮询一次。
  • 连接建立后,broker 先发送 Challenge {nonce, authority}。nonce 是不全为零的 32 字节随机值;authority 必须等于客户端期望的身份(relay authority,或 §5.4 的区域身份)。不一致时断开该连接。

请求格式为 {"operation": <名称>, "value": <载荷>},响应格式为 {"result": <名称>, "value": <载荷>};名称使用 snake_case,未知字段一律拒绝。

请求载荷发送方用途
directory—任意获取 relay 目录(§4)
createProof邀请方创建邀请
lookup数字码加入方查找邀请;以 offer 应答
joinProof加入方成为候选
resumeProof任一方连接丢失后以同一身份重新认证
poll—任一方读取会话 state
select身份邀请方选中一个候选
reject身份邀请方拒绝一个候选
deliverSealedToken邀请方交付封装后的凭据
cancel—任一方撤回(§3.7)
complete—加入方报告本地已就绪
响应载荷
challenge{nonce, authority}
directoryDirectory
created{digits, expires};expires 必须等于邀请方 Hello.expires
offer邀请方的 Proof
stateObservation {stage: waiting|selected|delivered|completed, candidates: Proof[], selected: Proof?, sealed: SealedToken?}
ok—
errorinvalid_code、unknown_relay、invalid_directory、invalid_message、invalid_proof、unavailable、not_selected、capacity、rate_limited、conflict、randomness、encryption、storage 之一

错误中从不包含短码、令牌、密钥或其他不可信输入。relay 返回的错误是最终结果;只有传输故障才会转到区域的下一个成员(§5.4)。

Hello {
version = 1
session 16 字节随机值,非零;由邀请方选定,加入方照抄
role invite | join
identity 32 字节 Ed25519 身份键 I
ephemeral 32 字节 X25519 公钥,每次尝试新生成
authority relay 或区域身份,host:port
expires Unix 秒;now < expires <= now + 900
}
Proof { hello, challenge(32 字节), signature }
signature = Ed25519_I( "ns-quick-identity-v1\0" || challenge || JSON(hello) )

签名由节点运行时经所有者认证的 IPC 完成;身份私钥从不进入 CLI 进程。校验使用严格模式的 Ed25519。一个 Hello 在整个会话中固定不变,因此以新挑战重新连接也不能更换密钥、接收方或过期时间。设备不能与自己配对。

  1. 创建。 必要时启动邀请方节点。会合服务取当前已注册的第一个已配置 relay,没有则取第一个已配置 relay。邀请方发送 create;broker 返回 8 位随机数字(在 00000000–99999999 中均匀选取,冲突由 broker 重试)与过期时间。
  2. 显示。 当 broker 是目录中的 relay 时,短码为目录前缀加数字(KT58307264);否则为 8 位数字加显式 relay(ns quick join 58307264 --relay host)。--qr 显示短码;自定义 relay 时显示完整的加入命令。
  3. 查找。 加入方解析短码(§4.2)、确定 relay 并发送 lookup。它校验 offer:证明有效、角色为 invite、authority 等于确定的 relay 身份;使用 --peer 时还要求等于固定的身份。
  4. 加入。 必要时启动加入方节点(不重启正在运行的节点,也不触碰账户模式),加入方用自己的 Hello(session、authority、expires 相同)发送 join。
  5. 确认。 双方通过可信渠道比对对方设备显示的完整指纹并回答 y。非交互使用必须固定对方身份:invite --recipient 指纹、join --peer 指纹。持有短码、显示名或 --yes 都不能确认身份。
  6. 选择。 邀请方对拒绝的候选发送 reject 并继续等待,对确认的候选发送 select。选中后数字码从查找中移除,其他候选被丢弃。
  7. 签发与封装。 至少还要剩 5 s。邀请方运行时为加入方的 I 签发限定接收方的 PairingToken,not_after 不晚于邀请过期时间减 5 s,然后封装(§3.6)并发送 deliver。若没有已注册的通信 relay,返回 relay_unavailable,邀请失败。
  8. 打开与导入。 加入方等待 state.selected 等于自己的 Hello 且出现 sealed,打开后检查令牌的签发方是邀请方、接收方是自己、not_after 不超过邀请过期时间。随后以令牌的 home relay 启动或复用节点,通过隐藏的 pair 操作导入令牌(Quick 协议 §7.5)。
  9. 完成。 加入方配对达到 ready 后发送 complete。邀请方等到阶段 completed,再确认本地已接纳这个确切身份。以本地就绪为准:complete 丢失从不撤销已建立的配对。
  10. 清理。 邀请方事后总是撤销临时令牌,这会停止后续准入但保留已接纳的对端(Quick 协议 §7.4)。未完成的流程发送 cancel;加入方会取消未完成的本地配对,但从不取消已就绪的配对。

--wait(默认值与最大值均为 900 s)从第一次连接 relay 起限制整条命令;--ttl(1 s 到 15 min)限制邀请本身。Ctrl-C 取消并以 130 退出。

shared = X25519(ephemeral_inviter, ephemeral_joiner) // 必须是 contributory
key = HKDF-SHA256(salt = "ns-quick-token-v1", ikm = shared, info = JSON([inviter_hello, joiner_hello]))
sealed = XChaCha20-Poly1305(key, nonce = 24 字节随机值, aad = JSON([inviter_hello, joiner_hello]), token)

封装要求角色为 invite / join、session、authority 与 expires 相同、身份不同。令牌最大 2 048 字节。broker 只持有两份公开证明与密文,从不接触令牌或任一方的临时私钥。这是绑定了双方签名身份的认证密钥交换,而不是 PAKE:认证依赖用户比对指纹,而不是数字码。

事件broker 的处理
选中接收方移除查找码与其他候选
拒绝移除该请求;保留有界的拒绝标记直到过期
邀请方或被选中的加入方取消移除会话
未被选中的候选取消只移除它自己的请求
加入方完成移除密文;保留幂等回执最多 30 s
邀请方观察到完成移除回执
传输中断同一签名身份可在剩余租期内 resume
邀请方或被选中的加入方 180 s 无活动移除会话(每 10 s 检查一次)
绝对过期10 s 内移除;生命周期从不超过 15 分钟,重启也不会延长

查找与未被选中的候选不能让被放弃的会话继续存活。会合状态保存在私有存储中,与 relay 的永久安全标记分开;丢失它从不影响数据载体。

上限值
保存的邀请10 000
每个身份的有效邀请3
每个邀请的候选4
被拒绝的身份16
消息大小16 KiB
查找每个源 IP 每分钟 12 次,全局每秒 100 次
创建每个源 IP 每分钟 30 次,全局每秒 100 次;重试同一个有效邀请不计次
每个连接的请求每秒 10 次

被拒绝的猜测从不让其他邀请失效。保存邀请的上限并不代表并发能力;relay 的连接与组帧内存预算同样适用。

Directory { version u64 >= 1, relays: RelayEntry[] } // 最多 576 条
RelayEntry { id u16 != 0, code: 两个字母, authority: host:port }
  • 短码字母取自 ABCDEFGHJKLMNPQRSTUVWXYZ(不含 I 与 O)。id、code、authority 各自唯一。
  • id 及其 code 永久不变:更新可以增加条目或修改 authority,但从不删除 id、重新分配短码或复用已退役的短码。任何变更都需要更高的版本;同一版本必须完全相同。
  • 内置版本 1:KT(id 1)hkg-a1.43.159.53.106.sslip.io:51822、KM(id 2)hkg-a2.124.156.159.181.sslip.io:51822、SG(id 3)sin-a1.43.133.56.69.sslip.io:51822。这些是协议分配,不代表部署状态。
  • 未知前缀会触发一次刷新:通过校验过的 TLS 向第一个内置 relay 发送 directory 请求,只有当结果是内置目录的合法更新时才接受。刷新从不是邀请搜索。
输入含义
两个字母 + 8 位数字(KT58307264,不区分大小写)按前缀定位目录中的 relay;与之冲突的 --relay 会被拒绝
8 位数字PIN;必须恰好配一个显式 --relay,从不使用记住的或默认的 relay

忽略首尾空白。前缀是公开的路由信息,不提供熵;数字只用于定位,从不用于授权。

不使用 --config使用 --config FILE
1. 本次命令的 --relay / --reset-relays1. 本次命令的 --relay / --reset-relays
2. 保存的偏好2. 该文件(即使只写了部分内容也以它为准)
3. 隐式的 quick.toml3. 内置列表
4. 内置列表

成功的显式选择保存在状态目录专属的私有偏好中,从不改写 quick.toml、身份或配对。如果启动后保存失败,命令会报告失败,而不是声称已保存。

按顺序使用目录中的三台 relay。DNS 只在启动时解析,每次最多 5 s;解析失败的条目会被报告,并在本次运行中省略,全部失败则报错。默认 UDP 绑定为 IPv4;仅有 IPv6 的主机需设置 transport.udp_bind。只有内置列表能同时注册到多台 relay 并在其间回退。

自定义 relay 列表恰好一个条目:一台 relay 或一个 relay 区域。条目更多的列表在启动时被拒绝,除非它与内置列表相同。已知的 relay 短 ID 保留其显式的 UDP / WSS / 信任映射;新的 authority 对该 authority 使用系统根证书校验 TLS。WSS 回退始终是 wss://{trusted_authority}/keynet/v1。

区域条目写明区域身份,并按就近顺序列出 1 到 8 个成员:

[[transport.relays]]
identifier = "region.example:51822"
[[transport.relays.members]]
udp = "m1.example:51822"
trusted_authority = "m1.example:51822"
[[transport.relays.members]]
udp = "m2.example:51822"
trusted_authority = "m2.example:51822"
trust_store = "/var/lib/ns/quick-node/region-roots.pem"
  • 区域条目没有顶层的 udp、wss、trusted_authority 或 trust_store。每个成员的 authority 是规范形式,在条目内唯一,其 UDP 端口等于 authority 的端口。标识不必与任何成员相同。配置 schema 版本为 2。
  • 节点维护一个成员游标。一轮注册只发给当前成员;其 WSS 回退以该成员的 trust store 拨号该成员。一轮注册过期而未确认、会话到达存活期限或漏掉两次 presence 确认、以及 WSS 连接、注册或载体失败,都会让游标按正常退避移到下一个成员,到末尾后回到开头。已确认的成员在健康期间保持为当前成员。
  • REGISTER、令牌与公布的 relay 列表都携带区域身份(Quick 协议 §12.8);配对介绍发往节点当前注册的成员。
  • 通过区域进行 Quick 配对要求区域身份为 host:port。Hello.authority 与期望的挑战 authority 都是区域身份;每个连接发往 wss://{成员 authority}/quick/v1,按该成员的 TLS 信任校验。只在传输故障时按顺序尝试成员,每次尝试最多遍历一轮。加入方的连接与查找共用一轮;重新连接时在任一应答的成员上恢复。
  • 在 relay 侧,一个区域在每个 owner epoch 内只有一个会合服务 owner;其他成员经区域控制链路把会合事务转发给它。owner 的选定、栅栏与重载由 NSGW 的 quick pairing 设计规定。

不带 relay 选项的命令原样复用正在运行的节点;修改文件只在下次启动时生效。改变有序的 relay 映射需要重启:交互时命令显示当前与请求的 relay 并询问;非交互时需要 --switch-relay。选择相同则无需同意。替换启动失败时,尽可能恢复先前的配置并报告结果。已签发的令牌保持原来的 home relay。

平台TUN本地 DNS
Linux内核 TUN(默认名 quick0),配合 iproute2在节点所在网络命名空间监听 127.0.0.54:53;默认与主机 DNS 接管一起关闭,因为可能与 systemd-resolved 冲突;开启后适用 Quick 协议 §7.8
macOS原生 utun默认开启:在 /private/etc/resolver 下按 zone 写入文件,只在 root 拥有且受保护的目录中创建,从不跟随链接,down 时撤回
WindowsWintun(与架构匹配的 wintun.dll)默认开启:一条带标记的 NRPT 规则把 quick 命名空间指向 127.0.0.54;与之冲突的有效域策略会被报告,不会被覆盖

只有 quick 自己的命名空间路由到本地解析器;从不替换全局 DNS,私有名字从不回落公网 DNS。修改前先校验已有资源;冲突或已被替换的资源不会被接管。从不触碰账户模式的解析器资源。

  • 只要数据面持有与对端的当前 WireGuard 会话,且 transport 选定了直连路径,或选定的 relay 的确切载体是 relay 客户端的活动会话,对端即为 ready。relay 发送使用同一判定,因此 peers 报告的是流量实际走的路径。停止应答的 relay 成员会一直被报告,直到节点在别处完成注册。
  • 直连路径遵循 Quick 协议 §12.7:探测回复只有来自确切元组时才能晋升;选定的路径在收到任意来源、与 nonce 相关联的健康回复时保持存活;连续三次挑战无应答则退回 relay。
  • 已保存的对端不保证可达;配对完成与实时可达是两件不同的事。
上限值
每个网络的对端1 024,即 transport 的容量;超出的配对被拒绝(Quick 协议 §1.1 的预算是设计上限)
TUN MTU至少 1 376,即直连路径必须承载的数据面路径上限
队列上限每个都必须至少能容纳一个报文;更小的值在启动时被拒绝

只有致命情况才会停止节点:存储 I/O 失败或损坏、内部不变量被破坏、受监管的任务或内部通道死亡、运行时配置错误,或已记录的锁存条件(本地 relay 会话持久化失败或提供方失败)。其余情况都是本地背压或计数丢弃:

  • transport 数据只把网络的 transport 队列填到一个上限,为一个对端最大的控制突发留出空间;其余数据在有界的数据面队列中等待,并抑制 TUN 读取。控制记录与握手可以使用整个队列。
  • 放不下的控制工作、relay 输出与配对介绍等待下一个 tick;回复放不下的控制记录在任何状态变更之前被拒绝,并在发送方重试时应用。
  • 仍找不到空间的报文被丢弃,由其协议的定时器恢复。对端或网络造成的失败(不可达路径、无法解析的配对端点、伪造的源地址、失败的桥接流、低阶 WireGuard 密钥)只影响该报文、路径或流。

丢弃按对端计入节点诊断中的 output_drops 与 input_drops。关闭时执行每一个清理步骤,并在全部执行后报告第一个失败。

  • 持久状态:<state-dir>/quick(身份、配对、别名)。down 会保留它。
  • 日志:按 NS_LOG_LEVEL 输出到 stderr,同时写入 <state-dir>/quick-runtime.log(权限 0600,达到 4 MiB 时轮转为一个 .1 文件;不安全的文件会被拒绝,此时继续运行但不写文件日志)。不含秘密的路径诊断:<state-dir>/quick-node-diagnostics.json,正常关闭时删除。
  • 运行时归属按网络命名空间划分;两个 Quick 节点不能共用一个命名空间。
  • 在 macOS 与 Windows 上,只有运行时已停止、且主机路由与 split DNS 已验证恢复,停止才算成功(上限 60 s;Linux 为 10 s)。否则 down 失败,并保留受保护的清理记录,下次 up 会先恢复它。
  • 早期版本按设备划分的网络状态会被 up 识别并报告 upgrade_required。节点运行时 upgrade --yes 会拒绝执行;它写入并校验一个私有备份(从不覆盖已有备份),保留 I,清除旧关系并安装默认上下文;对端需要重新配对,地址或名字可能变化。upgrade --restore BACKUP --yes 只恢复同一设备的备份,供旧版二进制使用。

NS Docker 镜像用各自的 Compose 模板运行两种模式。Quick 模式使用 ns quick up --foreground;它只监管自己启动的 worker,转发诊断,并在终止时等待有界清理。正式容器(NS_CONTAINER=1)在 Quick 模式运行期间持有账户模式租约作为互斥栅栏。ns health 在两种模式下都检查本地归属;--ready 还会检查运行时的 IPC。就绪从不要求对端可达。保留状态卷即可保留身份与配对。

invite --json 与 join --json 向 stdout 写入按行分隔的事件对象:

{"schema_version":1,"operation":"quick.pairing","stage":"invitation_created","message":"…","details":{…}}
阶段详情
invitation_createdcode、relay、join_command、expires、identity(包含短码:请保密 stdout)
confirmation_requiredidentity(本机)、peer
request_rejected—
connecting—
receipt_pending—(配对已完成;broker 回执会自动过期)
cleanup_pending—(relay 或本地清理未确认;有界过期仍然生效)
complete—
failed输入错误时带 code:invalid_arguments、confirmation_required
cancelled—

退出码:0 表示完成;2 表示输入错误,此时尚未创建任何运行时状态;130 表示取消;其他情况非零,通常为 1。JSON 模式下失败不向 stderr 写入人类可读的错误。流程开始前的参数解析错误保持带版本的命令错误信封。人类可读的进度始终写入 stderr。

其他命令使用 Quick 协议 §13.5 的单对象信封,schema_version:2。status 还会报告实际运行的 relay 配置(含各条目的 members 及其来源)与 connectivity.registered_relays。

  • 数字码与前缀只是定位符。授权只来自用户对完整指纹的确认(或固定的密钥),以及随后 Quick 协议对限定接收方令牌的准入。
  • broker 能看到邀请时间、源地址、双方公开的身份证明与密文。它不能伪造证明(每份证明都由 I 对一个写明 relay 的新挑战签名),不能在选中后替换候选(封装密钥绑定了双方的 hello),也不能读取凭据。
  • 恶意的 broker 可以拒绝服务,或向用户展示错误的候选;防线是指纹比对。不要把它弱化为只比对名字。
  • 短码从不出现在日志、调试输出、错误或命令行参数可见的状态中,除非用户自己输入;join 通过隐藏提示或 --stdin 读取短码。
  • Windows 运行时与安装器迁移的原生验收。
  • 开放隐藏的 bridge、别名快照与身份恢复命令。
  • 支持多于一个自定义 relay 条目。
  • 移动端与 libns 入口。
  • 文件发送不属于 Quick 模式,而是应用模式的应用(文件发送,已设计,未实现)。