首页
/ kitty 文件传输协议(FTC):基于 OSC 5113 转义序列的 TTY 文件传输规范全解析

kitty 文件传输协议(FTC):基于 OSC 5113 转义序列的 TTY 文件传输规范全解析

2026-09-05 14:15:34作者:鲍丁臣Ursa

本文以 kitty 仓库中的协议规范文档 file-transfer-protocol.rst 为主体,完整讲解这套“文件传输协议(File Transmission Code,FTC)”的设计理念、会话流程、元数据规范、符号链接/硬链接保留、rsync 二进制增量、zlib 压缩与免交互鉴权机制,并结合 kitty 源码给出 协议实现 的关键佐证。读完后,你将能够:理解为什么 TTY 能作为唯一管道完成跨系统文件传输、手写或调试基于 OSC 5113 的传输客户端、以及读懂 kitty 端协议处理器的实现细节。

一、协议定位:为什么需要“走 TTY 的文件传输”

规范开宗明义:在某些场景下,TTY 是两台互联系统之间唯一方便的管道——嵌套的 SSH 会话、串口(serial line)等。此时没有网络文件传输通道可用,只能把文件数据编码成终端输出流来回传递。

该协议提供以下能力:

  • 传输普通文件、目录以及符号链接与硬链接,并保留大部分元数据(路径、权限、修改时间);
  • 可选 zlib 压缩,以及基于 rsync 算法的二进制差量(delta)传输,加速“只改了少量内容的重复传输”;
  • 所有数据均以 base64 编码后走 TTY,因此规范明确承认:它永远无法与更直接的文件传输机制竞争性能——它的价值在于“能用”,而非“快”。

二、整体设计:以“传输会话”为中心

协议的核心抽象是传输会话(session)。设计动机是安全:不可信软件不应能读写另一台机器的文件系统,因此任何实际数据传输之前,会话必须先在终端模拟器一侧得到用户批准——除非提供了预共享密码(见后文 bypass 机制)

关键设计规则:

  • 会话分为**发送(send)接收(receive)**两种:send 会话中文件从远端客户端流向运行终端模拟器的机器,receive 会话则相反;
  • 每个会话都是“先发元数据、再发实际数据”的命令序列;
  • 每条命令都携带 id(会话 ID,应是一个随机的、近似唯一的标识符,用于避免冲突)和一个 action 字段(指明该命令的作用),其余字段随命令类型而变;
  • 会话是双向的:命令既发给终端模拟器,终端模拟器也会回发命令。

2.1 发送文件到终端模拟器一侧

客户端首先发起 start send 命令,然后必须等待终端的 status 应答(允许或拒绝);在收到应答之前不得再发送该会话的任何命令。终端模拟器若在发出 OK 之前就收到该会话的其他命令,应直接丢弃该会话:

→ action=send id=someid

← action=status id=someid status=OK
# 或被拒绝时:
← action=status id=someid status=EPERM:User refused the transfer

获批后,客户端逐条发送 file 命令携带文件元数据:

→ action=file id=someid file_id=f1 name=/path/to/destination
→ action=file id=someid file_id=f2 name=/path/to/destination2 ftype=directory

终端对目录回 OK,对普通文件回 STARTED;若目标位置不可写则回错误:

← action=status id=someid file_id=f1 status=STARTED
← action=status id=someid file_id=f2 status=OK
← action=status id=someid file_id=f1 status=EPERM:No permission   # 出错示例

数据用 data 命令逐块发送,无需等待 STARTED(终端必须丢弃未 STARTED 文件的数据)。单块上限 4096 字节,以 end_data 收尾:

→ action=data id=someid file_id=f1 data=chunk of bytes
→ action=data id=someid file_id=f1 data=chunk of bytes
...
→ action=end_data id=someid file_id=f1 data=chunk of bytes

每收到一个数据包,终端回一次确认;end_data 之后回最终确认;写盘出错则回错误码并忽略该文件的后续命令:

← action=status id=someid file_id=f1 status=PROGRESS size=bytes written
← action=status id=someid file_id=f1 status=OK size=bytes written
← action=status id=someid file_id=f1 status=EIO:Failed to write to file   # 出错示例

文件全部传完后,客户端以 finish 结束会话。此时终端**提交(commit)**会话——应用文件元数据、创建链接等;若有任何错误则回错误消息:

→ action=finish id=someid
← action=status id=someid status=Some error occurred

2.2 从终端模拟器一侧接收文件

客户端先声明要接收的路径数量,随后逐条列出路径,然后必须等待终端的 OK,在此之前发送任何命令都是协议错误:

→ action=receive id=someid size=num_of_paths
→ action=file id=someid file_id=f1 name=/some/path
→ action=file id=someid file_id=f2 name=/some/path2
...

← action=status id=someid status=OK
# 或
← action=status id=someid status=EPERM:User refused the transfer

获批后终端发送所有请求文件的元数据;若是目录则递归遍历列出全部文件,且符号链接绝不能被跟随,只能作为 symlink 本身列出:

← action=file id=someid file_id=f1 mtime=XXX permissions=XXX name=/absolute/path status=file_id1 size=size_in_bytes file_type=type parent=file_id of parent

这里有两个字段值得细读:file_id 沿用客户端发送的值,而 status 字段存放该文件在终端侧的真实 file_id——因为客户端查询的一个路径(目录)会展开成多个实际文件。parent 字段是包含该文件的目录的真实 file_id,让客户端可以无歧义地重建文件树。

所有文件列完之后,终端回一个带 home 目录路径的 OK(供客户端解析 ~):

← action=status id=someid status=OK name=/path/to/home
# 列出文件失败时:
← action=status id=someid file_id=f1 status=ENOENT:Does not exist

接着客户端按终端发来的 file_id 请求数据(不得请求目录和绝对路径符号链接的数据),终端每次只发一个文件的数据,每块 ≤ 4096 字节,以 end_data 收尾;读取出错则回错误:

→ action=file id=someid file_id=f1 name=/some/path

← action=data id=someid file_id=f1 data=chunk of bytes
...
← action=end_data id=someid file_id=f1 data=chunk of bytes
← action=status id=someid file_id=f1 status=EIO:Could not read    # 出错示例

读完后客户端发送 finished 结束会话(注意发送会话的结束命令是 finish,接收会话是 finished,规范原文如此):

→ action=finished id=someid

2.3 取消会话

客户端可以在任意时刻(例如用户按下 Ctrl-C)取消会话。发送 cancel 后终端丢弃会话并回 CANCELED 确认。规范特别强调:客户端必须等待 CANCELED 应答,丢弃此前的其他响应——否则客户端退出后,这些响应可能被直接打印到屏幕上:

→ action=cancel id=someid
← action=status id=someid status=CANCELED

2.4 静默应答(quiet 参数)

协议中有大量终端应答(确认收到数据、授权、确认取消等)。对于 shell 脚本这类极简客户端,可以在开始命令中加 quiet 键抑制应答:

→ action=send id=someid quiet=1

取值:1 = 抑制确认类应答(仅保留错误);2 = 抑制全部应答包括错误,只保留真正的数据响应。注意 quiet=1 连“传输获准”的确认也会抑制,因此通常只在配合 bypass 免交互鉴权时有用。kitty 实现中对应逻辑在 file_transmission.pyActiveReceive 构造器中:send_acknowledgements = quiet < 1send_errors = quiet < 2,与规范一一对应。

三、文件元数据:跨平台的最小公共集

元数据包含文件路径、权限和修改时间。不同操作系统支持的元数据类型不同,规范定义了跨多数平台可用的最小公共集

3.1 文件路径

  • 必须是合法 UTF-8 编码的 POSIX 路径(/ 分隔);Linux 上的非 UTF-8 路径不受支持;
  • 前导 ~/ 表示相对 HOME 目录;路径必须是绝对路径或相对 HOME;
  • 单个路径组件不超过 255 字节 UTF-8,总长不超过 4096 字节;
  • Windows 路径必须用正斜杠,首段为带冒号的盘符:C:\some\file.txt 表示为 /C:/some/file.txt
  • 为最大可移植性,路径中应当避免以下字符(实现可以支持它们,遇到不可表示的路径时报错):
\ * : < > ? | /

3.2 修改时间

纳秒为单位表示自 UNIX epoch 起的时间戳。文件系统若无法保存这么高的精度,应取最接近的近似值。kitty 实现直接使用 st_mtime_ns(见 make_ftc 函数iter_file_metadata 内的 mtime=sr.st_mtime_ns)。

3.3 文件权限

以普通 UNIX 权限位数字表示(读写执行 + sticky、set-group-id、set-user-id),实现应尽力保留尽可能多的位。Windows 侧只有 read-only 位,转换规则为:

  • 读取元数据时:若 read-only 位未设置,则所有 WRITE 位置位;反之清零。所有 READ 位恒置位;文件可被 Windows 直接执行时 EXECUTE 位置位;
  • 写入时:若 user 可写位未置位则设置 read-only 位,其余 UNIX 位忽略;
  • 不做 Windows ACL 到权限位的映射。

四、符号链接与硬链接的保留

符号链接和硬链接都可以保留。注意:当符号链接目标以实际路径形式传输时,其编码方式与路径规则相同,由接收侧翻译成本地操作系统合适的路径;若无法翻译,则不创建该符号链接或创建一个断链。

4.1 发送链接到终端

file 命令用 file_type 区分链接类型:

→ action=file id=someid file_id=f1 name=/path/to/link file_type=link        # 硬链接
→ action=file id=someid file_id=f2 name=/path/to/symlink file_type=symlink

发数据阶段,硬链接的“数据”就是目标文件的 file_id(前提是目标文件也在传输中;否则该硬链接应按普通文件传输):

→ action=end_data id=someid file_id=f1 data=target_file_id_encoded_as_utf8

符号链接分三种情况:目标是正在传输的文件时用 fid:(相对路径)或 fid_abs:(绝对路径)前缀加目标 file_id;目标不在传输中时用 path: 前缀加符号链接中的实际路径:

→ action=end_data id=someid file_id=f1 data=fid:target_file_id_encoded_as_utf8
→ action=end_data id=someid file_id=f1 data=fid_abs:target_file_id_encoded_as_utf8

4.2 从终端接收链接

接收方向上,链接数据分两部分。终端发初始文件列表时,链接条目的 file_type 设为对应链接类型,data 字段设为目标文件的 file_id(若目标也在列表中):

← action=file id=someid file_id=f1 status=file_id1 ...
← action=file id=someid file_id=f1 status=file_id2 file_type=symlink data=file_id1 ...

即第二条是符号链接,其 data 字段指向第一条 status 字段的值;硬链接同理。客户端不应为硬链接请求数据,而应在传输完成后直接创建硬链接。对符号链接,终端必须在 data 字段发送实际的 UTF-8 目标路径,客户端可原样使用(目标不是传输文件时),或据此决定创建相对/绝对符号链接(目标是传输文件时)。kitty 发送侧的目录递归遍历与 file_id 分配逻辑可参见 iter_file_metadata:它用 status=str(next(counter)) 分配单调递增的真实 file_id,并用 (st_dev, st_ino) 作为 key 记录硬链接同一性。

五、二进制差量传输(rsync 增量)

对“两端仅少量变化”的大文件重复传输,只传变化的块可以显著加速。协议内置了基于 rsync 算法的支持:接收端先发送包含文件各块哈希的签名(signature),发送端只发送变化的块,接收端据此把文件更新到与发送端一致。

触发方式:请求文件时把 transmission_type 键设为 rsync。发送方向与接收方向的细节不同:

5.1 发送到终端

元数据中附加 transmission_type=rsync。若文件已存在且终端能发送签名,STARTED 应答会回带 transmission_type=rsync;随后终端用 data/end_data 发送签名,客户端解析完整签名后回传 delta:

→ action=file id=someid file_id=f1 name=/path/to/destination transmission_type=rsync
← action=status id=someid file_id=f1 status=STARTED transmission_type=rsync
← action=data id=someid file_id=f1 data=...
...
← action=end_data id=someid file_id=f1 data=...
→ action=data id=someid file_id=f1 data=...
...
→ action=end_data id=someid file_id=f1 data=...

5.2 从终端接收

客户端在请求数据时附加 transmission_type=rsync,表示自己会发送该文件的签名;终端收完签名后回传 delta 数据流,客户端用它更新本地文件。

5.3 签名与 delta 的二进制格式

以下所有整数一律使用小端编码(与机器架构无关)。哈希族:XXH3 系列(XXH3-128、XXH3-64)与 rsync 滚动校验和。

签名的 12 字节头:

uint16 version          // 当前除 block_size 外全部为 0
uint16 checksum_type    // 必须为 0,表示传输后完整性校验用 XXH3-128
uint16 strong_hash_type // 必须为 0,表示块强哈希用 XXH3-64
uint16 weak_hash_type   // 必须为 0,表示块弱哈希用 rsync 滚动校验和
uint32 block_size       // 通常为文件大小的平方根,实现可自定算法

头之后是块签名列表。块数量未知以便流式传输,流结束由 action=end_data 指示。每条签名:

