Protocol Analysis
AnyTLS
如何对抗网络封锁
一份聚焦底层原理的深度分析——不讲文件结构,不贴代码片段。从审查系统的识别逻辑出发,推演 AnyTLS 如何用三个协调机制打破流量指纹识别的成本对称性。
基于 anytls-go v0.0.12 源码
2026.06
https://github.com/anytls/anytls-go

目录

01审查系统是如何识别代理流量的
02AnyTLS 的核心洞察:从「隐藏」到「演化」
03机制一:包长混淆——为什么需要条件随机而非固定模式
04机制二:连接复用——压制握手频率信号
05机制三:动态更新——改变攻防的成本结构
06三个机制的协同:一个完整的反识别系统
07诚实评估:AnyTLS 能做什么,不能做什么
01

审查系统是如何识别代理流量的

理解 AnyTLS 的前提是理解它的对手。现代网络审查不是靠规则匹配——它靠机器学习模型对流量样本进行离线训练。任何固定模式的代理流量,只要样本量足够,最终都会成为它的训练数据。

审查系统的运作模型

要理解 AnyTLS 为什么这样设计,必须先把审查方的模型想清楚。现代大规模网络审查系统——以 GFW 为代表——的运作逻辑可以概括为四个阶段:

  1. 采集:在骨干网节点镜像流量,提取每个 TLS 连接的元数据——包长序列、到达时间、方向分布、连接持续时间。
  2. 标注:通过主动探测(假装客户端连接已知代理端口)或被动关联(IP 与已知代理服务器的通信)获取带标签样本。
  3. 训练:用标注样本训练分类器,学习区分「正常 TLS 流量」和「代理 TLS 流量」的决策边界。
  4. 阻断:部署模型到线网,对命中分类器的新连接执行 RST 注入或路由黑洞。

关键认知:这不是一次性工程。这是一个持续运行的机器学习流水线——每天有新的流量样本流入,模型不断更新。代理协议的开发者面对的对手不是一个静态的规则集,而是一个 在线的、不断学习的分类系统

TLS-in-TLS 为什么有可识别的指纹

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)」的模式——这在正常流量中几乎不会出现。

审查系统不需要知道数据包的内容——它只看到加密后的长度和时序。而内层 TLS ClientHello 的长度高度可预测(TLS 版本号、密码套件列表、SNI 扩展的长度都是固定的),所以它在外层 TLS 密文中被加密后产生的密文帧长度也是高度可预测的。不需要解密,长度本身就足够作为分类特征。

以往方案为什么失败

XTLS-Vision 是理解 AnyTLS 最好的对比对象。Vision 的解决方案是:把内层 ClientHello 拆分成固定大小(900-1400 字节)的片段发送,让包长分布接近正常 TLS 数据流。这个思路在方向上是正确的——确实有大量正常 TLS 数据包落在这个长度区间。

但 Vision 有一个致命的架构缺陷:拆分逻辑写死在代码中。全球所有 XTLS-Vision 节点产生的流量共享完全相同的包长分布模式。审查方只需要:

  1. 逆向一份 XTLS 客户端的二进制(几小时的工作量),或
  2. 收集足够多的被标记为「疑似代理」的流量样本做聚类分析

——就能得到一个完美识别全球所有 XTLS-Vision 流量的分类器。固定模式意味着 一次破解,永久有效,全局覆盖

某些 Golang 灵车代理早已标配 uTLS,为什么还被墙?某些 Golang 灵车代理即使不用 uTLS,直接使用 Golang TLS 栈,为什么不被墙? — AnyTLS FAQ,暗示问题不在于 TLS 指纹本身,而在于代理层的行为模式
02

AnyTLS 的核心洞察:从「隐藏」到「演化」

AnyTLS 做了一个根本性的战略转向:不寻找一个「看起来像正常流量」的固定模式——因为任何固定模式终将被训练——而是让模式本身变成一个可快速更换的变量。这改变了审查方和代理方之间的成本对称性。

两种对抗哲学

伪装派(XTLS-Vision 代表)演化派(AnyTLS 代表)
核心假设存在一种「足够像正常流量」的固定模式任何固定模式都会被训练,关键是谁变得更快
策略精心设计一个包长分布,尽量贴近真实 h2让每个 Server 定义独特的包长模板,并通过网络下发更新
被识别后的代价重写代码 → 发布新版本 → 推动全体用户升级服务器改一行配置文本
多部署差异化不可能——全球所有节点共享同一套行为天然支持——每个 Server 定义自己的特征
审查方的训练效率高:一个模型覆盖全体用户低:需要每个 Server 单独收集样本训练

这里的关键词是 训练效率。机器学习分类器需要足够多的标注样本才能收敛到一个可部署的置信度。当 100 个 AnyTLS Server 使用 100 种不同的包长模式,审查方需要对 100 个不同的分布分别建模。而当 Server 端可以 随时更换 这个模式时,训练好的模型在部署之前就已经过期了。

AnyTLS 的战略逻辑可以概括为:如果你无法成为隐身人,那就每天换一张脸,让追踪你的数据库永远落后一步。这不是密码学意义上的安全性——它不依赖任何数学困难问题。这是一种 工程经济学 意义上的对抗:让维持封锁的运营成本高于封锁带来的收益。

三个协调机制的概览

AnyTLS 不是靠一个技巧,而是靠 三个在不同层面工作的机制协同配合

  1. 包长混淆(数据包层面):让审查系统看到的每个连接的包长序列不再是固定的,而是由一个可配置的模板驱动——每个 Server 定义自己的模板,同一个 Server 的不同 Client 使用相同的模板,但不同 Server 之间完全不同。
  2. 连接复用(连接层面):将多个代理请求承载在同一个 TLS 连接上,压制「连接建立频率」这个可被统计的宏观信号。
  3. 动态特征更新(系统层面):Client 第一次连接时使用默认模板,收到 Server 下发的新模板后,后续所有连接切换为新特征。这保证了即使默认模板被识别,也只有第一个连接的极少量数据包暴露在已知特征下。

这三个机制的协作逻辑是:包长混淆让单个连接难以被归类,连接复用让批量连接的低频事件信号被压制,动态更新让任何一个模板被识别后也不会波及其他 Server 或同一 Server 的后续连接

03

机制一:包长混淆——为什么需要条件随机

包长混淆是 AnyTLS 最底层的对抗手段。它不需要「看起来像正常流量」——它只需要让审查系统看到的包长序列,不是一个固定的、可被收集建模的模式。而实现这一点的关键技术,是一个看起来简单但实际上精心设计的配置系统。

问题:为什么随机填充不够?

最简单的包长混淆——在数据包后面追加随机长度的填充——看似有效,实则有两个致命问题:

第一,纯粹的随机均匀分布本身就是一个可被识别的指纹。真实 TLS 流量的包长分布不是均匀的——它有明确的峰值(MTU 附近的大包、ACK 小包、TLS 握手阶段的固定长度包)。如果一个连接的每个包长都均匀分布在 200-1400 之间,审查方不需要训练——直接用 Kolmogorov-Smirnov 检验就能发现这个连接的包长分布与正常流量有显著差异。

第二,持续填充会制造无意义的数据量膨胀。如果每个数据包都追加几百字节的填充,一个 10MB 的下载可能变成 15MB。这不仅浪费带宽,还在宏观层面制造了「数据量异常偏高」的信号。

AnyTLS 的方案:条件拆包 + 有限混淆窗口

AnyTLS 的 paddingScheme 系统在两个维度上做了精确控制:

维度一:只混淆前 N 个包(有限窗口)

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 段?

Server 差异化:同一个协议,不同的脸

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 个上行包的数量」,它无法区分多出来的包是拆包产生的还是正常的后续数据包。纯粹的长度填充不会改变包的数量,在这个维度上没有对抗效果。

04

机制二:连接复用——压制握手频率信号

包长混淆解决的是「单个连接长什么样」的问题。但审查系统还有另一个维度的数据:连接建立的频率。一个代理客户端在短时间内向同一服务器发起多次 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 的复用策略

AnyTLS 在 TLS 连接之上实现了一个轻量的多路复用协议。技术细节不重要——重要的是它的设计意图:把 N 个代理请求压缩到 1 个 TLS 连接上,让审查方看到的不是一个有 10 次握手的统计异常,而是一个有 1 次握手的长寿命连接

Client 维护两个池:

当用户浏览器发起一个新请求时,Client 首先查空闲池——如果最近有一个刚释放的 Session,直接在上面创建新 Stream,不需要新的 TLS 握手。只有在没有可用空闲连接时才建立新 Session。

空闲池的清理策略——每 30 秒检查一次,关闭空闲超过 60 秒的连接——在「减少握手频率」和「避免维持无用连接」之间找到了一个平衡。但这里的权衡有一个前提判断:即使维持一个偶尔空闲的连接,其信号代价也远小于建立一个新连接。一个空闲 60 秒后被关闭的连接,在审查方的日志中只是一条长寿命的普通 TLS 连接;一个新建立的连接则是一个可被计数的握手事件。

复用策略的副作用是正面的:减少的握手次数不仅降低了被检测的风险,也降低了代理的延迟——省去了 TLS 握手的 RTT。这是一个罕见的设计决策,其安全目标和性能目标恰好指向同一方向。

复用和包长混淆的协同

复用和包长混淆不是两个独立的机制——它们在一个关键点上交织。任何 TLS 握手后的第一个数据包(包含内层 ClientHello)都是指纹最集中的包。复用意味着 这个「第一个数据包」只发生在 Session 创建时——后续的 Stream 直接在已建立的加密通道上传输,不再产生新的握手和新的「第一个数据包」

也就是说,复用不仅减少了被统计的握手次数,还减少了 需要被包长混淆处理的数据包总量。如果每个代理请求都独立建立 TLS 连接,每个请求都需要 paddingScheme 对前 8 个包进行混淆——10 个请求 = 80 个被混淆的包 = 80 个可能暴露 paddingScheme 特征的样本点。复用将混淆限制在 Session 建立时的前几个包,大幅缩小了审查方可以用来反向工程 paddingScheme 的样本空间。

05

机制三:动态更新——改变攻防的成本结构

前两个机制解决的是「让流量难以被归类」的问题。但 AnyTLS 还有一个更深的层次:它让流量特征本身成为一个可以在网络上实时更新的变量。这是 AnyTLS 与所有同类协议最根本的战略差异。

前提:默认特征必须暴露

为了启动通信,Client 必须使用某种 paddingScheme 来发送第一个连接。AnyTLS 在代码中硬编码了一个默认方案——开发者也坦然承认,这只是一个示例,「不保证不会被墙」。

这看起来像一个漏洞——如果默认特征被识别,所有 Client 的第一次连接不都会被阻断吗?AnyTLS 的回应是承认这个事实,然后设计一个机制让默认特征的暴露窗口 尽可能窄

  1. Client 的 Session 1 使用默认 paddingScheme 建立连接
  2. 在 Session 1 的握手帧(cmdSettings)中,Client 发送当前 paddingScheme 的 MD5 摘要给 Server
  3. Server 比对——如果不匹配,立即发送 cmdUpdatePaddingScheme 帧,payload 就是 Server 端的完整 paddingScheme 文本
  4. Client 收到后更新全局 paddingFactory——Session 1 仍然使用旧方案,但 Session 2 及之后的所有连接使用新方案

关键设计:更新的是 Client 全局的 paddingFactory,不是当前 Session。这意味着 同一个 Client 的生命周期内,Session 1 使用默认特征,Session 2+ 使用 Server 定制的特征——而 Session 1 之后由于连接复用的存在,可能很久都不会创建 Session 2。

考虑一个典型场景:Client 启动,Session 1 建立(使用默认 scheme,前 3 个包暴露已知特征)。Server 下发新 scheme。Client 将 Session 1 用于所有后续代理请求(连接复用),直到该 Session 因超时或错误被关闭。然后创建 Session 2——此时使用新 scheme,前 8 个包已经是审查方从未见过的模式。即使在 Client 的整个运行期间创建了 100 个 Session,只有第 1 个 Session 的前 3 个包匹配任何已知特征。这个比例——约 3/800,不到 0.4%——远低于 ML 分类器做出高置信度决策所需的样本比例。

为什么这个机制改变了成本结构

审查方的 ML 流水线需要 持续收集新样本 来更新模型。AnyTLS 让这个收集过程变得异常困难:

这不是密码学安全性——AnyTLS 没有声称任何数学上的不可区分性。这是一种 运营层面的非对称性:代理方改变特征的边际成本接近于零(改一行配置文本),而审查方重新训练和部署模型的边际成本显著不为零(收集样本、标注、训练、测试、部署到线网)。

当改变一次特征的代价从「重写代码并说服所有用户升级」降低到「服务器改一行配置」,攻防的成本对称性就发生了根本改变。 — AnyTLS 的设计哲学
06

三个机制的协同:一个完整的反识别系统

单独看每个机制都很有趣,但 AnyTLS 真正的力量在于三个机制之间的相互放大。它们不是你选一个用的工具箱——它们是一个必须有彼此才能完整运作的连锁系统。

协同关系图

理解三个机制之间的依赖关系,是理解 AnyTLS 架构设计的关键:

这个协同关系的数学直觉是:审查方对单个 Client 的有效样本量 ≈ Session 创建次数 × 暴露已知特征的包数 ÷ 总包数。有了复用(减少 Session 创建次数)、包长混淆(混淆前 N 个包)、动态更新(N 在首次后切换到未知模式),这个分子趋近于零。

与传统方案的对比

TrojanXTLS-VisionShadowsocksAnyTLS
传输层TLSTLS + 自定义拆包自研加密TLS + 可替换
包长混淆固定算法(硬编码)可配置模板(Server 定义)
连接复用部分(XTLS 支持 mux)无(每次新连接)内置,Session 级复用 + 空闲池
特征可更新性不可更新不可更新(需发新版)不可更新运行时下发新特征
被识别后的影响全局全局全局(但可换加密参数)仅影响单个 Server
设计哲学伪装成普通 TLS让包长接近正常流量自定义加密 = 非标端口 = 「这里有问题」不伪装,不隐藏——持续换脸

Trojan 的失败模式最有教育意义:「伪装成普通 TLS 流量」意味着如果你没有正确的密码,服务器会返回一个正常的 HTTP 页面(或直接关闭连接)——理论上审查方无法通过主动探测确认这是一个代理。但 Trojan 没有处理 被动流量分析 的问题:在流量层面,Trojan 连接在 TLS 握手后的数据流模式与正常网站访问完全不同。审查方不需要主动探测——它只需要被动统计。

Shadowsocks 走了另一个极端——自定义加密协议。这避免了 TLS-in-TLS 的结构性缺陷,但制造了一个更严重的问题:任何非标准端口上的非标准协议本身就是最明显的指纹。审查方不需要分析流量内容——它只需要注意到「443 端口上跑的居然不是 TLS」。

AnyTLS 在两端之间找到了一个立场:外层使用标准 TLS(不触发协议层面的异常),但内层通过三个机制的协同打破了流量分析层面的可识别性。

07

诚实评估:AnyTLS 能做什么,不能做什么

一个好的分析不以赞美收尾——它以诚实的边界评估收尾。AnyTLS 的设计者自己列出了 7 个已知弱点,这种坦诚本身就是工程成熟度的标志。以下是在设计者的自我评估基础上,从攻防两端做的完整分析。

它确实解决的问题

  1. 破解了「固定特征 → 全局识别」的路径。通过 Server 差异化配置和运行时更新,AnyTLS 使审查方无法用一个模型覆盖所有 AnyTLS 流量。这是它与 XTLS-Vision 等同类协议最根本的区别。
  2. 显著降低了可用的训练样本密度。三个机制的协同——包长混淆减少单连接的可预测性、复用减少可统计的连接事件、动态更新减少暴露窗口——使得审查方在单位时间内对单个 Server 可收集的有效样本量远低于训练一个高置信度分类器所需的最小样本量。
  3. 将攻防转化为不对称的成本竞赛。代理方的边际更新成本接近于零,而审查方的重新训练和部署成本显著不为零。

它无法解决的问题

  1. TLS-in-TLS 的结构性握手开销。嵌套 TLS 比普通 h2 需要更多的握手往返,且内层 ClientHello 的封装会导致数据包在第一个 RTT 内的总字节数偏高。这是 TLS-in-TLS 类协议的先天局限——除非进行 MITM 代理(破坏端到端加密),否则无法解决。
  2. 下行流量未做任何混淆。Server→Client 方向的包完全不做长度混淆——这是性能权衡:下行数据通常是目标网站的真实内容,对其进行混淆不仅消耗 CPU,还可能引入延迟。但这也意味着如果审查方有能力在双向上收集数据,下行方向是一个可被利用的辅助信号。
  3. 时序相关性。paddingScheme 改变了包的长度,但没有改变包的 发送时机。Client 在 TLS 握手后几乎同时发送前 3-4 个包,这个时间密集度本身就是可被统计的信号。引入人工延迟可以缓解,但会增加用户可感知的代理延迟。
  4. 包计数器与实际时序的偏差。paddingScheme 假设 pkt 计数器能准确反映「发包顺序」,但代理协议的发送时机由被代理的应用决定——浏览器什么时候发下一个 HTTP 请求,AnyTLS 控制不了。如果两个 Stream 的数据在同一时刻到达 writeConn,它们可能被合并为一次 TLS Write,导致 pkt 编号与实际发包次数出现偏差。
  5. 主动探测的残余风险。虽然认证密码能拒绝未授权的探测,但如果 Server 使用了自签 TLS 证书(如参考实现中的默认配置),审查方可以通过证书链异常来识别。不过这个风险可以通过配置正式 CA 签发的证书来消除——这不是协议层面的问题。

如果重新设计

从架构分析的角度,AnyTLS 当前版本有几个设计空间值得探索:

最终判断
AnyTLS 不是加密学意义上的创新——它没有发明新的密码算法。它的创新在于 工程经济学层面:认识到审查是 ML 驱动的持续过程后,将对抗目标从「找到完美的伪装」重新定义为「让特征变化比模型训练更快」。这是一个正确的战略判断。协议实现中的三个机制——条件包长混淆、连接复用、运行时特征更新——是这个战略的忠实执行者。它们作为一个系统运转时,确实在攻防两端制造了当前技术条件下难以弥合的成本不对称。