Skip to content

About

Nginx module that calcuates fingerprints from the JA4+ suite

Resources

Stars

128 stars

Watchers

4 watching

Forks

Latest commit

 

History

255 Commits

Folders and files

Repository files navigation

JA4+ Nginx Module

An nginx module exposing JA4+ fingerprints as nginx variables for logging and request handling.

The module requires rebuilding nginx with the ClientHello capture patch, which saves the raw TLS ClientHello for the module to parse. The patch applies to nginx 1.26 and later. JA4T additionally requires the SYN capture patch. JA4TS uses the independent SYN-ACK capture patch and optional ebpf core addon.

Supported fingerprints

Fingerprint Nginx variables Output
JA4 $http_ssl_ja4, $http_ssl_ja4_string TLS client fingerprint; _string exposes the unhashed components.
JA4one $http_ssl_ja4one TLS client fingerprint variant that excludes dynamic extensions from its extension count and hash.
JA4H $http_ssl_ja4h, $http_ssl_ja4h_string HTTP request fingerprint; _string exposes header names and cookie data before hashing.
JA4T $http_ssl_ja4t TCP SYN fingerprint. Requires SYN capture.
JA4TS $http_ssl_ja4ts Upstream TCP SYN-ACK fingerprint of the final upstream attempt. Requires SYN-ACK capture.

JA4 and JA4one require TLS and are empty on plain HTTP. JA4H works with both HTTP and HTTPS; JA4T and JA4TS are independent of TLS.

Quick start

The Dockerfile and Compose configuration in docker/ provide a development/reference environment. Make sure Docker Compose and OpenSSL are installed, and ports 80 and 443 are available.

From the docker directory, generate the local test certificate and key (neither is committed), then start nginx:

cd docker
mkdir -p logs
openssl req -x509 -nodes -newkey rsa:2048 \
    -keyout server.key -out server.crt \
    -days 30 -subj "/CN=localhost"
docker compose up --build

In another terminal, request the fingerprint response:

curl -k https://localhost/

-k accepts the self-signed test certificate. The response uses docker/nginx.conf; JA4T is empty until SYN capture is enabled. Stop the environment with docker compose down in the same directory.

Building with nginx

Install a C compiler, make, patch, and the OpenSSL, PCRE and zlib development libraries, then unpack the nginx source. See the Dockerfile for a complete reference build and dependency versions.

Starting from this repository's root, apply the patches to the nginx source tree before configuring:

ja4_module_dir="$(pwd)"
cd /path/to/nginx-source
patch -p1 < "$ja4_module_dir/patches/nginx-ssl-save-client-hello.patch"
# Optional: include this patch if you need JA4T.
patch -p1 < "$ja4_module_dir/patches/nginx-tcp-save-syn.patch"
# Optional: include this patch and the ebpf addon below if you need JA4TS.
patch -p1 < "$ja4_module_dir/patches/nginx-tcp-save-synack.patch"
./configure --add-module="$ja4_module_dir" \
    --add-module="$ja4_module_dir/ebpf" \
    --with-http_ssl_module --with-http_v2_module
make
make install

Add your usual nginx configure options, such as --prefix, as needed. Without the SYN capture patch, the module still builds, but JA4T returns no value and the tcp_save_syn directive is unavailable.

The ebpf addon additionally needs libbpf-dev (1.3+), libelf-dev and pkg-config. It must be linked statically and changes the module signature, so --with-compat dynamic modules must be built with the same configuration. Without the SYN-ACK capture patch, JA4TS returns no value and the tcp_save_synack directive is unavailable; with the patch but without the addon, tcp_save_synack on fails nginx startup.

Configuration

Reference the variables in your nginx configuration, for example to log fingerprints. Define the log format inside http and select it in your server:

http {
    tcp_save_synack on; # Requires the optional SYN-ACK capture patch and ebpf addon.

    log_format fingerprints '$remote_addr "$request" '
                            'ja4=$http_ssl_ja4 ja4one=$http_ssl_ja4one '
                            'ja4h=$http_ssl_ja4h ja4t=$http_ssl_ja4t '
                            'ja4ts=$http_ssl_ja4ts';

    server {
        listen 443 ssl;
        tcp_save_syn on; # Requires the optional SYN capture patch.
        ssl_certificate /path/to/server.crt;
        ssl_certificate_key /path/to/server.key;
        access_log logs/access.log fingerprints;

        location / {
            proxy_pass http://127.0.0.1:9000; # Your upstream application.
        }
    }
}

JA4T

JA4T requires the optional SYN capture patch and a Linux kernel with TCP_SAVE_SYN support. Enable capture in the relevant server block, or at http or stream scope.

