Skip to content

Commit

Permalink
ja3_fingerprint: add modify-incoming option (#10886)
Browse files Browse the repository at this point in the history
This adds the optional modify-incoming argument to the
ja3_fingerprint.so plugin. This argument alters the header modification
behavior to add the X-JA3-* headers to the incoming client request
rather than to the outgoing proxy request. This allows other plugins
later in the plugin.config file to view the added headers as if they
were added by the client, which can be helpful to some plugins.
  • Loading branch information
bneradt authored Dec 1, 2023
1 parent 947da77 commit 8057371
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 42 deletions.
10 changes: 10 additions & 0 deletions plugins/ja3_fingerprint/README
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ The following optional arguments can be used to configure the plugin's behavior:

Add flag --ja3raw if `X-JA3-Raw` is desired in addition to `X-JA3-Sig`.
Add flag --ja3log if local logging in standard logging directory is desired.
Add flag --modify-incoming if it is desired that the plugin modify the incoming
client request headers rather than the sent (proxy) request headers.
Regardless, the origin will receive the configured `X-JA3-*` headers. Using
this option allows other plugins that are configured later than
`ja3_fingerprint.so` in the `plugin.config` file to see the `X-JA3-*`
headers as if they were sent by the client. This option is only applicable
for ja3_fingerprint configured as a global plugin (i.e., a `plugin.config`
plugin) not as a remap plugin. This is because remap plugins by definition
are enaged upon remap completion and by that point it is too late to
meaningfully modify the client request headers.

3. plugin.config
In plugin.config, supply name of the plugin and any desired options. For example:
Expand Down
85 changes: 47 additions & 38 deletions plugins/ja3_fingerprint/ja3_fingerprint.cc
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,19 @@
limitations under the License.
*/

#include <cinttypes>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <getopt.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <algorithm>
#include <fstream>
#include <sstream>
#include <string>
#include <unordered_set>
#include <unordered_map>
#include <memory>
#include <regex>

#include "ts/apidefs.h"
#include "ts/ts.h"
#include "ts/remap.h"

Expand All @@ -51,10 +45,11 @@

const char *PLUGIN_NAME = "ja3_fingerprint";
static DbgCtl dbg_ctl{PLUGIN_NAME};
static TSTextLogObject pluginlog;
static int ja3_idx = -1;
static int enable_raw = 0;
static int enable_log = 0;
static TSTextLogObject pluginlog = nullptr;
static int ja3_idx = -1;
static int global_raw_enabled = 0;
static int global_log_enabled = 0;
static int global_modify_incoming_enabled = 0;

// GREASE table as in ja3
static const std::unordered_set<uint16_t> GREASE_table = {0x0a0a, 0x1a1a, 0x2a2a, 0x3a3a, 0x4a4a, 0x5a5a, 0x6a6a, 0x7a7a,
Expand All @@ -67,9 +62,9 @@ struct ja3_data {
};

struct ja3_remap_info {
int raw = false;
int log = false;
TSCont handler = nullptr;
int raw_enabled = false;
int log_enabled = false;
TSCont handler = nullptr;

~ja3_remap_info()
{
Expand Down Expand Up @@ -259,6 +254,12 @@ client_hello_ja3_handler(TSCont contp, TSEvent event, void *edata)
static int
req_hdr_ja3_handler(TSCont contp, TSEvent event, void *edata)
{
TSEvent expected_event = global_modify_incoming_enabled ? TS_EVENT_HTTP_READ_REQUEST_HDR : TS_EVENT_HTTP_SEND_REQUEST_HDR;
if (event != expected_event) {
TSError("[%s] req_hdr_ja3_handler(): Unexpected event, got %d, expected %d", PLUGIN_NAME, event, expected_event);
TSAssert(event == expected_event);
}

TSHttpTxn txnp = nullptr;
TSHttpSsn ssnp = nullptr;
TSVConn vconn = nullptr;
Expand All @@ -273,15 +274,19 @@ req_hdr_ja3_handler(TSCont contp, TSEvent event, void *edata)
ja3_data *data = static_cast<ja3_data *>(TSUserArgGet(vconn, ja3_idx));
if (data) {
// Decide global or remap
ja3_remap_info *info = static_cast<ja3_remap_info *>(TSContDataGet(contp));
bool raw_flag = info ? info->raw : enable_raw;
bool log_flag = info ? info->log : enable_log;
ja3_remap_info *remap_info = static_cast<ja3_remap_info *>(TSContDataGet(contp));
bool raw_flag = remap_info ? remap_info->raw_enabled : global_raw_enabled;
bool log_flag = remap_info ? remap_info->log_enabled : global_log_enabled;
Dbg(dbg_ctl, "req_hdr_ja3_handler(): Found ja3 string.");

// Get handle to headers
TSMBuffer bufp;
TSMLoc hdr_loc;
TSAssert(TS_SUCCESS == TSHttpTxnServerReqGet(txnp, &bufp, &hdr_loc));
if (global_modify_incoming_enabled) {
TSAssert(TS_SUCCESS == TSHttpTxnClientReqGet(txnp, &bufp, &hdr_loc));
} else {
TSAssert(TS_SUCCESS == TSHttpTxnServerReqGet(txnp, &bufp, &hdr_loc));
}

// Add JA3 md5 fingerprints
append_to_field(bufp, hdr_loc, "X-JA3-Sig", 9, data->md5_string, 32);
Expand All @@ -305,12 +310,13 @@ req_hdr_ja3_handler(TSCont contp, TSEvent event, void *edata)
}

static bool
read_config_option(int argc, const char *argv[], int &raw, int &log)
read_config_option(int argc, const char *argv[], int &raw, int &log, int &modify_incoming)
{
const struct option longopts[] = {
{"ja3raw", no_argument, &raw, 1},
{"ja3log", no_argument, &log, 1},
{nullptr, 0, nullptr, 0}
{"ja3raw", no_argument, &raw, 1},
{"ja3log", no_argument, &log, 1},
{"modify-incoming", no_argument, &modify_incoming, 1},
{nullptr, 0, nullptr, 0}
};

int opt = 0;
Expand All @@ -329,6 +335,7 @@ read_config_option(int argc, const char *argv[], int &raw, int &log)

Dbg(dbg_ctl, "read_config_option(): ja3 raw is %s", (raw == 1) ? "enabled" : "disabled");
Dbg(dbg_ctl, "read_config_option(): ja3 logging is %s", (log == 1) ? "enabled" : "disabled");
Dbg(dbg_ctl, "read_config_option(): ja3 modify-incoming is %s", (modify_incoming == 1) ? "enabled" : "disabled");
return true;
}

Expand All @@ -344,14 +351,14 @@ TSPluginInit(int argc, const char *argv[])
info.support_email = "zeyuany@oath.com";

// Options
if (!read_config_option(argc, argv, enable_raw, enable_log)) {
if (!read_config_option(argc, argv, global_raw_enabled, global_log_enabled, global_modify_incoming_enabled)) {
return;
}

if (TSPluginRegister(&info) != TS_SUCCESS) {
TSError("[%s] Unable to initialize plugin. Failed to register.", PLUGIN_NAME);
} else {
if (enable_log && !pluginlog) {
if (global_log_enabled && !pluginlog) {
TSAssert(TS_SUCCESS == TSTextLogObjectCreate(PLUGIN_NAME, TS_LOG_MODE_ADD_TIMESTAMP, &pluginlog));
Dbg(dbg_ctl, "log object created successfully");
}
Expand All @@ -360,7 +367,9 @@ TSPluginInit(int argc, const char *argv[])
TSUserArgIndexReserve(TS_USER_ARGS_VCONN, PLUGIN_NAME, "used to pass ja3", &ja3_idx);
TSHttpHookAdd(TS_SSL_CLIENT_HELLO_HOOK, ja3_cont);
TSHttpHookAdd(TS_VCONN_CLOSE_HOOK, ja3_cont);
TSHttpHookAdd(TS_HTTP_SEND_REQUEST_HDR_HOOK, TSContCreate(req_hdr_ja3_handler, nullptr));

TSHttpHookID const hook = global_modify_incoming_enabled ? TS_HTTP_READ_REQUEST_HDR_HOOK : TS_HTTP_SEND_REQUEST_HDR_HOOK;
TSHttpHookAdd(hook, TSContCreate(req_hdr_ja3_handler, nullptr));
}

return;
Expand Down Expand Up @@ -391,38 +400,40 @@ TSReturnCode
TSRemapNewInstance(int argc, char *argv[], void **ih, char * /* errbuf ATS_UNUSED */, int /* errbuf_size ATS_UNUSED */)
{
Dbg(dbg_ctl, "New instance for client matching %s to %s", argv[0], argv[1]);
std::unique_ptr<ja3_remap_info> pri{new ja3_remap_info};
std::unique_ptr<ja3_remap_info> remap_info{new ja3_remap_info};

// Parse parameters
if (!read_config_option(argc - 1, const_cast<const char **>(argv + 1), pri->raw, pri->log)) {
int discard_modify_incoming = -1; // Not used for remap.
if (!read_config_option(argc - 1, const_cast<const char **>(argv + 1), remap_info->raw_enabled, remap_info->log_enabled,
discard_modify_incoming)) {
Dbg(dbg_ctl, "TSRemapNewInstance(): Bad arguments");
return TS_ERROR;
}

if (pri->log && !pluginlog) {
if (remap_info->log_enabled && !pluginlog) {
TSAssert(TS_SUCCESS == TSTextLogObjectCreate(PLUGIN_NAME, TS_LOG_MODE_ADD_TIMESTAMP, &pluginlog));
Dbg(dbg_ctl, "log object created successfully");
}

// Create continuation
pri->handler = TSContCreate(req_hdr_ja3_handler, nullptr);
TSContDataSet(pri->handler, pri.get());
remap_info->handler = TSContCreate(req_hdr_ja3_handler, nullptr);
TSContDataSet(remap_info->handler, remap_info.get());

// Pass to other remap plugin functions
*ih = static_cast<void *>(pri.release());
*ih = static_cast<void *>(remap_info.release());
return TS_SUCCESS;
}

TSRemapStatus
TSRemapDoRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri)
{
auto pri = static_cast<ja3_remap_info *>(ih);
auto remap_info = static_cast<ja3_remap_info *>(ih);

// On remap, set up handler at send req hook to send JA3 data as header
if (!pri || !rri || !(pri->handler)) {
if (!remap_info || !rri || !(remap_info->handler)) {
TSError("[%s] TSRemapDoRemap(): Invalid private data or RRI or handler.", PLUGIN_NAME);
} else {
TSHttpTxnHookAdd(rh, TS_HTTP_SEND_REQUEST_HDR_HOOK, pri->handler);
TSHttpTxnHookAdd(rh, TS_HTTP_SEND_REQUEST_HDR_HOOK, remap_info->handler);
}

return TSREMAP_NO_REMAP;
Expand All @@ -431,9 +442,7 @@ TSRemapDoRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri)
void
TSRemapDeleteInstance(void *ih)
{
auto pri = static_cast<ja3_remap_info *>(ih);
if (pri) {
delete pri;
}
auto remap_info = static_cast<ja3_remap_info *>(ih);
delete remap_info;
ih = nullptr;
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,24 +31,32 @@ class JA3FingerprintTest:
_ts_counter: int = 0
_client_counter: int = 0

def __init__(self, test_remap: bool) -> None:
def __init__(self, test_remap: bool, modify_incoming: bool) -> None:
"""Configure the test processes in preparation for the TestRun.
:param test_remap: Whether to configure the plugin as a remap plugin
instead of as a global plugin.
:param modify_incoming: Whether ja3_fingerprint should be configured to
modify the client request rather than the proxy request.
"""
if test_remap and modify_incoming:
raise ValueError('modify-incoming is only allowed as a global plugin.')

self._test_remap = test_remap
if test_remap:
self._replay_file = 'ja3_fingerprint_remap.replay.yaml'
else:
self._replay_file = 'ja3_fingerprint_global.replay.yaml'

self._modify_incoming = modify_incoming

tr = Test.AddTestRun('Testing ja3_fingerprint plugin.')
self._configure_dns(tr)
self._configure_server(tr)
self._configure_trafficserver()
self._configure_client(tr)
self._await_ja3log()
self._verify_internal_headers()

def _configure_dns(self, tr: 'TestRun') -> None:
"""Configure a nameserver for the test.
Expand Down Expand Up @@ -86,7 +94,10 @@ def _configure_trafficserver(self) -> None:
f'map https://http2.server.com https://http2.backend.com:{server_port} '
'@plugin=ja3_fingerprint.so @pparam=--ja3log')
else:
self._ts.Disk.plugin_config.AddLine('ja3_fingerprint.so --ja3log --ja3raw')
arguments = '--ja3log --ja3raw'
if self._modify_incoming:
arguments += ' --modify-incoming'
self._ts.Disk.plugin_config.AddLine(f'ja3_fingerprint.so {arguments}')
self._ts.Disk.remap_config.AddLine(f'map https://http2.server.com https://http2.backend.com:{server_port}')

self._ts.Disk.records_config.update(
Expand Down Expand Up @@ -143,6 +154,33 @@ def _await_ja3log(self) -> None:
p.Command = f'echo await {ja3_path} creation'
p.StartBefore(waiter)

def _verify_internal_headers(self) -> None:
"""Verify that the correct headers were modified."""

# We use the grep command to get the small snippet of output we want
# from the traffic.out file. Gold file matching against long files, like
# traffic.out, is exceedingly slow.

tr = Test.AddTestRun('Verify the internal client request headers.')
traffic_out = self._ts.Disk.traffic_out.AbsPath
p = tr.Processes.Default
p.Command = f'grep --after-context=20 "Incoming Request" {traffic_out}'

if self._modify_incoming:
p.Streams.All += "modify-incoming-client.gold"
else:
p.Streams.All += "modify-sent-client.gold"

tr = Test.AddTestRun('Verify the internal proxy request headers.')
p = tr.Processes.Default
p.Command = f'grep --after-context=20 "Proxy\'s Request after hooks" {traffic_out}'

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


JA3FingerprintTest(test_remap=False)
JA3FingerprintTest(test_remap=True)
JA3FingerprintTest(test_remap=False, modify_incoming=False)
JA3FingerprintTest(test_remap=True, modify_incoming=False)
JA3FingerprintTest(test_remap=False, modify_incoming=True)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
+++++++++ Incoming Request +++++++++
-- State Machine Id``
GET ``
host: ``
content-length: ``
x-request: ``
uuid: ``
X-JA3-Sig: ``
x-JA3-RAW: ``
``
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
+++++++++ Proxy's Request after hooks +++++++++
``
+++++++++ Proxy's Request after hooks +++++++++
-- State Machine Id``
POST ``
Host: ``
Content-Type: ``
uuid: ``
x-request: ``
X-JA3-Sig: ``
``
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
+++++++++ Incoming Request +++++++++
``
+++++++++ Incoming Request +++++++++
-- State Machine Id``
POST ``
Host: ``
Content-Type: ``
uuid: ``
x-request: ``
``
15 changes: 15 additions & 0 deletions tests/gold_tests/pluginTest/ja3_fingerprint/modify-sent-proxy.gold
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
+++++++++ Proxy's Request after hooks +++++++++
``
+++++++++ Proxy's Request after hooks +++++++++
-- State Machine Id``
POST ``
Host: ``
Content-Type: ``
uuid: ``
x-request: ``
Client-ip: ``
X-Forwarded-For: ``
Via: ``
Transfer-Encoding: ``
X-JA3-Sig: ``
``

0 comments on commit 8057371

Please sign in to comment.