AnyTLS 协议深度解析:从原理到实现
一、引言:AnyTLS 是什么?
AnyTLS 是一个旨在缓解 嵌套 TLS 握手指纹(TLS-in-TLS)问题 的新兴代理协议。它的名字 "AnyTLS" 传递了两个核心含义:
- Any in TLS — 任意流量都可以被封装在 TLS 连接中传输
- Any TLS — 理论上可以运行在任何 TLS 实现之上
该协议由社区开发者设计,其参考实现 anytls-go 以 Go 语言编写,目前已广泛集成到多个主流代理平台中,包括 sing-box、mihomo(Clash Meta)、Shadowrocket 等。
二、背景:为什么要设计 AnyTLS?
2.1 TLS-in-TLS 指纹问题
传统的代理协议(如 Trojan、VLESS over TLS)通常会在客户端和目标服务器之间建立两层 TLS:
- 外层 TLS:代理客户端 ↔ 代理服务器之间的加密隧道
- 内层 TLS:代理服务器 ↔ 目标服务器之间的加密连接
然而,深度包检测(DPI)系统可以通过分析 嵌套 TLS 握手 的流量特征来识别代理行为。具体来说,当 DPI 系统观察到以下特征时,就可能判定这是一个代理隧道:
- 两次 TLS 握手发生在同一个 TCP 连接的后半段:正常的浏览器行为是 TLS 握手后就完成了,不会有第二次握手
- 数据包长度模式异常:TLS 握手产生的数据包大小具有特定的分布特征
- 数据包到达时间间隔异常:代理隧道的数据包时序与正常浏览行为不同
- TLS 头部和证书特征:特定的 TLS 栈实现(如 Go 的 TLS 库)会产生独特的 ClientHello 指纹
2.2 已有方案的局限性
| 方案 | 原理 | 局限性 |
|---|---|---|
| uTLS 指纹伪装 | 修改 ClientHello 使其看起来像浏览器 | 只解决了握手包特征,未解决 TLS-in-TLS 的结构问题 |
| XTLS-Vision | 直接透传内层 TLS 流量,避免二次加密 | 写死的长度处理逻辑,一旦特征被识别就难以改变 |
| 流量混淆 | 随机填充或固定长度填充 | 缺乏策略性,容易产生新的可识别特征 |
| WebSocket/CDN 中转 | 通过 WebSocket 或 CDN 隐藏代理流量 | 增加了延迟和复杂度,不适用于所有场景 |
AnyTLS 正是为了解决这些问题而设计的。它通过 灵活的分包和填充策略 以及 动态可更新的流量特征,从根本上提高了抗检测能力。
三、AnyTLS 协议架构
3.1 整体层次结构
AnyTLS 的协议栈分为四个层次:
┌──────────────────────────────────────────────┐
│ TCP Proxy (代理中继) │
├──────────────────────────────────────────────┤
│ Stream (流) — 复用的数据通道 │
├──────────────────────────────────────────────┤
│ Session (会话) — 命令/事件循环 │
├──────────────────────────────────────────────┤
│ TLS (传输层安全性协议) │
├──────────────────────────────────────────────┤
│ TCP (传输控制协议) │
└──────────────────────────────────────────────┘
- TLS 层:提供加密传输和身份验证,是整个协议的安全基础
- Session 层:在 TLS 之上运行的事件循环,处理认证、命令分发和会话管理
- Stream 层:在 Session 内复用的多个逻辑数据流,每个 Stream 对应一个代理请求
- TCP Proxy 层:实际的中继逻辑,包括 SOCKS5 地址解析和数据转发
3.2 核心设计原则
- 连接复用:多个代理请求共享同一条 TLS 连接,减少握手开销
- 策略性填充:通过可配置的填充方案控制数据包长度分布
- 动态特征更新:允许服务器在运行时推送新的填充方案,使流量特征灵活可变
- 保持简单:不处理 TLS 指纹伪装(由外部工具实现),专注于解决核心问题
四、协议详述
4.1 TLS 握手
AnyTLS 协议本身不定义 TLS 握手的具体参数,而是将 TLS 层的配置交由上层应用决定。这意味着:
- 可以使用自签名证书
- 可以使用 Let's Encrypt 等 CA 签发的证书
- 可以通过 uTLS 等库实现 ClientHello 指纹伪装
- 可以配合 fallback 机制应对主动探测
这种设计使得 AnyTLS 具有良好的灵活性,可以根据部署环境自由选择 TLS 实现。
4.2 客户端认证
TLS 握手完成后,客户端会立即发送认证请求。认证包的格式如下:
sha256(password) |
padding0 length |
padding0 |
|---|---|---|
| 32 字节 | 2 字节(大端序 uint16) | 可变长度 |
认证流程:
- 客户端计算密码的 SHA-256 哈希值(32 字节)
- 生成一个 2 字节的
padding0长度值 - 发送 32 字节的密码哈希 + 2 字节长度 + 填充数据
- 服务器接收后验证密码哈希
- 认证成功:进入会话层事件循环
- 认证失败:关闭连接,或 fallback 到一个普通的 HTTP 服务(用于应对主动探测)
认证阶段的总开销仅为 34 字节(不含填充),非常轻量。
4.3 会话层(Session Layer)
认证完成后,客户端和服务器进入会话层事件循环。会话层的基本通信单元是 frame(帧),格式如下:
command |
streamId |
data length |
data |
|---|---|---|---|
| 1 字节 | 4 字节(大端序 uint32) | 2 字节(大端序 uint16) | 可变长度 |
帧头开销约为 7 字节。
4.4 命令集
AnyTLS 协议目前有两个版本,共定义了 11 种命令:
版本 1(基础命令):
| 命令 | 编码 | 方向 | 说明 |
|---|---|---|---|
cmdWaste |
0 | 双向 | 填充数据包,接收方将其完整读出后无声丢弃 |
cmdSYN |
1 | 客户端→服务器 | 打开一条新的 Stream(数据通道) |
cmdPSH |
2 | 双向 | 推送数据,data 字段承载实际的传输数据 |
cmdFIN |
3 | 双向 | 关闭指定 Stream(类似 TCP 的 FIN 标志) |
cmdSettings |
4 | 客户端→服务器 | 发送客户端设置(版本号、软件名称、填充方案 MD5) |
cmdAlert |
5 | 服务器→客户端 | 服务器发送警告信息,收到后双方关闭会话 |
cmdUpdatePaddingScheme |
6 | 服务器→客户端 | 请求客户端更新填充方案 |
版本 2(增强命令):
| 命令 | 编码 | 方向 | 说明 |
|---|---|---|---|
cmdSYNACK |
7 | 服务器→客户端 | 确认 Stream 已打开,可携带错误信息 |
cmdHeartRequest |
8 | 双向 | 心跳探测请求 |
cmdHeartResponse |
9 | 双向 | 心跳探测响应 |
cmdServerSettings |
10 | 服务器→客户端 | 服务器发送设置信息 |
4.5 命令详解
cmdWaste(0)
这是填充策略的核心命令。任意一方收到 cmdWaste 后,都必须将其 data 完整读出并无声丢弃。这些数据通常用零填充,目的是调整数据包的长度分布,使其看起来更像正常的 TLS 流量。
cmdSYN(1)与 cmdSYNACK(7)
当客户端需要代理一个新的连接时,它会发送 cmdSYN 命令,并分配一个在 Session 内单调递增的 streamId。
- 版本 1:服务器收到
cmdSYN即开始代理,不返回确认 - 版本 2:服务器在出站 TCP 连接握手完成后,发送
cmdSYNACK回复。如果cmdSYNACK不带 data,表示代理成功;如果带 data,data 表示错误信息,客户端收到后必须关闭对应 Stream
这个改进解决了版本 1 中的一个重要问题:当隧道连接意外断开且客户端未收到 RST 时,可能会导致很长的超时。有了 cmdSYNACK,客户端可以在超时后主动关闭卡住的连接。
cmdPSH(2)
承载 Stream 的实际传输数据。这是数据量最大的命令类型。
cmdFIN(3)
关闭指定的 Stream。与 TCP 不同的是:
- 正常关闭时,收到
cmdFIN后不需要回复cmdFIN - Session 关闭时不需要发送
cmdFIN
cmdSettings(4)与 cmdServerSettings(10)
客户端设置(cmdSettings)格式:
v=2
client=anytls/0.0.1
padding-md5=(md5_of_padding_scheme)
采用 UTF-8 编码,key=value 格式,不同项目用 \n 分隔。
服务器设置(cmdServerSettings)格式:
v=2
版本协商机制:
- v2 服务器 + v1 客户端:客户端发送版本 1,服务器不启用 v2 特性
- v1 服务器 + v2 客户端:客户端发送版本 2,但服务器不认识,不会回复
cmdServerSettings。客户端未收到回复,默认版本为 1,不启用 v2 特性 - v2 服务器 + v2 客户端:双方在
cmdSettings/cmdServerSettings中确认支持 v2,启用全部特性
这种向后兼容的版本协商机制确保了不同版本的实现可以共存。
cmdAlert(5)
服务器发送警告文本,客户端打印到日志后双方关闭会话。用于拒绝不合规的客户端连接并说明原因。
cmdUpdatePaddingScheme(6)
这是 AnyTLS 最核心的创新之一。当服务器发现客户端使用的填充方案 MD5 与自身不同时,会发送此命令请求更新。
有了这个设计,当默认填充方案产生的流量特征被识别时:
- 只有第一个连接使用旧的、已知的填充方案
- 服务器立即下发新的填充方案
- 后续所有连接都使用新方案
这意味着理论上可以被识别的连接比例将极低,因为每个客户端只需要发送少量已知特征的数据,然后就能切换到服务器指定的安全特征。
cmdHeartRequest(8)与 cmdHeartResponse(9)
版本 2 的心跳机制,用于检测和恢复卡住的隧道连接。如果长时间未收到心跳响应,客户端可以主动关闭并重建连接。
五、填充方案(Padding Scheme)
填充方案是 AnyTLS 抗检测能力的核心。它通过一个可配置的策略来控制每个数据包的长度和行为。
5.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
4=500-1000
5=500-1000
6=500-1000
7=500-1000
5.2 语法元素
| 元素 | 含义 |
|---|---|
stop=N |
在第 N 个包之后停止处理填充(只处理包 0 到 N-1) |
N=X-Y |
第 N 个包的长度在 X 到 Y 字节范围内随机 |
c |
检查符号:如果上一个分包发送完后用户数据已无剩余,则停止发送后续填充包 |
X-Y,Z-W |
逗号分隔表示分包策略:将数据分成多个指定大小的包发送 |
5.3 填充策略的工作流程
-
包 0(padding0):处于认证阶段,不支持分包。客户端将指定长度的填充与密码哈希一起发送
-
包 1 开始:进入会话阶段,采用策略分包和/或填充:
- 如果分包发送完之后,用户数据仍有剩余,则直接发送剩余数据
- 如果分包发送完之前,用户数据已发送完毕,则发送
cmdWaste携带填充数据
-
包计数器:以
Write(TLS)的次数为准
包 1 通常包含 cmdSettings 和首个 Stream 的 cmdSYN + cmdPSH(代理目标地址)。
包 2 通常是代理自用户的第一个数据包,比如 TLS ClientHello。
5.4 示例:模拟 XTLS-Vision
stop=3
0=900-1400
1=900-1400
2=900-1400
这个方案可以模拟 XTLS-Vision 的流量特征。但 AnyTLS 的作者指出,XTLS-Vision 的弊端在于写死的长度处理逻辑——一旦 GFW 更新特征库就能识别。而 AnyTLS 的动态更新机制使得特征可以随时改变。
5.5 动态更新流程
客户端 服务器
│ │
│─── sha256(pwd) ──────────→│ TLS握手 + 认证(使用默认padding)
│ │
│─── cmdSettings ──────────→│ 发送填充方案MD5
│ padding-md5=XXXX │
│ │ ── 比对MD5,发现客户端方案已过时
│←── cmdUpdatePaddingScheme─│ 下发新的填充方案
│ stop=8, 0=30-30,... │
│ │
│ 后续所有连接使用新方案 │
六、连接复用(Multiplexing)
AnyTLS 要求客户端必须实现会话层复用功能。
6.1 复用策略
-
创建新 Stream 之前:检查是否有"空闲"的 Session
- 如果有:取
Seq最大的 Session(即最近使用的),在该 Session 上开启新的 Stream - 如果没有:创建新的 Session,
Seq单调递增
- 如果有:取
-
Stream 关闭时:如果对应 Session 的事件循环未遇到错误,将 Session 放入"空闲会话池",记录空闲起始时间
-
定期清理(如每 30 秒检查一次):
- 关闭并删除持续空闲超过一定时间(如 60 秒)的 Session
- 至少保留前 N 个空闲 Session 不关闭(为后续代理保留"预备会话")
6.2 复用策略的考量
复用策略高度概括为:优先复用最新的会话,优先清理最老的会话。这样做的好处是:
- 最新的会话最可能保持活跃,复用它可以减少资源浪费
- 清理最旧的会话可以释放资源
- 保留一定数量的预备会话可以减少新建连接的开销
七、代理中继(Proxy)
7.1 TCP 代理
每个 Stream 打开后,客户端向服务器发送 SocksAddr 格式(RFC 1928 §5)的地址信息,表示代理请求的目标地址,然后开始双向代理中继。
7.2 UDP 代理
AnyTLS 本身不直接支持 UDP 代理。对于 UDP 流量,它使用 sing-box 的 udp-over-tcp 协议版本 2:将 UDP 数据封装在 TCP Stream 中传输,代理目标地址为虚拟的 sp.v2.udp-over-tcp.arpa。
八、URI 格式
AnyTLS 定义了简洁的 URI 格式(参考了 Hysteria2 的 URI 方案):
anytls://[auth@]hostname[:port]/?[key=value]&[key=value]...
组件
| 组件 | 说明 |
|---|---|
| 协议名 | anytls |
| 认证 | 密码放在 URI 的 auth 部分(相当于标准 URI 的 username),特殊字符需百分号编码 |
| 地址 | 服务器地址和可选端口,默认端口 443 |
| sni | TLS SNI(Server Name Indication),值为 IP 地址时客户端不发送 SNI |
| insecure | 是否允许不安全的 TLS 连接(1=允许,0=不允许) |
示例
anytls://letmein@example.com/?sni=real.example.com
anytls://letmein@example.com/?sni=127.0.0.1&insecure=1
anytls://0fdf77d7-d4ba-455e-9ed9-a98dd6d5489a@[2409:8a71:6a00:1953::615]:8964/?insecure=1
九、已知弱点
AnyTLS 的作者坦诚地列出了协议的已知弱点,这些弱点目前可能还不会轻易导致协议被封锁,但值得关注:
-
TLS-over-TLS 的握手往返次数:AnyTLS 比普通 HTTP/2 请求需要更多次的 TLS 握手。没有 MITM 代理的情况下难以避免这种情况
-
下行流量未处理:当前版本只处理上行(客户端→服务器)的流量特征,不处理下行。虽然修复这个问题不会破坏兼容性,但会影响性能
-
填充方案语法有限:目前只支持"单一固定长度"和"单一范围内随机"两种模式。剩余数据也只能直接发送。需要重新设计一套更复杂的语法
-
数据包发送时序:AnyTLS 几乎同时发送三个或更多数据包,尤其是在 TLS 握手后的第一个 RTT 内。即使单个包的长度符合要求,
到达时间-包长-包数量和到达时间-通信数据量等统计特征仍可能被利用 -
包计数器不代表发包时机:无法预测被代理方的发送时机,因此包计数器不一定代表真正的发包时序
-
TLS-over-TLS 开销:导致数据包长度增大和缺失小数据包,以及可能持续超过 MTU 限制
-
主动探测风险:虽然不是 HTTP 服务器,仍然存在被主动探测的风险(尽管 GFW 的主动探测行为已不多见)
十、实现与应用
10.1 参考实现
anytls-go(https://github.com/anytls/anytls-go)
- 语言:Go
- 状态:参考实现,简洁的示例,不旨在成为通用代理工具
- 功能:提供服务器和客户端,支持 Socks5 代理
10.2 集成平台
| 平台 | 类型 | 说明 |
|---|---|---|
| sing-box | 通用代理平台 | 同时实现了服务器端和客户端,最完整的 AnyTLS 支持 |
| mihomo(Clash Meta) | 通用代理客户端 | 实现了 AnyTLS 客户端和服务器端 |
| Shadowrocket | iOS 代理客户端 | 2.2.65+ 版本实现了 AnyTLS 客户端 |
| Throne | 跨平台 GUI | 基于 sing-box 的桌面代理工具,支持 AnyTLS |
10.3 Rust 实现
anytls-rs 是一个 Rust 语言的 AnyTLS 实现,表明了该协议的跨语言兼容性。
十一、与其他代理协议的对比
| 协议 | 传输层 | 复用 | 填充策略 | 抗检测 | 特点 |
|---|---|---|---|---|---|
| AnyTLS | TLS | ✅ | ✅ 动态策略 | ⭐⭐⭐⭐⭐ | 灵活可变的流量特征 |
| Trojan | TLS | ❌ | ❌ | ⭐⭐⭐ | 简单,Go 实现 TLS 指纹明显 |
| VLESS + XTLS-Vision | TLS/XTLS | ❌ | ✅ 固定策略 | ⭐⭐⭐⭐ | 速度好,但填充策略固定 |
| Hysteria2 | QUIC | ✅ | ❌ | ⭐⭐⭐ | UDP 协议,速度优但易 QoS |
| NaiveProxy | Chromium 栈 | ✅ | HTTP/2 天然填充 | ⭐⭐⭐⭐ | 使用浏览器内核 TLS 栈 |
| Shadowsocks | 自定义加密 | ❌ | ❌ | ⭐⭐ | 无 TLS 特征,但 AEAD 可识别 |
十二、总结与展望
AnyTLS 作为一个新兴的代理协议,其核心理念是 "可变的流量特征比不可识别的流量特征更有价值"。通过策略性的分包填充和动态更新机制,AnyTLS 从根本上改变了传统代理协议"静态防御"的思路。
AnyTLS 的设计哲学
- 关注可改变性,而非完美性:与其试图让流量变得完全不可识别,不如让它变得容易被改变
- 分层解耦:TLS 指纹伪装、证书管理、填充策略各司其职,互不干扰
- 简洁性:保持协议核心简单,复杂功能交由上层平台实现
未来方向
- 下行流量处理:对称的流量特征控制
- 更复杂的填充语法:支持更精细的数据包长度控制
- 与 QUIC 的结合:AnyTLS over QUIC 的可能性
- 更好的时序控制:减少数据包发送的"突发性"特征
AnyTLS 的出现代表了一种新的抗审查思路——与其与 DPI 系统进行"猫鼠游戏"式的零和博弈,不如让猫永远找不到同一只老鼠。这正是该协议最值得关注的设计哲学。
参考资源
- anytls-go 参考实现:https://github.com/anytls/anytls-go
- 协议文档:https://github.com/anytls/anytls-go/blob/main/docs/protocol.md
- sing-box:https://github.com/SagerNet/sing-box
- mihomo(Clash Meta):https://github.com/MetaCubeX/mihomo
- anytls-rs(Rust 实现):https://github.com/ssrlive/anytls-rs