tcp_save_syn on;

Capture defaults to off because saved SYN packets cost memory. A fingerprint such as 64240_2-4-8-1-3_1460_7 contains the TCP window size, option kinds, MSS and window scale.

TCP_SAVE_SYN applies to the listening socket. If server blocks share a listen address, enabling capture in either block saves SYNs for every connection on that socket. Use a dedicated address/port to isolate the cost.

JA4T is empty when nginx is built without NGX_HAVE_TCP_SAVE_SYN, capture is off, or no SYN is available (for example with SYN cookies, Unix sockets or QUIC). Behind a TCP proxy, it fingerprints the proxy's connection to nginx.

JA4TS

JA4TS fingerprints the upstream server's SYN-ACK for proxied HTTP requests. It requires the optional SYN-ACK capture patch and the ebpf addon (see Building with nginx), but not the SYN capture patch. It needs Linux 6.4+ with kernel BTF and Netfilter BPF support. The nginx master must run as root or with CAP_BPF, CAP_PERFMON and CAP_NET_ADMIN; workers need no extra privileges. Enable capture at http, server or location level:

tcp_save_synack on;

tcp_save_synack defaults to off. The setting of the location that proxied the request applies, so tcp_save_synack off; in a location excludes its requests. Stream proxying is not captured.

$http_ssl_ja4ts contains the window size, option kinds, MSS and window scale, such as 64240_2-4-8-1-3_1460_7, for the final upstream attempt; unlike $upstream_addr, it is not a per-attempt list. A reused keepalive connection reports the SYN-ACK of its original handshake. The value is empty when capture is off, the request was not proxied, the connection failed, or no SYN-ACK was captured.

Capture that is enabled but cannot be loaded fails nginx startup. nginx -t does not check whether the kernel accepts the BPF programs, and turning capture on in a running instance where it was off everywhere requires a restart rather than a reload.

Known limitations:

  • DNAT/SNAT to upstreams on other hosts or in other network namespaces works; an upstream in nginx's own namespace reached through NAT (e.g. REDIRECT) is not captured.
  • A SYN-ACK rewritten before netfilter (MSS clamping, XDP or TC programs) is fingerprinted as rewritten; asymmetric rewriting (decapsulation, DSR) prevents capture.
  • Non-root workers are verified on Linux 6.8. On older kernels with kernel.unprivileged_bpf_disabled set, workers may be refused map access.

Testing

Run the Test::Nginx suite from the repository root. The CI workflows show the build and dependency setup.

Test::Nginx

The Perl suite (test/*.t) checks module loading, variable behavior on plain HTTP, JA4H request fingerprints, TLS ClientHello cases and JA4T/JA4TS TCP fingerprints.

Use nginx built with both patches and HTTP/2 and HTTP/3 support. Install Test::Nginx with cpanm, and build curlu with Go 1.24.0 using the curlu version pinned in CI. Its curl wrapper must be on PATH for the TLS, HTTP/3 and JA4T cases.

tls-ja4-clients.t checks exact fingerprints from real clients and also needs Docker, which runs its clients.

cpanm --local-lib="$HOME/perl5" Test::Nginx
export PERL5LIB="$HOME/perl5/lib/perl5${PERL5LIB:+:$PERL5LIB}"
export TEST_NGINX_BINARY=/path/to/nginx/objs/nginx
export PATH="/path/to/curlu:$PATH"
prove -v test/*.t

JA4T tests need Linux, root, ip and nft; they are skipped without root or curlu. Run them with:

sudo -E env PATH="$PATH" PERL5LIB="$PERL5LIB" \
    TEST_NGINX_BINARY="$TEST_NGINX_BINARY" prove -v test/ja4t-variables.t

JA4TS tests (test/ja4ts-*.t) need nginx built with the SYN-ACK capture patch and the ebpf addon. ja4ts-config.t runs as a normal user; the variable and network suites need root:

sudo -E env PATH="$PATH" PERL5LIB="$PERL5LIB" \
    TEST_NGINX_BINARY="$TEST_NGINX_BINARY" \
    prove -v test/ja4ts-variables.t test/ja4ts-network.t

Docker smoke test

docker/smoke-test.sh checks the image from Quick start: nginx accepts the mounted configuration, and an HTTPS request returns well-formed fingerprints. Start the Quick start environment in the background with docker compose up -d --build, then run:

docker/smoke-test.sh

Questions

If you have questions, feel free to reach out to us at info@foxio.io.

License

See LICENSE for the FoxIO License 1.1 terms.

About

Nginx module that calcuates fingerprints from the JA4+ suite

Resources

Stars

128 stars

Watchers

4 watching

Forks

Releases

Packages

Contributors

Languages