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..1bdcf53bbe0 100644 --- a/plugins/experimental/jax_fingerprint/context_map.h +++ b/plugins/experimental/jax_fingerprint/context_map.h @@ -27,46 +27,62 @@ #pragma once -#include "config.h" #include "context.h" -#include +#include "ts/ts.h" + +#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::size_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::size_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 +91,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::size_t i = 0; i < _size; ++i) { + if (_slots[i].first == method_name) { + return _slots[i].second; + } + } + return nullptr; } /** @@ -88,10 +108,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::size_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 +128,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::size_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;