Envoy JA4 指纹 ALPN 十六进制编码修复:`ja4_alpn_hex_conversion_fix` 运行时开关解析
2026/9/11 14:44:54 网站建设 项目流程

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 flagenvoy.reloadable_features.ja4_alpn_hex_conversion_fix.

逐句拆解其技术要点:

  1. Bug 位置:JA4 指纹生成逻辑,具体位于 TLS Inspector 监听器过滤器内的JA4Fingerprinter实现(source/extensions/filters/listener/tls_inspector/ja4_fingerprint.cc)。
  2. Bug 现象:当 ClientHello 中的 ALPN 协议名首字符或末字符不是字母/数字(如包含-_/*等符号)时,生成的指纹编码不符合 JA4 规范。
  3. 修复方式:改为输出符合 JA4 规范的十六进制编码。
  4. 可控性:修复由运行时开关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
XXTLS 版本(13121110等)
d/iSNI 是否存在(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 字符编码)。因此旧实现存在两类偏差:

  1. 输出长度错误:非字母数字场景下输出了 4 个 hex 字符,破坏了tXXdYYZZ后紧跟 2 字符 ALPN 段的固定布局,导致后续字段解析错位、指纹长度不统一;
  2. 不符合规范语义: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+ 末字符低半字节0xf2f,恰好 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)原样输出;修复只影响“至少一端非字母数字”的边界场景,对绝大多数携带h2http/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文件,内容为truefalse。默认值为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 值(如0x0a0a0xaaaa0xfafa)返回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,便于复用与测试,也说明该实现是独立可测的模块。

升级与回滚建议

  1. 默认行为:新版本中envoy.reloadable_features.ja4_alpn_hex_conversion_fix默认开启,指纹生成自动遵循 JA4 规范,无需改动配置;
  2. 指纹一致性:若你正在使用基于 JA4 指纹的威胁情报/客户端识别系统,且客户端 ALPN 中包含非字母数字字符,升级后这些客户端的指纹会从旧的 4 字符输出变为新的 2 字符输出,需要同步更新下游分析规则;
  3. 回滚路径:在多实例滚动升级期间,可通过运行时文件将开关临时置为false,保持新旧实例输出一致,待全部升级完成后移除覆盖值即可;
  4. 验证手段:升级后可通过访问日志的%TLS_JA4_FINGERPRINT%抽查指纹,确认 ALPN 段恒为 2 字符且整体匹配规范正则,再逐步放开下游指纹库的比对。

【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询