Skip to content

tls_inspector: add optional protocol parameter to JA4Fingerprinter::create - #46659

Open
dangle1 wants to merge 3 commits into
envoyproxy:mainfrom
dangle1:feat/ja4-protocol-parameter
Open

tls_inspector: add optional protocol parameter to JA4Fingerprinter::create#46659
dangle1 wants to merge 3 commits into
envoyproxy:mainfrom
dangle1:feat/ja4-protocol-parameter

Conversation

@dangle1

@dangle1 dangle1 commented Aug 11, 2026

Copy link
Copy Markdown

Summary

Per the JA4 spec, the first character of a JA4 fingerprint identifies the transport carrying the handshake: t for TLS, q for QUIC, d for DTLS. Envoy's implementation currently hard-codes t with a comment noting that only TLS is handled.

This change adds a public JA4Fingerprinter::Protocol enum and a defaulted second argument to JA4Fingerprinter::create so callers can select the transport they observed.

  • The default (Protocol::TLS) preserves the exact prior behavior of every existing caller, including the built-in TLS inspector listener filter — its call site is deliberately left byte-identical, since that filter only sees stream TLS handshakes today and has no new information to convey.
  • A downstream deployment that terminates QUIC (or a future in-tree QUIC listener filter that wants JA4) can now pass Protocol::QUIC and produce spec-conformant q... fingerprints without touching the JA4 library.

Design notes

  • Protocol is nested inside JA4Fingerprinter and scoped enum class : char. This is a static utility class, so nesting keeps the enum tightly bound to its only consumer.
  • Mapping happens through a small switch in an anonymous namespace rather than treating the enum value as a raw char. That's easier to read and makes any future non-single-character protocol identifier a compile-time obligation rather than an implicit reinterpretation.

Test plan

  • Added TEST(JA4Fingerprinter, ProtocolParameterControlsFirstCharacter) in ja4_fingerprint_test.cc that:
    • Verifies the no-arg create(&hello) is byte-identical to create(&hello, Protocol::TLS).
    • Verifies Protocol::QUIC and Protocol::DTLS produce fingerprints starting with q and d respectively.
    • Verifies the protocol argument affects only the first character; all other bytes of the fingerprint match across protocol choices.
  • bazel test //test/extensions/filters/listener/tls_inspector:ja4_fingerprint_test passes locally.
  • git diff origin/main -- source/extensions/filters/listener/tls_inspector/tls_inspector.cc is empty — no existing call site changed.

Notes

  • The pinned expected hashes in tls_inspector_ja4_test.cc were computed under the current t-only default; since the default is preserved they remain valid without modification.
  • This change was drafted with AI assistance and reviewed by the author.

…reate

Per the JA4 spec, the first character of a JA4 fingerprint identifies the
transport carrying the handshake: `t` for TLS, `q` for QUIC, and `d` for
DTLS. Envoy's implementation currently hard-codes `t` with a comment
noting that only TLS is handled.

Add a public `JA4Fingerprinter::Protocol` enum and a defaulted second
argument to `JA4Fingerprinter::create` so callers can select the transport
they observed. The default (`Protocol::TLS`) preserves the exact prior
behavior of every existing caller, including the built-in TLS inspector
listener filter, whose call site is deliberately left byte-identical --
that filter only ever sees stream TLS handshakes today, so it has no new
information to convey.

A downstream deployment that terminates QUIC (or a future in-tree QUIC
listener filter that wants JA4) can now pass `Protocol::QUIC` and produce
spec-conformant `q...` fingerprints without touching the JA4 library.

Adds a unit test that exercises all three protocol values plus the no-arg
overload, and asserts that the protocol argument controls only the first
character of the fingerprint.

Spec reference:
https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4.md

Note: this change was drafted with AI assistance and reviewed by the author.
Signed-off-by: dangle <dangle@pinterest.com>
@repokitteh-read-only

Copy link
Copy Markdown

Hi @dangle1, welcome and thank you for your contribution.

We will try to review your Pull Request as quickly as possible.

In the meantime, please take a look at the contribution guidelines if you have not done so already.

🐱

Caused by: #46659 was opened by dangle1.

see: more, trace.

@agrawroh agrawroh left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please also add some more integration tests for this? We have a suite which replays the TCP handshake captures and make sure that we see expected JA4 values.

…eter

Extend the ``JA4Fingerprinter::Protocol`` parameter change with an
integration test that drives the full wire-bytes -> parse -> fingerprint
pipeline:

Decode Browser-1's captured Chrome-stable ClientHello (the same hex used
by tls_inspector_ja4_test.cc's JA4_TEST_VECTORS baseline) through
BoringSSL's SSL_parse_client_hello, then call JA4Fingerprinter::create
with each Protocol value and assert:
  - the no-arg default and Protocol::TLS produce the pinned Browser-1
    fingerprint from tls_inspector_ja4_test.cc, byte-identical, proving
    the default preserves prior behavior end-to-end;
  - Protocol::QUIC and Protocol::DTLS change only the first character
    (to 'q' and 'd' respectively) and leave every other byte of the
    fingerprint unchanged, per the JA4 spec.

Requires adding an ssl external dep and //source/common/ssl:ssl_lib
to ja4_fingerprint_test's BUILD deps for SSL_parse_client_hello and the
SSL_SELECT macro.

Note: this change was drafted with AI assistance and reviewed by the author.
Signed-off-by: dangle <dangle@pinterest.com>
@dangle1
dangle1 had a problem deploying to external-contributors August 13, 2026 18:35 — with GitHub Actions Error
Reviewers asked for integration coverage in the suite that replays captured
TCP-handshake ClientHellos and asserts expected JA4 values -- namely
`tls_inspector_ja4_test.cc`. This commit adds a second parameterized suite,
`TlsInspectorJA4ProtocolArgTest`, that iterates over the same
`JA4_TEST_VECTORS` corpus as `TlsInspectorJA4Test` and, for each captured
ClientHello:

- decodes the wire bytes and parses the ClientHello via
  `SSL_parse_client_hello` (the same parse path the listener filter drives
  via BoringSSL's ssl-early callback),
- calls `JA4Fingerprinter::create` with the no-arg default and with each
  `Protocol` value, and
- asserts that the default and `Protocol::TLS` match the corpus's pinned
  expected fingerprint byte-for-byte, and that `Protocol::QUIC` and
  `Protocol::DTLS` change only the first character.

This reuses the reviewer-approved capture corpus without wiring the new
argument into `tls_inspector.cc`, which is deliberately kept byte-identical
per the PR's contract (the listener filter has no way to distinguish
transports today, so any per-transport selection would be speculative).

Note: this change was drafted with AI assistance and reviewed by the author.
Signed-off-by: dangle <dangle@pinterest.com>
@dangle1
dangle1 requested a deployment to external-contributors August 13, 2026 18:41 — with GitHub Actions Waiting
@dangle1

dangle1 commented Aug 13, 2026

Copy link
Copy Markdown
Author

Hi @agrawroh, I've added integration tests. Please let me know if they are sufficient.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants