NS · 文件发送(应用)
状态:设计阶段,
ns尚未实现。 文件发送是应用模式的第一个应用(kind = "transfer"):在 NS 节点之间做一次性的、类似 croc 的文件传输。它运行在节点唯一的 nsplane 引擎之上,只使用节点已有的能力:relay 负责会合与转发,传输的每个字节都走 WireGuard 隧道。没有查找服务,不新增 relay 报文类型,也不引入第二套传输加密。已配对的两台设备使用现有隧道;未配对的两台设备建立一个只为本次传输存在的短期隧道,传输结束即删除。本页先给出节点运行在 Quick 模式时的设计;账户模式下的做法见 §9。节点协议(记录、relay 报文、隧道内控制通道)见 Quick 协议 v2。
1. 目标与非目标
Section titled “1. 目标与非目标”| 目标 | 推论 |
|---|---|
| 像 croc 一样的交互 | send 打印一张短票据;接收方确认文件清单后,receive <票据> 拉取文件 |
| 复用 NS 的 relay 与隧道 | 会合使用 ns 的应用通道与 relay 的会合信箱 /rendezvous/v1;字节始终在 WireGuard 内,经节点平常的直连或 relay 路径传输 |
| 已配对与未配对设备都能用 | 已配对设备使用现有对端;未配对设备使用仅限本次传输的对端,它从不进入允许名单 |
| relay 无法读取或改道传输 | 票据口令从不到达 relay;在安装任何隧道之前,基于口令的 PAKE 绑定双方身份键 |
| 可口述、一次性的票据 | relay 目录前缀 + 8 位数字 + 3 个单词,与 Quick 邀请短码的格式一致 |
非目标:不带票据的推送(Taildrop 方式)、在 relay 上离线投递或存储、经第三个节点的多跳转发、按目录或名字查找发送方。
ns send <path>... # 打印票据,等待一个接收方ns receive [TICKET] # 别名 recv;省略 TICKET 时在隐藏提示中粘贴- 两个命令使用正在运行的节点;节点未运行时按其当前模式启动它,权限规则与该模式的命令相同(Quick 模式见 Quick 模式 §2.5)。
receive显示发送方的 20 字符节点 ID、发送方是已配对对端时的本地昵称,以及文件清单;除非给出--yes,否则要求确认。--yes只接受文件清单,从不接纳对端。--to DIR指定目标目录(默认当前目录)。send --peers-only签发对端票据(§3.2),只有已配对的设备能兑现,且不需要 relay。--json按invite/join的方式输出按行分隔的生命周期事件(ticket_created、receiver_connected、manifest、progress、completed、failed)。ticket_created包含票据:请保密截获的 stdout。- 退出码与其他
ns命令一致:输入错误为 2,运行时失败非零(通常为 1),取消为 130。
3.1 Relay 票据(默认)
Section titled “3.1 Relay 票据(默认)”KT58307264-apple-river-cloud└┘└──────┘ └───────────────┘ │ │ 口令:从 2048 词表中随机取 3 个单词(约 33 位),每次 send 随机生成 │ 会话数字:在该 relay 上定位传输会话,与 Quick 邀请相同 relay 目录前缀(与 Quick 邀请使用同一张表)- 前缀与数字是 relay 能看到的路由信息,遵循 Quick 邀请的规则:未知前缀触发一次可信目录刷新;自定义 relay 时打印
ns receive 58307264-apple-river-cloud --relay <host>。 - 单词从不离开两端。它们作为 §5.1 PAKE 的输入,因此即使知道数字的 relay 运营方,每次尝试也最多只能在线猜一次。
3.2 对端票据(send --peers-only)
Section titled “3.2 对端票据(send --peers-only)”k7qf3m9x2p-4-apple-river-cloud└────────┘ │ └───────────────┘ 节点 ID │ 口令 前缀 会话编号(1–2 位数字,按发送方编号)- 前缀是发送方 20 字符节点 ID(Quick 协议 v2 §2.2)的前 10–12 个字符。它只用于路由:接收方在自己的对端表中查找,并尝试每个节点 ID 以该前缀开头、状态为
ready的对端。对端票据从不联系 relay 会合服务,未配对的设备无法兑现。
3.3 屏幕上的保密
Section titled “3.3 屏幕上的保密”与 Quick 短码相同:口令从不作为单独的命令行参数接受;不带 TICKET 的 receive 在隐藏提示中读取;--stdin 为自动化读取恰好一行票据。调试日志从不包含票据的任何部分。
channel(ticket) -> (sender I, receiver I, K_ch, 发送方可达信息) // 应用通道 pake:经 relay 信箱,或在已有对端的隧道内tunnel(I_peer) -> 现有 ready 对端 | 仅限本次传输的对端 // 节点的 nsplane 引擎,直连或经 relaysession(tcp, K) -> transfer // 唯一实现,始终在隧道内4.1 路径
Section titled “4.1 路径”| 情形 | 会合 | 隧道 |
|---|---|---|
Relay 票据,发送方已是接收方的 ready 对端 | 应用通道 pake,经 /rendezvous/v1 信箱 | 现有对端 |
Relay 票据,未配对(或已配对但不是 ready) | 应用通道 pake,经 /rendezvous/v1 信箱 | 仅限本次传输的对端(§4.3) |
| 对端票据 | 无(本地对端表);应用通道 pake 在隧道内运行 | 现有对端 |
“Ready” 就是 ns quick peers 已经报告的判定:数据面持有与该对端的当前 WireGuard 会话,且 transport 选定了直连路径,或选定的 relay 的确切载体是 relay 客户端的活动会话。已配对但不是 ready 的对端不视为可达;使用 relay 票据时,接收方回退到该对端的正常重连,只有 5 s 内仍未 ready 才报告 peer_unreachable(它从不为已配置的身份再安装第二个对端)。
4.2 经 relay 会合服务会合
Section titled “4.2 经 relay 会合服务会合”文件发送是应用通道上的一个应用,relay 票据的 profile 为 {kind: "transfer", auth: pake, lifetime: session, ttl: 600 s, app_port: 47901}。会合、CPace、加密的 HELLO、猜错口令的处理与安全性质见应用通道 §4、§5.2 与 §8;本节只列文件发送特有的部分:
- 创建。
send生成 3 个单词的口令,以应用通道pake打开信箱,把得到的数字与口令组成票据(§3.1),信箱最多 10 分钟。 - 兑现。
receive从票据得到 relay、数字与口令,按应用通道 §5.2 加入。第一个加入者直接被接纳,数字码随即从查找中移除:每张票据只有一个接收方。 - HELLO。 发送方的 HELLO 可以携带直连 authority(
endpoint),供接收方优先尝试直连。接收方除应用通道的校验外,还检查本地移除记录:接收方已移除的发送方身份会被拒绝,反之亦然。 - 猜错口令。 发送方解不开 HELLO 时
reject该接收方,信箱回到等待状态;第 3 次后票据作废(§7)。 - 结束会合。 双方的 HELLO 都校验通过后关闭信箱;会合服务除正常的有界过期外不保留记录。此后每个字节都经隧道传输。
会合服务能得知 kind、数字、时间、两个 WebSocket 源地址、每个信箱的临时参与方公钥以及消息的大小;它得不到口令、任何一方的设备身份键或文件元数据。通信 relay(与会合服务可能不同)看到的就是已注册的记录、slot 关系与加密报文的大小(Quick 协议 v2 §8)。
4.3 仅限本次传输的对端
Section titled “4.3 仅限本次传输的对端”对未配对的两台设备,双方各安装一个只为本次会话存在的对端:
| 属性 | 普通已配对对端 | 仅限本次传输的对端 |
|---|---|---|
| 准入 | 应用通道 confirm 配对或按节点 ID 放行 + 允许名单 | 应用通道 pake + 校验通过的 HELLO;从不加入允许名单或待批准列表;不留下移除记录 |
| WireGuard PSK | 配对派生的 PSK;按节点 ID 放行的对端没有 | 应用通道的 psk(应用通道 §4.1) |
AllowedIPs | 对端的 N6、N4 /128(同一个 /127 地址组) | 只有 N6 /128 |
| IPv4 别名、DNS 名字、PTR | 分配并发布 | 无 |
| 入站过滤(在 Quick 协议 v2 §5 的入站目的地检查之后) | 发往本机的任意流量 | 发送方:只接受来自 N6(peer)、发往 [N6(self)]:47901 的 TCP;接收方:只接受对应的返回流;另加用于端点快照与探测的隧道内控制端口 47900 |
| 生命周期 | 直到被移除 | 直到会话完成、失败或过期(自创建起 ≤ 10 分钟,加上传输持续有进展的时间,最长 24 h);两端连同其拒绝路由一起删除 |
是否出现在 peers | 是 | 只在节点状态中显示为进行中的传输(Quick 模式为 ns quick status) |
仅限本次传输的对端就是应用通道的 session 对端(应用通道 §6)。到达对端完全沿用 Quick 协议 v2:双方照常在各自的 relay 注册;接收方把 WireGuard 握手发起报文作为 DATA 发往 HELLO 中记录的 relays 里发送方的 home relay slot,发送方经接收方的 relay 返回。会话建立后,隧道内的 ENDPOINTS 与经认证的探测(v2 §9)像对待任何对端一样晋升直连路径。relay 看到的是普通 DATA,无需改动。
如果两台设备之后通过 quick invite / join 配对,先删除仅限本次传输的对端;其 PSK 从不复用。
容量:仅限本次传输的对端计入每个网络 1 024 个对端的上限,另有每个节点默认最多 8 个并发的单独上限。超过任一上限时,send 与 receive 在任何会合或隧道工作之前以容量错误失败。
5. 会话协议
Section titled “5. 会话协议”所有路径共用一个实现。接收方在隧道内向 [N6(sender)]:47901 建立 TCP。WireGuard 已经用对端的 K 加密并认证了这条流,而签名记录把 K 关联到 I,因此会话层不再增加传输加密,只把这条流绑定到本张票据。
5.1 密钥协商
Section titled “5.1 密钥协商”- Relay 票据。 CPace 已经在会合中完成(§4.2)。接收方在隧道内的第一帧是应用通道的 OPEN(
channel_id为信箱id,带K_confirm的 MAC),发送方以自己的 OPEN 应答(应用通道 §7)。MAC 中的身份必须与承载这条流的 WireGuard 对端一致。 - 对端票据。 OPEN 不带 MAC,随后在隧道内运行应用通道
pake(应用通道 §5.2),关联数据额外包含node_id_prefix ‖ session_number。接收方从承载这条流的 WireGuard 对端取得I_sender,并检查其节点 ID 以票据前缀开头。
两条路径都必须做 PAKE:隧道证明的是对端身份,而不是对端持有这张票据。
带长度前缀的帧 u32 length ‖ u8 type ‖ payload,每帧最大 1 MiB;类型 0 是应用通道的 OPEN,文件发送的帧从类型 1 开始:
- 清单(发送方):
[{path, size, mode, hash}],使用相对路径与 BLAKE3 哈希,附总大小与文件数。 - 确认(接收方):接受或拒绝;断点续传时给出每个文件在暂存区已有的字节偏移。
- 数据:文件序号、偏移、数据块。
- 校验:接收方校验每个哈希,把文件从暂存区移到目标位置并确认;双方结束会话,并删除仅限本次传输的对端。
6. 文件安全
Section titled “6. 文件安全”| 规则 | 说明 |
|---|---|
| 路径 | 相对路径、以 / 分隔并规范化;拒绝绝对路径、..、空路径段、盘符、NUL 与控制字符,以及 Windows 保留名。目录参数递归发送。 |
| 特殊文件 | 发送时跳过符号链接、设备、FIFO 与套接字,并在清单中标为已跳过;接收方从不创建它们。 |
| 上限 | 不限制文件数与总大小。接收方在确认前检查目标卷的剩余空间是否足够容纳清单总量,并在写入过程中再次检查。 |
| 暂存 | 文件先落在目标卷上的私有暂存目录(仅所有者可访问),哈希校验通过后才移到最终位置。 |
| 重名 | 从不静默覆盖已存在的目标文件:接收方会询问,或在 --yes 时写为 name (1).ext。 |
| 权限位 | 只携带可执行位;不携带所有者及其他元数据。 |
7. 安全参数
Section titled “7. 安全参数”| 主题 | 规则 |
|---|---|
| PAKE 尝试 | 一个会话允许 3 次 PAKE 失败,之后作废。约 33 位熵下,3 次猜测约 3·10⁹ 分之一的概率成功。第一次失败就作废会让一次错误猜测取消整个传输。 |
| 会合服务滥用 | 会合信箱的按源与全局限速(会合协议 §6)同样适用于 transfer 信箱;查找未知数字消耗一次尝试。 |
| 接收方尝试 | 面对多个对端票据候选时,接收方累计 3 次 PAKE 失败后停止,并提示票据可能有误。 |
| 过期 | 若无人兑现,票据在 send 后 10 分钟过期;会合服务执行同样的上限。 |
| 一次性 | 成功、失败、过期与取消时都销毁票据、PAKE 密钥、仅限本次传输的 PSK 与对端。崩溃遗留的此类对端在节点下次启动时删除。 |
| 密钥分离 | 应用通道的 K_ch 通过不同的 HKDF 标签分别派生 HELLO 的 AEAD 密钥、确认 MAC 与 WireGuard PSK,且都绑定 kind。传输用到的密钥都不是配对凭据。 |
| 身份暴露 | 与 Quick 配对不同,用户不比对完整指纹:持有票据即授权一次只读传输。receive 在确认前仍显示发送方节点 ID,以及已知对端的昵称。 |
直连路径的吞吐受 WireGuard 与主机限制。经 relay 时适用默认的 relay 转发预算:每个发送方与每个目的地 2 MiB/s,突发 4 MiB(§5.2.1)。传输期间节点持续探测直连路径,progress 事件报告当前字节是直连还是经 relay。只能经 relay 的大文件传输会比较慢;本设计不提高 relay 预算。
9. 账户模式与后续扩展
Section titled “9. 账户模式与后续扩展”- 账户模式。 节点运行在账户模式时,使用同样的会话协议经 Node IP / Node DNS 传输,受 Node L3 ACL 约束(同一 Account 的流放行,跨 Account 的流需显式 Grant);对端由 NSD 已知的 Node 身份确定,不经会合信箱。没有现成 Node 对端时的做法待设计(应用模式 §3)。
- 并行流,用于高延迟的直连路径。
- 移动端与
libns入口。
10. 需要的改动
Section titled “10. 需要的改动”| 组件 | 改动 |
|---|---|
ns | ns send / ns receive 命令;会合信箱客户端与应用通道(CPace、HELLO、OPEN、会话对端);transfer 的 profile、票据与会话协议状态机;存储投影与数据面中的”仅限本次传输”对端类型(不进允许名单、不发布别名或 DNS、入站端口过滤、有界生命周期、启动时清理);端口 47901 上的会话协议 |
| nsplane | 节点唯一的数据面引擎;会话对端的安装与移除、隧道内连接(netstack)、按来源命名空间的 ACL 与 pinhole(会话结束或过期时自动关闭) |
| relay | 实现与应用无关的 /rendezvous/v1 会合服务(会合协议);不包含任何文件发送专用逻辑 |
| Quick 协议 v2 | 在控制端口 47900 旁保留隧道内 TCP 端口 47901;把仅限本次传输的对端记为本地的、未准入的对端类别。v2 §8 的 relay 报文不变 |
11. 已有方案
Section titled “11. 已有方案”| 项目 | 相关机制 | 采纳或否定 |
|---|---|---|
| magic-wormhole | 口令短语 + SPAKE2,信箱与中转 relay | 采纳:信箱式会合,以及经不可信会合服务运行 PAKE |
| croc | 口令短语 + PAKE;自 Tailcat 起,每次传输在进程内建立 WireGuard 隧道,密钥由 PAKE 派生 | 最接近的模型:限于本次传输的 WireGuard 对端,这里装进节点唯一的 nsplane 引擎,而不是每次传输另起一个引擎 |
| iroh / sendme | 节点 ID 票据、QUIC、打洞、relay 回退 | 否定:票据长且没有 PAKE,另起一套传输栈 |
| Syncthing | 设备 ID = 证书哈希;发现服务与 relay 服务器 | 同样是身份到路由的思路;NS 不需要发现服务 |
| Tailscale Taildrop | 在隧道内向 PeerAPI 做 HTTP PUT;仅限同一用户的设备 | 采纳暂存目录;否定推送模式 |
| OnionShare | 通过 Tor onion 服务分享 | 仅在需要更强匿名性时参考 |
croc Tailcat(2026-09 核对源码):croc vendor 了 tailscale/tailcat,每次传输构建一个带 gVisor netstack 的用户态 WireGuard 引擎;双方节点密钥由 PAKE 密钥派生,端点通过 DERP 以 disco CallMeMaybe 交换,传输后撤销接收方密钥(https://github.com/schollz/croc,internal/tailcat/)。
12. 待决事项
Section titled “12. 待决事项”- 词表:BIP39 英文词表,还是更短、更易口述的词表再加第四个单词。
- 节点归属于其他用户时,
receive是否也应可用(目前不可,与 Quick 模式的命令一致)。 - 已配对但不是
ready的对端,是否允许在旁边再建一个仅限本次传输的对端,而不是以peer_unreachable失败。 - 24 小时进展上限。