Copilot commented on code in PR #12995: URL: https://github.com/apache/trafficserver/pull/12995#discussion_r2969070801
########## plugins/experimental/jax_fingerprint/plugin.cc: ########## @@ -0,0 +1,473 @@ +/** @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 <ts/apidefs.h> +#include <ts/ts.h> +#include <ts/remap.h> +#include <ts/remap_version.h> + +#include <getopt.h> + +#include <cstddef> +#include <cstdint> +#include <cstdio> +#include <cstring> +#include <memory> +#include <string> +#include <string_view> +#include <version> + +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<char *const *>(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; + } + } + + if (strcmp(config.method.name, "uninitialized") == 0) { + TSError("[%s] Method must be specified", PLUGIN_NAME); + return false; + } + + Dbg(dbg_ctl, "JAx method is %s", config.method.name); + Dbg(dbg_ctl, "JAx mode is %d", static_cast<int>(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"); + Dbg(dbg_ctl, "JAx standalone mode is %s", config.standalone ? "ENABLED" : "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: + 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); + } + 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<TSVConn>(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) { +#ifdef __cpp_lib_generic_unordered_lookup + if (!config.servernames.contains(std::string_view(servername, servername_len))) { +#else + if (!config.servernames.contains({servername, static_cast<size_t>(servername_len)})) { +#endif + Dbg(dbg_ctl, "Server name %.*s is not in the server name set", servername_len, servername); + 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; + } + } + + 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<TSHttpTxn>(edata); + if (txnp == nullptr) { + Dbg(dbg_ctl, "Failed to get txn object."); + return TS_SUCCESS; + } + + TSHttpSsn ssnp = TSHttpTxnSsnGet(txnp); + if (ssnp == nullptr) { + Dbg(dbg_ctl, "Failed to get ssn object."); + return TS_SUCCESS; + } + + TSVConn vconn = TSHttpSsnClientVConnGet(ssnp); + if (vconn == nullptr) { + Dbg(dbg_ctl, "Failed to get vconn object."); + 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) { + if (container == vconn) { + // JAxContext should be created on client hello hook + Dbg(dbg_ctl, "No context. Skipping."); + return TS_SUCCESS; + } + 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, config.log_handle); + } + + modify_headers(ctx, txnp, config); + + return TS_SUCCESS; +} + +int +handle_http_txn_close(void *edata, PluginConfig &config) +{ + TSHttpTxn txnp = static_cast<TSHttpTxn>(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<TSVConn>(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<PluginConfig *>(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); + TSHttpTxnReenable(static_cast<TSHttpTxn>(edata), TS_EVENT_HTTP_CONTINUE); + 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, config->log_handle)) { + TSError("[%s] Failed to create log.", PLUGIN_NAME); + return; + } else { + Dbg(dbg_ctl, "Created log file."); + } + } + + 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); + 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, char *errbuf, int errbuf_size) +{ + Dbg(dbg_ctl, "JAx Remap Plugin initializing.."); + CHECK_REMAP_API_COMPATIBILITY(api_info, errbuf, errbuf_size); + + 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<const char **>(argv + 1), *config)) { + delete config; + Dbg(dbg_ctl, "Bad arguments"); + 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."); + } + } + + 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) { + Dbg(dbg_ctl, "Standalone mode. Adding hooks."); + 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); Review Comment: In `TSRemapNewInstance()`, standalone remap instances call `TSHttpHookAdd()` with a continuation stored in `config->handler`. Those are *global* hook registrations, but `TSRemapDeleteInstance()` later calls `TSContDestroy(config->handler)`, leaving the global hook list holding a pointer to a destroyed continuation (use-after-free / assertion when the hook fires, e.g. after remap.config reload). To avoid this, don’t register global hooks from per-instance code; instead add per-transaction hooks via `TSHttpTxnHookAdd()` in `TSRemapDoRemap()`, or keep a process-lifetime continuation that is never destroyed. ########## doc/admin-guide/plugins/jax_fingerprint.en.rst: ########## @@ -0,0 +1,182 @@ +.. 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 client fingerprints based on the JA4+ or JA3 algorithms 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 plugin, 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 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 + +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 <JA4|JA4H|JA3> + +Fingerprinting method (e.g. JA4, JA3, etc.) to use. This option must be speficied. Review Comment: Spelling: “speficied” → “specified”. ```suggestion Fingerprinting method (e.g. JA4, JA3, etc.) to use. This option must be specified. ``` ########## 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 <algorithm> +#include <cctype> +#include <cstddef> +#include <cstdint> +#include <cstdio> +#include <iterator> +#include <string> +#include <string_view> + +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<char>(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<char>(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(); + } Review Comment: `remove_trailing_character()` is forward-declared as `static` but defined without `static`, causing a linkage mismatch and compile failure. Make the definition match the `static` declaration (or remove `static` consistently). ########## 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 <algorithm> +#include <cctype> +#include <cstddef> +#include <cstdint> +#include <cstdio> +#include <iterator> +#include <string> +#include <string_view> + +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<char>(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<char>(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) +{ Review Comment: The definition of `convert_ALPN_to_two_char_string()` has external linkage, but it is forward-declared as `static` at file scope. This will not compile due to conflicting linkage; make the definition `static` as well (or remove `static` from the declaration) to keep linkage consistent. ########## doc/admin-guide/plugins/jax_fingerprint.en.rst: ########## @@ -0,0 +1,182 @@ +.. 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 client fingerprints based on the JA4+ or JA3 algorithms 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 plugin, 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 in a hybrid setup (both global and remap plugin), configure it in both :file:`plugin.config` and +:file:`remap.config` without `--standalone` option. Review Comment: RST inline markup: `--standalone` is written with single backticks, which in reStructuredText is interpreted text and may not render as literal code. Use double-backticks (``--standalone``) for consistency with other plugin docs and other option references in this doc. ```suggestion :file:`remap.config` without ``--standalone`` option. ``` ########## plugins/experimental/jax_fingerprint/plugin.cc: ########## @@ -0,0 +1,473 @@ +/** @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 <ts/apidefs.h> +#include <ts/ts.h> +#include <ts/remap.h> +#include <ts/remap_version.h> + +#include <getopt.h> + +#include <cstddef> +#include <cstdint> +#include <cstdio> +#include <cstring> +#include <memory> +#include <string> +#include <string_view> +#include <version> + +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<char *const *>(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; + } + } + + if (strcmp(config.method.name, "uninitialized") == 0) { + TSError("[%s] Method must be specified", PLUGIN_NAME); + return false; + } + + Dbg(dbg_ctl, "JAx method is %s", config.method.name); + Dbg(dbg_ctl, "JAx mode is %d", static_cast<int>(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"); + Dbg(dbg_ctl, "JAx standalone mode is %s", config.standalone ? "ENABLED" : "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: + 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); + } + 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<TSVConn>(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) { +#ifdef __cpp_lib_generic_unordered_lookup + if (!config.servernames.contains(std::string_view(servername, servername_len))) { +#else + if (!config.servernames.contains({servername, static_cast<size_t>(servername_len)})) { +#endif + Dbg(dbg_ctl, "Server name %.*s is not in the server name set", servername_len, servername); + 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; + } + } + + 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<TSHttpTxn>(edata); + if (txnp == nullptr) { + Dbg(dbg_ctl, "Failed to get txn object."); + return TS_SUCCESS; + } + + TSHttpSsn ssnp = TSHttpTxnSsnGet(txnp); + if (ssnp == nullptr) { + Dbg(dbg_ctl, "Failed to get ssn object."); + return TS_SUCCESS; + } + + TSVConn vconn = TSHttpSsnClientVConnGet(ssnp); + if (vconn == nullptr) { + Dbg(dbg_ctl, "Failed to get vconn object."); + 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) { + if (container == vconn) { + // JAxContext should be created on client hello hook + Dbg(dbg_ctl, "No context. Skipping."); + return TS_SUCCESS; + } + 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, config.log_handle); + } + + modify_headers(ctx, txnp, config); + + return TS_SUCCESS; +} + +int +handle_http_txn_close(void *edata, PluginConfig &config) +{ + TSHttpTxn txnp = static_cast<TSHttpTxn>(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<TSVConn>(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<PluginConfig *>(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); + TSHttpTxnReenable(static_cast<TSHttpTxn>(edata), TS_EVENT_HTTP_CONTINUE); + break; Review Comment: `main_handler()` always calls `TSHttpTxnReenable(static_cast<TSHttpTxn>(edata), ...)` for `TS_EVENT_HTTP_READ_REQUEST_HDR` even if `handle_read_request_hdr()` detected `txnp == nullptr` and returned early. If `edata` is ever null/invalid, this will crash; either remove the `nullptr` early-return (if impossible) or guard the `TSHttpTxnReenable()` call behind a non-null `TSHttpTxn` check. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
