跳转到内容

NS · 文件发送(应用)

状态:文件协议 v1 已接入 CLI,本轮范围已完成,延期验收另列(2026-10-25)。 文件发送是应用模式的第一个应用(kind = "transfer"):在 NS 节点之间做一次性的、类似 croc 的文件传输。它运行在节点唯一的 nsplane 引擎之上,只使用节点已有的能力:relay 负责会合与转发,传输的每个字节都走 WireGuard 隧道。没有查找服务,不新增 relay 报文类型,也不引入第二套传输加密。已配对的两台设备使用现有隧道;未配对的两台设备建立一个只为本次传输存在的短期隧道,传输结束即删除。本页先给出节点运行在 Quick 模式时的设计;账户模式下的做法见 §9。节点协议(记录、relay 报文、隧道内控制通道)见 Quick 协议 v2。

文件传输已实现已有对端、公共及自定义 relay、NSD 精确来源、多输入目录、安全发布与新授权续传。公共目录共享前缀和未知前缀的一次可信刷新已接入;Linux/macOS 原生验收与 A/B 完整质量门按各自固定候选记录。后续集成候选 6e214817d 的 1,494 项 macOS 回归及四组 E2E(Quick 多文件、NSD 新授权续传、同 Account 双 Network 正反对照、公共前缀票据收发)通过,不将较早候选的全量 QA 外推为本轮重新执行。精确源码、制品摘要和覆盖范围见文件传输计划及验收记录。本轮范围完成不等于全部平台已发布。

C05 Account A→B→A 按用户 2026-10-25 决定延期,移出本轮必需验收与合并门,仅在用户通知后恢复;仍未验证,不计为通过。Windows 原生文件传输验收继续阻断首次包含此功能的 Windows 发布。内置公共目录迁移到 *.relay.ns.io 仍未完成,允许当前部署差异;跟踪与关闭条件见会合协议 §5.4。

目标推论
像 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 时在隐藏提示中粘贴
  • --network <selector> 选择 quick 或精确 nsd:<uuid>/<network>;两个命令使用同一个节点 daemon,并按应用模式 §3.1选择精确来源:仅恰好一个已连接 membership 时可省略,多来源必须明确。启动 daemon 仍遵守已有权限规则,但不自动注册 NSD 或加入 Quick,也不按“当前模式”、ready 状态或上次使用选择来源。
  • 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。清理失败和完成未确认必须附带结构化原因,不能输出 completed。
  • 未配对 Quick 路径要求双方各自显式给出 --allow-unknown-peer;它仅批准本次来源、本次票据认证成功的一个临时 peer,不写长期允许名单。默认只允许本来源已授权的 peer;NSD 或 --peers-only 与此选项组合报输入错误。持有票据、--yes 均不隐含本地引入权限。
  • receive --resume <checkpoint> [TICKET] 使用本用户的私有 checkpoint,但仍须输入新票据、重新授权并确认清单。--to 如提供必须与 checkpoint 的目标目录身份一致,否则失败;省略时使用 checkpoint 中经安全重新打开并核实的目标目录。
  • receive --discard <checkpoint> 仅离线清理本用户、本次事务尚未发布的暂存与元数据,不需要 daemon,不联网;与票据、--stdin、--resume、--to、--yes、--relay、--network 和 --allow-unknown-peer 互斥,不删除已发布文件。无法证明所有权时拒绝清理。
KT58307264-apple-river-cloud
└┘└──────┘ └───────────────┘
│ │ 口令:从 BIP39 英文 2048 词表随机取 3 个单词(约 33 位)
│ 会话数字:在该 relay 上定位传输会话,与 Quick 邀请相同
relay 目录前缀(与 Quick 邀请使用同一张表)
  • 前缀与数字是 relay 能看到的路由信息,遵循 Quick 邀请的规则:未知前缀触发一次可信目录刷新;自定义 relay 时打印 ns receive 58307264-apple-river-cloud --relay <host>。
  • 固定使用 BIP39 英文词表的原始索引顺序,逐词独立、均匀、有放回地用 CSPRNG 选取;允许同词重复。这是口令取样,不使用助记词校验和或种子派生。
  • PRS 是三个小写 ASCII 单词以单个 ASCII - 连接后的精确 bytes,不含前缀/数字。不 trim、不改大小写、不接受同义词或 Unicode 近似字符。自动化 --stdin 只移除一个末尾 LF 或 CRLF,第二行或其它额外字符拒绝;通道本身仍不解释词表。
  • 单词从不离开两端。它们作为 §5.1 PAKE 的输入,因此即使知道数字的 relay 运营方,每次尝试也最多只能在线猜一次。
k7qf3m9x2p-4-apple-river-cloud
└────────┘ │ └───────────────┘
节点 ID │ 口令
前缀 会话编号(1–2 位数字,按发送方编号)
  • 前缀是发送方 20 字符节点 ID(Quick 协议 v2 §2.2)的前 10–12 个字符。它只用于路由:接收方只在精确选定来源及本地授权允许的对端中,查找节点 ID 以该前缀开头、状态为 ready 的候选;不从其它 Network 或 Quick 补充候选。对端票据从不联系 relay 会合服务,Quick 下未配对的设备无法兑现。

与 Quick 短码相同:口令从不作为单独的命令行参数接受;不带 TICKET 的 receive 在隐藏提示中读取;--stdin 为自动化读取恰好一行票据。调试日志从不包含票据的任何部分。

channel(ticket) -> (sender I, receiver I, K_ch, 发送方可达信息) // 应用通道 pake:经 relay 信箱,或在已有对端的隧道内
tunnel(I_peer) -> 现有 ready 对端 | 仅限本次传输的对端 // 节点的 nsplane 引擎,直连或经 relay
session(tcp, K) -> transfer // 唯一实现,始终在隧道内
情形会合隧道
Relay 票据,发送方已是接收方的 ready 对端应用通道 pake,经 /rendezvous/v1 信箱现有对端
Relay 票据,Quick 已配对但不是 ready应用通道 pake,经 /rendezvous/v1 信箱现有对端的正常重连;不安装临时替代对端
Relay 票据,Quick 未配对应用通道 pake,经 /rendezvous/v1 信箱仅限本次传输的对端(§4.3),还须获得引入未知 peer 的授权
对端票据无(本地对端表);应用通道 pake 在隧道内运行现有对端

以上路径均受精确来源与本地 grant 限制;relay 票据不能使 NSD 缺少投影 peer 的来源回退 Quick。复用现有对端时不替换其 PSK;会话结束只撤销本次授权与 flow,不删除共享 peer。

“Ready” 就是 ns quick peers 已经报告的判定:数据面持有与该对端的当前 WireGuard 会话,且 transport 选定了直连路径,或选定的 relay 的确切载体是 relay 客户端的活动会话。已配对但不是 ready 的对端不视为可达;使用 relay 票据时,接收方回退到该对端的正常重连,只有 5 s 内仍未 ready 才报告 peer_unreachable(它从不为已配置的身份再安装第二个对端)。

文件发送是应用通道上的一个应用,relay 票据的 profile 为 {kind: "transfer", auth: pake, lifetime: session, ttl: 600 s, app_port: 47901}。relay 路径的 app_context 固定为空 bytes;其路由绑定来自已核对的 RelayId、mailbox_id 与 kind,不把文件清单、口令或本机 selector 放入公开 transcript。会合、CPace、加密的 HELLO、猜错口令的处理与安全性质见应用通道 §4、§5.2 与 §8;本节只列文件发送特有的部分:

  1. 创建。 send 生成 3 个单词的口令,以应用通道 pake 打开信箱,把得到的数字与口令组成票据(§3.1),信箱最多 10 分钟。
  2. 兑现。 receive 从票据得到 relay、数字与口令,按应用通道 §5.2 加入。第一个加入者直接被接纳,数字码随即从查找中移除:每张票据只有一个接收方。
  3. HELLO。 发送方的 HELLO 可以携带直连 authority(endpoint),供接收方优先尝试直连。接收方除应用通道的校验外,还检查本地移除记录:接收方已移除的发送方身份会被拒绝,反之亦然。
  4. 猜错口令。 发送方解不开 HELLO 时 reject 该接收方,信箱回到等待状态;第 3 次后票据作废(§7)。
  5. 结束会合。 遵循 PAKE 精确 profile 的 HELLO 校验、FINISH 确认和本地完成条件,再按规定顺序安装/接纳、排空 relay 消息并关闭信箱。会合服务除正常的有界过期外不保留记录。此后每个字节都经隧道传输。

会合服务能得知 kind、数字、时间、两个 WebSocket 源地址、每个信箱的临时参与方公钥以及消息的大小;它得不到口令、任何一方的设备身份键或文件元数据。通信 relay(与会合服务可能不同)看到的就是已注册的记录、slot 关系与加密报文的大小(Quick 协议 v2 §8)。

对未配对的两台设备,双方各安装一个只为本次会话存在的对端:

属性普通已配对对端仅限本次传输的对端
准入应用通道 confirm 配对或按节点 ID 放行 + 允许名单应用通道 pake 完成 HELLO 校验与 FINISH 确认,满足 PAKE 精确 profile 的本地完成条件;从不加入允许名单或待批准列表;不留下移除记录
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);先关闭本次接纳、撤销 flow 并等待退休回执,再释放本次临时对端及其拒绝路由
是否出现在 peers是只在节点状态中显示为进行中的传输(Quick 模式为 ns quick status)

仅限本次传输的对端就是应用通道的 session 对端(应用通道 §6)。到达对端完全沿用 Quick 协议 v2:双方照常在各自的 relay 注册;接收方把 WireGuard 握手发起报文作为 DATA 发往 HELLO 中记录的 relays 里发送方的 home relay slot,发送方经接收方的 relay 返回。会话建立后,隧道内的 ENDPOINTS 与经认证的探测(v2 §9)像对待任何对端一样晋升直连路径。relay 看到的是普通 DATA,无需改动。

来源离开、身份替换、撤权或进程租约失效也触发本次会话清理,传输进展不能延续已撤销的授权。清理失败必须报告,不能提前报告取消完成;其它会话的 flow 与共享 peer 保持不变,重新授权不复活旧句柄。

如果两台设备之后通过 quick invite / join 配对,先删除仅限本次传输的对端;其 PSK 从不复用。

容量:仅限本次传输的对端计入每个网络 1 024 个对端的上限,另有每个节点默认最多 8 个并发的单独上限。超过任一上限时,send 与 receive 在任何会合或隧道工作之前以容量错误失败。

所有路径共用一个实现。接收方在隧道内向 [N6(sender)]:47901 建立 TCP。WireGuard 已经用对端的 K 加密并认证了这条流,而签名记录把 K 关联到 I,因此会话层不再增加传输加密,只把这条流绑定到本张票据。

  • Relay 票据。 CPace 已经在会合中完成(§4.2)。接收方在隧道内的第一帧是应用通道的 OPEN(channel_id 为信箱 id,带 K_confirm 的 MAC),发送方以自己的 OPEN 应答(应用通道 §7)。MAC 中的身份必须与承载这条流的 WireGuard 对端一致。
  • 对端票据。 OPEN 不带 MAC;双向 OPEN 后,接收方发送 LOCATE 定位邀请,再在隧道内运行应用通道 pake(应用通道 §5.2)。app_context = CBOR([1, node_id_prefix text, session_number text]),使用规范 CBOR;前缀与 1–2 位 ASCII 数字均取票据原文,不截短前缀、不把编号转换为整数(如 4 与 04 不等同)。LOCATE 的 invitation_locator 就是这些 bytes,发送方按已登记邀请精确匹配并在 PARAMS 中返回;接收方必须再次核对。并发活动邀请遵循精确 profile 的上下文唯一性规则。此绑定进入 C/th,不修改 CPace 的 ADa/ADb;接收方还须从承载流的 WireGuard 对端取得 I_sender,检查其节点 ID 以票据前缀开头。

