Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 61 additions & 5 deletions plugins/experimental/ja4_fingerprint/plugin.cc
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@
#include <openssl/ssl.h>

#include <arpa/inet.h>
#include <getopt.h>
#include <netinet/in.h>

#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <memory>
#include <string>
#include <string_view>
Expand Down Expand Up @@ -79,8 +81,37 @@ constexpr int SSL_SUCCESS{1};

DbgCtl dbg_ctl{PLUGIN_NAME};

int global_preserve_enabled{0};

} // end anonymous namespace

static bool
read_config_option(int argc, char const *argv[], int &preserve)
{
const struct option longopts[] = {
{"preserve", no_argument, &preserve, 1},
{nullptr, 0, nullptr, 0}
};

optind = 0;
int opt{0};
while ((opt = getopt_long(argc, const_cast<char *const *>(argv), "", longopts, nullptr)) >= 0) {
switch (opt) {
case '?':
Dbg(dbg_ctl, "Unrecognized command argument.");
case 0:
case -1:
break;
default:
Dbg(dbg_ctl, "Unexpected options error.");
return false;
}
}

Dbg(dbg_ctl, "JA4 preserve is %s", (preserve == 1) ? "enabled" : "disabled");
return true;
}

static int *
get_user_arg_index()
{
Expand Down Expand Up @@ -112,12 +143,16 @@ make_word(unsigned char lowbyte, unsigned char highbyte)
}

