跳转至

RFC: The Anytls Protocol Version 2 Category: Informational Date: April 2026

Anytls 协议规范版本 2 (The Anytls Protocol Version 2)

摘要 (Abstract)

本文档描述了 Anytls 协议,这是一种基于传输层安全性 (TLS) 运行的安全、多路复用的代理协议。该协议旨在通过动态填充方案提供强大的流量混淆以对抗流量分析,在单个会话中多路复用多个逻辑流,并通过回退到标准协议来防御主动探测。

1. 简介 (Introduction)

Anytls 是一种在标准 TLS 连接之上建立安全会话的代理协议。它由初始认证阶段和随后多路复用多个逻辑流的会话层组成。为了对抗高级的深度包检测 (DPI) 启发式算法,Anytls 利用动态的 paddingScheme(填充方案)来主动改变流量指纹。

2. 约定和术语 (Conventions and Terminology)

本文档中的关键词“必须”(MUST)、“绝对不能”(MUST NOT)、“要求”(REQUIRED)、“将”(SHALL)、“绝对不将”(SHALL NOT)、“应该”(SHOULD)、“不应该”(SHOULD NOT)、“建议”(RECOMMENDED)、“可以”(MAY) 和“可选”(OPTIONAL) 需按照 RFC 2119 中的描述进行解释。

  • Session (会话): 运行 Anytls 会话层的单一已建立的 TLS 连接。
  • Stream (流): 在会话中用于代理特定连接的多路复用逻辑通道。
  • Client (客户端): 发起 Anytls 连接的软件。
  • Server (服务端): 接收连接并执行代理的 Anytls 端点。

3. 协议架构 (Protocol Architecture)

整体协议栈的层级如下:

+-------------------------------------------------+
|               用户 TCP/UDP 代理                 |
+-------------------------------------------------+
|               Anytls 流层 (Stream)              |
+-------------------------------------------------+
|               Anytls 会话层 (Session)           |
+-------------------------------------------------+
|             传输层安全性 (TLS)                  |
+-------------------------------------------------+
|             传输控制协议 (TCP)                  |
+-------------------------------------------------+

4. 认证阶段 (Authentication Phase)

TLS 握手完成之后,客户端必须立即发送认证请求。

4.1. 客户端认证请求

认证载荷的结构如下:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
+                                                               +
|                                                               |
+                                                               +
|                                                               |
+                                                               +
|                                                               |
+                                                               +
|                                                               |
+                                                               +
|                                                               |
+                                                               +
|                                                               |
+                                                               +
|                  sha256(password) (32 Bytes)                  |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|       padding0 length         |    padding0 (可变长度) ...    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  • sha256(password): 32 字节。预共享协议密码的 SHA-256 哈希值。
  • padding0 length: 2 字节 (大端序 uint16)。后续填充数据的长度。
  • padding0: 可变长度。随机填充数据。

注:认证部分的开销为 34 字节(不包含可变长度的填充)。

4.2. 服务端认证响应

服务端必须读出第一个数据包并校验认证请求(包括完整读出 padding0)。 - 如果认证成功,服务端进入会话循环。 - 如果认证失败,服务端必须立即关闭连接,或者回退 (fallback) 到标准的 HTTP/L7 服务以防御主动探测。

5. 会话层 (Session Layer)

认证完成后,双方进入会话事件循环。

5.1. 帧格式 (Frame Format)

会话层使用以下帧格式进行通信:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|    command    |               streamId                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|               |          data length          |               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         data (可变长度)                       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  • command: 1 字节 (uint8)。标识帧类型的命令。
  • streamId: 4 字节 (大端序 uint32)。标识逻辑流。
  • data length: 2 字节 (大端序 uint16)。载荷的长度。
  • data: 可变长度的载荷。

5.2. 命令 (Commands)

定义了以下命令。除非下文明确规定,否则命令绝对不能携带数据载荷 (data)。

命令名称 引入版本 方向 描述
cmdWaste 0 v1 双向 填充。必须完整读出数据并无声丢弃。
cmdSYN 1 v1 客户端 -> 服务端 打开一个新流。
cmdPSH 2 v1 双向 向流推送数据。
cmdFIN 3 v1 双向 关闭流 (EOF 标记)。
cmdSettings 4 v1 客户端 -> 服务端 客户端发送设置。
cmdAlert 5 v1 服务端 -> 客户端 服务端发送的致命错误警告。
cmdUpdatePaddingScheme 6 v1 服务端 -> 客户端 动态填充方案更新。
cmdSYNACK 7 v2 服务端 -> 客户端 流打开确认。
cmdHeartRequest 8 v2 双向 Keep-alive 心跳请求。
cmdHeartResponse 9 v2 双向 Keep-alive 心跳响应。
cmdServerSettings 10 v2 服务端 -> 客户端 服务端发送设置。

5.2.1. cmdSettings (4)

客户端在开启新会话时必须立即发送 cmdSettings 帧。如果服务端在收到 cmdSettings 之前收到 cmdSYN,必须拒绝此次会话。

其 data 载荷为采用 UTF-8 编码的键值对,键与值之间用 = 连接,不同项目之间用换行符 \n 分割。 示例:

v=2
client=anytls/0.0.1
padding-md5=(当前 paddingScheme 的 md5,小写 hex 编码)

5.2.2. cmdServerSettings (10)

如果客户端上报的版本 v >= 2,服务端在收到 cmdSettings 后必须立即回复 cmdServerSettings。 示例:

v=2

5.2.3. cmdAlert (5)

载荷包含服务端发送的警告文本信息。客户端必须将其读出并打印到日志,然后双方必须关闭会话。服务端可以使用此命令来拒绝过时或不合规的客户端连接。

5.2.4. cmdUpdatePaddingScheme (6)

如果服务端检测到客户端的 padding-md5(来自 cmdSettings)与服务端当前方案不同,服务端必须发送此命令。载荷格式在第 6 节中描述。

5.2.5. cmdHeartRequest (8) 和 cmdHeartResponse (9)

任意一方收到 cmdHeartRequest 后,必须回复 cmdHeartResponse。这些命令用于检测并恢复卡住的隧道连接。

5.3. 流生命周期命令 (Stream Lifecycle Commands)

5.3.1. cmdSYN (1)

通知服务端打开一条新的流。客户端必须为每个流生成在会话内单调递增的 streamId

5.3.2. cmdSYNACK (7)

对于版本 v >= 2 的客户端,服务端收到 cmdSYN 后,应该在代理出站连接 TCP 握手完成后,发送带有对应 streamIdcmdSYNACK。 - 若不带有 data,则表示代理流握手成功。 - 若带有 data,则 data 代表错误信息。客户端收到错误信息后必须关闭对应的流。

5.3.3. cmdPSH (2)

数据载荷承载该流的实际代理传输数据。

5.3.4. cmdFIN (3)

通知对端关闭指定 streamId 的流。 - 当会话正常时,本端收到 cmdFIN 并关闭本地流后,不需要向对端回复 cmdFIN。 - 当会话已经关闭时,不需要发送 cmdFIN

6. 动态填充方案 (Dynamic Padding Scheme)

填充方案规定了如何对数据包进行分片和填充,以混淆流量特征。

6.1. 填充方案格式

该方案作为换行符分隔的字符串发送。示例:

stop=8
0=30-30
1=100-400
2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000
3=9-9,500-1000

6.2. 方案指令 (Scheme Directives)

  • stop: 例如,stop=8 表示只处理前 8 个数据包(索引 0 到 7)的填充方案。
  • 0=X-Y: 第 0 个数据包(认证期间的 padding0)的规则。该包不支持分片。客户端发送长度在 X 和 Y 字节之间的填充。
  • 1 及以上: 会话阶段数据包的规则。

