摘要:TLS 指纹识别是 GFW 检测代理流量的核心手段之一。即使流量内容完全加密,TLS 握手阶段的明文参数也会暴露客户端的身份。本文解释 JA3/JA4 指纹的计算方式、GFW 如何利用它识别代理、以及主流协议的应对策略。
TLS 握手中暴露了什么
TLS 连接建立时,客户端需要向服务器发送一个 Client Hello 消息,这是整个 TLS 握手的第一步。关键在于:这条消息是以明文发送的,其中包含了大量描述客户端能力的元数据。
以下是 Client Hello 中携带的关键字段:
- 支持的 TLS 版本列表:客户端声明自己支持哪些 TLS 协议版本(如 TLS 1.2、TLS 1.3)。
- 加密套件(Cipher Suites)列表及顺序:客户端罗列自己支持的加密算法组合,并且列出的顺序代表了优先级偏好。不同的 TLS 实现对加密套件的选择和排列方式截然不同。
- 扩展(Extensions)列表及顺序:TLS 扩展用于协商额外的功能,比如支持的协议、密钥交换方式等。不同客户端携带的扩展种类、数量和排列顺序都各有特征。
- 支持的椭圆曲线(Supported Groups):声明客户端支持哪些椭圆曲线用于密钥交换,例如 x25519、P-256、P-384 等。
- 签名算法(Signature Algorithms):客户端声明自己支持哪些签名算法用于证书验证,例如 ECDSA、RSA-PSS 等。
- ALPN(Application-Layer Protocol Negotiation):应用层协议协商,告知服务器客户端希望使用的上层协议,如 h2(HTTP/2)或 http/1.1。
- SNI(Server Name Indication):服务器名称指示,明文告知服务器客户端想要连接的域名。
这些参数的组合是高度特征化的。

图片来源:Cloudflare
不同的 TLS 库(如浏览器内置的 BoringSSL、Go 语言的 crypto/tls、Python 的 ssl 模块、Java 的 JSSE)会产生截然不同的 Client Hello。就像每个人走路的姿态不同一样,每种 TLS 实现发出的 Client Hello 也带有自己独特的”步态”。审查者只需要观察这个明文的握手消息,就能推断出对方使用的是什么客户端软件——即使后续的所有通信内容都是加密的。
JA3 指纹是什么
JA3 是由 Salesforce 的安全团队在 2017 年提出的一种 TLS 客户端指纹方法。它的核心思路非常直观:从 Client Hello 中提取最具辨识度的字段,拼接成一个字符串,然后计算哈希值作为指纹。
具体而言,JA3 从 Client Hello 中提取以下五个字段:
| 字段 | 说明 |
|---|---|
| TLS 版本 | 客户端声明的 TLS 协议版本号 |
| 加密套件(Cipher Suites) | 所有支持的加密套件的编号列表 |
| 扩展(Extensions) | 所有 TLS 扩展的编号列表 |
| 椭圆曲线(Elliptic Curves) | 支持的椭圆曲线编号列表 |
| 椭圆曲线点格式(EC Point Formats) | 支持的椭圆曲线点格式列表 |
JA3 将这五个字段的值按顺序用逗号连接成一个长字符串,格式如下:
1 | TLSVersion,Ciphers,Extensions,EllipticCurves,ECPointFormats |
然后对这个字符串取 MD5 哈希,得到一个 32 字符的十六进制字符串,即 JA3 指纹。例如:
1 | 769,47-53-5-10-49161-49162-49171-49172-50-56-19-4,0-10-11,23-24-25,0 |
这个指纹的核心价值在于区分不同的客户端实现。以下是一些关键的区分能力:
- Chrome 的 JA3 指纹 ≠ Firefox 的 JA3 指纹:因为 Chrome 使用 BoringSSL,Firefox 使用 NSS,两者对加密套件和扩展的选择完全不同。
- 浏览器的 JA3 指纹 ≠ Go crypto/tls 的 JA3 指纹:Go 语言标准库的 TLS 实现有自己独特的参数组合,与任何主流浏览器都不相同。
- 同一浏览器的不同版本指纹也可能不同:当浏览器升级并添加对新加密算法或新扩展的支持时,其 Client Hello 会随之变化,JA3 指纹也会改变。
简而言之,JA3 指纹就像是 TLS 客户端的一个”身份证号”,虽然不是百分之百唯一,但足以在大多数场景下区分客户端类型。

