理解 AnyTLS 的前提是理解它的对手。现代网络审查不是靠规则匹配——它靠机器学习模型对流量样本进行离线训练。任何固定模式的代理流量,只要样本量足够,最终都会成为它的训练数据。
要理解 AnyTLS 为什么这样设计,必须先把审查方的模型想清楚。现代大规模网络审查系统——以 GFW 为代表——的运作逻辑可以概括为四个阶段:
关键认知:这不是一次性工程。这是一个持续运行的机器学习流水线——每天有新的流量样本流入,模型不断更新。代理协议的开发者面对的对手不是一个静态的规则集,而是一个 在线的、不断学习的分类系统。
AnyTLS 属于 TLS-in-TLS 类协议——在一条外层 TLS 加密隧道中传输内层的用户 TLS 流量。这一类协议——包括 Trojan、XTLS、AnyTLS——共享一个结构性问题:内层 TLS 的 ClientHello 握手包在被外层加密后,会产生 一套特征高度稳定的密文包序列。
正常 h2 流量建立连接后的数据流模式是:ClientHello → ServerHello → Certificate → Server Finished → Client Finished → SETTINGS 帧 → HEADERS 帧 → DATA 帧。这是一个 双向交替 的过程,服务端和客户端轮流发言,每轮的包长和间隙遵循 HTTP/2 协议的语义约束。
代理流量的模式截然不同。客户端在 TLS 握手完成后立即 单向密集发送 一连串数据包——认证凭证、代理目标地址、被封装的内层 ClientHello。审查系统看到的是一个「TLS 握手后突然出现 3-4 个上行密文包,紧跟着另一个 TLS 握手包(内层的 ClientHello)」的模式——这在正常流量中几乎不会出现。
XTLS-Vision 是理解 AnyTLS 最好的对比对象。Vision 的解决方案是:把内层 ClientHello 拆分成固定大小(900-1400 字节)的片段发送,让包长分布接近正常 TLS 数据流。这个思路在方向上是正确的——确实有大量正常 TLS 数据包落在这个长度区间。
但 Vision 有一个致命的架构缺陷:拆分逻辑写死在代码中。全球所有 XTLS-Vision 节点产生的流量共享完全相同的包长分布模式。审查方只需要:
——就能得到一个完美识别全球所有 XTLS-Vision 流量的分类器。固定模式意味着 一次破解,永久有效,全局覆盖。
某些 Golang 灵车代理早已标配 uTLS,为什么还被墙?某些 Golang 灵车代理即使不用 uTLS,直接使用 Golang TLS 栈,为什么不被墙? — AnyTLS FAQ,暗示问题不在于 TLS 指纹本身,而在于代理层的行为模式
AnyTLS 做了一个根本性的战略转向:不寻找一个「看起来像正常流量」的固定模式——因为任何固定模式终将被训练——而是让模式本身变成一个可快速更换的变量。这改变了审查方和代理方之间的成本对称性。
| 伪装派(XTLS-Vision 代表) | 演化派(AnyTLS 代表) | |
|---|---|---|
| 核心假设 | 存在一种「足够像正常流量」的固定模式 | 任何固定模式都会被训练,关键是谁变得更快 |
| 策略 | 精心设计一个包长分布,尽量贴近真实 h2 | 让每个 Server 定义独特的包长模板,并通过网络下发更新 |
| 被识别后的代价 | 重写代码 → 发布新版本 → 推动全体用户升级 | 服务器改一行配置文本 |
| 多部署差异化 | 不可能——全球所有节点共享同一套行为 | 天然支持——每个 Server 定义自己的特征 |
| 审查方的训练效率 | 高:一个模型覆盖全体用户 | 低:需要每个 Server 单独收集样本训练 |
这里的关键词是 训练效率。机器学习分类器需要足够多的标注样本才能收敛到一个可部署的置信度。当 100 个 AnyTLS Server 使用 100 种不同的包长模式,审查方需要对 100 个不同的分布分别建模。而当 Server 端可以 随时更换 这个模式时,训练好的模型在部署之前就已经过期了。
AnyTLS 不是靠一个技巧,而是靠 三个在不同层面工作的机制协同配合:
这三个机制的协作逻辑是:包长混淆让单个连接难以被归类,连接复用让批量连接的低频事件信号被压制,动态更新让任何一个模板被识别后也不会波及其他 Server 或同一 Server 的后续连接。
包长混淆是 AnyTLS 最底层的对抗手段。它不需要「看起来像正常流量」——它只需要让审查系统看到的包长序列,不是一个固定的、可被收集建模的模式。而实现这一点的关键技术,是一个看起来简单但实际上精心设计的配置系统。
最简单的包长混淆——在数据包后面追加随机长度的填充——看似有效,实则有两个致命问题:
第一,纯粹的随机均匀分布本身就是一个可被识别的指纹。真实 TLS 流量的包长分布不是均匀的——它有明确的峰值(MTU 附近的大包、ACK 小包、TLS 握手阶段的固定长度包)。如果一个连接的每个包长都均匀分布在 200-1400 之间,审查方不需要训练——直接用 Kolmogorov-Smirnov 检验就能发现这个连接的包长分布与正常流量有显著差异。
第二,持续填充会制造无意义的数据量膨胀。如果每个数据包都追加几百字节的填充,一个 10MB 的下载可能变成 15MB。这不仅浪费带宽,还在宏观层面制造了「数据量异常偏高」的信号。
AnyTLS 的 paddingScheme 系统在两个维度上做了精确控制:
paddingScheme 在连接建立后的前 N 个包上执行包长混淆(默认 N=8),第 N+1 个包起完全透传——零填充、零拆分、零开销。设计者认识到,TLS-in-TLS 的指纹最集中在 连接建立的起始阶段——认证包、第一个数据包(内层 ClientHello)。一旦进入稳定数据传输阶段,加密后的数据流已经不具有可预测的模式,无需额外混淆。
这是一种 精准的模糊化:把混淆火力集中在审查系统最敏感的决策窗口,不在无关区域浪费资源。如果审查系统在做流级分类时只看前 10 个包做决策,那么第 11 个包之后的混淆都是徒劳的——AnyTLS 的有限窗口设计恰好匹配了这个攻击面。
c 检查符的精妙之处paddingScheme 不是简单地把数据按固定长度拆开——它引入了一个叫 c(checkmark)的机制,实现 有条件的拆包。理解 c 的作用是理解 paddingScheme 全部设计哲学的关键。
一个典型的 pkt 2 策略(pkt 2 承载了内层 ClientHello,是最关键的混淆目标):
2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000
这不是「拆成 5 个固定片段」——它是「最多拆成 5 个片段」。每个 c 在执行时做一次判断:上一个片段发送后,用户数据是否已经全部发完?如果是,则 终止——后面的片段不再生成,不会为了混淆而强行发送无意义的填充数据。如果用户数据还有剩余,则继续。
这意味着同一个 paddingScheme 作用于不同的 ClientHello(长度不同,因为 SNI 不同),会产生 不同数量的分包——短的 ClientHello 可能只需要 2-3 个分包,长的可能需要全部 5 个分段。审查系统看到的不再是一个固定长度模式的序列,而是一个 随用户数据的实际内容动态变化 的分包模式。
c 检查符,直接用固定 5 段拆分(每段 500-1000),就会产生一个新问题:短 ClientHello(如 800 字节)被拆成 500+300——后半段的 300 字节就是一个异常短的包。更糟的是,审查方如果知道拆分算法,可以反向计算原始 ClientHello 的长度,从而推断出 SNI。而 c 的提前终止意味着审查方无法确定「数据为什么在第 3 段就停了」——是数据发完了,还是策略本来就只定义到第 3 段?
paddingScheme 作为 Server 端文本配置的设计,实现了一个关键性质:不同 Server 可以定义完全不同的包长模式。Server A 可以设置 pkt 2 拆 5 段,Server B 可以设置 pkt 2 只拆 2 段但每段范围更大,Server C 可以设置 stop=3 只混淆前 3 个包。
这意味着审查系统不能对「AnyTLS 协议」做一个全局分类器——它必须对 每个具体的 Server 分别建模。一个能识别 Server A 流量的分类器,对 Server B 完全无效。这本质性地降低了审查方收集训练样本的效率。
还有一个更微妙的理由让 AnyTLS 选择了「拆包 + cmdWaste 填充」而非纯粹的随机长度填充。拆包改变了 包的数量——一个原本一个 TLS record 就能发送的 ClientHello 现在变成了 3-5 个独立的 TLS record。审查方如果统计「连接建立后前 3 个上行包的总数据量」,就会发现拆包后的总量明显偏高——但如果它进一步统计「前 10 个上行包的数量」,它无法区分多出来的包是拆包产生的还是正常的后续数据包。纯粹的长度填充不会改变包的数量,在这个维度上没有对抗效果。
包长混淆解决的是「单个连接长什么样」的问题。但审查系统还有另一个维度的数据:连接建立的频率。一个代理客户端在短时间内向同一服务器发起多次 TLS 握手——这个宏观行为本身就是可被统计的信号。
正常 HTTP/2 客户端访问一个网站时,浏览器会尝试 连接合并(connection coalescing)——如果多个域名解析到同一 IP 且使用相同的 TLS 证书,浏览器复用同一个 TCP+TLS 连接。这意味着正常用户在 1 分钟内对一个 IP 发起的 TLS 握手次数通常不超过 1-2 次。
代理客户端的行为正好相反。用户浏览器把代理当作一个 SOCKS5 本地端口,每个新目标域名都向代理发起新的 TCP 连接。如果代理不为这些请求做连接复用,每个域名都会触发一次新的 TLS 握手。一个浏览器标签页加载 10 个第三方域名 = 10 次 TLS 握手 = 审查方看到「这个 IP 在 3 秒内和同一个远端 IP 建立了 10 次 TLS 连接」——这在正常流量中极其罕见。
更糟的是,审查方不需要实时检测这个信号。它可以做 离线批处理:按 (源 IP, 目的 IP) 聚合一天的流量,统计每个配对的 TLS 握手次数。代理 IP 对会在分布的最右端形成一个明显的离群峰。
AnyTLS 在 TLS 连接之上实现了一个轻量的多路复用协议。技术细节不重要——重要的是它的设计意图:把 N 个代理请求压缩到 1 个 TLS 连接上,让审查方看到的不是一个有 10 次握手的统计异常,而是一个有 1 次握手的长寿命连接。
Client 维护两个池:
当用户浏览器发起一个新请求时,Client 首先查空闲池——如果最近有一个刚释放的 Session,直接在上面创建新 Stream,不需要新的 TLS 握手。只有在没有可用空闲连接时才建立新 Session。
空闲池的清理策略——每 30 秒检查一次,关闭空闲超过 60 秒的连接——在「减少握手频率」和「避免维持无用连接」之间找到了一个平衡。但这里的权衡有一个前提判断:即使维持一个偶尔空闲的连接,其信号代价也远小于建立一个新连接。一个空闲 60 秒后被关闭的连接,在审查方的日志中只是一条长寿命的普通 TLS 连接;一个新建立的连接则是一个可被计数的握手事件。
复用和包长混淆不是两个独立的机制——它们在一个关键点上交织。任何 TLS 握手后的第一个数据包(包含内层 ClientHello)都是指纹最集中的包。复用意味着 这个「第一个数据包」只发生在 Session 创建时——后续的 Stream 直接在已建立的加密通道上传输,不再产生新的握手和新的「第一个数据包」。
也就是说,复用不仅减少了被统计的握手次数,还减少了 需要被包长混淆处理的数据包总量。如果每个代理请求都独立建立 TLS 连接,每个请求都需要 paddingScheme 对前 8 个包进行混淆——10 个请求 = 80 个被混淆的包 = 80 个可能暴露 paddingScheme 特征的样本点。复用将混淆限制在 Session 建立时的前几个包,大幅缩小了审查方可以用来反向工程 paddingScheme 的样本空间。
前两个机制解决的是「让流量难以被归类」的问题。但 AnyTLS 还有一个更深的层次:它让流量特征本身成为一个可以在网络上实时更新的变量。这是 AnyTLS 与所有同类协议最根本的战略差异。
为了启动通信,Client 必须使用某种 paddingScheme 来发送第一个连接。AnyTLS 在代码中硬编码了一个默认方案——开发者也坦然承认,这只是一个示例,「不保证不会被墙」。
这看起来像一个漏洞——如果默认特征被识别,所有 Client 的第一次连接不都会被阻断吗?AnyTLS 的回应是承认这个事实,然后设计一个机制让默认特征的暴露窗口 尽可能窄:
关键设计:更新的是 Client 全局的 paddingFactory,不是当前 Session。这意味着 同一个 Client 的生命周期内,Session 1 使用默认特征,Session 2+ 使用 Server 定制的特征——而 Session 1 之后由于连接复用的存在,可能很久都不会创建 Session 2。
审查方的 ML 流水线需要 持续收集新样本 来更新模型。AnyTLS 让这个收集过程变得异常困难:
这不是密码学安全性——AnyTLS 没有声称任何数学上的不可区分性。这是一种 运营层面的非对称性:代理方改变特征的边际成本接近于零(改一行配置文本),而审查方重新训练和部署模型的边际成本显著不为零(收集样本、标注、训练、测试、部署到线网)。
当改变一次特征的代价从「重写代码并说服所有用户升级」降低到「服务器改一行配置」,攻防的成本对称性就发生了根本改变。 — AnyTLS 的设计哲学
单独看每个机制都很有趣,但 AnyTLS 真正的力量在于三个机制之间的相互放大。它们不是你选一个用的工具箱——它们是一个必须有彼此才能完整运作的连锁系统。
理解三个机制之间的依赖关系,是理解 AnyTLS 架构设计的关键:
这个协同关系的数学直觉是:审查方对单个 Client 的有效样本量 ≈ Session 创建次数 × 暴露已知特征的包数 ÷ 总包数。有了复用(减少 Session 创建次数)、包长混淆(混淆前 N 个包)、动态更新(N 在首次后切换到未知模式),这个分子趋近于零。
| Trojan | XTLS-Vision | Shadowsocks | AnyTLS | |
|---|---|---|---|---|
| 传输层 | TLS | TLS + 自定义拆包 | 自研加密 | TLS + 可替换 |
| 包长混淆 | 无 | 固定算法(硬编码) | 无 | 可配置模板(Server 定义) |
| 连接复用 | 无 | 部分(XTLS 支持 mux) | 无(每次新连接) | 内置,Session 级复用 + 空闲池 |
| 特征可更新性 | 不可更新 | 不可更新(需发新版) | 不可更新 | 运行时下发新特征 |
| 被识别后的影响 | 全局 | 全局 | 全局(但可换加密参数) | 仅影响单个 Server |
| 设计哲学 | 伪装成普通 TLS | 让包长接近正常流量 | 自定义加密 = 非标端口 = 「这里有问题」 | 不伪装,不隐藏——持续换脸 |
Trojan 的失败模式最有教育意义:「伪装成普通 TLS 流量」意味着如果你没有正确的密码,服务器会返回一个正常的 HTTP 页面(或直接关闭连接)——理论上审查方无法通过主动探测确认这是一个代理。但 Trojan 没有处理 被动流量分析 的问题:在流量层面,Trojan 连接在 TLS 握手后的数据流模式与正常网站访问完全不同。审查方不需要主动探测——它只需要被动统计。
Shadowsocks 走了另一个极端——自定义加密协议。这避免了 TLS-in-TLS 的结构性缺陷,但制造了一个更严重的问题:任何非标准端口上的非标准协议本身就是最明显的指纹。审查方不需要分析流量内容——它只需要注意到「443 端口上跑的居然不是 TLS」。
AnyTLS 在两端之间找到了一个立场:外层使用标准 TLS(不触发协议层面的异常),但内层通过三个机制的协同打破了流量分析层面的可识别性。
一个好的分析不以赞美收尾——它以诚实的边界评估收尾。AnyTLS 的设计者自己列出了 7 个已知弱点,这种坦诚本身就是工程成熟度的标志。以下是在设计者的自我评估基础上,从攻防两端做的完整分析。
从架构分析的角度,AnyTLS 当前版本有几个设计空间值得探索: