Skip to content

Repository files navigation

NOTICE

Development for JA4 on Nginx has been on pause due to other priorities taking development resources. This version of JA4 has known issues and bugs and may not produce correct JA4 values. Use at your own risk. We will continue development here as soon as resources become available. If you have questions, feel free to reach out to us at info@foxio.io.

JA4 on Nginx

This repository contains an nginx module that generates fingerprints from the JA4 suite. Additionally, a small patch to the nginx core is provided and necessary to for the module to function.

Usage

Docker images and compose files are available in ./docker. The QUIC and ModSecurity images are still WIP.

You can quickly test out this module with:

  1. cd docker
  2. docker-compose up --build

A multi-stage Dockerfile is also included in the project root. You can run the build directly from the root using:

docker-compose up --build

You can also build from source with:

  1. docker build -t ja4-nginx:source .
  2. docker run -p 80:80 -p 443:443 ja4-nginx:source

Testing

There are two complementary suites:

Suite Path Framework What it covers
Test::Nginx test/*.t Test::Nginx + prove Module load, config/variables, plain-HTTP safety, JA4H (planned)
Integration / TLS test/*.py pytest + Docker JA4 fingerprint goldens, ClientHello edge cases (curl, uTLS, curl_cffi)

Test::Nginx tests (test/*.t)

Test::Nginx cases live under test/*.t. Each case embeds a small nginx config, starts nginx, issues a request with the default Test::Nginx client (Perl IO::Socket, plain HTTP/1.1), and asserts on the response.

Requirements

  • nginx built with this module (and the required core patch)
  • Perl modules: Test::Nginx (and dependencies)
# example: local install of Test::Nginx
cpanm --local-lib=~/perl5 Test::Nginx
export PERL5LIB=$HOME/perl5/lib/perl5${PERL5LIB:+:$PERL5LIB}

Run

export TEST_NGINX_BINARY=/path/to/nginx   # binary built with this module
prove -v test/*.t
# or a single file:
prove -v test/plain-http-variables.t

TEST_NGINX_SERVROOT is optional. Each .t file defaults it to test/servroot so Test::Nginx does not write t/servroot. Set the variable only if you need a different path.

Dump the HTTP response on success

TEST_NGINX_VERBOSE=1 prove -v test/plain-http-variables.t

Current files:

  • test/plain-http-variables.t — module loads ($http_ssl_ja4h), SSL JA4 vars empty and safe on plain HTTP

Runtime tree test/servroot/ is created by Test::Nginx and is gitignored.

Integration tests (test/*.py)

Integration tests run against Docker and validate the module’s TLS fingerprinting against predefined scenarios using golden files in test/testdata/.

Run tests:

pytest

Update golden files:

pytest --record

Current coverage includes various TLS versions, HTTP protocols, and ALPN/cipher/extension combinations.

Docker

We publish and host Docker images of release versions on GitHub Container Registry. You can pull the image with the following command:

docker pull ghcr.io/foxio-llc/ja4-nginx-module:v0.9.0-beta

Debugging

To develop and debug the Dockerfile container, I find it useful to run docker with --progress=plain.

Developer Guide

Build against official nginx: apply patches/nginx.patch to the nginx source tree, then configure with --add-module=/path/to/ja4-nginx-module.

cd nginx-${NGINX_VERSION}
patch -p1 < /path/to/ja4-nginx-module/patches/nginx.patch
./configure --add-module=/path/to/ja4-nginx-module --with-http_ssl_module ...
make && make install

The root Dockerfile is a full reference build. See also Usage and Testing above for Docker and pytest.

Creating a Release

  1. Tag the release git tag -a vx.y.z-beta -m "Release version x.y.z"
  2. Run script ./release.sh
  3. Push tag to GitHub git push origin vx.y.z-beta
  4. Create a release on GitHub Manually upload the tar.gz file and the sha256sum

Release a Docker Image to GitHub Container Registry

Update the file docker/Dockerfile to pull from the most recently published release. Then build and tag the image: cd docker READ BELOW UPDATE JA4_MODULE_VERSION IN DOCKERFILE TO BUILD FROM NEW RELEASE docker build -t ghcr.io/foxio-llc/ja4-nginx-module:vx.y.z-beta .

Then push the image to the GitHub Container Registry: docker push ghcr.io/foxio-llc/ja4-nginx-module:vx.y.z-beta

Architecture

Nginx Variables

We create an Nginx variable for each JA4 fingerprint.

These can be accessed through configuration files for logging purposes, in server definition blocks for custom headers, etc.

All of the logic around these variables are in two files:

  1. ngx_http_ja4_module.c
  2. ngx_http_ja4_module.h

Nginx Configuration

An Nginx variable simply needs a string for its name, and a function that calculates and returns the value.

By using this syntax:

static ngx_http_variable_t ngx_http_ssl_ja4_variables_list[] = {
    {ngx_string("http_ssl_ja4"),
     NULL,
     ngx_http_ssl_ja4,
     0, 0, 0},
}

The function the variable maps to, in this case ngx_http_ssl_ja4, receives the request sent to Nginx, a variable that will store the result, and a pointer to the variable's data.

static ngx_int_t ngx_http_ssl_ja4(ngx_http_request_t *r, ngx_http_variable_value_t *v, uintptr_t data);

So, this function is called for each request and it is expected to return the data intended for the variable.

In this function, we call two important functions. First:

int ngx_ssl_ja4(ngx_connection_t *c, ngx_pool_t *pool, ngx_ssl_ja4_t *ja4);

The first gets the connection object from the request (This is an Nginx native structure that we've modified with the ja4-nginx repository to store additional data for the JA4 fingerprint), pulls in SSL data from that object, and processes it to be stored in a custom structure (defined in the header file) for this module's Nginx variable.

For this example:

typedef struct ngx_ssl_ja4_s
{
    const char *version; // TLS version

    unsigned char transport; // 'q' for QUIC, 't' for TCP

    unsigned char has_sni; // 'd' if SNI is present, 'i' otherwise

    size_t ciphers_sz;       // Count of ciphers
    unsigned short *ciphers; // List of ciphers

    size_t extensions_sz;       // Count of extensions
    unsigned short *extensions; // List of extensions

    size_t sigalgs_sz;       // Count of signature algorithms
    char **sigalgs; // List of signature algorithms

    // For the first and last ALPN extension values
    char *alpn_first_value;

    char cipher_hash[65];           // 32 bytes * 2 characters/byte + 1 for '\0'
    char cipher_hash_truncated[13]; // 12 bytes * 2 characters/byte + 1 for '\0'

    char extension_hash[65];           // 32 bytes * 2 characters/byte + 1 for '\0'
    char extension_hash_truncated[13]; // 6 bytes * 2 characters/byte + 1 for '\0'

} ngx_ssl_ja4_t;

The second important function is the one that actually calculates the JA4 fingerprint:

void ngx_ssl_ja4_fp(ngx_pool_t *pool, ngx_ssl_ja4_t*ja4, ngx_str_t *out);

It simply takes the data structure and uses it to calculate what the single string value of the JA4 fingerprint should be.

About

Nginx module that calcuates fingerprints from the JA4+ suite

Resources

Stars

121 stars

Watchers

3 watching

Forks

Releases

Packages

Contributors

Languages