图片来源:Ntop
JA4 与 JA3 的区别
JA4 是 2023 年由 FoxIO 推出的下一代 TLS 指纹方案,旨在解决 JA3 的若干局限性。
JA3 的主要问题在于:MD5 哈希将所有信息压缩成一个不可读的字符串,丢失了结构信息,且 MD5 本身存在碰撞风险。此外,JA3 仅针对 TCP 上的 TLS 设计,无法覆盖基于 UDP 的 QUIC 协议。
JA4 的改进主要体现在以下几个方面:
- 结构化的指纹格式:JA4 的指纹不再是一个单一的哈希值,而是由多个有意义的片段组成。格式大致为
协议类型_TLS版本_SNI标志_加密套件数量_扩展数量_ALPN首选项_加密套件哈希_扩展哈希。这意味着即使不查表,也能从指纹中直接读出部分信息,例如客户端使用的是 TLS 1.3 还是 1.2,是否携带了 SNI 等。 - 更全面的参数覆盖:JA4 在计算时考虑了更多的 Client Hello 字段,提供了更精细的区分能力。
- QUIC/HTTP3 支持:JA4 明确支持 QUIC 协议中的 Client Hello 指纹提取,而 JA3 对此没有定义。随着 HTTP/3 的普及,这一点变得越来越重要。
- JA4 指纹族:JA4 实际上是一个指纹家族,包括 JA4(TLS 客户端)、JA4S(TLS 服务端)、JA4H(HTTP 客户端)、JA4X(X.509 证书)等多个变体,构成了一个完整的流量指纹体系。
对于代理用户来说,JA4 意味着审查者拥有了更精确、更难规避的指纹识别工具。
这和代理有什么关系
核心问题:代理客户端的 TLS 指纹与真实浏览器不同。
大多数主流代理工具——V2Ray、Xray、Sing-box——都是用 Go 语言编写的。Go 语言的标准库 crypto/tls 有自己的 TLS 实现,它产生的 Client Hello 在加密套件选择、扩展排列、椭圆曲线偏好等方面与任何主流浏览器都存在明显差异。
这意味着什么?假设你使用 VLESS+TLS 协议连接到一台伪装成 apple.com 的服务器。从 GFW 的视角来看:
- SNI 显示这是到 apple.com 的连接——看起来正常。
- 但 JA3 指纹显示客户端是 Go 程序——这就不对了。正常用户用浏览器访问 apple.com,不会用 Go 程序。
- 指纹与 SNI 的矛盾暴露了代理行为——GFW 可以据此判定这是一条代理连接,进而对该连接进行干扰、限速或封锁。
这就是许多用户容易忽视的一个事实:“套 TLS” 并不等于 “安全”。TLS 加密的是握手之后的应用层数据,但握手过程本身(特别是 Client Hello)是明文的。如果你的代理工具使用了一个与浏览器截然不同的 TLS 库,那么 TLS 握手本身就在向审查者宣告:”我不是浏览器,我是一个代理程序”。
更具体地说,Go 语言 crypto/tls 的 Client Hello 有几个典型的”泄露点”:
- 加密套件的选择和顺序与浏览器不同
- 携带的 TLS 扩展集合与浏览器不同
- 缺少浏览器特有的 GREASE(Generate Random Extensions And Sustain Extensibility)值
- 椭圆曲线和签名算法的偏好列表与浏览器不同
这些差异对于 GFW 来说是非常容易检测的。
各协议的应对方案
uTLS:模拟浏览器指纹
uTLS 是目前最广泛使用的应对方案。它是一个 Go 语言库,核心功能是手动构造 Client Hello 消息,使其看起来与特定浏览器的 Client Hello 完全一致。
uTLS 的工作原理并不复杂:它预先收集了各种浏览器(Chrome、Firefox、Safari、Edge 等)在不同版本下的 Client Hello 参数模板。当代理客户端需要发起 TLS 连接时,uTLS 不使用 Go 标准库的默认参数,而是按照选定的浏览器模板来构造 Client Hello,包括完全一致的加密套件列表及顺序、扩展列表及顺序、椭圆曲线选择等。
在 Xray-core 和 Sing-box 中,用户可以通过 fingerprint 参数来启用 uTLS,并指定要模拟的浏览器类型:
1 | { |
可选的值通常包括:chrome、firefox、safari、edge、randomized 等。
然而,uTLS 也存在一些局限性:
- 更新滞后:浏览器会频繁更新,每次更新都可能改变 Client Hello 参数。uTLS 需要持续跟进浏览器的变化,如果更新不及时,模拟的指纹可能与当前最新版浏览器不一致。
- GREASE 的模拟难度:GREASE 是一种由浏览器引入的机制,会在 Client Hello 中随机插入未知的扩展值或加密套件值,以确保服务端实现能正确忽略未知参数。浏览器每次连接时 GREASE 值都会变化,而 uTLS 的 GREASE 实现是否足够随机、是否符合真实浏览器的分布规律,是一个持续存在的挑战。
- 只解决了 Client Hello 的问题:TLS 握手不只有 Client Hello,还有 Client 在后续阶段的行为(如密钥交换方式、Session Resumption 行为等)。uTLS 主要集中在 Client Hello 层面,对握手的其他阶段覆盖有限。
尽管如此,uTLS 仍然是目前最实用且最易部署的方案,能有效对抗基于 JA3/JA4 的被动指纹检测。
Reality:从根本上解决问题
Reality 协议(由 XTLS 团队开发,集成在 Xray-core 中)采用了一种比 uTLS 更彻底的方法。它的思路不是”模拟浏览器去欺骗审查者”,而是”让审查者看到的就是一个真实的 TLS 连接”。
Reality 的工作流程如下:
- 客户端使用 uTLS 发送 Client Hello:客户端首先利用 uTLS 构造一个模拟浏览器指纹的 Client Hello,发送给 Reality 服务端。从 GFW 的角度看,这个 Client Hello 的指纹与真实浏览器一致。
- 服务端将 Client Hello 转发给真实目标:Reality 服务端收到 Client Hello 后,将其原封不动地转发给一个预先配置的真实目标网站(例如
apple.com或www.microsoft.com)。 - 真实目标返回 Server Hello 和证书:目标网站正常处理这个 Client Hello,返回自己的 Server Hello、真实的 TLS 证书以及完成握手所需的参数。
- 服务端将真实响应转发给客户端:Reality 服务端把从目标网站收到的 Server Hello 和证书原样转发给客户端。
- 切换到代理加密通道:在 TLS 握手表面上完成后,客户端和服务端之间切换到代理自己的加密通道进行实际数据传输。
这种设计的巧妙之处在于:
- GFW 看到的是一个完全真实的 TLS 连接:Client Hello 指纹正常(来自 uTLS),Server Hello 和证书也是真实的(来自目标网站),整个握手过程没有任何可疑之处。
- 主动探测也无法识别:如果审查者主动连接 Reality 服务端,服务端会直接将其代理到真实目标网站。审查者看到的就是一个正常运行的网站,完全无法判断服务端背后是否运行着代理服务。
- 不需要自己的 TLS 证书:Reality 服务端不需要申请和维护 TLS 证书,因为它使用的是目标网站的真实证书。这也消除了因证书特征(如 Let’s Encrypt 的签发模式)被识别的风险。
Reality 的主要注意事项是目标网站(dest)的选择:应选择支持 TLS 1.3、有 HTTP/2 支持、在目标地区正常可访问且不会频繁变化的大型网站。
ECH (Encrypted Client Hello):未来方向
ECH(Encrypted Client Hello) 是 TLS 协议层面的一项标准化工作,其设计目的就是加密 Client Hello 中的敏感字段,从根本上消除 TLS 握手阶段的信息泄露。
ECH 的基本原理是:服务器预先通过 DNS 记录(HTTPS RR)发布一个公钥,客户端用这个公钥加密 Client Hello 中的敏感部分(称为 Inner Client Hello),只留下一个不包含有用信息的 Outer Client Hello 在明文中传输。服务器收到后用对应私钥解密 Inner Client Hello,获取客户端的真实请求参数。
如果 ECH 得到广泛部署,其影响是深远的:
- JA3/JA4 指纹将失去大部分价值:因为审查者只能看到 Outer Client Hello,而 Outer Client Hello 是标准化的,不再携带客户端的特征信息。
- SNI 也将被加密:审查者无法知道用户访问的是哪个域名。
然而,ECH 在当前阶段无法作为可靠的反审查手段,原因如下:
- GFW 已经在封锁 ECH:中国的网络审查系统已经开始检测并阻断携带 ECH 扩展的 TLS 连接。对于审查者来说,如果无法读取 Client Hello 的内容,直接阻断这类连接是最简单的策略。
- 部署范围有限:虽然 Chrome 和 Firefox 已经在新版本中支持了 ECH,但服务端的部署率仍然较低。ECH 需要 DNS 基础设施(特别是 HTTPS 记录类型)的配合,大规模普及还需要时间。
- DNS 污染的连锁影响:ECH 的公钥通过 DNS 分发,而 GFW 有成熟的 DNS 污染能力。如果 DNS 查询无法返回正确的 ECH 公钥,客户端就无法启用 ECH。
因此,ECH 代表的是一个正确的长期方向,但短期内用户仍然需要依赖 uTLS 和 Reality 等方案来应对 TLS 指纹检测。
实践:如何检查自己的指纹
理论之外,用户应当亲自验证自己的代理连接是否存在指纹泄露问题。以下是几种实用的检测方法:
方法一:使用在线检测工具
- 直接用浏览器访问 ja3er.com,记录页面显示的 JA3 指纹值。这是你浏览器的真实指纹。
- 通过代理访问同一网站,记录此时显示的 JA3 指纹值。这是你的代理客户端呈现给服务器的指纹。
- 对比两个指纹:
- 如果两者一致或非常接近,说明 uTLS 模拟成功,你的代理连接在 JA3 层面与浏览器无法区分。
- 如果两者明显不同,说明你的代理连接携带了非浏览器指纹,存在被识别的风险。
方法二:使用 Wireshark 抓包分析
对于需要更详细分析的用户,可以使用 Wireshark 在本地抓包:
- 启动 Wireshark,开始捕获网络流量。
- 在过滤器中输入
tls.handshake.type == 1过滤 Client Hello 消息。 - 展开 Client Hello 的详细信息,逐一检查 Cipher Suites、Extensions、Supported Groups 等字段。
- 对比代理连接的 Client Hello 与直接浏览器连接的 Client Hello,观察是否存在差异。
方法三:使用命令行工具
一些开源工具可以直接计算和展示 TLS 指纹,例如使用 curl 配合支持 JA3 输出的代理服务器,或者使用专门的 TLS 指纹分析工具如 ja3 和 ja4 的开源实现。
如果检测结果显示指纹不匹配,应检查代理客户端的配置,确保 fingerprint 参数已正确设置且选择了合适的浏览器指纹模板。
常见问题
Q: uTLS 选哪个浏览器指纹最好?
推荐选择 Chrome。原因很简单:Chrome 在全球浏览器市场中占据最高的份额(超过 60%)。选择 Chrome 指纹意味着你的 TLS 连接在统计上看起来最”普通”,不会因为使用了一个罕见的浏览器指纹而引起注意。相比之下,选择 Safari 或 Firefox 虽然也能工作,但这些浏览器的市场份额较低,如果某个 IP 地址同时发起了大量看起来来自 Safari 的连接,在统计分析中可能显得异常。此外,Chrome 指纹在 uTLS 中的维护也最为积极,通常能最快跟进新版本的变化。
Q: 不配置 fingerprint 参数会怎样?
如果不显式配置 fingerprint 参数,代理客户端将使用 Go 语言原生的 crypto/tls 库发起 TLS 连接。这意味着你的连接会携带一个典型的 Go 程序指纹,与任何主流浏览器都不相同。对于 GFW 的 TLS 指纹检测系统来说,这几乎等于在明文告诉审查者”我是一个 Go 语言程序”。在当前的网络审查环境下,不配置 fingerprint 参数是一个显著的安全隐患,强烈建议所有使用 TLS 类协议的用户都明确设置此参数。
Q: JA3 指纹可以完全伪造吗?
技术上可以通过 uTLS 完全伪造 JA3 指纹。uTLS 允许逐字段地构造 Client Hello,理论上可以精确复制任何浏览器的参数组合。但”完美度”取决于几个因素:uTLS 库中浏览器指纹模板的更新是否及时、GREASE 等随机化机制的模拟是否足够真实、以及 TLS 握手后续阶段的行为是否与浏览器一致。在实践中,uTLS 对 JA3 指纹的模拟已经足够欺骗目前已知的大多数检测系统,但不能排除未来出现更精细的检测手段(例如结合 JA3 之外的握手行为特征进行综合分析)。