uint64 index        // 零基块号;块在文件中的位置 = index * block_size
uint32 weak_hash    // 弱但易算的块哈希
uint64 strong_hash  // 几乎不会碰撞的强哈希

发送端收到签名后,基于真实文件内容计算 delta——即一个“操作(operation)”列表。每个操作先 1 字节类型,后变长数据:

操作 类型值 载荷 语义
Block 0 8 字节 uint64 块索引 从现有文件原样拷贝该块到输出
Data 1 4 字节 uint32 长度 + 载荷 载荷原样写入输出
Hash 2 2 字节 uint16 长度 + 校验和 输出文件整体校验和必须与此匹配,算法由签名头指定
BlockRange 3 8 字节起始块索引 + 4 字节 N 拷贝起始块后再连续拷贝 N 个块

kitty 的 rsync 核心算法实现位于 C 文件 algorithm.c(Go 侧接口声明见 rsync.pyi),接收侧打补丁的落盘逻辑见 file_transmission.py 中的 PatchFile 类——它先把补丁写入目标同目录下的临时文件,close() 时经 Patcher.finish_delta_data() 校验后再 os.replace 原子替换,保证损坏的 delta 不会破坏已有文件。

六、压缩

单个文件可请求压缩传输,目前仅支持 RFC 1950 的 ZLIB deflate,通过在请求文件的命令中加 compression=zlib 指定:

# 发送到终端时,file 元数据命令可带:
→ action=file id=someid file_id=f1 name=/path/to/destination compression=zlib

# 从终端接收时,客户端请求数据阶段的 file 命令可带:
→ action=file id=someid file_id=f1 name=/some/path compression=zlib

kitty 侧的编解码器即 file_transmission.py 中的 ZlibCompressor/ZlibDecompressor(后者用 zlib.decompressobj(wbits=0) 做流式解压,end_data 时 flush)。

七、绕过交互式用户授权(bypass)

为免交互确认,协议支持预共享密码。客户端在发起会话时发送“密码哈希 + 会话 ID”:

→ action=send id=someid bypass=sha256:hash_value

具体地,设会话 ID 为 mysession、共享密钥为 mypassword,则 bypass 值为 SHA256("mysession" + ";" + "mypassword")

→ action=send id=mysession bypass=sha256:192bd215915eeaa8c2b2a4c0f8f851826497d12b30036d8b5b1b4fc4411caf2c

格式为 hash_function_name:hash_value(无空格),当前仅支持 SHA256。规范同时给出了明确的安全警告:

哈希并不能有效隐藏密码值,因此该功能只应安全/可信环境中使用。虽有比 SHA256 更慢的哈希函数,但会显著增加会话启动延迟,而且没有任何哈希函数在数学上被证明不可暴力破解。

sha 开头的前缀是保留的;终端实现可以使用自己的更高级方案。kitty 就是例子:它在 check_bypass 函数 中实现了 kitty-1 协议——客户端用 Kitty 通过环境变量注入的公钥(KITTY_PUBLIC_KEY,见时间戳:会话ID;密码 做 AES-256-GCM 加密后 base85 编码发送;终端侧用本地私钥派生密钥解密,校验时间戳偏差不超过 ±5 分钟(防重放)后再用 hmac.compare_digest 比对明文。对应的用户配置项是 file_transfer_confirmation_bypass,用于存放共享密码;官方说明强调仅在可信计算机、可信网络或加密通道上使用,因为它会让远端机器上任何程序获得读写本地文件系统的权限。

八、转义序列编码:OSC 5113

传输命令编码为 OSC 转义序列:

<OSC> 5113 ; key=value ; key=value ... <ST>

其中 OSC 是字节 0x1b 0x5dST0x1b 0x5c。键只含 [a-zA-Z0-9_] 字符;值的编码取决于键。解码时必须忽略未知键。5113 是常量,未被任何已知 OSC 码占用,它是单词 "file" 的数字化。kitty 中该常量定义在 control-codes.h#define FILE_TRANSFER_CODE 5113,VT 解析器在 vt-parser.c 的 OSC 分发处将其路由到 Python 侧的 file_transmission 处理器。

8.1 键与值类型表

下表为规范定义的完整键表(Key name 列是实际序列化用的短名):

Key Key name 值类型 说明
action ac enum send, file, data, end_data, receive, cancel, status, finish
compression zip enum none, zlib
file_type ft enum regular, directory, symlink, link
transmission_type tt enum simple, rsync
id id safe_string 近似唯一值,避免冲突
file_id fid safe_string 会话内每文件唯一
bypass pw safe_string bypass 密码与会话 ID 的哈希
quiet q integer 0 - 详细,1 - 仅错误,2 - 完全静默
mtime mod integer 文件修改时间,纳秒(自 UNIX epoch)
permissions prm integer UNIX 文件权限位
size sz integer 字节数
name n base64_string 文件路径
status st base64_string 状态消息
parent pr safe_string 父目录的 file_id
data d base64_bytes 二进制数据

Key name 是转义序列中实际发送的键名,例如 permissions=123 序列化为 prm=123,目的是压缩开销。

值类型定义:

  • enum:允许值集合之一,如 ac=file
  • safe_string:仅含 [0-9a-zA-Z_:./@-] 中字符的字符串(注意不含分号,分号是字段分隔符);
  • integer:十进制数字字符 [0-9] 组成,可带前导 -;缺省即零;
  • base64_string:标准 base64 编码的 UTF-8 字符串;
  • base64_bytes:标准 base64 编码的二进制数据。

序列化示例:

action=send id=test name=somefile size=3 data=01 02 03

编码为:

<OSC> 5113 ; ac=send ; id=test ; n=c29tZWZpbGU= ; sz=3 ; d=AQID <ST>

c29tZWZpbGUsomefile 的 base64,AQID 是字节 0x01 0x02 0x03 的 base64。编码形式中的空格仅为阅读清晰,实际序列中忽略。

kitty 的序列化/反序列化在 FileTransmissionCommand 数据类 中:每个字段用 metadata={'sname': ...} 声明短名,get_serialized_fields() 遍历字段、跳过默认值并按类型选择 enum 名字 / base64 / safe_string / 十进制字符串编码;deserialize() 则反向解析,且对未知键(fmap.get(key) 未命中)直接跳过——正好实现“未知键必须忽略”的要求。safe_string 的字符集校验即 safe_string_pat() 的正则 [^0-9a-zA-Z_:./@-],与规范完全一致。

九、kitty 实现中的补充约束

规范是协议层的契约,kitty 作为参考实现还附加了一些运行期约束,从源码结构看可以确认:

  • 会话超时:空闲超过 10 分钟(EXPIRE_TIME = 10)的会话会被过期清理;
  • 并发上限:MAX_ACTIVE_RECEIVES = MAX_ACTIVE_SENDS = 10,同时活跃的收发会话各有上限;
  • 数据块 4096 字节的拆分由 split_for_transfer() 统一完成,mark_last 保证最后一块以 end_data 动作收尾;
  • bypass 密码比对全程使用 hmac.compare_digest 常量时间比较,避免时序侧信道。

十、实战:用 transfer kitten 作为协议客户端

仓库内置的 transfer kitten 就是本协议的完整客户端实现(Go 核心在 kittens/transfer/,包括 send.go/receive.go/ftc.go),典型用法:

# 发送文件到终端所在的机器(接收端按提示批准)
kitty @ transfer --send file.txt

# 从终端所在机器接收文件
kitty @ transfer --receive remote/path

# 使用预共享密码跳过确认(对应协议的 bypass 机制)
kittens/transfer/main.py --permissions-bypass 'mypassword' --send file.txt

其中 --permissions-bypass 的值也可以是文件路径或描述符(见 main.goread_bypass),kitten 会据此生成规范要求的 bypass=sha256:...(或 kitty-1:...)字段。测试方面,kitty_tests/file_transmission.py 覆盖序列化往返、元数据遍历与 bypass 校验,kittens/transfer/send_test.goftc_test.go 覆盖客户端侧命令构造,可作为协议行为的事实性验证依据。

结语

kitty 的文件传输协议用一条极其朴素的思路——把 key=value 命令塞进一个空闲的 OSC 码(5113)、所有二进制走 base64——换取了在任何 TTY 管道上都能工作的通用性。会话批准(或 bypass)、元数据先行/数据后行、4096 字节分块、fid/fid_abs/path: 三前缀链接编码、小端二进制的 rsync 签名与 delta 操作码、以及 quiet 静默级别,构成了这套协议的全部骨架。理解了本文的规范细节与 kitty/file_transmission.py 的对应实现,既可以自行编写最小客户端(哪怕 shell 脚本),也能深入调试 kitty 侧的传输行为。

登录后查看全文
热门项目推荐
相关项目推荐