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 5379c9a72e9..c2847b3237c 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) 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..0fc4af83876 --- /dev/null +++ b/doc/admin-guide/plugins/jax_fingerprint.en.rst @@ -0,0 +1,180 @@ +.. 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 + +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. +The value must be provided as a single comma separated value (no space) of server names. + +.. 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 logging. + +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 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**:: + + [timestamp] Client:
: + +**Example**:: + + [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 +===================================== + +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..fe7897c5854 100644 --- a/plugins/experimental/CMakeLists.txt +++ b/plugins/experimental/CMakeLists.txt @@ -68,6 +68,9 @@ endif() if(BUILD_JA4_FINGERPRINT) add_subdirectory(ja4_fingerprint) endif() +if(BUILD_JAX_FINGERPRINT) + add_subdirectory(jax_fingerprint) +endif() if(BUILD_MAGICK) add_subdirectory(magick) endif() diff --git a/plugins/experimental/jax_fingerprint/CMakeLists.txt b/plugins/experimental/jax_fingerprint/CMakeLists.txt new file mode 100644 index 00000000000..98ec1080028 --- /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_jax) +endif() diff --git a/plugins/experimental/jax_fingerprint/config.h b/plugins/experimental/jax_fingerprint/config.h new file mode 100644 index 00000000000..df1d49452bf --- /dev/null +++ b/plugins/experimental/jax_fingerprint/config.h @@ -0,0 +1,66 @@ +/** @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 temporary 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; + TSTextLogObject log_handle = nullptr; +}; diff --git a/plugins/experimental/jax_fingerprint/context.cc b/plugins/experimental/jax_fingerprint/context.cc new file mode 100644 index 00000000000..dbe469db118 --- /dev/null +++ b/plugins/experimental/jax_fingerprint/context.cc @@ -0,0 +1,72 @@ +/** @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'; + break; + 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..de5fe1dee9f --- /dev/null +++ b/plugins/experimental/jax_fingerprint/context.h @@ -0,0 +1,47 @@ +/** @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 + +#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..cb4debe5a35 --- /dev/null +++ b/plugins/experimental/jax_fingerprint/header.cc @@ -0,0 +1,162 @@ +/** @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; + } + } + 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) { + 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/header.h b/plugins/experimental/jax_fingerprint/header.h new file mode 100644 index 00000000000..4fe8c0c19aa --- /dev/null +++ b/plugins/experimental/jax_fingerprint/header.h @@ -0,0 +1,34 @@ +/** @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 + +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); +bool has_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..609884bdf1b --- /dev/null +++ b/plugins/experimental/jax_fingerprint/ja3/ja3_method.cc @@ -0,0 +1,128 @@ +/** @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]; + 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(','); + + // Get elliptic curves + 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) && len >= 2) { + // 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..3a858d4b26c --- /dev/null +++ b/plugins/experimental/jax_fingerprint/ja3/ja3_utils.h @@ -0,0 +1,73 @@ +/** @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. + + */ + +#pragma once + +#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..b6be1e40cf5 --- /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, false 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..0714194d245 --- /dev/null +++ b/plugins/experimental/jax_fingerprint/ja4/ja4_method.cc @@ -0,0 +1,159 @@ +/** @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 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; + } + } + 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..a0db8a8285b --- /dev/null +++ b/plugins/experimental/jax_fingerprint/ja4h/ja4h.cc @@ -0,0 +1,141 @@ +/** @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 +#include +#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, "h3")) { + 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); + if (mloc) { + 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); + if (mloc) { + 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)); + 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 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 std::tolower(c1) == c2; })) { + do_hash = false; + } + } + + 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); + 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..db2f5b98593 --- /dev/null +++ b/plugins/experimental/jax_fingerprint/ja4h/ja4h.h @@ -0,0 +1,45 @@ +/** @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 + +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..bb5431958b9 --- /dev/null +++ b/plugins/experimental/jax_fingerprint/ja4h/ja4h_method.cc @@ -0,0 +1,127 @@ +/** @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(); + 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); + } 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 (static_cast(j) < accept_lang.size() && accept_lang[j] < 'A' && accept_lang[j] != ';') { + ++j; + } + if (static_cast(j) == accept_lang.size() || 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..e18ad3d49ce --- /dev/null +++ b/plugins/experimental/jax_fingerprint/log.cc @@ -0,0 +1,52 @@ +/** @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 + +bool +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, 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!"); + } +} + +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 new file mode 100644 index 00000000000..8dad8fa2be0 --- /dev/null +++ b/plugins/experimental/jax_fingerprint/log.h @@ -0,0 +1,30 @@ +/** @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" + +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/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..e1627ea30f2 --- /dev/null +++ b/plugins/experimental/jax_fingerprint/plugin.cc @@ -0,0 +1,461 @@ +/** @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 +#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: + 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(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(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(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) { + 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(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); + TSHttpTxnReenable(static_cast(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(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) { + 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); + 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/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..8c2fd20d753 --- /dev/null +++ b/plugins/experimental/jax_fingerprint/userarg.cc @@ -0,0 +1,72 @@ +/** @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" + +int +reserve_user_arg(PluginConfig &config) +{ + char name[strlen(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; + } + 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 +fill_user_arg_index(PluginConfig &config) +{ + char name[strlen(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..7c23e5b0d49 --- /dev/null +++ b/plugins/experimental/jax_fingerprint/userarg.h @@ -0,0 +1,33 @@ +/** @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 "config.h" + +#include "ts/ts.h" + +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);