void
TSPluginInit(int /* argc ATS_UNUSED */, char const ** /* argv ATS_UNUSED */)
TSPluginInit(int argc, char const **argv)
{
if (!register_plugin()) {
TSError("[%s] Failed to register.", PLUGIN_NAME);
return;
}
if (!read_config_option(argc, argv, global_preserve_enabled)) {
TSError("[%s] Failed to parse options.", PLUGIN_NAME);
return;
}
reserve_user_arg();
if (!create_log_file()) {
TSError("[%s] Failed to create log.", PLUGIN_NAME);
Expand Down Expand Up @@ -334,12 +369,36 @@ handle_read_request_hdr(TSCont cont, TSEvent event, void *edata)
return TS_SUCCESS;
}

// Check if a header field exists in the request.
static bool
header_exists(TSMBuffer bufp, TSMLoc hdr_loc, char const *field, int field_len)
{
TSMLoc loc = TSMimeHdrFieldFind(bufp, hdr_loc, field, field_len);
if (loc != TS_NULL_MLOC) {
TSHandleMLocRelease(bufp, hdr_loc, loc);
return true;
}
return false;
}

void
append_JA4_headers(TSCont /* cont ATS_UNUSED */, TSHttpTxn txnp, std::string const *fingerprint)
{
TSMBuffer bufp;
TSMLoc hdr_loc;
if (TS_SUCCESS == TSHttpTxnClientReqGet(txnp, &bufp, &hdr_loc)) {
if (TS_SUCCESS != TSHttpTxnClientReqGet(txnp, &bufp, &hdr_loc)) {
Dbg(dbg_ctl, "Failed to get headers.");
return;
}

// When preserve is enabled, check if ANY JA4 header exists. If so, skip
// adding ALL JA4 headers to avoid mismatched fingerprint data when requests
// traverse multiple proxies.
bool const ja4_header_exists = header_exists(bufp, hdr_loc, "ja4", 3) ||
header_exists(bufp, hdr_loc, JA4_VIA_HEADER.data(), static_cast<int>(JA4_VIA_HEADER.length()));
bool const skip_ja4_headers = global_preserve_enabled && ja4_header_exists;

if (!skip_ja4_headers) {
append_to_field(bufp, hdr_loc, "ja4", 3, fingerprint->data(), fingerprint->size());

TSMgmtString proxy_name = nullptr;
Expand All @@ -351,9 +410,6 @@ append_JA4_headers(TSCont /* cont ATS_UNUSED */, TSHttpTxn txnp, std::string con
append_to_field(bufp, hdr_loc, JA4_VIA_HEADER.data(), static_cast<int>(JA4_VIA_HEADER.length()), proxy_name,
static_cast<int>(std::strlen(proxy_name)));
TSfree(proxy_name);

} else {
Dbg(dbg_ctl, "Failed to get headers.");
}

TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc);
Expand Down
50 changes: 35 additions & 15 deletions plugins/ja3_fingerprint/ja3_fingerprint.cc
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,18 @@ append_to_field(TSMBuffer bufp, TSMLoc hdr_loc, const char *field, int field_len
TSHandleMLocRelease(bufp, hdr_loc, target);
}

// Check if a header field exists in the request.
static bool
header_exists(TSMBuffer bufp, TSMLoc hdr_loc, char const *field, int field_len)
{
TSMLoc loc = TSMimeHdrFieldFind(bufp, hdr_loc, field, field_len);
if (loc != TS_NULL_MLOC) {
TSHandleMLocRelease(bufp, hdr_loc, loc);
return true;
}
return false;
}

static ja3_data *
create_ja3_data(TSVConn const ssl_vc)
{
Expand Down Expand Up @@ -258,23 +270,31 @@ modify_ja3_headers(TSCont contp, TSHttpTxn txnp, ja3_data const *ja3_vconn_data)
TSAssert(TS_SUCCESS == TSHttpTxnServerReqGet(txnp, &bufp, &hdr_loc));
}

TSMgmtString proxy_name = nullptr;
if (TS_SUCCESS != TSMgmtStringGet("proxy.config.proxy_name", &proxy_name)) {
TSError("[%s] Failed to get proxy name for %s, set 'proxy.config.proxy_name' in records.config", PLUGIN_NAME,
JA3_VIA_HEADER.data());
proxy_name = TSstrdup("unknown");
}
append_to_field(bufp, hdr_loc, JA3_VIA_HEADER.data(), static_cast<int>(JA3_VIA_HEADER.length()), proxy_name,
static_cast<int>(std::strlen(proxy_name)), preserve_flag);
TSfree(proxy_name);
// When preserve is enabled, check if ANY JA3 header exists. If so, skip
// adding ALL JA3 headers to avoid mismatched fingerprint data when requests
// traverse multiple proxies.
bool const ja3_header_exists = header_exists(bufp, hdr_loc, JA3_VIA_HEADER.data(), static_cast<int>(JA3_VIA_HEADER.length())) ||
header_exists(bufp, hdr_loc, "x-ja3-sig", 9) || header_exists(bufp, hdr_loc, "x-ja3-raw", 9);
bool const skip_ja3_headers = preserve_flag && ja3_header_exists;

if (!skip_ja3_headers) {
TSMgmtString proxy_name = nullptr;
if (TS_SUCCESS != TSMgmtStringGet("proxy.config.proxy_name", &proxy_name)) {
TSError("[%s] Failed to get proxy name for %s, set 'proxy.config.proxy_name' in records.config", PLUGIN_NAME,
JA3_VIA_HEADER.data());
proxy_name = TSstrdup("unknown");
}
append_to_field(bufp, hdr_loc, JA3_VIA_HEADER.data(), static_cast<int>(JA3_VIA_HEADER.length()), proxy_name,
static_cast<int>(std::strlen(proxy_name)), false);

// Add JA3 md5 fingerprints
append_to_field(bufp, hdr_loc, "x-ja3-sig", 9, ja3_vconn_data->md5_string, 32, preserve_flag);
// Add JA3 md5 fingerprints.
append_to_field(bufp, hdr_loc, "x-ja3-sig", 9, ja3_vconn_data->md5_string, 32, false);

// If raw string is configured, added JA3 raw string to header as well
if (raw_flag) {
append_to_field(bufp, hdr_loc, "x-ja3-raw", 9, ja3_vconn_data->ja3_string.data(), ja3_vconn_data->ja3_string.size(),
preserve_flag);
// If raw string is configured, add JA3 raw string to header as well.
if (raw_flag) {
append_to_field(bufp, hdr_loc, "x-ja3-raw", 9, ja3_vconn_data->ja3_string.data(), ja3_vconn_data->ja3_string.size(), false);
}
TSfree(proxy_name);
}
TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,18 @@ def _configure_server(self, tr: 'TestRun') -> None:
self._server.Streams.All += Testers.ContainsExpression("https-request", "Verify the HTTPS request was received.")
self._server.Streams.All += Testers.ContainsExpression("http2-request", "Verify the HTTP/2 request was received.")
if not self._test_remap:
# Verify --preserve worked.
self._server.Streams.All += Testers.ContainsExpression("x-ja3-raw: .*,", "Verify the new raw header was added.")
# The first request has no existing JA3 headers, so headers are added.
self._server.Streams.All += Testers.ContainsExpression(
"x-ja3-raw: first-signature", "Verify the already-existing raw header was preserved.")
self._server.Streams.All += Testers.ExcludesExpression(
"x-ja3-raw: first-signature;", "Verify no extra values were added due to preserve.")
"x-ja3-raw: .*,", "Verify the new raw header was added.", reflags=re.IGNORECASE)
self._server.Streams.All += Testers.ContainsExpression("x-ja3-via: test.proxy.com", "The x-ja3-via string was added.")
# The second request has existing JA3 headers. With --preserve,
# no new JA3 headers are added (including x-ja3-sig).
self._server.Streams.All += Testers.ContainsExpression(
"x-ja3-raw: first-signature", "Verify the already-existing raw header was preserved.", reflags=re.IGNORECASE)
self._server.Streams.All += Testers.ExcludesExpression(
"x-ja3-sig:.*http2-request",
"Verify no JA3-Sig was added when other JA3 headers existed.",
reflags=re.IGNORECASE | re.DOTALL)

def _configure_trafficserver(self) -> None:
"""Configure Traffic Server to be used in the test."""
Expand Down Expand Up @@ -189,8 +194,10 @@ def _verify_internal_headers(self) -> None:

if self._modify_incoming:
p.Streams.All += "modify-incoming-proxy.gold"
elif self._test_remap:
p.Streams.All += "modify-sent-proxy-remap.gold"
else:
p.Streams.All += "modify-sent-proxy.gold"
p.Streams.All += "modify-sent-proxy-global.gold"


JA3FingerprintTest(test_remap=False, modify_incoming=False)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,14 @@ sessions:
content:
size: 399

# With --preserve and any JA3 header present, no new headers are added.
proxy-request:
headers:
fields:
- [ x-request, { value: 'http2-request', as: equal } ]
- [ x-ja3-via, { value: 'first-via', as: equal } ]
- [ X-JA3-Sig, { as: present } ]
- [ X-JA3-Raw, { as: present } ]
- [ x-ja3-raw, { value: 'first-signature', as: equal } ]
- [ x-ja3-sig, { as: absent } ]

server-response:
headers:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
-- State Machine Id``
POST /some/path/http2``
``
x-ja3-sig: ``
x-ja3-raw: first-signature``
x-ja3-via: first-via``
``
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
+++++++++ Proxy's Request after hooks +++++++++
-- State Machine Id``
POST /some/path/http2``
``
x-ja3-raw: first-signature``
x-ja3-via: first-via``
``
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
POST /some/path/http2``
``
x-ja3-sig: ``
x-ja3-via: ``
``
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ meta:
version: "1.0"

sessions:

# Session 1: No pre-existing JA4 headers - new headers should be added.
- protocol:
- name: http
version: 1
Expand All @@ -33,11 +35,12 @@ sessions:
fields:
- [ Connection, keep-alive ]
- [ Content-Length, 0 ]
- [ uuid, no-existing-headers ]

proxy-request:
headers:
fields:
- [ ja4, { as: contains } ]
- [ ja4, { as: present } ]
- [ x-ja4-via, { value: 'test.proxy.com', as: equal } ]

server-response:
Expand All @@ -46,3 +49,70 @@ sessions:
content:
encoding: plain
data: Yay!

# Session 2: Pre-existing JA4 headers - with preserve, no new headers added.
- protocol:
- name: http
version: 1
- name: tcp
- name: ip

transactions:
- client-request:
method: "GET"
version: "1.1"
url: /resource-with-headers
headers:
fields:
- [ Connection, keep-alive ]
- [ Content-Length, 0 ]
- [ uuid, existing-ja4-headers ]
- [ ja4, upstream-fingerprint ]
- [ x-ja4-via, upstream.proxy.com ]

# With --preserve and existing JA4 headers, no new headers should be added.
proxy-request:
headers:
fields:
- [ ja4, { value: 'upstream-fingerprint', as: equal } ]
- [ x-ja4-via, { value: 'upstream.proxy.com', as: equal } ]

server-response:
status: 200
reason: OK
content:
encoding: plain
data: Preserved!

# Session 3: Only x-ja4-via exists - should still trigger preserve for all.
- protocol:
- name: http
version: 1
- name: tcp
- name: ip

transactions:
- client-request:
method: "GET"
version: "1.1"
url: /resource-via-only
headers:
fields:
- [ Connection, keep-alive ]
- [ Content-Length, 0 ]
- [ uuid, existing-via-only ]
- [ x-ja4-via, upstream.proxy.com ]

# With --preserve and only x-ja4-via present, no JA4 headers should be added.
proxy-request:
headers:
fields:
- [ ja4, { as: absent } ]
- [ x-ja4-via, { value: 'upstream.proxy.com', as: equal } ]

server-response:
status: 200
reason: OK
content:
encoding: plain
data: Via only!
Loading