数据包计数 (Packet Counting): 数据包索引以底层 TLS 连接上调用 Write() 的次数为准。 - 数据包 1 通常包括:cmdSettings + 首个流的 cmdSYN + cmdPSH(包含代理目标地址)。 - 数据包 2 通常包含来自用户的首个代理数据块(例如被代理连接的 TLS ClientHello)。

分片和填充逻辑 (Fragmentation and Padding Logic): 如果数据包由 A-B,C-D 规则控制: 1. 用户数据被分片。第一个分块的尺寸在 A 和 B 之间随机选择。第二个在 C 和 D 之间,依此类推。(尺寸指 TLS PlainText 长度,不计算 TLS 加密等开销)。 2. 如果所有指定的分片发送完之后,用户数据仍有剩余,则直接原生发送剩余数据。 3. 如果在所有分片发送完之前,用户数据已发送完毕,则端点必须发送充满填充(建议用 0)的 cmdWaste 以满足尺寸要求。 4. 检查符号 (c): 如果存在 ,c, 分隔符,并且上一个分片发送完毕后用户数据已无剩余,则实现必须立即从 Write 操作返回,绝对不发送 c 之后定义的后续填充包。

6.3. 方案生命周期 (Scheme Lifecycle)

  • 客户端必须在 Client 对象中存储作用于连接到该服务端的 paddingScheme
  • 客户端第一次连接必须使用默认的 paddingScheme
  • 收到 cmdUpdatePaddingScheme 后,客户端在后续与该服务端新建的会话中必须使用下发的新方案。这确保了当默认方案的特征被封锁时,只有极少部分初始连接会暴露特征。

7. 连接复用 (Connection Multiplexing)

客户端必须使用连接池实现会话层复用功能。总体架构为: TCP Proxy -> Stream -> Session -> TLS -> TCP

7.1. 复用策略 (Multiplexing Strategy)

  • 在创建新的会话层之前,客户端必须检查池中是否有“空闲”的会话。
  • 如果有,客户端必须选取序号 (Seq) 最大的会话来开启新流。
  • 如果没有空闲会话,则创建新会话。Seq 在客户端实例内必须单调递增。
  • 当流正常关闭且会话无错误时,会话将放入“空闲会话池”,并设置其空闲起始时间为 now
  • 客户端应该定期(例如 30 秒)清理持续空闲超过一定时间(例如 60 秒)的会话。
  • 服务端也可以定期清理长期无上下行数据的会话。

8. 代理协议交互 (Proxy Protocol Interaction)

8.1. TCP 代理

流打开后(cmdSYN),客户端必须在 cmdPSH 中发送 RFC 1928 (SOCKS5) 格式的目标地址。随后开始双向代理中继。

8.2. UDP 代理

UDP 代理依赖于 sing-box udp-over-tcp v2 协议。客户端将其视作对特殊域名 sp.v2.udp-over-tcp.arpa 的 TCP 代理请求。

9. 协议参数 (Protocol Parameters)

Anytls 协议参数不包括 TLS 配置。Anytls 特定参数包括:

9.1. 客户端参数

  • password (String, REQUIRED): 协议认证的密码。
  • idleSessionCheckInterval (Duration, OPTIONAL): 检查空闲会话池的间隔时间。
  • idleSessionTimeout (Duration, OPTIONAL): 会话在被关闭前可以保持空闲的最大时长。
  • minIdleSession (Integer, OPTIONAL): 作为热备保留的最小空闲会话数量。

9.2. 服务端参数

  • paddingScheme (String, OPTIONAL): 强制下发给客户端的主填充方案。

10. 更新记录 (Version History)

  • v1: 初始实现。
  • v2 (v0.0.8 - 2025 年 4 月): 添加了 cmdSYNACK 以报告出站连接状态并处理卡住的隧道;添加了 cmdHeartRequest/cmdHeartResponse 心跳包;添加了 cmdServerSettings 用于协商。
  • v2 (v0.0.10 - 2025 年 9 月): 明确了关于会话和流关闭时 cmdFIN 的语义。