两条路径都必须做 PAKE:隧道证明的是对端身份,而不是对端持有这张票据。

本节为两条票据路径共用的规范,不定义新的通道控制帧。只有 OPEN、必要 PAKE 和本地接纳门均通过后,才可收发文件帧。发送方 S 是文件提供者(PAKE 的 o),接收方 R 是拉取者(p)。

沿用 u32 big-endian length || u8 class || u8 type || payload;length 包括 class/type/payload,范围 2..1048576。以下 class 均为 1,payload 为规范 CBOR 定长数组;整数用最短编码,无符号字段不接受负数,禁止不定长、浮点、tag、多余字段和尾随 bytes。文本为严格 UTF-8,bytes 不得替换为文本。未知版本、类型、枚举、错方向、越序或错误传输 ID 一律终止本会话,不协商降级。

v=1;tid 是 S 每次 send 用 CSPRNG 新建的 bytes16,独立于 channel_id、票据与 checkpoint,即使续传也重新生成。它只关联鉴权后的文件帧,不承担身份或授权。R 从首个 MANIFEST 绑定 tid,此后不能替换。索引、页号、大小、偏移、计数使用 u64,求和/递增溢出报错,不截断。每个编码后的帧必须先检查大小再分配内存。

type名称、方向精确 payload
1MANIFEST,S→R`[1, tid, page, final bool, entries array, totals array
2ACCEPT,R→S[1, tid, manifest_hash bytes32, page, final bool, files array]
3DATA,S→R[1, tid, index, offset, data bytes];data 长度 1..262144
4CHECK,按 phase`[1, tid, index u64
5ERROR,任一方向`[1, tid bytes16
6WAIT,任一方向`[1, tid, phase u8, index u64

ERROR 的 code 仅取 rejected、cancelled、invalid_manifest、resume_mismatch、invalid_offset、hash_mismatch、source_changed、disk_full、path_conflict、io_error、protocol_error、expired、authorization_revoked、cleanup_failed。不携带任意对端文本、绝对路径或秘密;详细本地原因只在本机安全展示。尚未绑定 tid 时仅 ERROR 可用 null;收到 ERROR 后不再发送数据或开始重试,按 §6.1 回收。坏帧不保证能收到 ERROR,EOF 也不能解释为成功。

清单 entry 固定为 [index, path text, kind u8, size, executable bool, hash bytes32|null, skipped u8|null]:

kind含义其它字段约束
0普通文件size 为精确字节数;hash 为整文件 BLAKE3 32 bytes;skipped=null
1目录(包含空目录)size=0、executable=false、hash=null、skipped=null
2跳过的特殊条目size=0、executable=false、hash=null;skipped 为 0 符号链接、1 设备、2 FIFO、3 socket、4 其它特殊类型

路径映射:每个输入以其最后一个非空名称作为顶层名,目录下递归使用 /;不传绝对路径。输入的顶层名重复、重叠或不能形成合法相对名称时在签发票据前报错,不自动合并两个目录。按路径原始 UTF-8 bytes 的字典序排序,index 从 0 连续编号;重复路径、父子类型冲突、缺失父目录条目和乱序拒绝。路径不做 Unicode 或大小写正规化;无法用 UTF-8 表示的源名称报错,不能有损替换。只跳过上述特殊类型,读取错误、源文件变化不能伪装为跳过。

S 在签发票据前完成文件清点、哈希与源文件身份/大小/修改状态记录;允许私有磁盘索引/外部排序,不要求全清单驻内存。发送时安全重新打开并核实源文件,流式重算哈希;发生变化或读取错误终止,绝不静默截断或把旧哈希与新内容配对。

MANIFEST 的 page 从 0 连续递增,跨页 index 连续,每页最多 1024 entries 且受 1 MiB 帧上限约束;S 可以选更小的页,但非末页不可为空。单条 entry 无法装入一帧时签发票据前失败,不拆分单个路径。只有 final=true 的末页携带 totals=[entry_count, file_count, total_bytes, manifest_hash];其它页 totals=null。file_count/total_bytes 只计 kind=0,entry_count 包含目录和跳过条目。空清单用一张 final 空页和零计数表示。末页后禁止额外 MANIFEST。

内容身份与分页、tid、票据和通道无关,按顺序流式计算:

manifest_hash = BLAKE3(
ASCII("ns/file-manifest/v1") || 0x00 ||
CBOR(entry_0) || ... || CBOR(entry_n-1) ||
CBOR([entry_count, file_count, total_bytes])
) // 原始 32 bytes,不是十六进制文本

R 将清单流式存入私有暂存索引,验证路径、总量和摘要后才展示完整确认、核算空间及选择重名策略。没有完整清单与用户确认前不得发送 ACCEPT 或写入最终路径;磁盘索引、分页 UI 不能静默省略条目。内存和单帧受限不代表限制总文件数/字节数,实际空间或数值不可表示时明确失败。

拒绝清单发送 ERROR/rejected。接受后 R 按普通文件 index 顺序发 ACCEPT,page 从 0 连续,每页最多 1024 项且受帧上限约束,非末页不可为空。每项是 [index, action u8, offset, prefix_hash bytes32];每个普通文件必须恰好出现一次,目录和跳过项不得出现;无普通文件用单张 final 空页。

  • action=0 表示接收/续传:0 <= offset <= size,prefix_hash 为暂存前 offset 字节的 BLAKE3。新文件 offset=0,使用 BLAKE3 空串摘要,不用零 bytes 代替。
  • action=1 表示该文件已由这个 checkpoint 发布:offset=size,prefix_hash=清单整文件 hash。R 必须重读并核实受记录保护的目标文件及其身份;不能把任何同名文件当成已接收,也不能据此覆盖现有文件。
  • R 在确认前验证 checkpoint 的 owner、目标目录身份、原发送方 I、精确本地来源和 manifest_hash;确认后再在 phase 1 重读内容校验偏移。这些绑定不恢复任何授权。失配报 resume_mismatch,明确重新开始或清理,不能偷偷降为覆盖。
  • S 在收到完整 final ACCEPT 后检查文件集合与 manifest_hash,并从当前源文件重算各 prefix_hash;失配终止,不直接信任远端偏移。校验可流式进行,超时按 §5.6;未完成全部范围检查前不得 DATA。

目录重名同样必须先决定目标映射:--yes 为冲突目录选择新的 name (n) 根,并使其子树映射到新根,不静默合并到既有目录。交互模式可选择改名或拒绝本次接收,不提供覆盖选项;名称预留到发布之间仍发生竞争时,按同一规则重新选择未占用名称并更新本地记录,无法安全完成则失败。

checkpoint 用本地版本化、原子更新的记录保存内容身份、目标映射、暂存所有权、持久化偏移及发布状态。元数据提交前先使对应文件内容持久化;重启只信任重新校验后的内容前缀,不信任仅来自长度或未落盘的计数。禁止保存票据、口令、capability、PAKE 密钥或旧网络授权。旧版本或损坏记录拒绝自动恢复。重名选定的目标名称映射保存在本地,不改变清单路径或 manifest_hash。

S 逐个按普通文件 index 处理,最多一个文件处于发送/发布阶段,不并行流:

  1. action=0 从 ACCEPT.offset 开始连续 DATA;每块 offset 必须等于 R 当前期待值,且不超 size。重复、回退、空块、跨文件或超范围均拒绝,TCP 重传不产生业务重发。action=1 不发 DATA。
  2. S 完成源内容复核后发 CHECK [1, tid, index, 0](FILE_END);空文件、完整暂存和 action=1 也必须有此帧。未达 size 的 FILE_END 为截断错误。
  3. R 对 action=0 校验整文件 BLAKE3,设置允许的可执行位,使内容持久化,再以无覆盖原语发布并持久化目录/事务结果;不能用存在性预检加普通覆盖 rename 代替。action=1 再确认已记录文件身份未改变。成功后才回 CHECK [1, tid, index, 1](FILE_COMMITTED)。S 收到后才能处理下一文件。
  4. 所有文件都取得 FILE_COMMITTED 后,S 发 CHECK [1, tid, null, 2](TRANSFER_END)。R 确认完整清单的目录/文件结果均已记录,回 [1, tid, null, 3](ALL_COMMITTED)。跳过条目保留为报告,不伪称已传输。
  5. S 收到 ALL_COMMITTED 后发送 [1, tid, null, 4](RECEIPT),排空有界发送队列。R 收到后结束应用交换。双方各自清理本次暂存/checkpoint、关闭本次接纳并等待 flow/临时 peer 退休,才输出本地 completed。

CHECK phase 0/2/4 只能 S→R,1/3 只能 R→S;phase 0/1 必须有当前文件 index,2/3/4 必须为 null。其它 phase、重复回执、错 index 或提前 TRANSFER_END 均为 protocol_error。目录按安全规则创建,已创建/已发布部分不承诺整体回滚;崩溃后的未确定发布状态通过受保护事务记录、文件身份和哈希核对,不能猜测或删除未知文件。

完成是本地可证明的结果:S 必须收到 ALL_COMMITTED,R 必须收到 RECEIPT,且各自本地清理完成。它不证明另一方收到最终控制字节或完成远端清理。回执丢失时未取得所需回执的一方输出 failed、reason=completion_unconfirmed,同时报告已知的已发布文件数/字节;S 只把收到 FILE_COMMITTED 的文件计为已确认发布,其它文件结果未知;不能将 EOF、IPC closed/retired 或哈希正确单独当作完成。重试以新票据重新开始,并按 checkpoint 核实内容。

邀请在创建时固定 invitation_deadline = created + 600 s,无人兑现则过期;会合的实际 expires 更早时取更早值。PAKE 的 PARAMS.deadline、HELLO.not_after 和 min(deadline, attempt_start+60 s) 校验完全不变,重试不重置期限。只有在期限内完成 FINISH、OPEN 和本地接纳的内置 transfer,才进入文件阶段;认证成功后旧邀请消费,过期不撤销已建立的文件会话。

双方在创建各自内置会话授权时记录单调时钟 session_start;文件阶段本地绝对截止为 session_start+86400 s,更短的来源/策略/进程租约优先。peer 路径的 lifetime=none 只表示不安装 peer,不表示文件会话无限期。恢复会创建新授权,但不能复活旧 flow;清单扫描在发送方创建邀请/授权之前进行。

文件阶段保留 30 秒无完整有效帧的连接空闲限值。WAIT 仅在已绑定 tid 的当前合法阶段可发,ERROR/终止后禁止;处理方长操作每至多 10 秒报告一次;对端可用同阶段、同 index、position=0 的 WAIT 响应来证明连接存活,响应不得再触发即时响应循环。重复 WAIT 不构成工作进展:

phase阶段与处理方index/position不可延长的边界
0R 等待清单确认/冲突选择null/0从 R 收到末页、S 发出末页分别起算最多 600 秒;仅此阶段允许用户等待
1R 校验 checkpoint 或 S 校验续传源前缀正在校验的普通文件 index、已读字节已确认的清单范围内严格推进;无实质进展最多 900 秒
2R 校验收到的完整文件当前文件 index、已校验字节position 不超过 size;无实质进展最多 900 秒
3R 发布/持久化或双方完成清理准备当前 index 或最终 null,position=0WAIT 只能保活,无实质进展最多 900 秒

R 在确认清单后才进入 checkpoint 校验阶段;phase 0 的 600 秒仅覆盖用户决策,不能被重复页面或 WAIT 重置。没有 checkpoint 时直接发 ACCEPT。S 的 phase 1 只能在完整 ACCEPT 后开始。各方向只能报告自身当前工作,不以对端 WAIT 声明替代本地授权或允许跳过阶段。

清单处理、DATA 和 ACCEPT 分页也受无实质进展 900 秒的活动租约限制。只有新的合法页、连续文件字节、预期回执,或当前工作阶段严格增加且不超过清单大小的校验位置,才更新活动租约;每个合法工作阶段内按文件 index/position 单调推进;只有 §5.4–5.5 允许的阶段转换才可开始新的计数,不能重入已结束阶段循环刷新预算,WAIT 响应不计进展。确认、磁盘和 CPU 工作均不能越过绝对 24 小时上限。等待清理的本地操作有界结束并报告失败,不能因为网络已断仍把进展计为有效。

本节是内置文件应用的阶段契约,不放宽第三方 peer IPC、隧道 PAKE IPC 的 deadline 限制;普通 heartbeat、任意 APP 帧或伪造计数都不能自动续期内置 transfer。

规则说明
路径相对路径、以 / 分隔;拒绝绝对路径、./..、空段、盘符、NUL/控制字符、反斜线、Windows 禁止字符与保留名(含扩展名形式、末尾空格/点)。不正规化大小写或 Unicode。目录参数递归发送。
特殊文件发送时跳过符号链接、设备、FIFO 与套接字,并在清单中标为已跳过;接收方从不创建它们。
上限不限制文件数与总大小。接收方在确认前检查目标卷的剩余空间是否足够容纳清单总量,并在写入过程中再次检查。
暂存文件先落在目标卷上的私有暂存目录(仅所有者可访问),哈希校验通过后才移到最终位置。
重名从不静默覆盖已存在的目标文件:接收方会询问,或在 --yes 时写为 name (1).ext。
权限位只携带可执行位;不携带所有者及其他元数据。

源、目标和 checkpoint 操作均在 CLI 调用用户权限内执行,不把路径交给高权限 daemon。安全目录相对打开、禁止跟随符号链接/重解析点和对象身份复核贯穿遍历、写入、恢复、发布及清理;目标检查后被替换时失败,不因字符串位于目标目录就继续。对目标文件系统大小写/正规化等价冲突逐项处理,不能覆盖既有文件或合并互斥目录。新建的空目录也纳入本次所有权记录;取消仅移除仍为空、可证明由本次创建的目录。

事件网络授权本地文件处理与报告
成功按 flow 退休顺序撤销保留已发布文件,删除本次暂存与 checkpoint;清理成功才 completed
本机主动取消,或收到已认证的 ERROR/cancelled立即停止新数据/接纳,尝试传递取消,等待本地退休删除本次未发布暂存和对应元数据;已发布文件保留,报告部分完成和退出码 130
意外断线、崩溃、超时、撤权不保留旧会话授权;撤权不允许靠进展延续保留可验证的私有 checkpoint;后续恢复必须新票据/授权,不自动重连
数据/清单校验失败终止本会话不发布坏内容;最多保留本地可重新验证的 checkpoint,错误或不明归属对象不自动采用
清理失败接纳保持关闭,不恢复句柄failed/cleanup_failed,报告本次残留和可用的清理入口,不能伪称取消已完全完成

网络中断可能使远端收不到取消,此时远端只能按意外断线保留 checkpoint;本机明确报告远端取消/文件清理未确认,不能声称两端都已删除。取消意图在删除前写入本地私有事务记录,重启不得将“取消清理中”的记录恢复为可续传;只允许继续受限清理。--discard 不接受对端提供的路径,也不删除无法证明所有权的临时文件。

checkpoint 不设自动续传或后台自动删除任务;失败事件提供本机恢复/清理位置。接收端可在确认前重新扫描和哈希它,普通同名文件不能被当作 checkpoint。容量不足、权限错误和断电边界仍须 Linux/macOS/Windows 原生验收,文档状态不代表这些能力已经实现。

主题规则
PAKE 尝试一个会话允许 3 次 PAKE 失败,之后作废。约 33 位熵下,3 次猜测约 3·10⁹ 分之一的概率成功。第一次失败就作废会让一次错误猜测取消整个传输。
会合服务滥用会合信箱的按源与全局限速(会合协议 §6)同样适用于 transfer 信箱;查找未知数字消耗一次尝试。
接收方尝试面对多个对端票据候选时,接收方累计 3 次 PAKE 失败后停止,并提示票据可能有误。
过期若无人兑现,票据在 send 后 10 分钟过期;会合服务执行同样的上限。
一次性前两次允许重试的 PAKE 失败仅清理 attempt,保留原票据期限与累计预算;认证成功即消费票据,不能再次接纳。最终失败、过期、取消或撤权时销毁邀请;会话结束按逐 flow 退休顺序清理 PAKE 密钥、本次临时 PSK 与对端,既有 peer/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 预算。

  • 账户模式。 使用精确选定的 nsd:<uuid>/<network> 所投影的 Node 对端,不经会合信箱;本地 app grant、该来源有效策略及远端接纳共同约束会话。Node IP / Node DNS 仅用于寻址,同一 Account 或其它来源的宽 ACL 不能绕过 OPEN/PAKE 与会话授权。首版缺少该来源的投影 peer 时不支持,不回退 Quick(应用模式 §3.1)。
  • 并行流,用于高延迟的直连路径。
  • 移动端与 libns 入口。
组件改动
nsns 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 报文不变
项目相关机制采纳或否定
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/)。

2026-10-23 已确认:BIP39 英文词表随机 3 词;主动取消清理未发布暂存、意外中断保留可验证 checkpoint;邀请 10 分钟、文件会话绝对上限 24 小时。跨 OS 用户接收继续不支持。以上是协议选择,不是实现或平台验收结果。

内置接入见应用模式 §3.5。文件协议与第三方 IPC 不混用,relay、nsshared 和 nsplane 不增加文件专用报文。文件事务、来源撤销、健康邻居保持、无 DNS、直连/relay 与三平台原生文件系统分别验收,证据按候选和平台保留。C05 Account A→B→A 依赖在线重新接纳能力,按用户 2026-10-25 决定延期且不阻塞本轮完成或合并;仅在用户通知后恢复,并在真实 ns/nsd/nsgw 组合中验证重新接纳、旧句柄永久失效和健康邻居原连接保持。Windows 原生验收仍是对应发布门;两项均不得记为已验证。

以下十六进制包含完整帧头;固定 tid 仅用于示例,生产使用 CSPRNG。编码示例不证明生产实现或密码安全。

示例bytes预期
FILE_END,tid=16 个零 bytes,index=0000000170104840150000000000000000000000000000000000000当前文件数据已完整时接受
ERROR/rejected,尚未绑定 tid0000000f01058401f66872656a6563746564f6合法 ERROR 编码;不等于文件完成
DATA,tid=16 个零 bytes,index=0,offset=0,内容 ASCII a0000001901038501500000000000000000000000000000000000004161已完整接受清单且 size 至少为 1 时接受
FILE_END 使用非最短整数 0将 FILE_END 的末尾 00 改为 1800,length 相应加 1拒绝非规范 CBOR
帧过大length=00100001在按声明长度分配前拒绝

实现测试必须覆盖:同一语义清单不同分页得到相同 manifest_hash;缺页/重复页/总量不符拒绝;ACCEPT 漏项或 prefix_hash 失配拒绝;零文件/空文件/仅目录;DATA 偏移越界和提前 FILE_END;旧 tid/跨来源邀请拒绝;接收拒绝、取消清理失败、崩溃前后发布边界;ALL_COMMITTED/RECEIPT 丢失不误报;WAIT 重复或倒退不续期、600/900/86400 秒边界;其它 flow 原连接与共享 PSK 不受影响。