From 1b34950dbc4ba9e6ed74f152533209dd15fd0604 Mon Sep 17 00:00:00 2001 From: Masakazu Kitajo Date: Thu, 12 Mar 2026 15:31:44 -0600 Subject: [PATCH 01/23] Add jax_fingerprint plugin --- .../plugins/jax_fingerprint.en.rst | 179 ++++++++ plugins/experimental/CMakeLists.txt | 1 + .../jax_fingerprint/CMakeLists.txt | 41 ++ plugins/experimental/jax_fingerprint/config.h | 65 +++ .../experimental/jax_fingerprint/context.cc | 71 +++ .../experimental/jax_fingerprint/context.h | 46 ++ .../experimental/jax_fingerprint/header.cc | 140 ++++++ plugins/experimental/jax_fingerprint/header.h | 31 ++ .../jax_fingerprint/ja3/ja3_method.cc | 125 ++++++ .../jax_fingerprint/ja3/ja3_method.h | 32 ++ .../jax_fingerprint/ja3/ja3_utils.cc | 108 +++++ .../jax_fingerprint/ja3/ja3_utils.h | 71 +++ .../jax_fingerprint/ja3/test_ja3.cc | 96 ++++ .../experimental/jax_fingerprint/ja4/ja4.cc | 175 ++++++++ .../experimental/jax_fingerprint/ja4/ja4.h | 171 +++++++ .../jax_fingerprint/ja4/ja4_method.cc | 153 +++++++ .../jax_fingerprint/ja4/ja4_method.h | 32 ++ .../jax_fingerprint/ja4/test_ja4.cc | 425 ++++++++++++++++++ .../ja4/tls_client_hello_summary.cc | 112 +++++ .../experimental/jax_fingerprint/ja4h/ja4h.cc | 107 +++++ .../experimental/jax_fingerprint/ja4h/ja4h.h | 24 + .../jax_fingerprint/ja4h/ja4h_method.cc | 121 +++++ .../jax_fingerprint/ja4h/ja4h_method.h | 32 ++ plugins/experimental/jax_fingerprint/log.cc | 44 ++ plugins/experimental/jax_fingerprint/log.h | 28 ++ plugins/experimental/jax_fingerprint/method.h | 42 ++ .../experimental/jax_fingerprint/plugin.cc | 425 ++++++++++++++++++ plugins/experimental/jax_fingerprint/plugin.h | 31 ++ .../experimental/jax_fingerprint/userarg.cc | 71 +++ .../experimental/jax_fingerprint/userarg.h | 32 ++ 30 files changed, 3031 insertions(+) create mode 100644 doc/admin-guide/plugins/jax_fingerprint.en.rst create mode 100644 plugins/experimental/jax_fingerprint/CMakeLists.txt create mode 100644 plugins/experimental/jax_fingerprint/config.h create mode 100644 plugins/experimental/jax_fingerprint/context.cc create mode 100644 plugins/experimental/jax_fingerprint/context.h create mode 100644 plugins/experimental/jax_fingerprint/header.cc create mode 100644 plugins/experimental/jax_fingerprint/header.h create mode 100644 plugins/experimental/jax_fingerprint/ja3/ja3_method.cc create mode 100644 plugins/experimental/jax_fingerprint/ja3/ja3_method.h create mode 100644 plugins/experimental/jax_fingerprint/ja3/ja3_utils.cc create mode 100644 plugins/experimental/jax_fingerprint/ja3/ja3_utils.h create mode 100644 plugins/experimental/jax_fingerprint/ja3/test_ja3.cc create mode 100644 plugins/experimental/jax_fingerprint/ja4/ja4.cc create mode 100644 plugins/experimental/jax_fingerprint/ja4/ja4.h create mode 100644 plugins/experimental/jax_fingerprint/ja4/ja4_method.cc create mode 100644 plugins/experimental/jax_fingerprint/ja4/ja4_method.h create mode 100644 plugins/experimental/jax_fingerprint/ja4/test_ja4.cc create mode 100644 plugins/experimental/jax_fingerprint/ja4/tls_client_hello_summary.cc create mode 100644 plugins/experimental/jax_fingerprint/ja4h/ja4h.cc create mode 100644 plugins/experimental/jax_fingerprint/ja4h/ja4h.h create mode 100644 plugins/experimental/jax_fingerprint/ja4h/ja4h_method.cc create mode 100644 plugins/experimental/jax_fingerprint/ja4h/ja4h_method.h create mode 100644 plugins/experimental/jax_fingerprint/log.cc create mode 100644 plugins/experimental/jax_fingerprint/log.h create mode 100644 plugins/experimental/jax_fingerprint/method.h create mode 100644 plugins/experimental/jax_fingerprint/plugin.cc create mode 100644 plugins/experimental/jax_fingerprint/plugin.h create mode 100644 plugins/experimental/jax_fingerprint/userarg.cc create mode 100644 plugins/experimental/jax_fingerprint/userarg.h diff --git a/doc/admin-guide/plugins/jax_fingerprint.en.rst b/doc/admin-guide/plugins/jax_fingerprint.en.rst new file mode 100644 index 00000000000..005ccae5ced --- /dev/null +++ b/doc/admin-guide/plugins/jax_fingerprint.en.rst @@ -0,0 +1,179 @@ +.. Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + +.. include:: ../../common.defs + +.. _admin-plugins-jax-fingerprint: + +JAx Fingerprint Plugin +********************** + +Description +=========== + +The JAx Fingerprint plugin generates TLS client fingerprints based on the JA4 or JA3 algorithm designed by John Althouse. + +Fingerprints can be used for: + +* Client identification and tracking +* Bot detection and mitigation +* Security analytics and threat intelligence +* Understanding client implementation patterns + + +Plugin Configuration +==================== + +You can use the plugin as a global pugin, a remap plugin, or both. + +To use the plugin as a global plugin, add the following line to :file:`plugin.config`:: + + jax_fingerprint.so --standalone + +To use the plugin as a remap plugin, append the following line to a remap rule on :file:`remap.config`:: + + @plugin=jax_fingerprint.so @pparam --standalone + +To use the plugin as both global and remap plugin (hybrid setup), have the both without `--standalone` option. + + +.. option:: --standalone + +This option enables you to use the plugin as either a global plugin, or a remap plugin. In other +words, the option needs to be specified if you do not use the hybrid setup. + +.. option:: --method + +Fingerprinting method (e.g. JA4, JA3, etc.) to use. + +.. option:: --mode + +This option specifies what to do if requests from clients have the header names that are specified +by `--header` and/or `via-header`. Available setting values are "overwrite", "keep" and "append". + +.. option:: --servernames + +This option specifies server name(s) for which the plugin generates fingerprints. + +.. option:: --header + +This option specifies the name of the header field where the plugin stores the generated fingerprint value. If not specified, header generation will be suppressed. + +.. option:: --via-header + +This option specifies the name of the header field where the plugin stores the generated fingerprint-via value. If not specified, header generation will be suppressed. + +.. option:: --log-filename + +This option specifies the filename for the plugin log file. If not specified, log output will be suppressed. + + +Plugin Behavior +=============== + +Global plugin setup +------------------- + +Global plugin setup is the best if you: +- Need a fingerprint on every request + +Remap plugin setup +------------------ + +Remap plugin setup is the best if you: +- Need a fingerprint only on specific paths, or +- Cannot use Global plugin setup + +Note: For JA3 and JA4, fingerprints are always generated at the beginning of connections. Using remap plugin setup only reduces the +overhead of adding HTTP headers and loggingg. + +Hybrid setup +------------ + +Hybrid setup is the best if you: +- Need a fingerprint only for specific server names (in TLS SNI extension), and +- Need a fingerprint only on specific paths + + +Log Output +========== + +The plugin output a log file in the Traffic Server log directory (typically ``/var/log/trafficserver/``) if a log filename is +specified by `--log-filename` option. + +**Log Format**:: + + [timestamp] Client:
: + +**Example**:: + + [Jan 29 10:15:23.456] Client IP: 192.168.1.100 JA4: t13d1516h2_8daaf6152771_b186095e22b6 + [Jan 29 10:15:24.123] Client IP: 10.0.0.50 JA4: t13d1715h2_8daaf6152771_02713d6af862 + + +Using HTTP Headers in Origin Requests +===================================== + +Origin servers can access the generated fingerprint through the injected HTTP header. +This allows the origin to: + +* Make access control decisions based on client fingerprints +* Log fingerprints for security analysis +* Track client populations and TLS implementation patterns + +The fingerprint-via header allows origin servers to track which Traffic Server proxy handled the request when multiple proxies are deployed. + + +Debugging +========= + +To enable debug logging for the plugin, set the following in :file:`records.yaml`:: + + records: + diags: + debug: + enabled: 1 + tags: jax_fingerprint + + +Requirements +============ + +* Traffic Server must be built with TLS support (OpenSSL or BoringSSL) if you use JA3 or JA4 + + +See Also +======== +* JA3 Technical Specification: https://github.com/FoxIO-LLC/ja3 +* JA4+ Technical Specification: https://github.com/FoxIO-LLC/ja4 + + +Example Configuration +===================== + +Enable JA4 fingerprinting by hybrid (global + remap) setup +---------------------------------------------------------- + +This configuration adds x-my-ja4 header if a connection is established for either abc.example or xyz.example. + +**plugin.config**:: + + jax_fingerprint.so --method JA4 --servernames abc.example,xyz.example + +**remap.config**:: + + map / http://origin.example/ @plugin=jax_fingerprint.so @pparam=--method=JA4 @pparam=--header=x-my-ja4 diff --git a/plugins/experimental/CMakeLists.txt b/plugins/experimental/CMakeLists.txt index 8d63d3356df..70ae2d60703 100644 --- a/plugins/experimental/CMakeLists.txt +++ b/plugins/experimental/CMakeLists.txt @@ -67,6 +67,7 @@ if(BUILD_INLINER) endif() if(BUILD_JA4_FINGERPRINT) add_subdirectory(ja4_fingerprint) + add_subdirectory(jax_fingerprint) endif() if(BUILD_MAGICK) add_subdirectory(magick) diff --git a/plugins/experimental/jax_fingerprint/CMakeLists.txt b/plugins/experimental/jax_fingerprint/CMakeLists.txt new file mode 100644 index 00000000000..e01b461968f --- /dev/null +++ b/plugins/experimental/jax_fingerprint/CMakeLists.txt @@ -0,0 +1,41 @@ +####################### +# +# Licensed to the Apache Software Foundation (ASF) under one or more contributor license +# agreements. See the NOTICE file distributed with this work for additional information regarding +# copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License +# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing permissions and limitations under +# the License. +# +####################### + +add_atsplugin( + jax_fingerprint + plugin.cc + context.cc + userarg.cc + header.cc + log.cc + ja3/ja3_method.cc + ja3/ja3_utils.cc + ja4/ja4_method.cc + ja4/ja4.cc + ja4/tls_client_hello_summary.cc + ja4h/ja4h_method.cc + ja4h/ja4h.cc +) +target_link_libraries(jax_fingerprint PRIVATE OpenSSL::Crypto OpenSSL::SSL) +verify_global_plugin(jax_fingerprint) + +if(BUILD_TESTING) + add_executable(test_jax ja3/test_ja3.cc ja3/ja3_utils.cc ja4/test_ja4.cc ja4/ja4.cc ja4/tls_client_hello_summary.cc) + target_link_libraries(test_jax PRIVATE Catch2::Catch2WithMain) + + add_catch2_test(NAME test_jax COMMAND test_ja4) +endif() diff --git a/plugins/experimental/jax_fingerprint/config.h b/plugins/experimental/jax_fingerprint/config.h new file mode 100644 index 00000000000..9874f4de5ad --- /dev/null +++ b/plugins/experimental/jax_fingerprint/config.h @@ -0,0 +1,65 @@ +/** @file + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ + +#pragma once + +#include "ts/ts.h" +#include "method.h" + +#include +#include + +enum class Mode : int { + OVERWRITE, + KEEP, + APPEND, +}; + +enum class PluginType : int { + GLOBAL, + REMAP, +}; + +// This hash function enables looking up the set by a string_view without making a temporal string object. +struct StringHash { + // Enable heterogeneous lookup + using is_transparent = void; + + size_t + operator()(std::string_view sv) const + { + return std::hash{}(sv); + } +}; + +struct PluginConfig { + PluginType plugin_type = PluginType::GLOBAL; + Mode mode = Mode::OVERWRITE; + struct Method method = {"uninitialized", Method::Type::CONNECTION_BASED, nullptr, nullptr}; + std::string header_name = ""; + std::string via_header_name = ""; + std::string log_filename = ""; + int user_arg_index = -1; + TSCont handler = nullptr; // For remap plugin + bool standalone = false; + std::unordered_set> servernames; +}; diff --git a/plugins/experimental/jax_fingerprint/context.cc b/plugins/experimental/jax_fingerprint/context.cc new file mode 100644 index 00000000000..2c5c5cbf122 --- /dev/null +++ b/plugins/experimental/jax_fingerprint/context.cc @@ -0,0 +1,71 @@ +/** @file + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#include "plugin.h" +#include "context.h" + +JAxContext::JAxContext(const char *method_name, sockaddr const *s_sockaddr) : _method_name(method_name) +{ + _addr[0] = '\0'; + + if (s_sockaddr == nullptr) { + return; + } + + switch (s_sockaddr->sa_family) { + case AF_INET: + inet_ntop(AF_INET, &reinterpret_cast(s_sockaddr)->sin_addr, _addr, INET_ADDRSTRLEN); + break; + case AF_INET6: + inet_ntop(AF_INET6, &reinterpret_cast(s_sockaddr)->sin6_addr, _addr, INET6_ADDRSTRLEN); + break; + case AF_UNIX: + strncpy(_addr, reinterpret_cast(s_sockaddr)->sun_path, sizeof(_addr) - 1); + _addr[sizeof(_addr) - 1] = '\0'; + default: + break; + } +} + +const std::string & +JAxContext::get_fingerprint() const +{ + return this->_fingerprint; +} + +void +JAxContext::set_fingerprint(const std::string &fingerprint) +{ + this->_fingerprint = fingerprint; + Dbg(dbg_ctl, "Fingerprint: %s", this->_fingerprint.c_str()); +} + +const char * +JAxContext::get_addr() const +{ + return this->_addr; +} + +const char * +JAxContext::get_method_name() const +{ + return this->_method_name; +} diff --git a/plugins/experimental/jax_fingerprint/context.h b/plugins/experimental/jax_fingerprint/context.h new file mode 100644 index 00000000000..754cd3cb2bd --- /dev/null +++ b/plugins/experimental/jax_fingerprint/context.h @@ -0,0 +1,46 @@ +/** @file + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ + +#pragma once + +#include +#include +#include + +#include + +class JAxContext +{ +public: + JAxContext(const char *name, sockaddr const *s_sockaddr); + + const std::string &get_fingerprint() const; + void set_fingerprint(const std::string &fingerprint); + + const char *get_addr() const; + const char *get_method_name() const; + +private: + std::string _fingerprint; + char _addr[PATH_MAX + 1]; + const char *_method_name; +}; diff --git a/plugins/experimental/jax_fingerprint/header.cc b/plugins/experimental/jax_fingerprint/header.cc new file mode 100644 index 00000000000..1477728681b --- /dev/null +++ b/plugins/experimental/jax_fingerprint/header.cc @@ -0,0 +1,140 @@ +/** @file + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#include "plugin.h" +#include "header.h" + +#include "ts/ts.h" + +#include +#include + +static void +put_header(TSHttpTxn txnp, const std::string &name, const std::string &value, bool overwrite) +{ + TSMBuffer bufp; + TSMLoc hdr_loc; + if (TS_SUCCESS != TSHttpTxnClientReqGet(txnp, &bufp, &hdr_loc)) { + Dbg(dbg_ctl, "Failed to get headers."); + return; + } + + TSMLoc target = TSMimeHdrFieldFind(bufp, hdr_loc, name.c_str(), name.length()); + if (target == TS_NULL_MLOC) { + // Add - Create a new field with the value + Dbg(dbg_ctl, "Add %s: %s", name.c_str(), value.c_str()); + TSMimeHdrFieldCreateNamed(bufp, hdr_loc, name.c_str(), name.length(), &target); + TSMimeHdrFieldValueStringSet(bufp, hdr_loc, target, -1, value.c_str(), value.length()); + TSMimeHdrFieldAppend(bufp, hdr_loc, target); + TSHandleMLocRelease(bufp, hdr_loc, target); + } else if (overwrite) { + // Replace - Set the value to the first field and remove all duplicate fields + Dbg(dbg_ctl, "Replace %s field value with %s", name.c_str(), value.c_str()); + TSMLoc tmp = nullptr; + bool first = true; + while (target) { + tmp = TSMimeHdrFieldNextDup(bufp, hdr_loc, target); + if (first) { + first = false; + TSMimeHdrFieldValueStringSet(bufp, hdr_loc, target, -1, value.c_str(), value.size()); + } else { + TSMimeHdrFieldDestroy(bufp, hdr_loc, target); + } + TSHandleMLocRelease(bufp, hdr_loc, target); + target = tmp; + } + } else { + // Append - Find the last duplicate field and set the value to it + Dbg(dbg_ctl, "Append %s to %s field value", value.c_str(), name.c_str()); + TSMLoc dup = TSMimeHdrFieldNextDup(bufp, hdr_loc, target); + while (dup != TS_NULL_MLOC) { + TSHandleMLocRelease(bufp, hdr_loc, target); + target = dup; + dup = TSMimeHdrFieldNextDup(bufp, hdr_loc, target); + } + TSMimeHdrFieldValueStringInsert(bufp, hdr_loc, target, -1, value.c_str(), value.length()); + TSHandleMLocRelease(bufp, hdr_loc, target); + } + + TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc); +} + +static void +put_via_header(TSHttpTxn txnp, const std::string &via_header, bool overwrite) +{ + TSMgmtString proxy_name = nullptr; + if (TS_SUCCESS == TSMgmtStringGet("proxy.config.proxy_name", &proxy_name)) { + put_header(txnp, via_header, proxy_name, overwrite); + TSfree(proxy_name); + } else { + TSError("[%s] Failed to get proxy name for %s, set 'proxy.config.proxy_name' in records.config", PLUGIN_NAME, + via_header.c_str()); + put_header(txnp, via_header, "unknown", overwrite); + } +} + +void +set_header(TSHttpTxn txnp, const std::string &header, const std::string &fingerprint) +{ + put_header(txnp, header, fingerprint, true); +} + +void +append_header(TSHttpTxn txnp, const std::string &header, const std::string &fingerprint) +{ + put_header(txnp, header, fingerprint, false); +} + +void +set_via_header(TSHttpTxn txnp, const std::string &via_header) +{ + put_via_header(txnp, via_header, true); +} + +void +append_via_header(TSHttpTxn txnp, const std::string &via_header) +{ + put_via_header(txnp, via_header, false); +} + +void +remove_header(TSHttpTxn txnp, const std::string &header) +{ + TSMBuffer bufp; + TSMLoc hdr_loc; + if (TS_SUCCESS != TSHttpTxnClientReqGet(txnp, &bufp, &hdr_loc)) { + Dbg(dbg_ctl, "Failed to get headers."); + return; + } + + TSMLoc target = TSMimeHdrFieldFind(bufp, hdr_loc, header.c_str(), header.length()); + if (target != TS_NULL_MLOC) { + // Remove all + Dbg(dbg_ctl, "Remove all %s field", header.c_str()); + TSMLoc tmp = nullptr; + while (target) { + tmp = TSMimeHdrFieldNextDup(bufp, hdr_loc, target); + TSMimeHdrFieldDestroy(bufp, hdr_loc, target); + TSHandleMLocRelease(bufp, hdr_loc, target); + target = tmp; + } + } +} diff --git a/plugins/experimental/jax_fingerprint/header.h b/plugins/experimental/jax_fingerprint/header.h new file mode 100644 index 00000000000..f098bf16b27 --- /dev/null +++ b/plugins/experimental/jax_fingerprint/header.h @@ -0,0 +1,31 @@ +/** @file + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ + +#pragma once + +#include "ts/ts.h" + +void append_header(TSHttpTxn txnp, const std::string &header, const std::string &fingerprint); +void append_via_header(TSHttpTxn txnp, const std::string &via_header); +void set_header(TSHttpTxn txnp, const std::string &header, const std::string &fingerprint); +void set_via_header(TSHttpTxn txnp, const std::string &via_header); +void remove_header(TSHttpTxn txnp, const std::string &header); diff --git a/plugins/experimental/jax_fingerprint/ja3/ja3_method.cc b/plugins/experimental/jax_fingerprint/ja3/ja3_method.cc new file mode 100644 index 00000000000..6bc2487ca4d --- /dev/null +++ b/plugins/experimental/jax_fingerprint/ja3/ja3_method.cc @@ -0,0 +1,125 @@ +/** @file + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ + +#include "ts/ts.h" + +#include "../plugin.h" +#include "../context.h" +#include "ja3_method.h" +#include "ja3_utils.h" + +#include +#include +#include + +#include + +namespace ja3_method +{ + +void on_client_hello(JAxContext *, TSVConn); + +struct Method method = { + "JA3", + Method::Type::CONNECTION_BASED, + on_client_hello, + nullptr, +}; + +} // namespace ja3_method + +namespace +{ +constexpr int ja3_hash_included_byte_count{16}; +static_assert(ja3_hash_included_byte_count <= MD5_DIGEST_LENGTH); + +constexpr int ja3_hash_hex_string_with_null_terminator_length{2 * ja3_hash_included_byte_count + 1}; + +} // end anonymous namespace + +static std::string +get_fingerprint(TSClientHello ch) +{ + std::string raw; + std::size_t len{}; + const unsigned char *buf{}; + + // Get version + unsigned int version = ch.get_version(); + raw.append(std::to_string(version)); + raw.push_back(','); + + // Get cipher suites + raw.append(ja3::encode_word_buffer(ch.get_cipher_suites(), ch.get_cipher_suites_len())); + raw.push_back(','); + + // Get extensions + auto ext_types = ch.get_extension_types(); + len = 0; + auto first = ext_types.begin(); + auto last = ext_types.end(); + while (first != last) { + ++first; + ++len; + } + if (len > 0) { + int extension_ids[len]; + std::copy(ext_types.begin(), ext_types.end(), extension_ids); + raw.append(ja3::encode_integer_buffer(extension_ids, len)); + } + raw.push_back(','); + + // Get elliptic curves + if (TS_SUCCESS == TSClientHelloExtensionGet(ch, 0x0a, &buf, &len)) { + // Skip first 2 bytes since we already have length + raw.append(ja3::encode_word_buffer(buf + 2, len - 2)); + } + raw.push_back(','); + + // Get elliptic curve point formats + if (TS_SUCCESS == TSClientHelloExtensionGet(ch, 0x0b, &buf, &len)) { + // Skip first byte since we already have length + raw.append(ja3::encode_byte_buffer(buf + 1, len - 1)); + } + Dbg(dbg_ctl, "Hashing %s", raw.c_str()); + + char fingerprint[ja3_hash_hex_string_with_null_terminator_length]; + unsigned char digest[MD5_DIGEST_LENGTH]; + MD5(reinterpret_cast(raw.c_str()), raw.length(), digest); + for (int i{0}; i < ja3_hash_included_byte_count; ++i) { + std::snprintf(&(fingerprint[i * 2]), sizeof(fingerprint) - (i * 2), "%02x", static_cast(digest[i])); + } + + return {fingerprint}; +} + +void +ja3_method::on_client_hello(JAxContext *ctx, TSVConn vconn) +{ + TSClientHello ch = TSVConnClientHelloGet(vconn); + + if (!ch) { + Dbg(dbg_ctl, "Could not get TSClientHello object."); + } else { + ctx->set_fingerprint(get_fingerprint(ch)); + } +} diff --git a/plugins/experimental/jax_fingerprint/ja3/ja3_method.h b/plugins/experimental/jax_fingerprint/ja3/ja3_method.h new file mode 100644 index 00000000000..0ca4a41aa9e --- /dev/null +++ b/plugins/experimental/jax_fingerprint/ja3/ja3_method.h @@ -0,0 +1,32 @@ +/** @file + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ + +#pragma once + +#include "../method.h" + +namespace ja3_method +{ + +extern struct Method method; + +} diff --git a/plugins/experimental/jax_fingerprint/ja3/ja3_utils.cc b/plugins/experimental/jax_fingerprint/ja3/ja3_utils.cc new file mode 100644 index 00000000000..b54e5066c83 --- /dev/null +++ b/plugins/experimental/jax_fingerprint/ja3/ja3_utils.cc @@ -0,0 +1,108 @@ +/** @file ja3_utils.cc + + Plugin JA3 Fingerprint calculates JA3 signatures for incoming SSL traffic. + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ + +#include +#include +#include +#include + +namespace ja3 +{ + +// GREASE table as in ja3 +static std::unordered_set const GREASE_table = {0x0a0a, 0x1a1a, 0x2a2a, 0x3a3a, 0x4a4a, 0x5a5a, 0x6a6a, 0x7a7a, + 0x8a8a, 0x9a9a, 0xaaaa, 0xbaba, 0xcaca, 0xdada, 0xeaea, 0xfafa}; + +static constexpr std::uint16_t +from_big_endian(unsigned char lowbyte, unsigned char highbyte) +{ + return (static_cast(lowbyte) << 8) | highbyte; +} + +static bool +ja3_should_ignore(std::uint16_t n) +{ + return GREASE_table.find(n) != GREASE_table.end(); +} + +std::string +encode_byte_buffer(unsigned char const *buf, int const len) +{ + std::string result; + if (len > 0) { + // Benchmarks show that reserving space in the string here would cause + // a 40% increase in runtime for a buffer with 10 elements... so we + // don't do it. + result.append(std::to_string(buf[0])); + std::for_each(buf + 1, buf + len, [&result](unsigned char i) { + result.push_back('-'); + result.append(std::to_string(i)); + }); + } + return result; +} + +std::string +encode_word_buffer(unsigned char const *buf, int const len) +{ + std::string result; + auto it{buf}; + while ((it < (buf + len)) && ja3_should_ignore(from_big_endian(it[0], it[1]))) { + it += 2; + } + if (it < (buf + len)) { + // Benchmarks show that reserving buf.size() - 1 space in the string here + // would have no impact on performance. Since the string may not even need + // that much due to GREASE values present in the buffer, we don't do it. + result.append(std::to_string(from_big_endian(it[0], it[1]))); + it += 2; + for (; it < buf + len; it += 2) { + auto const value{from_big_endian(it[0], it[1])}; + if (!ja3_should_ignore(value)) { + result.push_back('-'); + result.append(std::to_string(value)); + } + } + } + return result; +} + +std::string +encode_integer_buffer(int const *buf, int const len) +{ + std::string result; + auto it{std::find_if(buf, buf + len, [](int i) { return !ja3_should_ignore(i); })}; + if (it < (buf + len)) { + result.append(std::to_string(*it)); + std::for_each(it + 1, buf + len, [&result](int const i) { + if (!ja3_should_ignore(i)) { + result.push_back('-'); + result.append(std::to_string(i)); + } + }); + } + return result; +} + +} // end namespace ja3 diff --git a/plugins/experimental/jax_fingerprint/ja3/ja3_utils.h b/plugins/experimental/jax_fingerprint/ja3/ja3_utils.h new file mode 100644 index 00000000000..93a0d8fe392 --- /dev/null +++ b/plugins/experimental/jax_fingerprint/ja3/ja3_utils.h @@ -0,0 +1,71 @@ +/** @file ja3_utils.h + + Plugin JA3 Fingerprint calculates JA3 signatures for incoming SSL traffic. + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ + +#include + +namespace ja3 +{ + +/** Encode a buffer of 8bit values. + * + * The values will be converted to their decimal string representations and + * joined with the '-' character. + * + * @param buf The buffer to encode. This should be an SSL buffer of 8bit + * values. + * @param len The length of the buffer. If the length is zero, buf will + * not be dereferenced. + * @return The string-encoded ja3 representation of the buffer. + */ +std::string encode_byte_buffer(unsigned char const *buf, int const len); + +/** Encode a buffer of big-endian 16bit values. + * + * The values will be converted to their decimal string representations and + * joined with the '-' character. Any GREASE values in the buffer will be + * ignored. + * + * @param buf The buffer to encode. This should be a big-endian SSL buffer + * of 16bit values. + * @param len The length of the buffer. If the length is zero, buf will not + * be dereferenced. + * @return The string-encoded ja3 representation of the buffer. + */ +std::string encode_word_buffer(unsigned char const *buf, int const len); + +/** Encode a buffer of integers. + * + * The values will be converted to their decimal string representations and + * joined with the '-' character. Any GREASE values in the buffer will be + * ignored. + * + * @param buf The buffer to encode. The buffer underlying the span should be + * an SSL buffer of ints. + * @param len The length (number of values) in the buffer. If the length is + * zero, buf will not be dereferenced. + * @return The string-encoded ja3 representation of the buffer. + */ +std::string encode_integer_buffer(int const *buf, int const len); + +} // end namespace ja3 diff --git a/plugins/experimental/jax_fingerprint/ja3/test_ja3.cc b/plugins/experimental/jax_fingerprint/ja3/test_ja3.cc new file mode 100644 index 00000000000..1e5de5e25d5 --- /dev/null +++ b/plugins/experimental/jax_fingerprint/ja3/test_ja3.cc @@ -0,0 +1,96 @@ +/** @file test_utils.cc + + Unit tests for ja3. + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ + +#include "ja3_utils.h" + +#include + +TEST_CASE("ja3 byte buffer encoding") +{ + unsigned char const buf[]{0x8, 0x3, 0x4}; + + SECTION("empty buffer") + { + auto got{ja3::encode_byte_buffer(nullptr, 0)}; + CHECK("" == got); + } + + SECTION("1 value") + { + auto got{ja3::encode_byte_buffer(buf, 1)}; + CHECK("8" == got); + } + + SECTION("3 values") + { + auto got{ja3::encode_byte_buffer(buf, 3)}; + CHECK("8-3-4" == got); + } +} + +TEST_CASE("ja3 word buffer encoding") +{ + unsigned char const buf[]{0x0, 0x5, 0x0a, 0x0a, 0x0, 0x8, 0xda, 0xda, 0x1, 0x0}; + + SECTION("empty buffer") + { + auto got{ja3::encode_word_buffer(nullptr, 0)}; + CHECK("" == got); + } + + SECTION("1 value") + { + auto got{ja3::encode_word_buffer(buf, 2)}; + CHECK("5" == got); + } + + SECTION("5 values including GREASE values") + { + auto got{ja3::encode_word_buffer(buf, 10)}; + CHECK("5-8-256" == got); + } +} + +TEST_CASE("ja3 integer buffer encoding") +{ + int const buf[]{5, 2570, 8, 56026, 256}; + + SECTION("empty buffer") + { + auto got{ja3::encode_integer_buffer(nullptr, 0)}; + CHECK("" == got); + } + + SECTION("1 value") + { + auto got{ja3::encode_integer_buffer(buf, 1)}; + CHECK("5" == got); + } + + SECTION("5 values including GREASE values") + { + auto got{ja3::encode_integer_buffer(buf, 5)}; + CHECK("5-8-256" == got); + } +} diff --git a/plugins/experimental/jax_fingerprint/ja4/ja4.cc b/plugins/experimental/jax_fingerprint/ja4/ja4.cc new file mode 100644 index 00000000000..f82f42a0d10 --- /dev/null +++ b/plugins/experimental/jax_fingerprint/ja4/ja4.cc @@ -0,0 +1,175 @@ +/** @file + * + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ + +#include "ja4.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +static char convert_protocol_to_char(JA4::Protocol protocol); +static std::string convert_TLS_version_to_string(std::uint16_t TLS_version); +static char convert_SNI_to_char(JA4::SNI SNI_type); +static std::string convert_count_to_two_digit_string(std::size_t count); +static std::string convert_ALPN_to_two_char_string(std::string_view ALPN); +static void remove_trailing_character(std::string &s); +static std::string hexify(std::uint16_t n); + +namespace +{ +constexpr std::size_t U16_HEX_BUF_SIZE{4}; +} // end anonymous namespace + +std::string +JA4::make_JA4_a_raw(TLSClientHelloSummary const &TLS_summary) +{ + std::string result; + result.reserve(9); + result.push_back(convert_protocol_to_char(TLS_summary.protocol)); + result.append(convert_TLS_version_to_string(TLS_summary.TLS_version)); + result.push_back(convert_SNI_to_char(TLS_summary.get_SNI_type())); + result.append(convert_count_to_two_digit_string(TLS_summary.get_cipher_count())); + result.append(convert_count_to_two_digit_string(TLS_summary.get_extension_count())); + result.append(convert_ALPN_to_two_char_string(TLS_summary.ALPN)); + return result; +} + +static char +convert_protocol_to_char(JA4::Protocol protocol) +{ + return static_cast(protocol); +} + +static std::string +convert_TLS_version_to_string(std::uint16_t TLS_version) +{ + switch (TLS_version) { + case 0x304: + return "13"; + case 0x303: + return "12"; + case 0x302: + return "11"; + case 0x301: + return "10"; + case 0x300: + return "s3"; + case 0x200: + return "s2"; + case 0x100: + return "s1"; + case 0xfeff: + return "d1"; + case 0xfefd: + return "d2"; + case 0xfefc: + return "d3"; + default: + return "00"; + } +} + +static char +convert_SNI_to_char(JA4::SNI SNI_type) +{ + return static_cast(SNI_type); +} + +static std::string +convert_count_to_two_digit_string(std::size_t count) +{ + std::string result; + if (count <= 9) { + result.push_back('0'); + } + // We could also clamp the lower bound to 1 since there must be at least 1 + // cipher, but 0 is more helpful for debugging if the cipher list is empty. + result.append(std::to_string(std::clamp(count, std::size_t{0}, std::size_t{99}))); + return result; +} + +std::string +convert_ALPN_to_two_char_string(std::string_view ALPN) +{ + std::string result; + if (ALPN.empty()) { + result = "00"; + } else { + result.push_back(ALPN.front()); + result.push_back(ALPN.back()); + } + return result; +} + +std::string +JA4::make_JA4_b_raw(TLSClientHelloSummary const &TLS_summary) +{ + std::string result; + result.reserve(12); + std::vector temp = TLS_summary.get_ciphers(); + std::sort(temp.begin(), temp.end()); + + for (auto cipher : temp) { + result.append(hexify(cipher)); + result.push_back(','); + } + remove_trailing_character(result); + return result; +} + +std::string +JA4::make_JA4_c_raw(TLSClientHelloSummary const &TLS_summary) +{ + std::string result; + result.reserve(12); + std::vector temp = TLS_summary.get_extensions(); + std::sort(temp.begin(), temp.end()); + + for (auto extension : temp) { + result.append(hexify(extension)); + result.push_back(','); + } + remove_trailing_character(result); + return result; +} + +void +remove_trailing_character(std::string &s) +{ + if (!s.empty()) { + s.pop_back(); + } +} + +std::string +hexify(std::uint16_t n) +{ + char result[U16_HEX_BUF_SIZE + 1]{}; + std::snprintf(result, sizeof(result), "%.4x", n); + return result; +} diff --git a/plugins/experimental/jax_fingerprint/ja4/ja4.h b/plugins/experimental/jax_fingerprint/ja4/ja4.h new file mode 100644 index 00000000000..11c549a0dbd --- /dev/null +++ b/plugins/experimental/jax_fingerprint/ja4/ja4.h @@ -0,0 +1,171 @@ +/** @file + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ + +#pragma once + +#include +#include +#include +#include + +namespace JA4 +{ + +constexpr char PORTION_DELIMITER{'_'}; + +enum class Protocol { + DTLS = 'd', + QUIC = 'q', + TLS = 't', +}; + +enum class SNI { + to_domain = 'd', + to_IP = 'i', +}; + +/** + * Represents the data sent in a TLS Client Hello needed for JA4 fingerprints. + */ +class TLSClientHelloSummary +{ +public: + using difference_type = std::iterator_traits::iterator>::difference_type; + + Protocol protocol; + std::uint16_t TLS_version{0}; // 0 is not the default, this is only to not have it un-initialized. + std::string ALPN; + + std::vector const &get_ciphers() const; + void add_cipher(std::uint16_t cipher); + + std::vector const &get_extensions() const; + void add_extension(std::uint16_t extension); + + /** + * Get the number of ciphers excluding GREASE values. + * + * @return Returns the count of non-GREASE ciphers. + */ + difference_type get_cipher_count() const; + + /** + * Get the number of extensions excluding GREASE values. + * + * @return Returns the count of non-GREASE extensions. + */ + difference_type get_extension_count() const; + + /** Get the SNI type, domain or IP. + * + * @return Returns SNI::to_domain or SNI::to_IP. + */ + SNI get_SNI_type() const; + +private: + std::vector _ciphers; + std::vector _extensions; + int _extension_count_including_sni_and_alpn{0}; + SNI _SNI_type{SNI::to_IP}; +}; + +/** + * Calculate the a portion of the JA4 fingerprint for the given client hello. + * + * The a portion of the fingerprint encodes the protocol, TLS version, SNI + * type, number of cipher suites, number of extensions, and first ALPN value. + * + * For more information see: + * https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4.md. + * + * @param TLS_summary The TLS client hello. + * @return Returns a string containing the a portion of the JA4 fingerprint. + */ +std::string make_JA4_a_raw(TLSClientHelloSummary const &TLS_summary); + +/** + * Calculate the b portion of the JA4 fingerprint for the given client hello. + * + * The b portion of the fingerprint is a comma-delimited list of lowercase hex + * numbers representing the cipher suites in sorted order. GREASE values are + * ignored. + * + * For more information see: + * https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4.md. + * + * @param TLS_summary The TLS client hello. + * @return Returns a string containing the b portion of the JA4 fingerprint. + */ +std::string make_JA4_b_raw(TLSClientHelloSummary const &TLS_summary); + +/** + * Calculate the c portion of the JA4 fingerprint for the given client hello. + * + * The b portion of the fingerprint is a comma-delimited list of lowercase hex + * numbers representing the extensions in sorted order. GREASE values and the + * SNI and ALPN extensions are ignored. + * + * For more information see: + * https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4.md. + * + * @param TLS_summary The TLS client hello. + * @return Returns a string containing the c portion of the JA4 fingerprint. + */ +std::string make_JA4_c_raw(TLSClientHelloSummary const &TLS_summary); + +/** + * Calculate the JA4 fingerprint for the given TLS client hello. + * + * @param TLS_summary The TLS client hello. If there was no ALPN in the + * Client Hello, TLS_summary.ALPN should either be empty or set to "00". + * Behavior when the number of digits in TLS_summary.TLS_version is greater + * than 2, the number of digits in TLS_summary.ALPN is greater than 2 + * (except when TLS_summary.ALPN is empty) is unspecified. + * @param UnaryOp hasher A hash function. For a specification-compliant + * JA4 fingerprint, this should be a sha256 hash. + * @return Returns a string containing the JA4 fingerprint. + */ +template +std::string +make_JA4_fingerprint(TLSClientHelloSummary const &TLS_summary, UnaryOp hasher) +{ + std::string result; + result.append(make_JA4_a_raw(TLS_summary)); + result.push_back(JA4::PORTION_DELIMITER); + result.append(hasher(make_JA4_b_raw(TLS_summary)).substr(0, 12)); + result.push_back(JA4::PORTION_DELIMITER); + result.append(hasher(make_JA4_c_raw(TLS_summary)).substr(0, 12)); + return result; +} + +/** + * Check whether @a value is a GREASE value. + * + * These are reserved extensions randomly advertised to keep implementations + * well lubricated. They are ignored in all parts of JA4 because of their + * random nature. + * + * @return Returns true if the value is a GREASE value, fales otherwise. + */ +bool is_GREASE(std::uint16_t value); + +} // end namespace JA4 diff --git a/plugins/experimental/jax_fingerprint/ja4/ja4_method.cc b/plugins/experimental/jax_fingerprint/ja4/ja4_method.cc new file mode 100644 index 00000000000..4f56ba86f5d --- /dev/null +++ b/plugins/experimental/jax_fingerprint/ja4/ja4_method.cc @@ -0,0 +1,153 @@ +/** @file + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#include "ts/ts.h" + +#include "../plugin.h" +#include "../context.h" +#include "ja4_method.h" +#include "ja4.h" + +#include +#include + +constexpr unsigned int EXT_ALPN{0x10}; +constexpr unsigned int EXT_SUPPORTED_VERSIONS{0x2b}; + +namespace ja4_method +{ + +void on_client_hello(JAxContext *, TSVConn); + +struct Method method = { + "JA4", + Method::Type::CONNECTION_BASED, + on_client_hello, + nullptr, +}; + +} // namespace ja4_method + +static std::uint16_t +get_version(TSClientHello ch) +{ + unsigned char const *buf{}; + std::size_t buflen{}; + if (TS_SUCCESS == TSClientHelloExtensionGet(ch, EXT_SUPPORTED_VERSIONS, &buf, &buflen)) { + std::uint16_t max_version{0}; + size_t n_versions = buf[0]; + for (size_t i = 1; i + 1 < buflen && i < (n_versions * 2) + 1; i += 2) { + std::uint16_t version = (buf[i] << 8) | buf[i + 1]; + if (!JA4::is_GREASE(version) && version > max_version) { + max_version = version; + } + } + return max_version; + } else { + Dbg(dbg_ctl, "No supported_versions extension... using legacy version."); + return ch.get_version(); + } +} + +static std::string +get_first_ALPN(TSClientHello ch) +{ + unsigned char const *buf{}; + std::size_t buflen{}; + std::string result{""}; + if (TS_SUCCESS == TSClientHelloExtensionGet(ch, EXT_ALPN, &buf, &buflen)) { + // The first two bytes are a 16bit encoding of the total length. + unsigned char first_ALPN_length{buf[2]}; + TSAssert(buflen > 4); + TSAssert(0 != first_ALPN_length); + result.assign(&buf[3], (&buf[3]) + first_ALPN_length); + } + + return result; +} + +static constexpr std::uint16_t +make_word(unsigned char lowbyte, unsigned char highbyte) +{ + return (static_cast(highbyte) << 8) | lowbyte; +} + +static void +add_ciphers(JA4::TLSClientHelloSummary &summary, TSClientHello ch) +{ + const uint8_t *buf = ch.get_cipher_suites(); + size_t buflen = ch.get_cipher_suites_len(); + + if (buflen > 0) { + for (std::size_t i = 0; i + 1 < buflen; i += 2) { + summary.add_cipher(make_word(buf[i], buf[i + 1])); + } + } else { + Dbg(dbg_ctl, "Failed to get ciphers."); + } +} + +static void +add_extensions(JA4::TLSClientHelloSummary &summary, TSClientHello ch) +{ + for (auto ext_type : ch.get_extension_types()) { + summary.add_extension(ext_type); + } +} + +static std::string +hash_with_SHA256(std::string_view sv) +{ + Dbg(dbg_ctl, "Hashing %s", std::string{sv}.c_str()); + unsigned char hash[SHA256_DIGEST_LENGTH]; + SHA256(reinterpret_cast(sv.data()), sv.size(), hash); + std::string result; + result.resize(SHA256_DIGEST_LENGTH * 2 + 1); + for (int i{0}; i < SHA256_DIGEST_LENGTH; ++i) { + std::snprintf(result.data() + (i * 2), result.size() - (i * 2), "%02x", hash[i]); + } + return result; +} + +static std::string +get_fingerprint(TSClientHello ch) +{ + JA4::TLSClientHelloSummary summary{}; + summary.protocol = JA4::Protocol::TLS; + summary.TLS_version = get_version(ch); + summary.ALPN = get_first_ALPN(ch); + add_ciphers(summary, ch); + add_extensions(summary, ch); + std::string result{JA4::make_JA4_fingerprint(summary, hash_with_SHA256)}; + return result; +} + +void +ja4_method::on_client_hello(JAxContext *ctx, TSVConn vconn) +{ + TSClientHello ch = TSVConnClientHelloGet(vconn); + + if (!ch) { + Dbg(dbg_ctl, "Could not get TSClientHello object."); + } else { + ctx->set_fingerprint(get_fingerprint(ch)); + } +} diff --git a/plugins/experimental/jax_fingerprint/ja4/ja4_method.h b/plugins/experimental/jax_fingerprint/ja4/ja4_method.h new file mode 100644 index 00000000000..7171dfdacfe --- /dev/null +++ b/plugins/experimental/jax_fingerprint/ja4/ja4_method.h @@ -0,0 +1,32 @@ +/** @file + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ + +#pragma once + +#include "../method.h" + +namespace ja4_method +{ + +extern struct Method method; + +} diff --git a/plugins/experimental/jax_fingerprint/ja4/test_ja4.cc b/plugins/experimental/jax_fingerprint/ja4/test_ja4.cc new file mode 100644 index 00000000000..8860d105f17 --- /dev/null +++ b/plugins/experimental/jax_fingerprint/ja4/test_ja4.cc @@ -0,0 +1,425 @@ +/** @file + * + Unit tests for JA4 fingerprint calculation. + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ + +#include "ja4.h" + +#include + +#include +#include +#include +#include +#include + +static std::string call_JA4(JA4::TLSClientHelloSummary const &TLS_summary); +static std::string inc(std::string_view sv); + +TEST_CASE("JA4") +{ + JA4::TLSClientHelloSummary TLS_summary{}; + + SECTION("Given the protocol is TCP, " + "when we create a JA4 fingerprint, " + "then the first character thereof should be 't'.") + { + TLS_summary.protocol = JA4::Protocol::TLS; + + CHECK("t" == call_JA4(TLS_summary).substr(0, 1)); + } + + SECTION("Given the protocol is QUIC, " + "when we create a JA4 fingerprint, " + "then the first character thereof should be 'q'.") + { + TLS_summary.protocol = JA4::Protocol::QUIC; + CHECK(call_JA4(TLS_summary).starts_with('q')); + } + + SECTION("Given the protocol is DTLS, " + "when we create a JA4 fingerprint, " + "then the first character thereof should be 'd'.") + { + TLS_summary.protocol = JA4::Protocol::DTLS; + CHECK(call_JA4(TLS_summary).starts_with('d')); + } + + SECTION("Given the TLS version is unknown, " + "when we create a JA4 fingerprint, " + "then indices [1,2] thereof should contain \"00\".") + { + TLS_summary.TLS_version = 0x123; + CHECK("00" == call_JA4(TLS_summary).substr(1, 2)); + TLS_summary.TLS_version = 0x234; + CHECK("00" == call_JA4(TLS_summary).substr(1, 2)); + } + + SECTION("Given the TLS version is known, " + "when we create a JA4 fingerprint, " + "then indices [1,2] thereof should contain the correct value.") + { + std::unordered_map values{ + {0x304, "13"}, + {0x303, "12"}, + {0x302, "11"}, + {0x301, "10"}, + {0x300, "s3"}, + {0x200, "s2"}, + {0x100, "s1"}, + {0xfeff, "d1"}, + {0xfefd, "d2"}, + {0xfefc, "d3"} + }; + for (auto const &[version, expected] : values) { + CAPTURE(version, expected); + TLS_summary.TLS_version = version; + CHECK(expected == call_JA4(TLS_summary).substr(1, 2)); + } + } + + SECTION("Given the SNI extension is present, " + "when we create a JA4 fingerprint, " + "then index 3 thereof should contain 'd'.") + { + TLS_summary.add_extension(0x0); + CHECK("d" == call_JA4(TLS_summary).substr(3, 1)); + } + + SECTION("Given the SNI extension is not present, " + "when we create a JA4 fingerprint, " + "then index 3 thereof should contain 'i'.") + { + TLS_summary.add_extension(0x31); + CHECK("i" == call_JA4(TLS_summary).substr(3, 1)); + } + + SECTION("Given there is one cipher, " + "when we create a JA4 fingerprint, " + "then indices [4,5] thereof should contain \"01\".") + { + TLS_summary.add_cipher(1); + CHECK("01" == call_JA4(TLS_summary).substr(4, 2)); + } + + SECTION("Given there are 9 ciphers, " + "when we create a JA4 fingerprint, " + "then indices [4,5] thereof should contain \"09\".") + { + for (int i{0}; i < 9; ++i) { + TLS_summary.add_cipher(i); + } + CHECK("09" == call_JA4(TLS_summary).substr(4, 2)); + } + + SECTION("Given there are 10 ciphers, " + "when we create a JA4 fingerprint, " + "then indices [4,5] thereof should contain \"10\".") + { + for (int i{0}; i < 10; ++i) { + TLS_summary.add_cipher(i); + } + CHECK("10" == call_JA4(TLS_summary).substr(4, 2)); + } + + SECTION("Given there are more than 99 ciphers, " + "when we create a JA4 fingerprint, " + "then indices [4,5] thereof should contain \"99\".") + { + for (int i{0}; i < 100; ++i) { + TLS_summary.add_cipher(i); + } + CHECK("99" == call_JA4(TLS_summary).substr(4, 2)); + } + + SECTION("Given the ciphers include a GREASE value, " + "when we create a JA4 fingerprint, " + "then that value should not be included in the count.") + { + TLS_summary.add_cipher(0x0a0a); + TLS_summary.add_cipher(72); + CHECK("01" == call_JA4(TLS_summary).substr(4, 2)); + } + + SECTION("Given there are no extensions, " + "when we create a JA4 fingerprint, " + "then indices [6,7] thereof should contain \"00\".") + { + CHECK("00" == call_JA4(TLS_summary).substr(6, 2)); + } + + SECTION("Given there are 9 extensions, " + "when we create a JA4 fingerprint, " + "then indices [6,7] thereof should contain \"09\".") + { + for (int i{0}; i < 9; ++i) { + TLS_summary.add_extension(i); + } + CHECK("09" == call_JA4(TLS_summary).substr(6, 2)); + } + + SECTION("Given there are 99 extensions, " + "when we create a JA4 fingerprint, " + "then indices [6,7] thereof should contain \"99\".") + { + for (int i{0}; i < 99; ++i) { + TLS_summary.add_extension(i); + } + CHECK("99" == call_JA4(TLS_summary).substr(6, 2)); + } + + SECTION("Given there are more than 99 extensions, " + "when we create a JA4 fingerprint, " + "then indices [6,7] thereof should contain \"99\".") + { + for (int i{0}; i < 100; ++i) { + TLS_summary.add_extension(i); + } + CHECK("99" == call_JA4(TLS_summary).substr(6, 2)); + } + + SECTION("Given the extensions include a GREASE value, " + "when we create a JA4 fingerprint, " + "then that value should not be included in the count.") + { + TLS_summary.add_extension(2); + TLS_summary.add_extension(0x0a0a); + CHECK("01" == call_JA4(TLS_summary).substr(6, 2)); + } + + // These may be covered by the earlier tests as well, but this documents the + // behavior explicitly. + SECTION("When we create a JA4 fingerprint, " + "then the SNI and ALPN extensions should be included in the count.") + { + TLS_summary.add_extension(0x0); + TLS_summary.add_extension(0x10); + CHECK("02" == call_JA4(TLS_summary).substr(6, 2)); + } + + SECTION("Given the ALPN value is empty, " + "when we create a JA4 fingerprint, " + "then indices [8,9] thereof should contain \"00\".") + { + TLS_summary.ALPN = ""; + CHECK("00" == call_JA4(TLS_summary).substr(8, 2)); + } + + // This should never happen in practice because all registered ALPN values + // are at least 2 characters long, but it's the correct behavior according + // to the spec. :-) + SECTION("Given the ALPN value is \"a\", " + "when we create a JA4 fingerprint, " + "then indices [8,9] thereof should contain \"aa\".") + { + TLS_summary.ALPN = 'a'; + CHECK("aa" == call_JA4(TLS_summary).substr(8, 2)); + } + + SECTION("Given the ALPN value is \"h3\", " + "when we create a JA4 fingerprint, " + "then indices [8,9] thereof should contain \"h3\".") + { + TLS_summary.ALPN = "h3"; + CHECK("h3" == call_JA4(TLS_summary).substr(8, 2)); + } + + SECTION("Given the ALPN value is \"imap\", " + "when we create a JA4 fingerprint, " + "then indices [8,9] thereof should contain \"ip\".") + { + TLS_summary.ALPN = "imap"; + CHECK("ip" == call_JA4(TLS_summary).substr(8, 2)); + } + + SECTION("When we create a JA4 fingeprint, " + "then index 10 thereof should contain '_'.") + { + CHECK("_" == call_JA4(TLS_summary).substr(10, 1)); + } + + SECTION("When we create a JA4 fingerprint, " + "then the b section should be passed through the hash function.") + { + TLS_summary.add_cipher(10); + CHECK("111b" == JA4::make_JA4_fingerprint(TLS_summary, [](std::string_view sv) { return inc(sv); }).substr(11, 4)); + } + + // As per the spec, we expect 4-character, comma-delimited hex values. + SECTION("Given only ciphers 2, 12, and 17 in that order, " + "when we create a JA4 fingerprint, " + "then the hash should be invoked with \"0002,000c,0011\".") + { + TLS_summary.add_cipher(2); + TLS_summary.add_cipher(12); + TLS_summary.add_cipher(17); + bool verified{false}; + // INFO doesn't work from inside the lambda body. :/ + JA4::make_JA4_fingerprint(TLS_summary, [&verified](std::string_view sv) { + if ("0002,000c,0011" == sv) { + verified = true; + } + return sv; + }); + CHECK(verified); + } + + SECTION("When we create a JA4 fingerprint, " + "then the cipher values should be sorted before hashing.") + { + TLS_summary.add_cipher(17); + TLS_summary.add_cipher(2); + TLS_summary.add_cipher(12); + bool verified{false}; + // INFO doesn't work from inside the lambda body. :/ + JA4::make_JA4_fingerprint(TLS_summary, [&verified](std::string_view sv) { + if ("0002,000c,0011" == sv) { + verified = true; + } + return sv; + }); + CHECK(verified); + } + + SECTION("When we create a JA4 fingerprint, " + "then GREASE values in the cipher list should be ignored.") + { + TLS_summary.add_cipher(0x0a0a); + TLS_summary.add_cipher(2); + bool verified{false}; + // INFO doesn't work from inside the lambda body. :/ + JA4::make_JA4_fingerprint(TLS_summary, [&verified](std::string_view sv) { + if ("0002" == sv) { + verified = true; + } + return sv; + }); + CHECK(verified); + } + + // All the tests from now on have enough ciphers to ensure a long enough + // hash using our default hash (the id function) so that the length of the + // JA4 fingerprint will be valid. + TLS_summary.add_cipher(1); + TLS_summary.add_cipher(2); + TLS_summary.add_cipher(3); + + SECTION("When we create a JA4 fingerprint, " + "then we should truncate the section b hash to 12 characters.") + { + CHECK("001,0002,000_" == JA4::make_JA4_fingerprint(TLS_summary, [](std::string_view sv) { + return sv.empty() ? sv : sv.substr(1); + }).substr(11, 13)); + } + + SECTION("When we create a JA4 fingeprint, " + "then index 10 thereof should contain '_'.") + { + CHECK("_" == call_JA4(TLS_summary).substr(23, 1)); + } + + SECTION("When we create a JA4 fingerprint, " + "then the c section should be passed through the hash function.") + { + TLS_summary.add_extension(10); + CHECK("111b" == JA4::make_JA4_fingerprint(TLS_summary, [](std::string_view sv) { return inc(sv); }).substr(24, 4)); + } + + // As per the spec, we expect 4-character, comma-delimited hex values. + SECTION("Given only extensions 2, 12, and 17 in that order, " + "when we create a JA4 fingerprint, " + "then the hash should be invoked with \"0002,000c,0011\".") + { + TLS_summary.add_extension(2); + TLS_summary.add_extension(12); + TLS_summary.add_extension(17); + + bool verified{false}; + // INFO doesn't work from inside the lambda body. :/ + JA4::make_JA4_fingerprint(TLS_summary, [&verified](std::string_view sv) { + if ("0002,000c,0011" == sv) { + verified = true; + } + return sv; + }); + CHECK(verified); + } + + SECTION("When we create a JA4 fingerprint, " + "then the extension values should be sorted before hashing.") + { + TLS_summary.add_extension(17); + TLS_summary.add_extension(2); + TLS_summary.add_extension(12); + bool verified{false}; + // INFO doesn't work from inside the lambda body. :/ + JA4::make_JA4_fingerprint(TLS_summary, [&verified](std::string_view sv) { + if ("0002,000c,0011" == sv) { + verified = true; + } + return sv; + }); + CHECK(verified); + } + + SECTION("When we create a JA4 fingerprint, " + "then we ignore GREASE, SNI, ALPN, and SNI values in the extensions.") + { + TLS_summary.add_extension(0x0a0a); + TLS_summary.add_extension(0x0); + TLS_summary.add_extension(0x10); + TLS_summary.add_extension(5); + bool verified{false}; + // INFO doesn't work from inside the lambda body. :/ + JA4::make_JA4_fingerprint(TLS_summary, [&verified](std::string_view sv) { + if ("0005" == sv) { + verified = true; + } + return sv; + }); + CHECK(verified); + } + + SECTION("When we create a JA4 fingerprint, " + "then we total length of the fingerprint should be 36 characters.") + { + TLS_summary.add_extension(1); + TLS_summary.add_extension(2); + TLS_summary.add_extension(3); + CHECK(36 == call_JA4(TLS_summary).size()); + } +} + +std::string +call_JA4(JA4::TLSClientHelloSummary const &TLS_summary) +{ + return JA4::make_JA4_fingerprint(TLS_summary, [](std::string_view sv) { return sv; }); +} + +std::string +inc(std::string_view sv) +{ + std::string result; + result.resize(sv.size()); + std::transform(sv.begin(), sv.end(), result.begin(), [](char c) { return c + 1; }); + return result; +} diff --git a/plugins/experimental/jax_fingerprint/ja4/tls_client_hello_summary.cc b/plugins/experimental/jax_fingerprint/ja4/tls_client_hello_summary.cc new file mode 100644 index 00000000000..cc35a1c83bf --- /dev/null +++ b/plugins/experimental/jax_fingerprint/ja4/tls_client_hello_summary.cc @@ -0,0 +1,112 @@ +/** @file + * + TLSClientHelloSummary data structure for JA4 fingerprint calculation. + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ + +#include "ja4.h" + +#include +#include +#include +#include +#include + +namespace +{ + +constexpr std::array GREASE_values{0x0a0a, 0x1a1a, 0x2a2a, 0x3a3a, 0x4a4a, 0x5a5a, 0x6a6a, 0x7a7a, + 0x8a8a, 0x9a9a, 0xaaaa, 0xbaba, 0xcaca, 0xdada, 0xeaea, 0xfafa}; +constexpr std::uint16_t extension_SNI{0x0}; +constexpr std::uint16_t extension_ALPN{0x10}; + +} // end anonymous namespace + +static bool is_ignored_non_GREASE_extension(std::uint16_t extension); + +std::vector const & +JA4::TLSClientHelloSummary::get_ciphers() const +{ + return this->_ciphers; +} + +void +JA4::TLSClientHelloSummary::add_cipher(std::uint16_t cipher) +{ + if (is_GREASE(cipher)) { + return; + } + + this->_ciphers.push_back(cipher); +} + +std::vector const & +JA4::TLSClientHelloSummary::get_extensions() const +{ + return this->_extensions; +} + +void +JA4::TLSClientHelloSummary::add_extension(std::uint16_t extension) +{ + if (is_GREASE(extension)) { + return; + } + + if (extension_SNI == extension) { + this->_SNI_type = SNI::to_domain; + } + + ++this->_extension_count_including_sni_and_alpn; + if (!is_ignored_non_GREASE_extension(extension)) { + this->_extensions.push_back(extension); + } +} + +JA4::TLSClientHelloSummary::difference_type +JA4::TLSClientHelloSummary::get_cipher_count() const +{ + return this->_ciphers.size(); +} + +JA4::TLSClientHelloSummary::difference_type +JA4::TLSClientHelloSummary::get_extension_count() const +{ + return this->_extension_count_including_sni_and_alpn; +} + +bool +is_ignored_non_GREASE_extension(std::uint16_t extension) +{ + return (extension_SNI == extension) || (extension_ALPN == extension); +} + +JA4::SNI +JA4::TLSClientHelloSummary::get_SNI_type() const +{ + return this->_SNI_type; +} + +bool +JA4::is_GREASE(std::uint16_t value) +{ + return std::binary_search(GREASE_values.begin(), GREASE_values.end(), value); +} diff --git a/plugins/experimental/jax_fingerprint/ja4h/ja4h.cc b/plugins/experimental/jax_fingerprint/ja4h/ja4h.cc new file mode 100644 index 00000000000..ff2692150af --- /dev/null +++ b/plugins/experimental/jax_fingerprint/ja4h/ja4h.cc @@ -0,0 +1,107 @@ +#include "ja4h.h" +#include + +Extractor::Extractor(TSHttpTxn txnp) : _txn(txnp) +{ + TSHttpTxnClientReqGet(txnp, &(this->_request), &(this->_req_hdr)); +} + +Extractor::~Extractor() +{ + if (this->_request != nullptr) { + TSHandleMLocRelease(this->_request, TS_NULL_MLOC, this->_req_hdr); + } +} + +std::string_view +Extractor::get_method() +{ + if (this->_request == nullptr) { + return ""; + } + + int method_len; + const char *method = TSHttpHdrMethodGet(this->_request, this->_req_hdr, &method_len); + + return {method, static_cast(method_len)}; +} + +int +Extractor::get_version() +{ + if (TSHttpTxnClientProtocolStackContains(this->_txn, "h2")) { + return 2 << 16; + } else if (TSHttpTxnClientProtocolStackContains(this->_txn, "h2")) { + return 3 << 16; + } else { + return TSHttpHdrVersionGet(this->_request, this->_req_hdr); + } +} + +bool +Extractor::has_cookie_field() +{ + TSMLoc mloc = TSMimeHdrFieldFind(this->_request, this->_req_hdr, TS_MIME_FIELD_COOKIE, TS_MIME_LEN_COOKIE); + TSHandleMLocRelease(this->_request, this->_req_hdr, mloc); + return mloc != TS_NULL_MLOC; +} + +bool +Extractor::has_referer_field() +{ + TSMLoc mloc = TSMimeHdrFieldFind(this->_request, this->_req_hdr, TS_MIME_FIELD_REFERER, TS_MIME_LEN_REFERER); + TSHandleMLocRelease(this->_request, this->_req_hdr, mloc); + return mloc != TS_NULL_MLOC; +} + +int +Extractor::get_field_count() +{ + return TSMimeHdrFieldsCount(this->_request, this->_req_hdr); +} + +std::string_view +Extractor::get_accept_language() +{ + TSMLoc mloc = TSMimeHdrFieldFind(this->_request, this->_req_hdr, TS_MIME_FIELD_ACCEPT_LANGUAGE, TS_MIME_LEN_ACCEPT_LANGUAGE); + if (mloc == TS_NULL_MLOC) { + return {}; + } + int value_len; + const char *value = TSMimeHdrFieldValueStringGet(this->_request, this->_req_hdr, mloc, 0, &value_len); + TSHandleMLocRelease(this->_request, this->_req_hdr, mloc); + return {value, static_cast(value_len)}; +} + +void +Extractor::get_headers_hash(unsigned char out[32]) +{ + SHA256_CTX sha256ctx; + SHA256_Init(&sha256ctx); + + TSMLoc field_loc = TSMimeHdrFieldGet(this->_request, this->_req_hdr, 0); + + while (field_loc != TS_NULL_MLOC) { + int field_name_len; + char *field_name = const_cast(TSMimeHdrFieldNameGet(this->_request, this->_req_hdr, field_loc, &field_name_len)); + if (field_name_len == TS_MIME_LEN_COOKIE) { + if (std::ranges::equal(std::string_view{field_name, static_cast(field_name_len)}, "cookie", + [](char c1, char c2) { return c1 == std::tolower(c2); })) { + continue; + }; + } else if (field_name_len == TS_MIME_LEN_REFERER) { + if (std::ranges::equal(std::string_view{field_name, static_cast(field_name_len)}, "referer", + [](char c1, char c2) { return c1 == std::tolower(c2); })) { + continue; + } + } + + SHA256_Update(&sha256ctx, field_name, field_name_len); + + TSMLoc next_field_loc = TSMimeHdrFieldNext(this->_request, this->_req_hdr, field_loc); + TSHandleMLocRelease(this->_request, this->_req_hdr, field_loc); + field_loc = next_field_loc; + } + + SHA256_Final(out, &sha256ctx); +} diff --git a/plugins/experimental/jax_fingerprint/ja4h/ja4h.h b/plugins/experimental/jax_fingerprint/ja4h/ja4h.h new file mode 100644 index 00000000000..ec19bb36add --- /dev/null +++ b/plugins/experimental/jax_fingerprint/ja4h/ja4h.h @@ -0,0 +1,24 @@ +#pragma once + +#include "ts/ts.h" +#include + +class Extractor +{ +public: + Extractor(TSHttpTxn txnp); + ~Extractor(); + std::string_view get_method(); + int get_version(); + bool has_cookie_field(); + bool has_referer_field(); + int get_field_count(); + std::string_view get_accept_language(); + void get_headers_hash(unsigned char out[32]); + +private: + TSHttpTxn _txn; + TSMBuffer _request = nullptr; + TSMLoc _req_hdr = nullptr; + ; +}; diff --git a/plugins/experimental/jax_fingerprint/ja4h/ja4h_method.cc b/plugins/experimental/jax_fingerprint/ja4h/ja4h_method.cc new file mode 100644 index 00000000000..ce842b154ba --- /dev/null +++ b/plugins/experimental/jax_fingerprint/ja4h/ja4h_method.cc @@ -0,0 +1,121 @@ +/** @file + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#include "ts/ts.h" + +#include "../plugin.h" +#include "../context.h" +#include "ja4h_method.h" +#include "ja4h.h" + +namespace ja4h_method +{ +void on_request(JAxContext *, TSHttpTxn); + +struct Method method = { + "JA4H", + Method::Type::REQUEST_BASED, + nullptr, + on_request, +}; + +} // namespace ja4h_method + +constexpr int JA4H_FINGERPRINT_LENGTH = 51; + +static std::string +get_fingerprint(TSHttpTxn txnp) +{ + char fingerprint[JA4H_FINGERPRINT_LENGTH]; + Extractor ex(txnp); + // JA4H_a + std::string_view method = ex.get_method(); + fingerprint[0] = std::tolower(method[0]); + fingerprint[1] = std::tolower(method[1]); + int version = ex.get_version(); + fingerprint[2] = 0x30 | (version >> 16); + fingerprint[3] = 0x30 | (version & 0xFFFF); + fingerprint[4] = ex.has_cookie_field() ? 'c' : 'n'; + fingerprint[5] = ex.has_referer_field() ? 'c' : 'n'; + int field_count = ex.get_field_count(); + if (field_count < 100) { + fingerprint[6] = 0x30 | (field_count / 10); + fingerprint[7] = 0x30 | (field_count % 10); + } else { + fingerprint[6] = '9'; + fingerprint[7] = '9'; + } + std::string_view accept_lang = ex.get_accept_language(); + if (accept_lang.empty()) { + fingerprint[8] = '0'; + fingerprint[9] = '0'; + fingerprint[10] = '0'; + fingerprint[11] = '0'; + } else { + for (int i = 0, j = 0; i < 4; ++i) { + while (accept_lang[j] < 'A' && accept_lang[j] != ';') { + ++j; + } + if (accept_lang[j] == ';') { + fingerprint[8 + i] = '0'; + } else { + fingerprint[8 + i] = std::tolower(accept_lang[j]); + ++j; + } + } + } + + fingerprint[12] = '_'; + + // JA4H_b + unsigned char hash[32]; + ex.get_headers_hash(hash); + for (int i = 0; i < 6; ++i) { + unsigned int h = hash[i] >> 4; + unsigned int l = hash[i] & 0x0F; + fingerprint[13 + (i * 2)] = h <= 9 ? (0x30 + h) : (0x60 + h - 10); + fingerprint[13 + (i * 2) + 1] = l <= 9 ? (0x30 + l) : (0x60 + l - 10); + } + + fingerprint[25] = '_'; + + // JA4H_c + // Not implemented + for (int i = 0; i < 12; ++i) { + fingerprint[26 + i] = '0'; + } + + fingerprint[38] = '_'; + + // JA4H_d + // Not implemented + for (int i = 0; i < 12; ++i) { + fingerprint[39 + i] = '0'; + } + + return {fingerprint, JA4H_FINGERPRINT_LENGTH}; +} + +void +ja4h_method::on_request(JAxContext *ctx, TSHttpTxn txnp) +{ + ctx->set_fingerprint(get_fingerprint(txnp)); +} diff --git a/plugins/experimental/jax_fingerprint/ja4h/ja4h_method.h b/plugins/experimental/jax_fingerprint/ja4h/ja4h_method.h new file mode 100644 index 00000000000..bc15165d4cc --- /dev/null +++ b/plugins/experimental/jax_fingerprint/ja4h/ja4h_method.h @@ -0,0 +1,32 @@ +/** @file + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ + +#pragma once + +#include "../method.h" + +namespace ja4h_method +{ + +extern struct Method method; + +} diff --git a/plugins/experimental/jax_fingerprint/log.cc b/plugins/experimental/jax_fingerprint/log.cc new file mode 100644 index 00000000000..910cd8fe6bc --- /dev/null +++ b/plugins/experimental/jax_fingerprint/log.cc @@ -0,0 +1,44 @@ +/** @file + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#include "plugin.h" +#include "log.h" + +#include "ts/ts.h" + +#include + +static TSTextLogObject log_handle = nullptr; + +bool +create_log_file(const std::string &filename) +{ + return (TS_SUCCESS == TSTextLogObjectCreate(filename.c_str(), TS_LOG_MODE_ADD_TIMESTAMP, &log_handle)); +} + +void +log_fingerprint(const JAxContext *ctx) +{ + if (TS_ERROR == TSTextLogObjectWrite(log_handle, "Client: %s\t%s: %s", ctx->get_addr(), ctx->get_method_name(), + ctx->get_fingerprint().c_str())) { + Dbg(dbg_ctl, "Failed to write to log!"); + } +} diff --git a/plugins/experimental/jax_fingerprint/log.h b/plugins/experimental/jax_fingerprint/log.h new file mode 100644 index 00000000000..b477703d149 --- /dev/null +++ b/plugins/experimental/jax_fingerprint/log.h @@ -0,0 +1,28 @@ +/** @file + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ + +#pragma once + +#include "context.h" + +bool create_log_file(const std::string &filename); +void log_fingerprint(const JAxContext *ctx); diff --git a/plugins/experimental/jax_fingerprint/method.h b/plugins/experimental/jax_fingerprint/method.h new file mode 100644 index 00000000000..c2bf6b34119 --- /dev/null +++ b/plugins/experimental/jax_fingerprint/method.h @@ -0,0 +1,42 @@ +/** @file + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ + +#pragma once + +#include "ts/ts.h" + +#include "context.h" + +using ClientHelloHandler = void (*)(JAxContext *, TSVConn); +using RequestReadHandler = void (*)(JAxContext *, TSHttpTxn); + +struct Method { + enum class Type { + CONNECTION_BASED, + REQUEST_BASED, + }; + + const char *name; + Type type; + ClientHelloHandler on_client_hello; + RequestReadHandler on_request; +}; diff --git a/plugins/experimental/jax_fingerprint/plugin.cc b/plugins/experimental/jax_fingerprint/plugin.cc new file mode 100644 index 00000000000..54f7c260674 --- /dev/null +++ b/plugins/experimental/jax_fingerprint/plugin.cc @@ -0,0 +1,425 @@ +/** @file + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#include "plugin.h" +#include "config.h" +#include "context.h" +#include "userarg.h" +#include "method.h" +#include "header.h" +#include "log.h" + +#include "ja4/ja4_method.h" +#include "ja4h/ja4h_method.h" +#include "ja3/ja3_method.h" + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +DbgCtl dbg_ctl{PLUGIN_NAME}; + +namespace +{ + +} // end anonymous namespace + +static bool +read_config_option(int argc, char const *argv[], PluginConfig &config) +{ + const struct option longopts[] = { + {"standalone", no_argument, nullptr, 's'}, + {"method", required_argument, nullptr, 'M'}, // JA4, JA4H, or JA3 + {"mode", required_argument, nullptr, 'm'}, // overwrite, keep, or append + {"header", required_argument, nullptr, 'h'}, + {"via-header", required_argument, nullptr, 'v'}, + {"log-filename", required_argument, nullptr, 'f'}, + {"servernames", required_argument, nullptr, 'S'}, + {nullptr, 0, nullptr, 0 } + }; + + optind = 0; + int opt{0}; + while ((opt = getopt_long(argc, const_cast(argv), "", longopts, nullptr)) >= 0) { + switch (opt) { + case '?': + Dbg(dbg_ctl, "Unrecognized command argument."); + break; + case 'M': + if (strcmp("JA4", optarg) == 0) { + config.method = ja4_method::method; + } else if (strcmp("JA4H", optarg) == 0) { + config.method = ja4h_method::method; + } else if (strcmp("JA3", optarg) == 0) { + config.method = ja3_method::method; + } else { + Dbg(dbg_ctl, "Unexpected method: %s", optarg); + return false; + } + break; + case 'm': + if (strcmp("overwrite", optarg) == 0) { + config.mode = Mode::OVERWRITE; + } else if (strcmp("keep", optarg) == 0) { + config.mode = Mode::KEEP; + } else if (strcmp("append", optarg) == 0) { + config.mode = Mode::APPEND; + } else { + Dbg(dbg_ctl, "Unexpected mode: %s", optarg); + return false; + } + break; + case 'h': + config.header_name = {optarg, strlen(optarg)}; + break; + case 'v': + config.via_header_name = {optarg, strlen(optarg)}; + break; + case 'f': + config.log_filename = {optarg, strlen(optarg)}; + break; + case 's': + config.standalone = true; + break; + case 'S': + for (std::string_view input(optarg, strlen(optarg)); !input.empty();) { + auto pos = input.find(','); + config.servernames.emplace(input.substr(0, pos)); + input.remove_prefix(pos == std::string_view::npos ? input.size() : pos + 1); + } + break; + case 0: + case -1: + break; + default: + Dbg(dbg_ctl, "Unexpected options error."); + return false; + } + } + + Dbg(dbg_ctl, "JAx method is %s", config.method.name); + Dbg(dbg_ctl, "JAx mode is %d", static_cast(config.mode)); + Dbg(dbg_ctl, "JAx header is %s", !config.header_name.empty() ? config.header_name.c_str() : "DISABLED"); + Dbg(dbg_ctl, "JAx via-header is %s", !config.via_header_name.empty() ? config.via_header_name.c_str() : "DISABLED"); + Dbg(dbg_ctl, "JAx log file is %s", !config.log_filename.empty() ? config.log_filename.c_str() : "DISABLED"); + for (auto &&servername : config.servernames) { + Dbg(dbg_ctl, "%s", servername.c_str()); + } + + return true; +} + +void +modify_headers(JAxContext *ctx, TSHttpTxn txnp, PluginConfig &config) +{ + if (!ctx->get_fingerprint().empty()) { + switch (config.mode) { + case Mode::KEEP: + break; + case Mode::OVERWRITE: + if (!config.header_name.empty()) { + set_header(txnp, config.header_name, ctx->get_fingerprint()); + } + if (!config.via_header_name.empty()) { + set_via_header(txnp, config.via_header_name); + } + break; + case Mode::APPEND: + if (!config.header_name.empty()) { + append_header(txnp, config.header_name, ctx->get_fingerprint()); + } + if (!config.via_header_name.empty()) { + append_via_header(txnp, config.via_header_name); + } + break; + default: + break; + } + } else { + Dbg(dbg_ctl, "No fingerprint attached to vconn!"); + if (config.mode == Mode::OVERWRITE) { + if (!config.header_name.empty()) { + remove_header(txnp, config.header_name); + } + if (!config.via_header_name.empty()) { + remove_header(txnp, config.via_header_name); + } + } + } +} + +int +handle_client_hello(void *edata, PluginConfig &config) +{ + TSVConn vconn = static_cast(edata); + JAxContext *ctx = get_user_arg(vconn, config); + + if (!config.servernames.empty()) { + const char *servername; + int servername_len; + servername = TSVConnSslSniGet(vconn, &servername_len); + if (servername != nullptr && servername_len > 0) { + if (!config.servernames.contains(std::string_view(servername, servername_len))) { + Dbg(dbg_ctl, "Server name %.*s is not in the server name set", servername_len, servername); + return TS_SUCCESS; + } + } + } + + if (nullptr == ctx) { + ctx = new JAxContext(config.method.name, TSNetVConnRemoteAddrGet(vconn)); + set_user_arg(vconn, config, ctx); + } + + if (config.method.on_client_hello) { + config.method.on_client_hello(ctx, vconn); + } + + TSVConnReenable(vconn); + + return TS_SUCCESS; +} + +int +handle_read_request_hdr(void *edata, PluginConfig &config) +{ + TSHttpTxn txnp = static_cast(edata); + if (txnp == nullptr) { + Dbg(dbg_ctl, "Failed to get txn object."); + TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE); + return TS_SUCCESS; + } + + TSHttpSsn ssnp = TSHttpTxnSsnGet(txnp); + if (ssnp == nullptr) { + Dbg(dbg_ctl, "Failed to get ssn object."); + TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE); + return TS_SUCCESS; + } + + TSVConn vconn = TSHttpSsnClientVConnGet(ssnp); + if (vconn == nullptr) { + Dbg(dbg_ctl, "Failed to get vconn object."); + TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE); + return TS_SUCCESS; + } + + void *container; + if (config.method.type == Method::Type::CONNECTION_BASED) { + container = vconn; + } else { + container = txnp; + } + JAxContext *ctx = get_user_arg(container, config); + if (nullptr == ctx) { + ctx = new JAxContext(config.method.name, TSNetVConnRemoteAddrGet(vconn)); + set_user_arg(container, config, ctx); + } + + if (config.method.on_request) { + config.method.on_request(ctx, txnp); + } + + if (!config.log_filename.empty()) { + log_fingerprint(ctx); + } + + modify_headers(ctx, txnp, config); + + if (config.method.type == Method::Type::CONNECTION_BASED) { + TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE); + } + return TS_SUCCESS; +} + +int +handle_http_txn_close(void *edata, PluginConfig &config) +{ + TSHttpTxn txnp = static_cast(edata); + + delete get_user_arg(txnp, config); + set_user_arg(txnp, config, nullptr); + + TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE); + return TS_SUCCESS; +} + +int +handle_vconn_close(void *edata, PluginConfig &config) +{ + TSVConn vconn = static_cast(edata); + + delete get_user_arg(vconn, config); + set_user_arg(vconn, config, nullptr); + + TSVConnReenable(vconn); + return TS_SUCCESS; +} + +int +main_handler(TSCont cont, TSEvent event, void *edata) +{ + int ret; + + auto config = static_cast(TSContDataGet(cont)); + + switch (event) { + case TS_EVENT_SSL_CLIENT_HELLO: + ret = handle_client_hello(edata, *config); + break; + case TS_EVENT_HTTP_READ_REQUEST_HDR: + ret = handle_read_request_hdr(edata, *config); + break; + case TS_EVENT_HTTP_TXN_CLOSE: + ret = handle_http_txn_close(edata, *config); + break; + case TS_EVENT_VCONN_CLOSE: + ret = handle_vconn_close(edata, *config); + break; + default: + Dbg(dbg_ctl, "Unexpected event %d.", event); + // We ignore the event, but we don't want to reject the connection. + ret = TS_SUCCESS; + } + + return ret; +} + +void +TSPluginInit(int argc, char const **argv) +{ + TSPluginRegistrationInfo info; + info.plugin_name = PLUGIN_NAME; + info.vendor_name = PLUGIN_VENDOR; + info.support_email = PLUGIN_SUPPORT_EMAIL; + + if (TS_SUCCESS != TSPluginRegister(&info)) { + TSError("[%s] Failed to register.", PLUGIN_NAME); + return; + } + + PluginConfig *config = new PluginConfig(); + config->plugin_type = PluginType::GLOBAL; + + if (!read_config_option(argc, argv, *config)) { + TSError("[%s] Failed to parse options.", PLUGIN_NAME); + return; + } + + if (!config->log_filename.empty()) { + if (!create_log_file(config->log_filename)) { + TSError("[%s] Failed to create log.", PLUGIN_NAME); + return; + } else { + Dbg(dbg_ctl, "Created log file."); + } + } + + reserve_user_arg(*config); + + TSCont cont = TSContCreate(main_handler, nullptr); + TSContDataSet(cont, config); + if (config->method.on_client_hello) { + TSHttpHookAdd(TS_SSL_CLIENT_HELLO_HOOK, cont); + } + if (config->standalone) { + TSHttpHookAdd(TS_HTTP_READ_REQUEST_HDR_HOOK, cont); + } + if (config->method.type == Method::Type::CONNECTION_BASED) { + TSHttpHookAdd(TS_VCONN_CLOSE_HOOK, cont); + } else { + TSHttpHookAdd(TS_HTTP_TXN_CLOSE_HOOK, cont); + } +} + +TSReturnCode +TSRemapInit(TSRemapInterface * /* api_info ATS_UNUSED */, char * /* errbuf ATS_UNUSED */, int /* errbuf_size ATS_UNUSED */) +{ + Dbg(dbg_ctl, "JAx Remap Plugin initializing.."); + + return TS_SUCCESS; +} + +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]); + auto config = new PluginConfig(); + config->plugin_type = PluginType::REMAP; + + // Parse parameters + if (!read_config_option(argc - 1, const_cast(argv + 1), *config)) { + Dbg(dbg_ctl, "Bad arguments"); + return TS_ERROR; + } + + reserve_user_arg(*config); + + // Create continuation + if (config->standalone) { + config->handler = TSContCreate(main_handler, nullptr); + if (config->method.on_client_hello) { + TSHttpHookAdd(TS_SSL_CLIENT_HELLO_HOOK, config->handler); + } + if (config->method.type == Method::Type::CONNECTION_BASED) { + TSHttpHookAdd(TS_VCONN_CLOSE_HOOK, config->handler); + } else { + TSHttpHookAdd(TS_HTTP_TXN_CLOSE_HOOK, config->handler); + } + TSContDataSet(config->handler, config); + } + + *ih = static_cast(config); + + return TS_SUCCESS; +} + +TSRemapStatus +TSRemapDoRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri) +{ + auto config = static_cast(ih); + + if (!config || !rri) { + TSError("[%s] Invalid private data or RRI or handler.", PLUGIN_NAME); + return TSREMAP_NO_REMAP; + } + + handle_read_request_hdr(rh, *config); + + return TSREMAP_NO_REMAP; +} + +void +TSRemapDeleteInstance(void *ih) +{ + auto config = static_cast(ih); + delete config; +} diff --git a/plugins/experimental/jax_fingerprint/plugin.h b/plugins/experimental/jax_fingerprint/plugin.h new file mode 100644 index 00000000000..e2bddda4ee4 --- /dev/null +++ b/plugins/experimental/jax_fingerprint/plugin.h @@ -0,0 +1,31 @@ +/** @file + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ + +#pragma once + +#include "ts/ts.h" + +constexpr char const *PLUGIN_NAME{"jax_fingerprint"}; +constexpr char const *PLUGIN_VENDOR{"Apache Software Foundation"}; +constexpr char const *PLUGIN_SUPPORT_EMAIL{"dev@trafficserver.apache.org"}; + +extern DbgCtl dbg_ctl; diff --git a/plugins/experimental/jax_fingerprint/userarg.cc b/plugins/experimental/jax_fingerprint/userarg.cc new file mode 100644 index 00000000000..4ba26a5f8fe --- /dev/null +++ b/plugins/experimental/jax_fingerprint/userarg.cc @@ -0,0 +1,71 @@ +/** @file + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#include "plugin.h" +#include "config.h" +#include "userarg.h" + +void +reserve_user_arg(PluginConfig &config) +{ + char name[sizeof(PLUGIN_NAME) + strlen(config.method.name) + 1]; + name[0] = '\0'; + strcat(name, PLUGIN_NAME); + strcat(name, config.method.name); + + TSUserArgType type; + if (config.method.type == Method::Type::CONNECTION_BASED) { + type = TS_USER_ARGS_VCONN; + } else { + type = TS_USER_ARGS_TXN; + } + TSUserArgIndexReserve(type, name, "used to pass JAx context between hooks", &config.user_arg_index); + Dbg(dbg_ctl, "user_arg_name: %s, user_arg_index: %d", name, config.user_arg_index); +} + +void +fill_user_arg_index(PluginConfig &config) +{ + char name[sizeof(PLUGIN_NAME) + strlen(config.method.name) + 1]; + name[0] = '\0'; + strcat(name, PLUGIN_NAME); + strcat(name, config.method.name); + + TSUserArgType type; + if (config.method.type == Method::Type::CONNECTION_BASED) { + type = TS_USER_ARGS_VCONN; + } else { + type = TS_USER_ARGS_TXN; + } + TSUserArgIndexNameLookup(type, name, &config.user_arg_index, nullptr); +} + +void +set_user_arg(void *container, PluginConfig &config, JAxContext *ctx) +{ + TSUserArgSet(container, config.user_arg_index, static_cast(ctx)); +} + +JAxContext * +get_user_arg(void *container, PluginConfig &config) +{ + return static_cast(TSUserArgGet(container, config.user_arg_index)); +} diff --git a/plugins/experimental/jax_fingerprint/userarg.h b/plugins/experimental/jax_fingerprint/userarg.h new file mode 100644 index 00000000000..4eff6dad08a --- /dev/null +++ b/plugins/experimental/jax_fingerprint/userarg.h @@ -0,0 +1,32 @@ +/** @file + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ + +#pragma once + +#include "context.h" + +#include "ts/ts.h" + +void reserve_user_arg(PluginConfig &config); +void fill_user_arg_index(PluginConfig &config); +void set_user_arg(void *container, PluginConfig &config, JAxContext *ctx); +JAxContext *get_user_arg(void *container, PluginConfig &config); From 473581610b5a763c936b504e41655915477bb506 Mon Sep 17 00:00:00 2001 From: Masakazu Kitajo Date: Wed, 18 Mar 2026 13:15:42 -0600 Subject: [PATCH 02/23] Add license headers --- .../experimental/jax_fingerprint/ja4h/ja4h.cc | 23 +++++++++++++++++++ .../experimental/jax_fingerprint/ja4h/ja4h.h | 22 ++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/plugins/experimental/jax_fingerprint/ja4h/ja4h.cc b/plugins/experimental/jax_fingerprint/ja4h/ja4h.cc index ff2692150af..0f451e7636f 100644 --- a/plugins/experimental/jax_fingerprint/ja4h/ja4h.cc +++ b/plugins/experimental/jax_fingerprint/ja4h/ja4h.cc @@ -1,3 +1,26 @@ +/** @file + * + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ + #include "ja4h.h" #include diff --git a/plugins/experimental/jax_fingerprint/ja4h/ja4h.h b/plugins/experimental/jax_fingerprint/ja4h/ja4h.h index ec19bb36add..532912653fc 100644 --- a/plugins/experimental/jax_fingerprint/ja4h/ja4h.h +++ b/plugins/experimental/jax_fingerprint/ja4h/ja4h.h @@ -1,3 +1,25 @@ +/** @file + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ + #pragma once #include "ts/ts.h" From 0cd2ceb7907a2b5619ca0d60eca38ff5264fa131 Mon Sep 17 00:00:00 2001 From: Masakazu Kitajo Date: Wed, 18 Mar 2026 13:26:09 -0600 Subject: [PATCH 03/23] Add the new documentation to the toc --- doc/admin-guide/plugins/index.en.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/admin-guide/plugins/index.en.rst b/doc/admin-guide/plugins/index.en.rst index 5379c9a72e9..dc355641117 100644 --- a/doc/admin-guide/plugins/index.en.rst +++ b/doc/admin-guide/plugins/index.en.rst @@ -179,6 +179,7 @@ directory of the |TS| source tree. Experimental plugins can be compiled by passi Hook Trace ICAP JA4 Fingerprint + JAx Fingerprint Maxmind ACL Memcache Memory Profile @@ -236,6 +237,9 @@ directory of the |TS| source tree. Experimental plugins can be compiled by passi :doc:`JA4 Fingerprint ` Calculates JA4 Fingerprints for incoming TLS traffic. +:doc:`JAx Fingerprint ` + Calculates JAx Fingerprints. + :doc:`MaxMind ACL ` ACL based on the maxmind geo databases (GeoIP2 mmdb and libmaxminddb) From 6d194ca0cae6f315d191dade81c8c8f5fd94beae Mon Sep 17 00:00:00 2001 From: Masakazu Kitajo Date: Wed, 18 Mar 2026 14:15:21 -0600 Subject: [PATCH 04/23] Include limit.h --- plugins/experimental/jax_fingerprint/context.h | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/experimental/jax_fingerprint/context.h b/plugins/experimental/jax_fingerprint/context.h index 754cd3cb2bd..de5fe1dee9f 100644 --- a/plugins/experimental/jax_fingerprint/context.h +++ b/plugins/experimental/jax_fingerprint/context.h @@ -25,6 +25,7 @@ #include #include #include +#include #include From dcad0ef77780b53902e87ca866fb2ef28c87c63f Mon Sep 17 00:00:00 2001 From: Masakazu Kitajo Date: Wed, 18 Mar 2026 14:29:45 -0600 Subject: [PATCH 05/23] Include sha.sh instead of sha2.h --- plugins/experimental/jax_fingerprint/ja4h/ja4h.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/experimental/jax_fingerprint/ja4h/ja4h.cc b/plugins/experimental/jax_fingerprint/ja4h/ja4h.cc index 0f451e7636f..dfc9472cf62 100644 --- a/plugins/experimental/jax_fingerprint/ja4h/ja4h.cc +++ b/plugins/experimental/jax_fingerprint/ja4h/ja4h.cc @@ -22,7 +22,7 @@ */ #include "ja4h.h" -#include +#include Extractor::Extractor(TSHttpTxn txnp) : _txn(txnp) { From 2b6218ca93effc1de6797ed7ffc54ab3f914b0c7 Mon Sep 17 00:00:00 2001 From: Masakazu Kitajo Date: Wed, 18 Mar 2026 14:41:41 -0600 Subject: [PATCH 06/23] Don't use std::copy --- plugins/experimental/jax_fingerprint/ja3/ja3_method.cc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/experimental/jax_fingerprint/ja3/ja3_method.cc b/plugins/experimental/jax_fingerprint/ja3/ja3_method.cc index 6bc2487ca4d..e27a41933fa 100644 --- a/plugins/experimental/jax_fingerprint/ja3/ja3_method.cc +++ b/plugins/experimental/jax_fingerprint/ja3/ja3_method.cc @@ -83,7 +83,10 @@ get_fingerprint(TSClientHello ch) } if (len > 0) { int extension_ids[len]; - std::copy(ext_types.begin(), ext_types.end(), extension_ids); + first = ext_types.begin(); + for (size_t i = 0; i < len; ++i, ++first) { + extension_ids[i] = *first; + } raw.append(ja3::encode_integer_buffer(extension_ids, len)); } raw.push_back(','); From 27346e9afd2680fad6359d4a78d90c510e293802 Mon Sep 17 00:00:00 2001 From: Masakazu Kitajo Date: Wed, 18 Mar 2026 15:06:54 -0600 Subject: [PATCH 07/23] Use heterogeneous lookup only if it's available --- plugins/experimental/jax_fingerprint/plugin.cc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/experimental/jax_fingerprint/plugin.cc b/plugins/experimental/jax_fingerprint/plugin.cc index 54f7c260674..99f15f9eeaf 100644 --- a/plugins/experimental/jax_fingerprint/plugin.cc +++ b/plugins/experimental/jax_fingerprint/plugin.cc @@ -44,6 +44,7 @@ #include #include #include +#include DbgCtl dbg_ctl{PLUGIN_NAME}; @@ -187,7 +188,11 @@ handle_client_hello(void *edata, PluginConfig &config) int servername_len; servername = TSVConnSslSniGet(vconn, &servername_len); if (servername != nullptr && servername_len > 0) { +#ifdef __cpp_lib_generic_unordered_lookup if (!config.servernames.contains(std::string_view(servername, servername_len))) { +#else + if (!config.servernames.contains({servername, servername_len})) { +#endif Dbg(dbg_ctl, "Server name %.*s is not in the server name set", servername_len, servername); return TS_SUCCESS; } From a4a71a7351a15f026852c8a6e485d6d013c91564 Mon Sep 17 00:00:00 2001 From: Masakazu Kitajo Date: Wed, 18 Mar 2026 15:23:20 -0600 Subject: [PATCH 08/23] Fix documentation format --- doc/admin-guide/plugins/jax_fingerprint.en.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/admin-guide/plugins/jax_fingerprint.en.rst b/doc/admin-guide/plugins/jax_fingerprint.en.rst index 005ccae5ced..8b30ca34cd0 100644 --- a/doc/admin-guide/plugins/jax_fingerprint.en.rst +++ b/doc/admin-guide/plugins/jax_fingerprint.en.rst @@ -89,24 +89,24 @@ Global plugin setup ------------------- Global plugin setup is the best if you: -- Need a fingerprint on every request + * Need a fingerprint on every request Remap plugin setup ------------------ Remap plugin setup is the best if you: -- Need a fingerprint only on specific paths, or -- Cannot use Global plugin setup + * Need a fingerprint only on specific paths, or + * Cannot use Global plugin setup -Note: For JA3 and JA4, fingerprints are always generated at the beginning of connections. Using remap plugin setup only reduces the +.. note:: For JA3 and JA4, fingerprints are always generated at the beginning of connections. Using remap plugin setup only reduces the overhead of adding HTTP headers and loggingg. Hybrid setup ------------ Hybrid setup is the best if you: -- Need a fingerprint only for specific server names (in TLS SNI extension), and -- Need a fingerprint only on specific paths + * Need a fingerprint only for specific server names (in TLS SNI extension), and + * Need a fingerprint only on specific paths Log Output From 9ab0323f92ae3dd134458c9209a8e35c77c28b75 Mon Sep 17 00:00:00 2001 From: Masakazu Kitajo Date: Wed, 18 Mar 2026 15:28:55 -0600 Subject: [PATCH 09/23] Fix documentation --- doc/admin-guide/plugins/jax_fingerprint.en.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/admin-guide/plugins/jax_fingerprint.en.rst b/doc/admin-guide/plugins/jax_fingerprint.en.rst index 8b30ca34cd0..ad2cafb3a63 100644 --- a/doc/admin-guide/plugins/jax_fingerprint.en.rst +++ b/doc/admin-guide/plugins/jax_fingerprint.en.rst @@ -121,8 +121,8 @@ specified by `--log-filename` option. **Example**:: - [Jan 29 10:15:23.456] Client IP: 192.168.1.100 JA4: t13d1516h2_8daaf6152771_b186095e22b6 - [Jan 29 10:15:24.123] Client IP: 10.0.0.50 JA4: t13d1715h2_8daaf6152771_02713d6af862 + [Jan 29 10:15:23.456] Client: 192.168.1.100 JA4: t13d1516h2_8daaf6152771_b186095e22b6 + [Jan 29 10:15:24.123] Client: 10.0.0.50 JA4: t13d1715h2_8daaf6152771_02713d6af862 Using HTTP Headers in Origin Requests From a4d582e46582447f6da0054fbc3b9bc2bee0b3f1 Mon Sep 17 00:00:00 2001 From: Masakazu Kitajo Date: Wed, 18 Mar 2026 17:28:48 -0600 Subject: [PATCH 10/23] Fix documentation format --- doc/admin-guide/plugins/jax_fingerprint.en.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/admin-guide/plugins/jax_fingerprint.en.rst b/doc/admin-guide/plugins/jax_fingerprint.en.rst index ad2cafb3a63..4a40a2a18f6 100644 --- a/doc/admin-guide/plugins/jax_fingerprint.en.rst +++ b/doc/admin-guide/plugins/jax_fingerprint.en.rst @@ -98,8 +98,7 @@ Remap plugin setup is the best if you: * Need a fingerprint only on specific paths, or * Cannot use Global plugin setup -.. note:: For JA3 and JA4, fingerprints are always generated at the beginning of connections. Using remap plugin setup only reduces the -overhead of adding HTTP headers and loggingg. +.. note:: For JA3 and JA4, fingerprints are always generated at the beginning of connections. Using remap plugin setup only reduces the overhead of adding HTTP headers and loggingg. Hybrid setup ------------ From 0d579cc1f5b4d9defe5131da925f9cc11055414c Mon Sep 17 00:00:00 2001 From: Masakazu Kitajo Date: Wed, 18 Mar 2026 21:08:05 -0600 Subject: [PATCH 11/23] Don't use std::ranges::equal --- plugins/experimental/jax_fingerprint/ja4h/ja4h.cc | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/plugins/experimental/jax_fingerprint/ja4h/ja4h.cc b/plugins/experimental/jax_fingerprint/ja4h/ja4h.cc index dfc9472cf62..3b4a8dda54b 100644 --- a/plugins/experimental/jax_fingerprint/ja4h/ja4h.cc +++ b/plugins/experimental/jax_fingerprint/ja4h/ja4h.cc @@ -108,13 +108,15 @@ Extractor::get_headers_hash(unsigned char out[32]) int field_name_len; char *field_name = const_cast(TSMimeHdrFieldNameGet(this->_request, this->_req_hdr, field_loc, &field_name_len)); if (field_name_len == TS_MIME_LEN_COOKIE) { - if (std::ranges::equal(std::string_view{field_name, static_cast(field_name_len)}, "cookie", - [](char c1, char c2) { return c1 == std::tolower(c2); })) { + auto field_name_sv = std::string_view(field_name, static_cast(field_name_len)); + if (std::equal(field_name_sv.begin(), field_name_sv.end(), std::string_view("cookie").begin(), + [](char c1, char c2) { return c1 == std::tolower(c2); })) { continue; }; } else if (field_name_len == TS_MIME_LEN_REFERER) { - if (std::ranges::equal(std::string_view{field_name, static_cast(field_name_len)}, "referer", - [](char c1, char c2) { return c1 == std::tolower(c2); })) { + auto field_name_sv = std::string_view(field_name, static_cast(field_name_len)); + if (std::equal(field_name_sv.begin(), field_name_sv.end(), std::string_view("referer").begin(), + [](char c1, char c2) { return c1 == std::tolower(c2); })) { continue; } } From 9a20fcd93b044385a93a4bb3f65c2e26b884b8e7 Mon Sep 17 00:00:00 2001 From: Masakazu Kitajo Date: Wed, 18 Mar 2026 21:10:51 -0600 Subject: [PATCH 12/23] Fix memory leak --- plugins/experimental/jax_fingerprint/plugin.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/experimental/jax_fingerprint/plugin.cc b/plugins/experimental/jax_fingerprint/plugin.cc index 99f15f9eeaf..1278bf997d5 100644 --- a/plugins/experimental/jax_fingerprint/plugin.cc +++ b/plugins/experimental/jax_fingerprint/plugin.cc @@ -382,6 +382,7 @@ TSRemapNewInstance(int argc, char *argv[], void **ih, char * /* errbuf ATS_UNUSE // Parse parameters if (!read_config_option(argc - 1, const_cast(argv + 1), *config)) { + delete config; Dbg(dbg_ctl, "Bad arguments"); return TS_ERROR; } From 6a742e20ead47430eb8a6b8883c8f4ed7d841e9d Mon Sep 17 00:00:00 2001 From: Masakazu Kitajo Date: Wed, 18 Mar 2026 21:14:18 -0600 Subject: [PATCH 13/23] Fix logic error --- plugins/experimental/jax_fingerprint/userarg.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/experimental/jax_fingerprint/userarg.cc b/plugins/experimental/jax_fingerprint/userarg.cc index 4ba26a5f8fe..687fab4eb80 100644 --- a/plugins/experimental/jax_fingerprint/userarg.cc +++ b/plugins/experimental/jax_fingerprint/userarg.cc @@ -26,7 +26,7 @@ void reserve_user_arg(PluginConfig &config) { - char name[sizeof(PLUGIN_NAME) + strlen(config.method.name) + 1]; + char name[strlen(PLUGIN_NAME) + strlen(config.method.name) + 1]; name[0] = '\0'; strcat(name, PLUGIN_NAME); strcat(name, config.method.name); @@ -44,7 +44,7 @@ reserve_user_arg(PluginConfig &config) void fill_user_arg_index(PluginConfig &config) { - char name[sizeof(PLUGIN_NAME) + strlen(config.method.name) + 1]; + char name[strlen(PLUGIN_NAME) + strlen(config.method.name) + 1]; name[0] = '\0'; strcat(name, PLUGIN_NAME); strcat(name, config.method.name); From d6876e302f7af0e0d2b508691c6b815439850617 Mon Sep 17 00:00:00 2001 From: Masakazu Kitajo Date: Wed, 18 Mar 2026 22:38:56 -0600 Subject: [PATCH 14/23] Adjust type difference --- plugins/experimental/jax_fingerprint/plugin.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/experimental/jax_fingerprint/plugin.cc b/plugins/experimental/jax_fingerprint/plugin.cc index 1278bf997d5..676e62b7df7 100644 --- a/plugins/experimental/jax_fingerprint/plugin.cc +++ b/plugins/experimental/jax_fingerprint/plugin.cc @@ -191,7 +191,7 @@ handle_client_hello(void *edata, PluginConfig &config) #ifdef __cpp_lib_generic_unordered_lookup if (!config.servernames.contains(std::string_view(servername, servername_len))) { #else - if (!config.servernames.contains({servername, servername_len})) { + if (!config.servernames.contains({servername, static_cast(servername_len)})) { #endif Dbg(dbg_ctl, "Server name %.*s is not in the server name set", servername_len, servername); return TS_SUCCESS; From 09b7d69eedc43d1afb0239397a648f898a01f481 Mon Sep 17 00:00:00 2001 From: Masakazu Kitajo Date: Thu, 19 Mar 2026 10:26:31 -0600 Subject: [PATCH 15/23] Address some copilot comments --- doc/admin-guide/plugins/jax_fingerprint.en.rst | 3 ++- plugins/experimental/jax_fingerprint/ja4h/ja4h.h | 1 - plugins/experimental/jax_fingerprint/plugin.cc | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/admin-guide/plugins/jax_fingerprint.en.rst b/doc/admin-guide/plugins/jax_fingerprint.en.rst index 4a40a2a18f6..9180c06b1e2 100644 --- a/doc/admin-guide/plugins/jax_fingerprint.en.rst +++ b/doc/admin-guide/plugins/jax_fingerprint.en.rst @@ -65,9 +65,10 @@ Fingerprinting method (e.g. JA4, JA3, etc.) to use. This option specifies what to do if requests from clients have the header names that are specified by `--header` and/or `via-header`. Available setting values are "overwrite", "keep" and "append". -.. option:: --servernames +.. option:: --servernames This option specifies server name(s) for which the plugin generates fingerprints. +The value must be provided as a single comma separated value (no space) of server names. .. option:: --header diff --git a/plugins/experimental/jax_fingerprint/ja4h/ja4h.h b/plugins/experimental/jax_fingerprint/ja4h/ja4h.h index 532912653fc..db2f5b98593 100644 --- a/plugins/experimental/jax_fingerprint/ja4h/ja4h.h +++ b/plugins/experimental/jax_fingerprint/ja4h/ja4h.h @@ -42,5 +42,4 @@ class Extractor TSHttpTxn _txn; TSMBuffer _request = nullptr; TSMLoc _req_hdr = nullptr; - ; }; diff --git a/plugins/experimental/jax_fingerprint/plugin.cc b/plugins/experimental/jax_fingerprint/plugin.cc index 676e62b7df7..6f32c9f79e1 100644 --- a/plugins/experimental/jax_fingerprint/plugin.cc +++ b/plugins/experimental/jax_fingerprint/plugin.cc @@ -194,6 +194,7 @@ handle_client_hello(void *edata, PluginConfig &config) if (!config.servernames.contains({servername, static_cast(servername_len)})) { #endif Dbg(dbg_ctl, "Server name %.*s is not in the server name set", servername_len, servername); + TSVConnReenable(vconn); return TS_SUCCESS; } } @@ -366,9 +367,10 @@ TSPluginInit(int argc, char const **argv) } TSReturnCode -TSRemapInit(TSRemapInterface * /* api_info ATS_UNUSED */, char * /* errbuf ATS_UNUSED */, int /* errbuf_size ATS_UNUSED */) +TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size) { Dbg(dbg_ctl, "JAx Remap Plugin initializing.."); + CHECK_REMAP_API_COMPATIBILITY(api_info, errbuf, errbuf_size); return TS_SUCCESS; } From c25913684e8d79de6b651e011a559e128052ecc4 Mon Sep 17 00:00:00 2001 From: Masakazu Kitajo Date: Thu, 19 Mar 2026 10:58:31 -0600 Subject: [PATCH 16/23] Address some more comments --- plugins/experimental/jax_fingerprint/config.h | 1 + .../experimental/jax_fingerprint/header.cc | 1 + .../experimental/jax_fingerprint/ja4h/ja4h.cc | 19 +++++++++----- .../jax_fingerprint/ja4h/ja4h_method.cc | 26 ++++++++++++------- plugins/experimental/jax_fingerprint/log.cc | 10 ++++--- plugins/experimental/jax_fingerprint/log.h | 5 ++-- .../experimental/jax_fingerprint/plugin.cc | 15 +++++++++-- 7 files changed, 53 insertions(+), 24 deletions(-) diff --git a/plugins/experimental/jax_fingerprint/config.h b/plugins/experimental/jax_fingerprint/config.h index 9874f4de5ad..da538d8da00 100644 --- a/plugins/experimental/jax_fingerprint/config.h +++ b/plugins/experimental/jax_fingerprint/config.h @@ -62,4 +62,5 @@ struct PluginConfig { TSCont handler = nullptr; // For remap plugin bool standalone = false; std::unordered_set> servernames; + TSTextLogObject log_handle = nullptr; }; diff --git a/plugins/experimental/jax_fingerprint/header.cc b/plugins/experimental/jax_fingerprint/header.cc index 1477728681b..1342f28cd40 100644 --- a/plugins/experimental/jax_fingerprint/header.cc +++ b/plugins/experimental/jax_fingerprint/header.cc @@ -137,4 +137,5 @@ remove_header(TSHttpTxn txnp, const std::string &header) target = tmp; } } + TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc); } diff --git a/plugins/experimental/jax_fingerprint/ja4h/ja4h.cc b/plugins/experimental/jax_fingerprint/ja4h/ja4h.cc index 3b4a8dda54b..43efd774e3c 100644 --- a/plugins/experimental/jax_fingerprint/ja4h/ja4h.cc +++ b/plugins/experimental/jax_fingerprint/ja4h/ja4h.cc @@ -54,7 +54,7 @@ Extractor::get_version() { if (TSHttpTxnClientProtocolStackContains(this->_txn, "h2")) { return 2 << 16; - } else if (TSHttpTxnClientProtocolStackContains(this->_txn, "h2")) { + } else if (TSHttpTxnClientProtocolStackContains(this->_txn, "h3")) { return 3 << 16; } else { return TSHttpHdrVersionGet(this->_request, this->_req_hdr); @@ -65,7 +65,9 @@ bool Extractor::has_cookie_field() { TSMLoc mloc = TSMimeHdrFieldFind(this->_request, this->_req_hdr, TS_MIME_FIELD_COOKIE, TS_MIME_LEN_COOKIE); - TSHandleMLocRelease(this->_request, this->_req_hdr, mloc); + if (mloc) { + TSHandleMLocRelease(this->_request, this->_req_hdr, mloc); + } return mloc != TS_NULL_MLOC; } @@ -73,7 +75,9 @@ bool Extractor::has_referer_field() { TSMLoc mloc = TSMimeHdrFieldFind(this->_request, this->_req_hdr, TS_MIME_FIELD_REFERER, TS_MIME_LEN_REFERER); - TSHandleMLocRelease(this->_request, this->_req_hdr, mloc); + if (mloc) { + TSHandleMLocRelease(this->_request, this->_req_hdr, mloc); + } return mloc != TS_NULL_MLOC; } @@ -107,21 +111,24 @@ Extractor::get_headers_hash(unsigned char out[32]) while (field_loc != TS_NULL_MLOC) { int field_name_len; char *field_name = const_cast(TSMimeHdrFieldNameGet(this->_request, this->_req_hdr, field_loc, &field_name_len)); + bool do_hash = true; if (field_name_len == TS_MIME_LEN_COOKIE) { auto field_name_sv = std::string_view(field_name, static_cast(field_name_len)); if (std::equal(field_name_sv.begin(), field_name_sv.end(), std::string_view("cookie").begin(), [](char c1, char c2) { return c1 == std::tolower(c2); })) { - continue; + do_hash = false; }; } else if (field_name_len == TS_MIME_LEN_REFERER) { auto field_name_sv = std::string_view(field_name, static_cast(field_name_len)); if (std::equal(field_name_sv.begin(), field_name_sv.end(), std::string_view("referer").begin(), [](char c1, char c2) { return c1 == std::tolower(c2); })) { - continue; + do_hash = false; } } - SHA256_Update(&sha256ctx, field_name, field_name_len); + if (do_hash) { + SHA256_Update(&sha256ctx, field_name, field_name_len); + } TSMLoc next_field_loc = TSMimeHdrFieldNext(this->_request, this->_req_hdr, field_loc); TSHandleMLocRelease(this->_request, this->_req_hdr, field_loc); diff --git a/plugins/experimental/jax_fingerprint/ja4h/ja4h_method.cc b/plugins/experimental/jax_fingerprint/ja4h/ja4h_method.cc index ce842b154ba..bb5431958b9 100644 --- a/plugins/experimental/jax_fingerprint/ja4h/ja4h_method.cc +++ b/plugins/experimental/jax_fingerprint/ja4h/ja4h_method.cc @@ -48,14 +48,20 @@ get_fingerprint(TSHttpTxn txnp) Extractor ex(txnp); // JA4H_a std::string_view method = ex.get_method(); - fingerprint[0] = std::tolower(method[0]); - fingerprint[1] = std::tolower(method[1]); - int version = ex.get_version(); - fingerprint[2] = 0x30 | (version >> 16); - fingerprint[3] = 0x30 | (version & 0xFFFF); - fingerprint[4] = ex.has_cookie_field() ? 'c' : 'n'; - fingerprint[5] = ex.has_referer_field() ? 'c' : 'n'; - int field_count = ex.get_field_count(); + if (method.length() >= 2) { + fingerprint[0] = std::tolower(method[0]); + fingerprint[1] = std::tolower(method[1]); + } else { + // This case seems to be undefined on the spec + fingerprint[0] = 'x'; + fingerprint[1] = 'x'; + } + int version = ex.get_version(); + fingerprint[2] = 0x30 | (version >> 16); + fingerprint[3] = 0x30 | (version & 0xFFFF); + fingerprint[4] = ex.has_cookie_field() ? 'c' : 'n'; + fingerprint[5] = ex.has_referer_field() ? 'c' : 'n'; + int field_count = ex.get_field_count(); if (field_count < 100) { fingerprint[6] = 0x30 | (field_count / 10); fingerprint[7] = 0x30 | (field_count % 10); @@ -71,10 +77,10 @@ get_fingerprint(TSHttpTxn txnp) fingerprint[11] = '0'; } else { for (int i = 0, j = 0; i < 4; ++i) { - while (accept_lang[j] < 'A' && accept_lang[j] != ';') { + while (static_cast(j) < accept_lang.size() && accept_lang[j] < 'A' && accept_lang[j] != ';') { ++j; } - if (accept_lang[j] == ';') { + if (static_cast(j) == accept_lang.size() || accept_lang[j] == ';') { fingerprint[8 + i] = '0'; } else { fingerprint[8 + i] = std::tolower(accept_lang[j]); diff --git a/plugins/experimental/jax_fingerprint/log.cc b/plugins/experimental/jax_fingerprint/log.cc index 910cd8fe6bc..a53e2c22685 100644 --- a/plugins/experimental/jax_fingerprint/log.cc +++ b/plugins/experimental/jax_fingerprint/log.cc @@ -26,17 +26,19 @@ #include -static TSTextLogObject log_handle = nullptr; - bool -create_log_file(const std::string &filename) +create_log_file(const std::string &filename, TSTextLogObject &log_handle) { return (TS_SUCCESS == TSTextLogObjectCreate(filename.c_str(), TS_LOG_MODE_ADD_TIMESTAMP, &log_handle)); } void -log_fingerprint(const JAxContext *ctx) +log_fingerprint(const JAxContext *ctx, TSTextLogObject &log_handle) { + if (log_handle == nullptr) { + Dbg(dbg_ctl, "Log handle is not initialized."); + return; + } if (TS_ERROR == TSTextLogObjectWrite(log_handle, "Client: %s\t%s: %s", ctx->get_addr(), ctx->get_method_name(), ctx->get_fingerprint().c_str())) { Dbg(dbg_ctl, "Failed to write to log!"); diff --git a/plugins/experimental/jax_fingerprint/log.h b/plugins/experimental/jax_fingerprint/log.h index b477703d149..7ea3b505cfc 100644 --- a/plugins/experimental/jax_fingerprint/log.h +++ b/plugins/experimental/jax_fingerprint/log.h @@ -22,7 +22,8 @@ #pragma once +#include "ts/ts.h" #include "context.h" -bool create_log_file(const std::string &filename); -void log_fingerprint(const JAxContext *ctx); +bool create_log_file(const std::string &filename, TSTextLogObject &log_handle); +void log_fingerprint(const JAxContext *ctx, TSTextLogObject &log_handle); diff --git a/plugins/experimental/jax_fingerprint/plugin.cc b/plugins/experimental/jax_fingerprint/plugin.cc index 6f32c9f79e1..132821f0751 100644 --- a/plugins/experimental/jax_fingerprint/plugin.cc +++ b/plugins/experimental/jax_fingerprint/plugin.cc @@ -34,6 +34,7 @@ #include #include #include +#include #include @@ -255,7 +256,7 @@ handle_read_request_hdr(void *edata, PluginConfig &config) } if (!config.log_filename.empty()) { - log_fingerprint(ctx); + log_fingerprint(ctx, config.log_handle); } modify_headers(ctx, txnp, config); @@ -341,7 +342,7 @@ TSPluginInit(int argc, char const **argv) } if (!config->log_filename.empty()) { - if (!create_log_file(config->log_filename)) { + if (!create_log_file(config->log_filename, config->log_handle)) { TSError("[%s] Failed to create log.", PLUGIN_NAME); return; } else { @@ -389,6 +390,16 @@ TSRemapNewInstance(int argc, char *argv[], void **ih, char * /* errbuf ATS_UNUSE return TS_ERROR; } + // Create a log file + if (!config->log_filename.empty()) { + if (!create_log_file(config->log_filename, config->log_handle)) { + TSError("[%s] Failed to create log.", PLUGIN_NAME); + return TS_ERROR; + } else { + Dbg(dbg_ctl, "Created log file."); + } + } + reserve_user_arg(*config); // Create continuation From 497671a28a6082cf20c7e68b88bc2ee8c45f71c2 Mon Sep 17 00:00:00 2001 From: Masakazu Kitajo Date: Thu, 19 Mar 2026 11:09:01 -0600 Subject: [PATCH 17/23] Address some more comments --- cmake/ExperimentalPlugins.cmake | 1 + doc/admin-guide/plugins/index.en.rst | 2 +- doc/admin-guide/plugins/jax_fingerprint.en.rst | 10 +++++----- plugins/experimental/CMakeLists.txt | 2 ++ plugins/experimental/jax_fingerprint/CMakeLists.txt | 2 +- plugins/experimental/jax_fingerprint/ja3/ja3_utils.h | 2 ++ 6 files changed, 12 insertions(+), 7 deletions(-) diff --git a/cmake/ExperimentalPlugins.cmake b/cmake/ExperimentalPlugins.cmake index 764571546c1..5e73ffa5ec0 100644 --- a/cmake/ExperimentalPlugins.cmake +++ b/cmake/ExperimentalPlugins.cmake @@ -43,6 +43,7 @@ auto_option(HTTP_STATS FEATURE_VAR BUILD_HTTP_STATS DEFAULT ${_DEFAULT}) auto_option(ICAP FEATURE_VAR BUILD_ICAP DEFAULT ${_DEFAULT}) auto_option(INLINER FEATURE_VAR BUILD_INLINER DEFAULT ${_DEFAULT}) auto_option(JA4_FINGERPRINT FEATURE_VAR BUILD_JA4_FINGERPRINT VAR_DEPENDS DEFAULT ${_DEFAULT}) +auto_option(JAX_FINGERPRINT FEATURE_VAR BUILD_JAX_FINGERPRINT VAR_DEPENDS DEFAULT ${_DEFAULT}) auto_option( MAGICK FEATURE_VAR diff --git a/doc/admin-guide/plugins/index.en.rst b/doc/admin-guide/plugins/index.en.rst index dc355641117..c2847b3237c 100644 --- a/doc/admin-guide/plugins/index.en.rst +++ b/doc/admin-guide/plugins/index.en.rst @@ -237,7 +237,7 @@ directory of the |TS| source tree. Experimental plugins can be compiled by passi :doc:`JA4 Fingerprint ` Calculates JA4 Fingerprints for incoming TLS traffic. -:doc:`JAx Fingerprint ` +:doc:`JAx Fingerprint ` Calculates JAx Fingerprints. :doc:`MaxMind ACL ` diff --git a/doc/admin-guide/plugins/jax_fingerprint.en.rst b/doc/admin-guide/plugins/jax_fingerprint.en.rst index 9180c06b1e2..715eb94b9cd 100644 --- a/doc/admin-guide/plugins/jax_fingerprint.en.rst +++ b/doc/admin-guide/plugins/jax_fingerprint.en.rst @@ -38,7 +38,7 @@ Fingerprints can be used for: Plugin Configuration ==================== -You can use the plugin as a global pugin, a remap plugin, or both. +You can use the plugin as a global plugin, a remap plugin, or both. To use the plugin as a global plugin, add the following line to :file:`plugin.config`:: @@ -46,7 +46,7 @@ To use the plugin as a global plugin, add the following line to :file:`plugin.co To use the plugin as a remap plugin, append the following line to a remap rule on :file:`remap.config`:: - @plugin=jax_fingerprint.so @pparam --standalone + @plugin=jax_fingerprint.so @pparam=--standalone To use the plugin as both global and remap plugin (hybrid setup), have the both without `--standalone` option. @@ -56,14 +56,14 @@ To use the plugin as both global and remap plugin (hybrid setup), have the both This option enables you to use the plugin as either a global plugin, or a remap plugin. In other words, the option needs to be specified if you do not use the hybrid setup. -.. option:: --method +.. option:: --method Fingerprinting method (e.g. JA4, JA3, etc.) to use. .. option:: --mode This option specifies what to do if requests from clients have the header names that are specified -by `--header` and/or `via-header`. Available setting values are "overwrite", "keep" and "append". +by `--header` and/or `--via-header`. Available setting values are "overwrite", "keep" and "append". .. option:: --servernames @@ -99,7 +99,7 @@ Remap plugin setup is the best if you: * Need a fingerprint only on specific paths, or * Cannot use Global plugin setup -.. note:: For JA3 and JA4, fingerprints are always generated at the beginning of connections. Using remap plugin setup only reduces the overhead of adding HTTP headers and loggingg. +.. note:: For JA3 and JA4, fingerprints are always generated at the beginning of connections. Using remap plugin setup only reduces the overhead of adding HTTP headers and logging. Hybrid setup ------------ diff --git a/plugins/experimental/CMakeLists.txt b/plugins/experimental/CMakeLists.txt index 70ae2d60703..fe7897c5854 100644 --- a/plugins/experimental/CMakeLists.txt +++ b/plugins/experimental/CMakeLists.txt @@ -67,6 +67,8 @@ if(BUILD_INLINER) endif() if(BUILD_JA4_FINGERPRINT) add_subdirectory(ja4_fingerprint) +endif() +if(BUILD_JAX_FINGERPRINT) add_subdirectory(jax_fingerprint) endif() if(BUILD_MAGICK) diff --git a/plugins/experimental/jax_fingerprint/CMakeLists.txt b/plugins/experimental/jax_fingerprint/CMakeLists.txt index e01b461968f..98ec1080028 100644 --- a/plugins/experimental/jax_fingerprint/CMakeLists.txt +++ b/plugins/experimental/jax_fingerprint/CMakeLists.txt @@ -37,5 +37,5 @@ if(BUILD_TESTING) add_executable(test_jax ja3/test_ja3.cc ja3/ja3_utils.cc ja4/test_ja4.cc ja4/ja4.cc ja4/tls_client_hello_summary.cc) target_link_libraries(test_jax PRIVATE Catch2::Catch2WithMain) - add_catch2_test(NAME test_jax COMMAND test_ja4) + add_catch2_test(NAME test_jax COMMAND test_jax) endif() diff --git a/plugins/experimental/jax_fingerprint/ja3/ja3_utils.h b/plugins/experimental/jax_fingerprint/ja3/ja3_utils.h index 93a0d8fe392..3a858d4b26c 100644 --- a/plugins/experimental/jax_fingerprint/ja3/ja3_utils.h +++ b/plugins/experimental/jax_fingerprint/ja3/ja3_utils.h @@ -22,6 +22,8 @@ */ +#pragma once + #include namespace ja3 From ad829a67ed32f0b589dde070ebf5edb7e92efba2 Mon Sep 17 00:00:00 2001 From: Masakazu Kitajo Date: Thu, 19 Mar 2026 14:21:19 -0600 Subject: [PATCH 18/23] Address some more comments --- plugins/experimental/jax_fingerprint/plugin.cc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/experimental/jax_fingerprint/plugin.cc b/plugins/experimental/jax_fingerprint/plugin.cc index 132821f0751..a2fa4db3060 100644 --- a/plugins/experimental/jax_fingerprint/plugin.cc +++ b/plugins/experimental/jax_fingerprint/plugin.cc @@ -221,7 +221,6 @@ handle_read_request_hdr(void *edata, PluginConfig &config) TSHttpTxn txnp = static_cast(edata); if (txnp == nullptr) { Dbg(dbg_ctl, "Failed to get txn object."); - TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE); return TS_SUCCESS; } @@ -261,9 +260,6 @@ handle_read_request_hdr(void *edata, PluginConfig &config) modify_headers(ctx, txnp, config); - if (config.method.type == Method::Type::CONNECTION_BASED) { - TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE); - } return TS_SUCCESS; } @@ -304,6 +300,7 @@ main_handler(TSCont cont, TSEvent event, void *edata) break; case TS_EVENT_HTTP_READ_REQUEST_HDR: ret = handle_read_request_hdr(edata, *config); + TSHttpTxnReenable(static_cast(edata), TS_EVENT_HTTP_CONTINUE); break; case TS_EVENT_HTTP_TXN_CLOSE: ret = handle_http_txn_close(edata, *config); @@ -440,5 +437,8 @@ void TSRemapDeleteInstance(void *ih) { auto config = static_cast(ih); + if (config->handler) { + TSContDestroy(config->handler); + } delete config; } From 5d450e203e94b37edd73810020e862865801feee Mon Sep 17 00:00:00 2001 From: Masakazu Kitajo Date: Thu, 19 Mar 2026 15:54:42 -0600 Subject: [PATCH 19/23] Address some more comments --- doc/admin-guide/plugins/jax_fingerprint.en.rst | 7 ++++--- plugins/experimental/jax_fingerprint/config.h | 2 +- plugins/experimental/jax_fingerprint/context.cc | 1 + .../jax_fingerprint/ja3/ja3_method.cc | 4 ++-- .../jax_fingerprint/ja4/ja4_method.cc | 10 ++++++++-- .../experimental/jax_fingerprint/ja4h/ja4h.cc | 6 ++++-- plugins/experimental/jax_fingerprint/log.cc | 6 ++++++ plugins/experimental/jax_fingerprint/log.h | 1 + plugins/experimental/jax_fingerprint/plugin.cc | 17 +++++++++++++++-- plugins/experimental/jax_fingerprint/userarg.cc | 5 +++-- plugins/experimental/jax_fingerprint/userarg.h | 2 +- 11 files changed, 46 insertions(+), 15 deletions(-) diff --git a/doc/admin-guide/plugins/jax_fingerprint.en.rst b/doc/admin-guide/plugins/jax_fingerprint.en.rst index 715eb94b9cd..cb81609f1fa 100644 --- a/doc/admin-guide/plugins/jax_fingerprint.en.rst +++ b/doc/admin-guide/plugins/jax_fingerprint.en.rst @@ -48,7 +48,8 @@ To use the plugin as a remap plugin, append the following line to a remap rule o @plugin=jax_fingerprint.so @pparam=--standalone -To use the plugin as both global and remap plugin (hybrid setup), have the both without `--standalone` option. +To use the plugin in a hybrid setup (both global and remap plugin), configure it in both :file:`plugin.config` and +:file:`remap.config` without `--standalone` option. .. option:: --standalone @@ -112,8 +113,8 @@ Hybrid setup is the best if you: Log Output ========== -The plugin output a log file in the Traffic Server log directory (typically ``/var/log/trafficserver/``) if a log filename is -specified by `--log-filename` option. +The plugin outputs a log file in the Traffic Server log directory (typically ``/var/log/trafficserver/``) if a log filename is +specified by ``--log-filename`` option. **Log Format**:: diff --git a/plugins/experimental/jax_fingerprint/config.h b/plugins/experimental/jax_fingerprint/config.h index da538d8da00..df1d49452bf 100644 --- a/plugins/experimental/jax_fingerprint/config.h +++ b/plugins/experimental/jax_fingerprint/config.h @@ -39,7 +39,7 @@ enum class PluginType : int { REMAP, }; -// This hash function enables looking up the set by a string_view without making a temporal string object. +// This hash function enables looking up the set by a string_view without making a temporary string object. struct StringHash { // Enable heterogeneous lookup using is_transparent = void; diff --git a/plugins/experimental/jax_fingerprint/context.cc b/plugins/experimental/jax_fingerprint/context.cc index 2c5c5cbf122..dbe469db118 100644 --- a/plugins/experimental/jax_fingerprint/context.cc +++ b/plugins/experimental/jax_fingerprint/context.cc @@ -40,6 +40,7 @@ JAxContext::JAxContext(const char *method_name, sockaddr const *s_sockaddr) : _m case AF_UNIX: strncpy(_addr, reinterpret_cast(s_sockaddr)->sun_path, sizeof(_addr) - 1); _addr[sizeof(_addr) - 1] = '\0'; + break; default: break; } diff --git a/plugins/experimental/jax_fingerprint/ja3/ja3_method.cc b/plugins/experimental/jax_fingerprint/ja3/ja3_method.cc index e27a41933fa..609884bdf1b 100644 --- a/plugins/experimental/jax_fingerprint/ja3/ja3_method.cc +++ b/plugins/experimental/jax_fingerprint/ja3/ja3_method.cc @@ -92,14 +92,14 @@ get_fingerprint(TSClientHello ch) raw.push_back(','); // Get elliptic curves - if (TS_SUCCESS == TSClientHelloExtensionGet(ch, 0x0a, &buf, &len)) { + if (TS_SUCCESS == TSClientHelloExtensionGet(ch, 0x0a, &buf, &len) && len >= 2) { // Skip first 2 bytes since we already have length raw.append(ja3::encode_word_buffer(buf + 2, len - 2)); } raw.push_back(','); // Get elliptic curve point formats - if (TS_SUCCESS == TSClientHelloExtensionGet(ch, 0x0b, &buf, &len)) { + if (TS_SUCCESS == TSClientHelloExtensionGet(ch, 0x0b, &buf, &len) && len >= 2) { // Skip first byte since we already have length raw.append(ja3::encode_byte_buffer(buf + 1, len - 1)); } diff --git a/plugins/experimental/jax_fingerprint/ja4/ja4_method.cc b/plugins/experimental/jax_fingerprint/ja4/ja4_method.cc index 4f56ba86f5d..0714194d245 100644 --- a/plugins/experimental/jax_fingerprint/ja4/ja4_method.cc +++ b/plugins/experimental/jax_fingerprint/ja4/ja4_method.cc @@ -53,8 +53,14 @@ get_version(TSClientHello ch) std::size_t buflen{}; if (TS_SUCCESS == TSClientHelloExtensionGet(ch, EXT_SUPPORTED_VERSIONS, &buf, &buflen)) { std::uint16_t max_version{0}; - size_t n_versions = buf[0]; - for (size_t i = 1; i + 1 < buflen && i < (n_versions * 2) + 1; i += 2) { + size_t versions_len = buf[0]; + + if (buflen < versions_len + 1) { + Dbg(dbg_ctl, "Malformed supported_versions extension (truncated vector)... using legacy version."); + return ch.get_version(); + } + + for (size_t i = 1; (i + 1) < (versions_len + 1); i += 2) { std::uint16_t version = (buf[i] << 8) | buf[i + 1]; if (!JA4::is_GREASE(version) && version > max_version) { max_version = version; diff --git a/plugins/experimental/jax_fingerprint/ja4h/ja4h.cc b/plugins/experimental/jax_fingerprint/ja4h/ja4h.cc index 43efd774e3c..a0db8a8285b 100644 --- a/plugins/experimental/jax_fingerprint/ja4h/ja4h.cc +++ b/plugins/experimental/jax_fingerprint/ja4h/ja4h.cc @@ -23,6 +23,8 @@ #include "ja4h.h" #include +#include +#include Extractor::Extractor(TSHttpTxn txnp) : _txn(txnp) { @@ -115,13 +117,13 @@ Extractor::get_headers_hash(unsigned char out[32]) if (field_name_len == TS_MIME_LEN_COOKIE) { auto field_name_sv = std::string_view(field_name, static_cast(field_name_len)); if (std::equal(field_name_sv.begin(), field_name_sv.end(), std::string_view("cookie").begin(), - [](char c1, char c2) { return c1 == std::tolower(c2); })) { + [](char c1, char c2) { return std::tolower(c1) == c2; })) { do_hash = false; }; } else if (field_name_len == TS_MIME_LEN_REFERER) { auto field_name_sv = std::string_view(field_name, static_cast(field_name_len)); if (std::equal(field_name_sv.begin(), field_name_sv.end(), std::string_view("referer").begin(), - [](char c1, char c2) { return c1 == std::tolower(c2); })) { + [](char c1, char c2) { return std::tolower(c1) == c2; })) { do_hash = false; } } diff --git a/plugins/experimental/jax_fingerprint/log.cc b/plugins/experimental/jax_fingerprint/log.cc index a53e2c22685..e18ad3d49ce 100644 --- a/plugins/experimental/jax_fingerprint/log.cc +++ b/plugins/experimental/jax_fingerprint/log.cc @@ -44,3 +44,9 @@ log_fingerprint(const JAxContext *ctx, TSTextLogObject &log_handle) Dbg(dbg_ctl, "Failed to write to log!"); } } + +void +flush_log_file(TSTextLogObject &log_handle) +{ + TSTextLogObjectDestroy(log_handle); +} diff --git a/plugins/experimental/jax_fingerprint/log.h b/plugins/experimental/jax_fingerprint/log.h index 7ea3b505cfc..8dad8fa2be0 100644 --- a/plugins/experimental/jax_fingerprint/log.h +++ b/plugins/experimental/jax_fingerprint/log.h @@ -27,3 +27,4 @@ bool create_log_file(const std::string &filename, TSTextLogObject &log_handle); void log_fingerprint(const JAxContext *ctx, TSTextLogObject &log_handle); +void flush_log_file(TSTextLogObject &log_handle); diff --git a/plugins/experimental/jax_fingerprint/plugin.cc b/plugins/experimental/jax_fingerprint/plugin.cc index a2fa4db3060..dd1259a2f5f 100644 --- a/plugins/experimental/jax_fingerprint/plugin.cc +++ b/plugins/experimental/jax_fingerprint/plugin.cc @@ -198,6 +198,10 @@ handle_client_hello(void *edata, PluginConfig &config) TSVConnReenable(vconn); return TS_SUCCESS; } + } else { + Dbg(dbg_ctl, "No SNI present but server name filtering is configured; skipping fingerprint generation"); + TSVConnReenable(vconn); + return TS_SUCCESS; } } @@ -347,7 +351,10 @@ TSPluginInit(int argc, char const **argv) } } - reserve_user_arg(*config); + if (reserve_user_arg(*config) == TS_ERROR) { + TSError("[%s] Failed to reserve user arg index.", PLUGIN_NAME); + return; + } TSCont cont = TSContCreate(main_handler, nullptr); TSContDataSet(cont, config); @@ -397,7 +404,10 @@ TSRemapNewInstance(int argc, char *argv[], void **ih, char * /* errbuf ATS_UNUSE } } - reserve_user_arg(*config); + if (reserve_user_arg(*config) == TS_ERROR) { + TSError("[%s] Failed to reserve user arg index.", PLUGIN_NAME); + return TS_ERROR; + } // Create continuation if (config->standalone) { @@ -440,5 +450,8 @@ TSRemapDeleteInstance(void *ih) if (config->handler) { TSContDestroy(config->handler); } + if (config->log_handle) { + flush_log_file(config->log_handle); + } delete config; } diff --git a/plugins/experimental/jax_fingerprint/userarg.cc b/plugins/experimental/jax_fingerprint/userarg.cc index 687fab4eb80..8c2fd20d753 100644 --- a/plugins/experimental/jax_fingerprint/userarg.cc +++ b/plugins/experimental/jax_fingerprint/userarg.cc @@ -23,7 +23,7 @@ #include "config.h" #include "userarg.h" -void +int reserve_user_arg(PluginConfig &config) { char name[strlen(PLUGIN_NAME) + strlen(config.method.name) + 1]; @@ -37,8 +37,9 @@ reserve_user_arg(PluginConfig &config) } else { type = TS_USER_ARGS_TXN; } - TSUserArgIndexReserve(type, name, "used to pass JAx context between hooks", &config.user_arg_index); + int ret = TSUserArgIndexReserve(type, name, "used to pass JAx context between hooks", &config.user_arg_index); Dbg(dbg_ctl, "user_arg_name: %s, user_arg_index: %d", name, config.user_arg_index); + return ret; } void diff --git a/plugins/experimental/jax_fingerprint/userarg.h b/plugins/experimental/jax_fingerprint/userarg.h index 4eff6dad08a..1a517c77af9 100644 --- a/plugins/experimental/jax_fingerprint/userarg.h +++ b/plugins/experimental/jax_fingerprint/userarg.h @@ -26,7 +26,7 @@ #include "ts/ts.h" -void reserve_user_arg(PluginConfig &config); +int reserve_user_arg(PluginConfig &config); void fill_user_arg_index(PluginConfig &config); void set_user_arg(void *container, PluginConfig &config, JAxContext *ctx); JAxContext *get_user_arg(void *container, PluginConfig &config); From bcff63e29c5c2ced8014256136e39c6029e2b2b3 Mon Sep 17 00:00:00 2001 From: Masakazu Kitajo Date: Thu, 19 Mar 2026 16:03:56 -0600 Subject: [PATCH 20/23] Fix the keep mode --- .../experimental/jax_fingerprint/header.cc | 19 +++++++++++++++++++ plugins/experimental/jax_fingerprint/header.h | 1 + .../experimental/jax_fingerprint/plugin.cc | 7 ++++++- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/plugins/experimental/jax_fingerprint/header.cc b/plugins/experimental/jax_fingerprint/header.cc index 1342f28cd40..2119c185f6a 100644 --- a/plugins/experimental/jax_fingerprint/header.cc +++ b/plugins/experimental/jax_fingerprint/header.cc @@ -139,3 +139,22 @@ remove_header(TSHttpTxn txnp, const std::string &header) } TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc); } + +bool +has_header(TSHttpTxn txnp, const std::string &header) +{ + TSMBuffer bufp; + TSMLoc hdr_loc; + if (TS_SUCCESS != TSHttpTxnClientReqGet(txnp, &bufp, &hdr_loc)) { + Dbg(dbg_ctl, "Failed to get headers."); + return false; + } + + TSMLoc target = TSMimeHdrFieldFind(bufp, hdr_loc, header.c_str(), header.length()); + if (target == TS_NULL_MLOC) { + return false; + } else { + TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc); + return true; + } +} diff --git a/plugins/experimental/jax_fingerprint/header.h b/plugins/experimental/jax_fingerprint/header.h index f098bf16b27..9f3366eae93 100644 --- a/plugins/experimental/jax_fingerprint/header.h +++ b/plugins/experimental/jax_fingerprint/header.h @@ -29,3 +29,4 @@ void append_via_header(TSHttpTxn txnp, const std::string &via_header); void set_header(TSHttpTxn txnp, const std::string &header, const std::string &fingerprint); void set_via_header(TSHttpTxn txnp, const std::string &via_header); void remove_header(TSHttpTxn txnp, const std::string &header); +bool has_header(TSHttpTxn txnp, const std::string &header); diff --git a/plugins/experimental/jax_fingerprint/plugin.cc b/plugins/experimental/jax_fingerprint/plugin.cc index dd1259a2f5f..bb73f7cad5b 100644 --- a/plugins/experimental/jax_fingerprint/plugin.cc +++ b/plugins/experimental/jax_fingerprint/plugin.cc @@ -145,7 +145,12 @@ modify_headers(JAxContext *ctx, TSHttpTxn txnp, PluginConfig &config) if (!ctx->get_fingerprint().empty()) { switch (config.mode) { case Mode::KEEP: - break; + if (!config.header_name.empty() && !has_header(txnp, config.header_name)) { + set_header(txnp, config.header_name, ctx->get_fingerprint()); + } + if (!config.via_header_name.empty() && !has_header(txnp, config.via_header_name)) { + set_via_header(txnp, config.via_header_name); + } case Mode::OVERWRITE: if (!config.header_name.empty()) { set_header(txnp, config.header_name, ctx->get_fingerprint()); From cb655d3d9f8a309161c4d0f10f6fb3d7dddad58a Mon Sep 17 00:00:00 2001 From: Masakazu Kitajo Date: Thu, 19 Mar 2026 16:12:31 -0600 Subject: [PATCH 21/23] Add a missing break --- plugins/experimental/jax_fingerprint/plugin.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/experimental/jax_fingerprint/plugin.cc b/plugins/experimental/jax_fingerprint/plugin.cc index bb73f7cad5b..2a5293d32be 100644 --- a/plugins/experimental/jax_fingerprint/plugin.cc +++ b/plugins/experimental/jax_fingerprint/plugin.cc @@ -151,6 +151,7 @@ modify_headers(JAxContext *ctx, TSHttpTxn txnp, PluginConfig &config) if (!config.via_header_name.empty() && !has_header(txnp, config.via_header_name)) { set_via_header(txnp, config.via_header_name); } + break; case Mode::OVERWRITE: if (!config.header_name.empty()) { set_header(txnp, config.header_name, ctx->get_fingerprint()); From c228c7de76a354385187ef0faca5ece7ece50ded Mon Sep 17 00:00:00 2001 From: Masakazu Kitajo Date: Thu, 19 Mar 2026 16:21:32 -0600 Subject: [PATCH 22/23] Address some more comments --- doc/admin-guide/plugins/jax_fingerprint.en.rst | 2 +- plugins/experimental/jax_fingerprint/header.cc | 2 ++ plugins/experimental/jax_fingerprint/ja4/ja4.h | 2 +- plugins/experimental/jax_fingerprint/plugin.cc | 2 -- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/admin-guide/plugins/jax_fingerprint.en.rst b/doc/admin-guide/plugins/jax_fingerprint.en.rst index cb81609f1fa..0fc4af83876 100644 --- a/doc/admin-guide/plugins/jax_fingerprint.en.rst +++ b/doc/admin-guide/plugins/jax_fingerprint.en.rst @@ -25,7 +25,7 @@ JAx Fingerprint Plugin Description =========== -The JAx Fingerprint plugin generates TLS client fingerprints based on the JA4 or JA3 algorithm designed by John Althouse. +The JAx Fingerprint plugin generates client fingerprints based on the JA4+ or JA3 algorithms designed by John Althouse. Fingerprints can be used for: diff --git a/plugins/experimental/jax_fingerprint/header.cc b/plugins/experimental/jax_fingerprint/header.cc index 2119c185f6a..cb4debe5a35 100644 --- a/plugins/experimental/jax_fingerprint/header.cc +++ b/plugins/experimental/jax_fingerprint/header.cc @@ -152,8 +152,10 @@ has_header(TSHttpTxn txnp, const std::string &header) TSMLoc target = TSMimeHdrFieldFind(bufp, hdr_loc, header.c_str(), header.length()); if (target == TS_NULL_MLOC) { + TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc); return false; } else { + TSHandleMLocRelease(bufp, hdr_loc, target); TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc); return true; } diff --git a/plugins/experimental/jax_fingerprint/ja4/ja4.h b/plugins/experimental/jax_fingerprint/ja4/ja4.h index 11c549a0dbd..b6be1e40cf5 100644 --- a/plugins/experimental/jax_fingerprint/ja4/ja4.h +++ b/plugins/experimental/jax_fingerprint/ja4/ja4.h @@ -164,7 +164,7 @@ make_JA4_fingerprint(TLSClientHelloSummary const &TLS_summary, UnaryOp hasher) * well lubricated. They are ignored in all parts of JA4 because of their * random nature. * - * @return Returns true if the value is a GREASE value, fales otherwise. + * @return Returns true if the value is a GREASE value, false otherwise. */ bool is_GREASE(std::uint16_t value); diff --git a/plugins/experimental/jax_fingerprint/plugin.cc b/plugins/experimental/jax_fingerprint/plugin.cc index 2a5293d32be..e1627ea30f2 100644 --- a/plugins/experimental/jax_fingerprint/plugin.cc +++ b/plugins/experimental/jax_fingerprint/plugin.cc @@ -237,14 +237,12 @@ handle_read_request_hdr(void *edata, PluginConfig &config) TSHttpSsn ssnp = TSHttpTxnSsnGet(txnp); if (ssnp == nullptr) { Dbg(dbg_ctl, "Failed to get ssn object."); - TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE); return TS_SUCCESS; } TSVConn vconn = TSHttpSsnClientVConnGet(ssnp); if (vconn == nullptr) { Dbg(dbg_ctl, "Failed to get vconn object."); - TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE); return TS_SUCCESS; } From a2ad4a192f3548879ed3bbc8200af2a37fa96da0 Mon Sep 17 00:00:00 2001 From: Masakazu Kitajo Date: Thu, 19 Mar 2026 17:09:43 -0600 Subject: [PATCH 23/23] Address comments --- plugins/experimental/jax_fingerprint/header.h | 2 ++ plugins/experimental/jax_fingerprint/userarg.h | 1 + 2 files changed, 3 insertions(+) diff --git a/plugins/experimental/jax_fingerprint/header.h b/plugins/experimental/jax_fingerprint/header.h index 9f3366eae93..4fe8c0c19aa 100644 --- a/plugins/experimental/jax_fingerprint/header.h +++ b/plugins/experimental/jax_fingerprint/header.h @@ -24,6 +24,8 @@ #include "ts/ts.h" +#include + void append_header(TSHttpTxn txnp, const std::string &header, const std::string &fingerprint); void append_via_header(TSHttpTxn txnp, const std::string &via_header); void set_header(TSHttpTxn txnp, const std::string &header, const std::string &fingerprint); diff --git a/plugins/experimental/jax_fingerprint/userarg.h b/plugins/experimental/jax_fingerprint/userarg.h index 1a517c77af9..7c23e5b0d49 100644 --- a/plugins/experimental/jax_fingerprint/userarg.h +++ b/plugins/experimental/jax_fingerprint/userarg.h @@ -23,6 +23,7 @@ #pragma once #include "context.h" +#include "config.h" #include "ts/ts.h"