Envoy JA4 指纹 ALPN 十六进制编码修复:ja4_alpn_hex_conversion_fix运行时开关解析
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
导读
本文围绕 Envoy 当前版本变更日志中的一条 bug 修复(changelogs/current/bug_fixes/ja4__alpn_non_alphanumeric.rst)展开:JA4 指纹生成在遇到非字母数字(non-alphanumeric)ALPN 字符时,原先未按 JA4 规范将其编码为符合规范的十六进制值,该问题现由运行时开关envoy.reloadable_features.ja4_alpn_hex_conversion_fix控制修复。读完本文,你将掌握:JA4 指纹的完整生成格式与%TLS_JA4_FINGERPRINT%格式化指令的用法、旧实现与新实现针对 ALPN 字符的编码差异、该运行时开关的配置方式,以及底层源码实现与测试用例的证据位置,便于在实际运维和二次开发中对照使用。
变更内容概述
原变更日志条目(即本文的关联文档)全文如下:
Fixed a bug where JA4 fingerprint generation did not correctly encode non-alphanumeric ALPN characters as spec-compliant hexadecimal values. The fix is guarded by the runtime flag
envoy.reloadable_features.ja4_alpn_hex_conversion_fix.
逐句拆解其技术要点:
- Bug 位置:JA4 指纹生成逻辑,具体位于 TLS Inspector 监听器过滤器内的
JA4Fingerprinter实现(source/extensions/filters/listener/tls_inspector/ja4_fingerprint.cc)。 - Bug 现象:当 ClientHello 中的 ALPN 协议名首字符或末字符不是字母/数字(如包含
-、_、/、*等符号)时,生成的指纹编码不符合 JA4 规范。 - 修复方式:改为输出符合 JA4 规范的十六进制编码。
- 可控性:修复由运行时开关
envoy.reloadable_features.ja4_alpn_hex_conversion_fix保护,便于灰度发布与回滚。
JA4 指纹与 ALPN 字符在其中的作用
JA4(JARM Advanced,FoxIO 提出的 TLS 指纹方案)是对 JA3 的改进。Envoy 在 ja4_fingerprint.h 的类注释中给出了其内部格式定义:
tXXdYYZZ_CIPHERHASH_EXTENSIONHASH各字段含义如下:
| 字段 | 说明 |
|---|---|
t | 协议类型(t表示 TLS,q表示 QUIC,d表示 DTLS);当前实现仅处理 TLS,固定为t |
XX | TLS 版本(13、12、11、10等) |
d/i | SNI 是否存在(d= 存在域名,i= 无 SNI) |
YY | 密码套件数量(2 位数字) |
ZZ | 扩展数量(2 位数字) |
_CIPHERHASH | 密码套件列表 SHA-256 哈希的前 12 个十六进制字符 |
_EXTENSIONHASH | 扩展列表 SHA-256 哈希的前 12 个十六进制字符 |
ALPN(Application-Layer Protocol Negotiation)字符是ZZ之后的两个字符,即getJA4AlpnChars()的返回值:取客户端 ALPN 协议列表中第一个协议名的首字符和该协议名的末字符,在 ja4_fingerprint.cc 中实现:
std::string JA4Fingerprinter::getJA4AlpnChars(const SSL_CLIENT_HELLO* ssl_client_hello) { // 从 ClientHello 的 ALPN 扩展(TLSEXT_TYPE_application_layer_protocol_negotiation)读取协议列表 // ... char first = proto[0]; char last = proto[proto.length() - 1]; // ... 根据运行时开关决定编码方式 return absl::StrFormat("%c%c", first, last); }常见的h2(HTTP/2)、http/1.1等协议名全部由字母数字组成,首末字符直接按原样输出即可。但当协议名首末字符为非字母数字(如-、_、/、*、:等)时,直接输出原字符会破坏 JA4 指纹的字符集约定(JA4 规范要求指纹仅含小写字母与数字),这正是本次修复要解决的问题。
Bug 根因:旧实现与非规范十六进制编码
在开启修复开关之前,旧逻辑位于 ja4_fingerprint.cc 的else分支:
} else { if (!absl::ascii_isalnum(first) || !absl::ascii_isalnum(last)) { // Convert to hex if non-alphanumeric return absl::StrFormat("%02x%02x", static_cast<uint8_t>(first), static_cast<uint8_t>(last)); } }问题在于:%02x%02x会把首、末字符各自的完整字节值转换为两个十六进制数(每个 1 字节 → 2 个 hex 字符),最终输出4 个 hex 字符。
而 JA4 规范要求 ALPN 部分始终恰好是 2 个字符(字母数字原样输出,非字母数字以 2 个 hex 字符编码)。因此旧实现存在两类偏差:
- 输出长度错误:非字母数字场景下输出了 4 个 hex 字符,破坏了
tXXdYYZZ后紧跟 2 字符 ALPN 段的固定布局,导致后续字段解析错位、指纹长度不统一; - 不符合规范语义:JA4 规范对 ALPN 段(
ZZ之后的两字符)定义为“首字符的高半字节(nibble)与末字符的低半字节拼成 1 字节,再以 2 个 hex 字符表示”,旧实现并未遵循这一拼装规则。
修复实现:符合 JA4 规范的 2 字符十六进制编码
开启运行时开关后,走 ja4_fingerprint.cc 的新分支:
if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.ja4_alpn_hex_conversion_fix")) { if (!absl::ascii_isalnum(first) || !absl::ascii_isalnum(last)) { // Per JA4 spec, output exactly 2 hex chars for non-alphanumeric ALPN values: // Extract the high nibble (>> 4) of the first byte and low nibble (& 0x0F) of the last byte. return absl::StrFormat("%x%x", (static_cast<uint8_t>(first) >> 4) & 0x0F, static_cast<uint8_t>(last) & 0x0F); } }新实现严格按 JA4 规范编码:
- 取首字符的高半字节:
(static_cast<uint8_t>(first) >> 4) & 0x0F,即第一个字节右移 4 位后取低 4 位; - 取末字符的低半字节:
static_cast<uint8_t>(last) & 0x0F,即最后一个字节的低 4 位; - 两者各格式化为 1 个 hex 字符(
%x%x),组合起来恰好 2 个 hex 字符,与字母数字场景的输出宽度保持一致。
例如,假设 ALPN 协议名首字符为-(ASCII 0x2D),末字符为/(ASCII 0x2F),则:
- 旧实现输出
2d2f(4 字符,非规范); - 新实现输出
22(首字符高半字节0x2+ 末字符低半字节0xf→2f,恰好 2 字符)。
这样无论 ALPN 协议名是否包含特殊符号,JA4 指纹的 ALPN 段始终固定为 2 个字符,保证整体指纹格式严格符合t[0-9]{2}[di][0-9]{2}[0-9]{2}[0-9a-z]{2}_[0-9a-f]{12}_[0-9a-f]{12}的规范模式(该正则模式也出现在 test/extensions/filters/listener/tls_inspector/ja4_fingerprint_test.cc 的测试注释中)。
值得注意的是:无论开关是否开启,当首末字符均为字母数字时,两者都走absl::StrFormat("%c%c", first, last)原样输出;修复只影响“至少一端非字母数字”的边界场景,对绝大多数携带h2、http/1.1等常规 ALPN 的客户端行为完全不变,因此默认开启该开关对存量指纹数据的影响面极小。
运行时开关:envoy.reloadable_features.ja4_alpn_hex_conversion_fix
该开关在 source/common/runtime/runtime_features.cc 中通过RUNTIME_GUARD宏注册:
RUNTIME_GUARD(envoy_reloadable_features_ja4_alpn_hex_conversion_fix);RUNTIME_GUARD是 Envoy 运行时特性体系的统一声明机制,会在启动时自动生成默认开启的布尔特性。其判定结果通过 source/common/runtime/runtime_features.h 暴露给调用方,由 ja4_fingerprint.cc 中的Runtime::runtimeFeatureEnabled(...)读取。
如何配置该开关
用户可通过 Envoy 的运行时配置文件(--runtime指定的文件目录或 admin 接口/runtime)覆盖默认值。典型配置片段如下:
runtime: symlink_root: /srv/runtime/current # 在 /srv/runtime/current/envoy/reloadable_features/ja4_alpn_hex_conversion_fix 文件中写入: # true -> 启用新编码(符合 JA4 规范,默认) # false -> 回退旧编码(%02x%02x 四字符输出)即:在运行时根目录下创建envoy/reloadable_features/ja4_alpn_hex_conversion_fix文件,内容为true或false。默认值为true(RUNTIME_GUARD 特性默认开启),因此无需任何配置即可获得修复后的行为;若在灰度升级期间需要与旧版本 Envoy 保持一致的指纹输出,可临时将该文件内容置为false实现回滚,待全量升级完成后再删除覆盖。
修复在指纹生成链路中的位置
JA4 指纹的整体生成入口是 ja4_fingerprint.cc 的JA4Fingerprinter::create(),它按顺序拼接各段:
std::string JA4Fingerprinter::create(const SSL_CLIENT_HELLO* ssl_client_hello) { return absl::StrCat( "t", // 协议类型,当前仅 TLS getJA4TlsVersion(ssl_client_hello), // TLS 版本 hasSNI(ssl_client_hello) ? "d" : "i", // SNI 是否存在 formatTwoDigits(countCiphers(ssl_client_hello)),// 密码套件数量 formatTwoDigits(countExtensions(ssl_client_hello)), // 扩展数量 getJA4AlpnChars(ssl_client_hello), // ALPN 首末字符(本次修复点) "_", getJA4CipherHash(ssl_client_hello), // 密码套件 SHA-256 前 12 hex "_", getJA4ExtensionHash(ssl_client_hello)); // 扩展 + 签名算法 SHA-256 前 12 hex }调用关系与配置入口如下:
- 配置开关:TLS Inspector 监听器过滤器的 proto 配置项
enable_ja4_fingerprinting(api/envoy/extensions/filters/listener/tls_inspector/v3/tls_inspector.proto),默认false,需要显式开启后才计算 JA4 指纹; - 过滤器调用:在 tls_inspector.cc 中,当
enableJA4Fingerprinting()为真时调用JA4Fingerprinter::create()生成指纹并写入 StreamInfo; - 对外输出:指纹可通过访问日志格式化指令
%TLS_JA4_FINGERPRINT%输出,详见 docs/root/configuration/advanced/substitution_formatter.rst。该指令适用于 HTTP/TCP/THRIFT 协议,若未使用 TLS 或握手未完成则输出"-"。
# 启用 TLS Inspector 并开启 JA4 指纹 listener_filters: - name: envoy.filters.listener.tls_inspector typed_config: "@type": type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector enable_ja4_fingerprinting: true # 访问日志中输出 JA4 指纹 access_log: - name: envoy.access_loggers.file typed_config: "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog path: /dev/stdout format: "%TLS_JA4_FINGERPRINT%\n"测试与验证
仓库中的单元测试位于 test/extensions/filters/listener/tls_inspector/ja4_fingerprint_test.cc,覆盖了以下关键点:
- GREASE 值过滤(
GreaseValueFiltering):验证isNotGrease()对 RFC 8701 定义的 16 个 GREASE 值(如0x0a0a、0xaaaa、0xfafa)返回false,对普通值返回true。GREASE 值在计数、哈希前被剔除,避免指纹被客户端随机填充干扰; - 指纹格式集成(
TlsInspectorJA4IntegrationTest):通过 tls_utility.h 的generateClientHello()构造真实的 TLS 1.2/TLS 1.3 ClientHello(含 SNI、ALPN 等变体),验证数据能够被正确识别(例如含 ALPNh2的报文能被检测到),其格式期望为正则t[0-9]{2}[di][0-9]{2}[0-9]{2}[0-9a-z]{2}_[0-9a-f]{12}_[0-9a-f]{12}——其中[0-9a-z]{2}正是 ALPN 段,要求严格为 2 个小写字母数字字符,与本次修复的目标一致。
此外,构建层面 source/extensions/filters/listener/tls_inspector/BUILD 将ja4_fingerprint.cc/.h单独封装为ja4_fingerprint_lib,便于复用与测试,也说明该实现是独立可测的模块。
升级与回滚建议
- 默认行为:新版本中
envoy.reloadable_features.ja4_alpn_hex_conversion_fix默认开启,指纹生成自动遵循 JA4 规范,无需改动配置; - 指纹一致性:若你正在使用基于 JA4 指纹的威胁情报/客户端识别系统,且客户端 ALPN 中包含非字母数字字符,升级后这些客户端的指纹会从旧的 4 字符输出变为新的 2 字符输出,需要同步更新下游分析规则;
- 回滚路径:在多实例滚动升级期间,可通过运行时文件将开关临时置为
false,保持新旧实例输出一致,待全部升级完成后移除覆盖值即可; - 验证手段:升级后可通过访问日志的
%TLS_JA4_FINGERPRINT%抽查指纹,确认 ALPN 段恒为 2 字符且整体匹配规范正则,再逐步放开下游指纹库的比对。
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考