From 61c87b6012bc2f8cccee38b3155a141e1eb34190 Mon Sep 17 00:00:00 2001 From: Masakazu Kitajo Date: Thu, 25 Jun 2026 20:43:23 -0600 Subject: [PATCH 1/2] jax_fingerprint: Reduce allocations and gate methods at build time Trim per-connection memory work in the hybrid (global + remap) setup by collapsing the per-connection table of fingerprint contexts to an inline structure and by passing fingerprints into the context without an intermediate copy. Lookup behavior is unchanged. Add ENABLE_JAX_METHODS as the configure-time switch for which fingerprint methods are compiled in. CMake derives the per-method preprocessor defines, the dispatcher table in plugin.cc, and the slot count of the inline context table from the same list. An empty list or an unknown method directory fails at configure time. Include a developer README covering the per-method file layout, the build-time switches, and the naming rules that the CMake glob relies on. --- .../jax_fingerprint/CMakeLists.txt | 56 ++++--- plugins/experimental/jax_fingerprint/README | 144 ++++++++++++++++++ .../experimental/jax_fingerprint/context.cc | 2 +- .../experimental/jax_fingerprint/context.h | 3 +- .../jax_fingerprint/context_map.h | 107 +++++++------ .../experimental/jax_fingerprint/plugin.cc | 41 +++-- 6 files changed, 265 insertions(+), 88 deletions(-) create mode 100644 plugins/experimental/jax_fingerprint/README diff --git a/plugins/experimental/jax_fingerprint/CMakeLists.txt b/plugins/experimental/jax_fingerprint/CMakeLists.txt index 8ace4738047..d87f29c2bf7 100644 --- a/plugins/experimental/jax_fingerprint/CMakeLists.txt +++ b/plugins/experimental/jax_fingerprint/CMakeLists.txt @@ -15,6 +15,37 @@ # ####################### +set(ENABLE_JAX_METHODS + "ja3;ja4;ja4h" + CACHE STRING "Semicolon-separated list of fingerprint methods to compile into jax_fingerprint" +) + +list(LENGTH ENABLE_JAX_METHODS _jax_method_count) +if(_jax_method_count EQUAL 0) + message(FATAL_ERROR "ENABLE_JAX_METHODS must list at least one method (e.g. ja3;ja4;ja4h)") +endif() + +set(_jax_plugin_method_sources "") +set(_jax_test_method_sources "") +set(_jax_method_defs "") +foreach(_m IN LISTS ENABLE_JAX_METHODS) + if(NOT IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/${_m}") + message(FATAL_ERROR "ENABLE_JAX_METHODS references unknown method '${_m}' (no ${_m}/ directory)") + endif() + file(GLOB _srcs CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/${_m}/*.cc") + set(_plugin_srcs ${_srcs}) + set(_test_srcs ${_srcs}) + list(FILTER _plugin_srcs EXCLUDE REGEX "/test\\.cc$") + # method.cc and tls_client_hello_summary.cc bind algorithm logic to ATS APIs (TSClientHello + # etc.) that the test binary does not link; everything else is pure algorithm/data and is + # safe to include in test_jax. + list(FILTER _test_srcs EXCLUDE REGEX "/(method|tls_client_hello_summary)\\.cc$") + list(APPEND _jax_plugin_method_sources ${_plugin_srcs}) + list(APPEND _jax_test_method_sources ${_test_srcs}) + string(TOUPPER ${_m} _m_upper) + list(APPEND _jax_method_defs "ENABLE_JAX_METHOD_${_m_upper}") +endforeach() + add_atsplugin( jax_fingerprint plugin.cc @@ -22,37 +53,20 @@ add_atsplugin( userarg.cc header.cc log.cc - ja3/method.cc - ja3/utils.cc - ja4/method.cc - ja4/ja4.cc - ja4/datasource.cc - ja4/tls_client_hello_summary.cc - ja4h/method.cc - ja4h/ja4h.cc - ja4h/datasource.cc common/utils.cc + ${_jax_plugin_method_sources} ) target_link_libraries(jax_fingerprint PRIVATE OpenSSL::Crypto OpenSSL::SSL) target_include_directories(jax_fingerprint BEFORE PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +target_compile_definitions(jax_fingerprint PRIVATE ${_jax_method_defs} JAX_FINGERPRINT_MAX_METHODS=${_jax_method_count}) verify_global_plugin(jax_fingerprint) verify_remap_plugin(jax_fingerprint) if(BUILD_TESTING) - add_executable( - test_jax - ja3/test.cc - ja3/utils.cc - ja4/test.cc - ja4/ja4.cc - ja4/datasource.cc - ja4h/test.cc - ja4h/ja4h.cc - ja4h/datasource.cc - common/utils.cc - ) + add_executable(test_jax common/utils.cc ${_jax_test_method_sources}) target_link_libraries(test_jax PRIVATE Catch2::Catch2WithMain OpenSSL::Crypto OpenSSL::SSL) target_include_directories(test_jax BEFORE PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_compile_definitions(test_jax PRIVATE ${_jax_method_defs}) add_catch2_test(NAME test_jax COMMAND test_jax) endif() diff --git a/plugins/experimental/jax_fingerprint/README b/plugins/experimental/jax_fingerprint/README new file mode 100644 index 00000000000..ca9502e0834 --- /dev/null +++ b/plugins/experimental/jax_fingerprint/README @@ -0,0 +1,144 @@ +ATS (Apache Traffic Server) JAx Fingerprint Plugin + +User-facing documentation lives in +`doc/admin-guide/plugins/jax_fingerprint.en.rst`. This README covers the +developer side: how the plugin is organised, and what it takes to add a new +fingerprinting method. + + +Plugin layout +------------- + +``` +jax_fingerprint/ + plugin.cc / plugin.h Plugin entry points, hook handlers, method dispatch. + context.cc / .h JAxContext: per-connection (or per-txn) fingerprint state. + context_map.h Inline fixed-size table that multiplexes JAxContexts + by method name on a single user-arg slot. + userarg.cc / userarg.h Shared user-arg slot reservation and accessors. + header.cc / .h Set/append/remove the fingerprint and via headers. + log.cc / .h Optional log file output for fingerprints. + method.h Abstract Method struct (name, type, callbacks). + Unaware of any concrete method. + config.h PluginConfig, parsing helpers. + common/ Shared utilities (hash stringification, etc.). + ja3/ ja4/ ja4h/ One subdirectory per fingerprinting method. +``` + +Each method subdirectory follows the conventions described below. + + +Adding a new method +------------------- + +Suppose you want to add a method called `mymethod`. The steps are: + + 1. Create a directory `mymethod/` under this plugin. + + 2. Drop in the source files (see *File naming* below for what each name + means to the build): + + mymethod/method.h Declares `extern struct Method method;` in + your namespace (e.g. `namespace mymethod`). + mymethod/method.cc Defines `Method method = { "MYMETHOD", ... }` + and the on_client_hello / on_request callback. + This is the file allowed to use TS APIs + (TSVConn, TSClientHello, ...). + mymethod/mymethod.cc/.h Pure algorithm. No TS API. + mymethod/datasource.cc/.h Abstract data source the algorithm consumes + (mirror ja4/datasource.h). + mymethod/test.cc Catch2 unit tests for the algorithm. + + 3. Register the method in plugin.cc. The dispatcher is a `constexpr` table + gated by `ENABLE_JAX_METHOD_*` macros. Add two entries: + + #ifdef ENABLE_JAX_METHOD_MYMETHOD + #include "mymethod/method.h" + #endif + + ... + + constexpr Method const *METHODS[] = { + ... + #ifdef ENABLE_JAX_METHOD_MYMETHOD + &mymethod::method, + #endif + }; + + 4. Enable the method at build time: + + cmake -B build -DENABLE_JAX_METHODS="ja3;ja4;ja4h;mymethod" + + CMake automatically picks up `mymethod/*.cc`, defines + `ENABLE_JAX_METHOD_MYMETHOD`, and bumps `JAX_FINGERPRINT_MAX_METHODS` to + match the list length. + + 5. Use the method: + + jax_fingerprint.so --method MYMETHOD --header x-my-fingerprint + + +File naming +----------- + +The top-level CMakeLists.txt globs `*.cc` from each enabled method directory +and applies these filters: + + test.cc Built only into the test_jax binary (Catch2 unit tests). + + method.cc Built only into jax_fingerprint.so. This is the one file + in each method directory that is allowed to call TS APIs + (TSVConn, TSClientHello, ...). Keep all TS glue here. + + every other Built into BOTH jax_fingerprint.so and test_jax. Keep these + .cc files free of TS API calls so the unit-test binary, which + does not link the TS plugin SDK, still builds. + +If you genuinely need to spread TS-API code across more than `method.cc` in +a method directory, you have to extend the exclusion regex in +CMakeLists.txt. There is one such historical exception today -- +`ja4/tls_client_hello_summary.cc` -- and the regex carves it out by name. +Prefer not to add new files in that category: split the algorithm so the +TS-facing piece stays in `method.cc` and the pure-data piece is its own +file. If a future case really does justify extending the regex, name the +file something that documents the constraint (for instance `*_ts.cc`) and +update the regex accordingly. + +The method directory name (lower case) maps to the macro +`ENABLE_JAX_METHOD_` (so `mymethod` -> `ENABLE_JAX_METHOD_MYMETHOD`). +`Method::name` should be the user-visible spelling (e.g. "MYMETHOD"), +matched against `--method` on the command line. + +`Method::name` must reference a string with static storage duration (a +string literal is the natural fit). ContextMap stores `std::string_view` +slot keys that alias `Method::name`; if `name` ever pointed at temporary +storage, the keys would dangle. + + +Build-time configuration +------------------------ + + ENABLE_JAX_METHODS Semicolon-separated list of methods to + compile in. Default: "ja3;ja4;ja4h". + Empty list or unknown directory name causes + a FATAL_ERROR at configure time. + + JAX_FINGERPRINT_MAX_METHODS Auto-derived from the length of + ENABLE_JAX_METHODS, passed to the plugin as + a compile definition. Bounds the inline + ContextMap slot array. The header has a + fallback `#define JAX_FINGERPRINT_MAX_METHODS 8` + so it is also valid standalone (e.g. for + IDE indexing). + + +Unit tests vs. AuTest +--------------------- + + * Catch2 unit tests (`test_jax`) live in each `*/test.cc` and cover pure + algorithm logic. Built when BUILD_TESTING is on; run via ctest or the + test_jax binary directly. + + * End-to-end AuTests live under `tests/gold_tests/pluginTest/jax_fingerprint/` + and exercise the plugin via Proxy Verifier replay yamls. They require a + full install (traffic_server, traffic_layout, the plugin .so). diff --git a/plugins/experimental/jax_fingerprint/context.cc b/plugins/experimental/jax_fingerprint/context.cc index f9f6b06a1c4..9eac2a093b9 100644 --- a/plugins/experimental/jax_fingerprint/context.cc +++ b/plugins/experimental/jax_fingerprint/context.cc @@ -59,7 +59,7 @@ JAxContext::get_fingerprint() const } void -JAxContext::set_fingerprint(const std::string &fingerprint) +JAxContext::set_fingerprint(std::string_view fingerprint) { this->_fingerprint = fingerprint; Dbg(dbg_ctl, "Fingerprint: %s", this->_fingerprint.c_str()); diff --git a/plugins/experimental/jax_fingerprint/context.h b/plugins/experimental/jax_fingerprint/context.h index 2d3bdaff6ed..ed727152dae 100644 --- a/plugins/experimental/jax_fingerprint/context.h +++ b/plugins/experimental/jax_fingerprint/context.h @@ -28,6 +28,7 @@ #include #include +#include class JAxContext { @@ -36,7 +37,7 @@ class JAxContext ~JAxContext(); const std::string &get_fingerprint() const; - void set_fingerprint(const std::string &fingerprint); + void set_fingerprint(std::string_view fingerprint); const char *get_addr() const; const char *get_method_name() const; diff --git a/plugins/experimental/jax_fingerprint/context_map.h b/plugins/experimental/jax_fingerprint/context_map.h index aa9b77a9bd8..6fe616d8e9a 100644 --- a/plugins/experimental/jax_fingerprint/context_map.h +++ b/plugins/experimental/jax_fingerprint/context_map.h @@ -27,46 +27,63 @@ #pragma once -#include "config.h" #include "context.h" -#include +#include "ts/ts.h" + +#include +#include +#include #include -#include -#include +#include + +#ifndef JAX_FINGERPRINT_MAX_METHODS +#define JAX_FINGERPRINT_MAX_METHODS 8 +#endif /** * @brief Container holding JAxContext instances for multiple methods. * * ATS has a limited number of user arg slots (~4 per type). When loading * many jax_fingerprint instances, we share a single slot and store all - * contexts in this map, keyed by method name. + * contexts in this inline fixed-size table, keyed by method name. The + * table size is set at build time via JAX_FINGERPRINT_MAX_METHODS. + * + * Lookup is a linear scan over std::string_view keys (Method::name points + * to a string literal with static storage duration, so storing the view + * is safe). */ class ContextMap { public: + static constexpr std::size_t MAX_METHODS = JAX_FINGERPRINT_MAX_METHODS; + static_assert(MAX_METHODS >= 1, "Must accommodate at least one fingerprinting method"); + ~ContextMap() { - for (auto &pair : m_contexts) { - delete pair.second; + for (std::uint8_t i = 0; i < _size; ++i) { + delete _slots[i].second; } } /** * @brief Store a context for a method. - * @param[in] method_name The method name (e.g., "JA3", "JA4"). + * @param[in] method_name The method name (e.g., "JA3", "JA4"). Must reference + * a string with lifetime >= the ContextMap (typically a string literal). * @param[in] ctx The context to store. Ownership is transferred. */ void set(std::string_view method_name, JAxContext *ctx) { - auto it = find_context(method_name); - if (it != m_contexts.end()) { - delete it->second; - it->second = ctx; - } else { - m_contexts.emplace(std::string{method_name}, ctx); + for (std::uint8_t i = 0; i < _size; ++i) { + if (_slots[i].first == method_name) { + delete _slots[i].second; + _slots[i].second = ctx; + return; + } } + TSReleaseAssert(_size < MAX_METHODS); + _slots[_size++] = {method_name, ctx}; } /** @@ -75,10 +92,14 @@ class ContextMap * @return The context, or nullptr if not found. */ JAxContext * - get(std::string_view method_name) + get(std::string_view method_name) const { - auto it = find_context(method_name); - return it != m_contexts.end() ? it->second : nullptr; + for (std::uint8_t i = 0; i < _size; ++i) { + if (_slots[i].first == method_name) { + return _slots[i].second; + } + } + return nullptr; } /** @@ -88,10 +109,16 @@ class ContextMap void remove(std::string_view method_name) { - auto it = find_context(method_name); - if (it != m_contexts.end()) { - delete it->second; - m_contexts.erase(it); + for (std::uint8_t i = 0; i < _size; ++i) { + if (_slots[i].first == method_name) { + delete _slots[i].second; + --_size; + if (i != _size) { + _slots[i] = _slots[_size]; + } + _slots[_size] = {}; + return; + } } } @@ -102,42 +129,10 @@ class ContextMap bool empty() const { - return m_contexts.empty(); + return _size == 0; } private: - using ContextStorage = std::unordered_map>; - - /** Find context by method name with C++20 generic lookup fallback. - * - * C++20 generic unordered lookup allows finding with std::string_view in a - * std::unordered_map without creating a temporary string. - * For standard libraries without this feature, we fall back to constructing - * a std::string for the lookup. - * - * @param[in] method_name The method name to look up. - * @return Iterator to the found element, or end() if not found. - */ - ContextStorage::iterator - find_context(std::string_view method_name) - { -#ifdef __cpp_lib_generic_unordered_lookup - return m_contexts.find(method_name); -#else - return m_contexts.find(std::string{method_name}); -#endif - } - - /** const_iterator @overload */ - ContextStorage::const_iterator - find_context(std::string_view method_name) const - { -#ifdef __cpp_lib_generic_unordered_lookup - return m_contexts.find(method_name); -#else - return m_contexts.find(std::string{method_name}); -#endif - } - - ContextStorage m_contexts; + std::array, MAX_METHODS> _slots{}; + std::uint8_t _size{0}; }; diff --git a/plugins/experimental/jax_fingerprint/plugin.cc b/plugins/experimental/jax_fingerprint/plugin.cc index aca0ba4effd..374d66faecb 100644 --- a/plugins/experimental/jax_fingerprint/plugin.cc +++ b/plugins/experimental/jax_fingerprint/plugin.cc @@ -27,9 +27,15 @@ #include "header.h" #include "log.h" +#ifdef ENABLE_JAX_METHOD_JA4 #include "ja4/method.h" +#endif +#ifdef ENABLE_JAX_METHOD_JA4H #include "ja4h/method.h" +#endif +#ifdef ENABLE_JAX_METHOD_JA3 #include "ja3/method.h" +#endif #include #include @@ -49,6 +55,21 @@ DbgCtl dbg_ctl{PLUGIN_NAME}; +namespace +{ +constexpr Method const *METHODS[] = { +#ifdef ENABLE_JAX_METHOD_JA4 + &ja4::method, +#endif +#ifdef ENABLE_JAX_METHOD_JA4H + &ja4h::method, +#endif +#ifdef ENABLE_JAX_METHOD_JA3 + &ja3::method, +#endif +}; +} // namespace + static bool read_config_option(int argc, char const *argv[], PluginConfig &config) { @@ -71,18 +92,20 @@ read_config_option(int argc, char const *argv[], PluginConfig &config) case '?': Dbg(dbg_ctl, "Unrecognized command argument."); break; - case 'M': - if (strcmp("JA4", optarg) == 0) { - config.method = ja4::method; - } else if (strcmp("JA4H", optarg) == 0) { - config.method = ja4h::method; - } else if (strcmp("JA3", optarg) == 0) { - config.method = ja3::method; - } else { + case 'M': { + bool found = false; + for (auto const *m : METHODS) { + if (m->name == optarg) { + config.method = *m; + found = true; + break; + } + } + if (!found) { Dbg(dbg_ctl, "Unexpected method: %s", optarg); return false; } - break; + } break; case 'm': if (strcmp("overwrite", optarg) == 0) { config.mode = Mode::OVERWRITE; From 759e31ebcb3d2fb523f6a13570b19b7bf1e06158 Mon Sep 17 00:00:00 2001 From: Masakazu Kitajo Date: Thu, 25 Jun 2026 21:21:11 -0600 Subject: [PATCH 2/2] Address copilot comments --- plugins/experimental/jax_fingerprint/context_map.h | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/plugins/experimental/jax_fingerprint/context_map.h b/plugins/experimental/jax_fingerprint/context_map.h index 6fe616d8e9a..1bdcf53bbe0 100644 --- a/plugins/experimental/jax_fingerprint/context_map.h +++ b/plugins/experimental/jax_fingerprint/context_map.h @@ -33,7 +33,6 @@ #include #include -#include #include #include @@ -61,7 +60,7 @@ class ContextMap ~ContextMap() { - for (std::uint8_t i = 0; i < _size; ++i) { + for (std::size_t i = 0; i < _size; ++i) { delete _slots[i].second; } } @@ -75,7 +74,7 @@ class ContextMap void set(std::string_view method_name, JAxContext *ctx) { - for (std::uint8_t i = 0; i < _size; ++i) { + for (std::size_t i = 0; i < _size; ++i) { if (_slots[i].first == method_name) { delete _slots[i].second; _slots[i].second = ctx; @@ -94,7 +93,7 @@ class ContextMap JAxContext * get(std::string_view method_name) const { - for (std::uint8_t i = 0; i < _size; ++i) { + for (std::size_t i = 0; i < _size; ++i) { if (_slots[i].first == method_name) { return _slots[i].second; } @@ -109,7 +108,7 @@ class ContextMap void remove(std::string_view method_name) { - for (std::uint8_t i = 0; i < _size; ++i) { + for (std::size_t i = 0; i < _size; ++i) { if (_slots[i].first == method_name) { delete _slots[i].second; --_size; @@ -134,5 +133,5 @@ class ContextMap private: std::array, MAX_METHODS> _slots{}; - std::uint8_t _size{0}; + std::size_t _size{0}; };