This is an automated email from the ASF dual-hosted git repository.
maskit pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/master by this push:
new 3f6f207e7e Add BoringSSL support to the ja4_fingerprint plugin (#12914)
3f6f207e7e is described below
commit 3f6f207e7ee849cd97bedf78ce010a2edd22bf53
Author: Masakazu Kitajo <[email protected]>
AuthorDate: Tue Mar 3 18:23:31 2026 -0700
Add BoringSSL support to the ja4_fingerprint plugin (#12914)
* got client hello routed to plugins
* Creates ja4 fingerprint with boringssl
* cleanup a bit
* make ssl_client_hello const
* spaces cleanup
* cleanup code
* more cleanup
* Update plugin.cc
* Update plugin.cc
* Update ts.h
* Update apidefs.h.in
* Update to make more clean
* Update data
* address comments
* Update ja4_fingerprint.en.rst
* Add docs
* Update TSVConnClientHelloGet.en.rst
* Update TSVConnClientHelloGet.en.rst
* Address comments
* Update TSClientHello.en.rst
* Update TSClientHello.en.rst
* Eliminate heap allocations
* Address Copilot comments
* Address Copilot comments
* Update doxygen comments
---------
Co-authored-by: Jasmine Emanouel <[email protected]>
---
cmake/ExperimentalPlugins.cmake | 10 +-
doc/admin-guide/plugins/index.en.rst | 4 +
doc/admin-guide/plugins/ja4_fingerprint.en.rst | 209 +++++++++++++++++++++
.../api/functions/TSVConnClientHelloGet.en.rst | 50 +++++
doc/developer-guide/api/types/TSClientHello.en.rst | 83 ++++++++
include/iocore/net/TLSSNISupport.h | 45 ++++-
include/ts/apidefs.h.in | 69 +++++++
include/ts/ts.h | 46 +++++
plugins/experimental/ja4_fingerprint/README.md | 2 +
plugins/experimental/ja4_fingerprint/plugin.cc | 75 ++++----
src/api/InkAPI.cc | 102 ++++++++++
src/iocore/net/TLSSNISupport.cc | 102 ++++++++++
12 files changed, 748 insertions(+), 49 deletions(-)
diff --git a/cmake/ExperimentalPlugins.cmake b/cmake/ExperimentalPlugins.cmake
index 52313acfdd..764571546c 100644
--- a/cmake/ExperimentalPlugins.cmake
+++ b/cmake/ExperimentalPlugins.cmake
@@ -42,15 +42,7 @@ auto_option(HOOK_TRACE FEATURE_VAR BUILD_HOOK_TRACE DEFAULT
${_DEFAULT})
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
- HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
- DEFAULT
- ${_DEFAULT}
-)
+auto_option(JA4_FINGERPRINT FEATURE_VAR BUILD_JA4_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 1def2223a7..5379c9a72e 100644
--- a/doc/admin-guide/plugins/index.en.rst
+++ b/doc/admin-guide/plugins/index.en.rst
@@ -178,6 +178,7 @@ directory of the |TS| source tree. Experimental plugins can
be compiled by passi
Header Frequency <header_freq.en>
Hook Trace <hook-trace.en>
ICAP <icap.en>
+ JA4 Fingerprint <ja4_fingerprint.en>
Maxmind ACL <maxmind_acl.en>
Memcache <memcache.en>
Memory Profile <memory_profile.en>
@@ -232,6 +233,9 @@ directory of the |TS| source tree. Experimental plugins can
be compiled by passi
:doc:`ICAP <icap.en>`
Pass response data to external server for further processing using the ICAP
protocol.
+:doc:`JA4 Fingerprint <ja4_fingerprint.en>`
+ Calculates JA4 Fingerprints for incoming TLS traffic.
+
:doc:`MaxMind ACL <maxmind_acl.en>`
ACL based on the maxmind geo databases (GeoIP2 mmdb and libmaxminddb)
diff --git a/doc/admin-guide/plugins/ja4_fingerprint.en.rst
b/doc/admin-guide/plugins/ja4_fingerprint.en.rst
new file mode 100644
index 0000000000..b8e5e5e37a
--- /dev/null
+++ b/doc/admin-guide/plugins/ja4_fingerprint.en.rst
@@ -0,0 +1,209 @@
+.. 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-ja4-fingerprint:
+
+JA4 Fingerprint Plugin
+**********************
+
+Description
+===========
+
+The JA4 Fingerprint plugin generates TLS client fingerprints based on the JA4
+algorithm designed by John Althouse. JA4 is the successor to the JA3
+fingerprinting algorithm and provides improved client identification for TLS
+connections.
+
+A JA4 fingerprint uniquely identifies TLS clients based on characteristics of
+their TLS ClientHello messages, including:
+
+* TLS version
+* ALPN (Application-Layer Protocol Negotiation) preferences
+* Cipher suites offered
+* TLS extensions present
+
+This information can be used for:
+
+* Client identification and tracking
+* Bot detection and mitigation
+* Security analytics and threat intelligence
+* Understanding client TLS implementation patterns
+
+How It Works
+============
+
+The plugin intercepts TLS ClientHello messages during the TLS handshake and
+generates a JA4 fingerprint consisting of three sections separated by
underscores:
+
+**Section a (unhashed)**: Basic information about the client including:
+
+ * Protocol (``t`` for TCP, ``q`` for QUIC)
+ * TLS version
+ * SNI (Server Name Indication) status
+ * Number of cipher suites
+ * Number of extensions
+ * First ALPN value
+
+**Section b (hashed)**: A SHA-256 hash of the sorted cipher suite list
+
+**Section c (hashed)**: A SHA-256 hash of the sorted extension list
+
+Example fingerprint::
+
+ t13d1516h2_8daaf6152771_b186095e22b6
+
+Key Differences from JA3
+-------------------------
+
+* Cipher suites and extensions are sorted before hashing for consistency
+* SNI and ALPN information is included in the fingerprint
+* More resistant to fingerprint randomization
+
+Plugin Configuration
+====================
+
+The plugin operates as a global plugin and has no configuration options.
+
+To enable the plugin, add the following line to :file:`plugin.config`::
+
+ ja4_fingerprint.so
+
+No additional parameters are required or supported.
+
+Plugin Behavior
+===============
+
+When loaded, the plugin will:
+
+1. **Capture TLS ClientHello**: Intercepts all incoming TLS connections during
+ the ClientHello phase
+
+2. **Generate Fingerprint**: Calculates the JA4 fingerprint from the
+ ClientHello data
+
+3. **Log to File**: Writes the fingerprint and client IP address to
+ ``ja4_fingerprint.log``
+
+4. **Add HTTP Headers**: Injects the following headers into subsequent HTTP
+ requests on the same connection:
+
+ * ``ja4``: Contains the JA4 fingerprint
+ * ``x-ja4-via``: Contains the proxy name (from ``proxy.config.proxy_name``)
+
+Log Output
+==========
+
+The plugin writes to ``ja4_fingerprint.log`` in the Traffic Server log
+directory (typically ``/var/log/trafficserver/``).
+
+**Log Format**::
+
+ [timestamp] Client IP: <ip_address> JA4: <fingerprint>
+
+**Example**::
+
+ [Jan 29 10:15:23.456] Client IP: 192.168.1.100 JA4:
t13d1516h2_8daaf6152771_b186095e22b6
+ [Jan 29 10:15:24.123] Client IP: 10.0.0.50 JA4:
t13d1715h2_8daaf6152771_02713d6af862
+
+Using JA4 Headers in Origin Requests
+=====================================
+
+Origin servers can access the JA4 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 ``x-ja4-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: ja4_fingerprint
+
+Debug output will appear in :file:`diags.log` and includes:
+
+* ClientHello processing events
+* Fingerprint generation details
+* Header injection operations
+
+Requirements
+============
+
+* Traffic Server must be built with TLS support (OpenSSL or BoringSSL)
+* The plugin operates on all TLS connections
+
+Configuration Settings
+======================
+
+The plugin requires the ``proxy.config.proxy_name`` setting to be configured
+for the ``x-ja4-via`` header. If not set, the plugin will log an error and use
+"unknown" as the proxy name.
+
+To set the proxy name in :file:`records.yaml`::
+
+ records:
+ proxy:
+ config:
+ proxy_name: proxy01
+
+Limitations
+===========
+
+* The plugin only operates in global mode (no per-remap configuration)
+* Logging cannot be disabled
+* Raw (unhashed) cipher and extension lists are not logged
+* Non-TLS connections do not generate fingerprints
+
+See Also
+========
+
+* JA4 Technical Specification:
https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4.md
+* JA4 is licensed under the BSD 3-Clause license
+
+Example Configuration
+=====================
+
+Complete example configuration for enabling JA4 fingerprinting:
+
+**plugin.config**::
+
+ ja4_fingerprint.so
+
+**records.yaml**::
+
+ records:
+ proxy:
+ config:
+ proxy_name: proxy-01
+ diags:
+ debug:
+ enabled: 1
+ tags: ja4_fingerprint
+
+After restarting Traffic Server, the plugin will begin fingerprinting TLS
+connections and logging to ``ja4_fingerprint.log``.
diff --git a/doc/developer-guide/api/functions/TSVConnClientHelloGet.en.rst
b/doc/developer-guide/api/functions/TSVConnClientHelloGet.en.rst
new file mode 100644
index 0000000000..acfd96a91d
--- /dev/null
+++ b/doc/developer-guide/api/functions/TSVConnClientHelloGet.en.rst
@@ -0,0 +1,50 @@
+.. 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
+
+.. default-domain:: cpp
+
+TSVConnClientHelloGet
+*********************
+
+Synopsis
+========
+
+.. code-block:: cpp
+
+ #include <ts/ts.h>
+
+.. function:: TSClientHello TSVConnClientHelloGet(TSVConn sslp)
+.. function:: TSReturnCode TSClientHelloExtensionGet(TSClientHello ch,
unsigned int type, const unsigned char **out, size_t *outlen)
+
+Description
+===========
+
+:func:`TSVConnClientHelloGet` retrieves ClientHello message data from the TLS
+virtual connection :arg:`sslp`. Returns a :type:`TSClientHello` always. The
availability
+of the returned object must be checked before use.
+
+.. important::
+
+ This function should only be called from the ``TS_EVENT_SSL_CLIENT_HELLO``
hook.
+ The returned :type:`TSClientHello` is only valid during the SSL ClientHello
event processing.
+ Using this function from other hooks may result in accessing invalid or
stale data.
+
+:func:`TSClientHelloExtensionGet` retrieves extension data for the specified
+:arg:`type` (e.g., ``0x10`` for ALPN). Returns :enumerator:`TS_SUCCESS` if
+found, :enumerator:`TS_ERROR` otherwise. The returned pointer in :arg:`out` is
+valid only while :arg:`ch` exists.
diff --git a/doc/developer-guide/api/types/TSClientHello.en.rst
b/doc/developer-guide/api/types/TSClientHello.en.rst
new file mode 100644
index 0000000000..a9fbcc981b
--- /dev/null
+++ b/doc/developer-guide/api/types/TSClientHello.en.rst
@@ -0,0 +1,83 @@
+.. 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
+
+.. default-domain:: cpp
+
+TSClientHello
+*************
+
+Synopsis
+========
+
+.. code-block:: cpp
+
+ #include <ts/apidefs.h>
+
+.. type:: TSClientHello
+
+.. type:: TSClientHello::TSExtensionTypeList
+
+ A type alias for an iterable container of extension type IDs.
+
+
+Description
+===========
+
+:type:`TSClientHello` is an opaque handle to a TLS ClientHello message sent by
+a client during the TLS handshake. It provides access to the client's TLS
+version, cipher suites, and extensions.
+
+The implementation abstracts differences between OpenSSL and BoringSSL to
+provide a consistent interface.
+
+Accessor Methods
+================
+
+The following methods are available to access ClientHello data:
+
+.. function:: bool is_available() const
+
+ Returns whether the object contains valid values. As long as
+ :func:`TSVConnClientHelloGet` is called for a TLS connection, the return
+ value should be `true`.
+
+.. function:: uint16_t get_version() const
+
+ Returns the TLS version from the ClientHello message.
+
+.. function:: const uint8_t* get_cipher_suites() const
+
+ Returns a pointer to the cipher suites buffer. The length is available via
+ :func:`get_cipher_suites_len()`.
+
+.. function:: size_t get_cipher_suites_len() const
+
+ Returns the length of the cipher suites buffer in bytes.
+
+.. function:: TSClientHello::TSExtensionTypeList get_extension_types() const
+
+ Returns an iterable container of extension type IDs present in the
ClientHello.
+ This method abstracts the differences between BoringSSL (which uses an
extensions
+ buffer) and OpenSSL (which uses an extension_ids array), providing a
consistent
+ interface regardless of the SSL library in use.
+
+.. function:: void* _get_internal() const
+
+ Returns a pointer to internal implementation data. This is an internal
accessor for advanced use
+ cases. This accessor is not part of the stable public API, and plugins must
not cast or rely
+ on the returned pointer type.
diff --git a/include/iocore/net/TLSSNISupport.h
b/include/iocore/net/TLSSNISupport.h
index 6897cce36a..22f3d751a8 100644
--- a/include/iocore/net/TLSSNISupport.h
+++ b/include/iocore/net/TLSSNISupport.h
@@ -23,6 +23,7 @@
*/
#pragma once
+#include "tscore/ink_config.h"
#include "tscore/ink_memory.h"
#include "SSLTypes.h"
@@ -40,6 +41,40 @@ public:
{
public:
ClientHello(ClientHelloContainer chc) : _chc(chc) {}
+ ~ClientHello();
+
+ class ExtensionIdIterator
+ {
+ public:
+#if HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
+ ExtensionIdIterator(int *ids, size_t len, size_t offset) :
_extensions(ids), _ext_len(len), _offset(offset) {}
+#elif HAVE_SSL_CTX_SET_SELECT_CERTIFICATE_CB
+ ExtensionIdIterator(const uint8_t *extensions, size_t len, size_t offset)
+ : _extensions(extensions), _ext_len(len), _offset(offset)
+ {
+ }
+#endif
+ ~ExtensionIdIterator() = default;
+
+ ExtensionIdIterator &operator++();
+ bool operator==(const ExtensionIdIterator &b) const;
+ int operator*() const;
+
+ private:
+#if HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
+ int *_extensions;
+#elif HAVE_SSL_CTX_SET_SELECT_CERTIFICATE_CB
+ const uint8_t *_extensions;
+#endif
+ size_t _ext_len = 0;
+ size_t _offset = 0;
+ };
+
+ uint16_t getVersion();
+ std::string_view getCipherSuites();
+ ExtensionIdIterator begin();
+ ExtensionIdIterator end();
+
/**
* @return 1 if successful
*/
@@ -47,6 +82,10 @@ public:
private:
ClientHelloContainer _chc;
+#if HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
+ int *_ext_ids = nullptr;
+ size_t _ext_len;
+#endif
};
virtual ~TLSSNISupport() = default;
@@ -56,7 +95,8 @@ public:
static void bind(SSL *ssl, TLSSNISupport *snis);
static void unbind(SSL *ssl);
- int perform_sni_action(SSL &ssl);
+ int perform_sni_action(SSL &ssl);
+ ClientHello *get_client_hello() const;
// Callback functions for OpenSSL libraries
/** Process a CLIENT_HELLO from a client.
@@ -114,5 +154,6 @@ private:
// Null-terminated string, or nullptr if there is no SNI server name.
std::unique_ptr<char[]> _sni_server_name;
- void _set_sni_server_name_buffer(std::string_view name);
+ void _set_sni_server_name_buffer(std::string_view name);
+ ClientHello *_ch = nullptr;
};
diff --git a/include/ts/apidefs.h.in b/include/ts/apidefs.h.in
index c979ac8e94..86aff5202f 100644
--- a/include/ts/apidefs.h.in
+++ b/include/ts/apidefs.h.in
@@ -43,10 +43,13 @@
*/
#include <cstdint>
+#include <memory>
+#include <vector>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <tuple>
+#include <type_traits>
/** Apply printf format string compile-time argument checking to a function.
*
@@ -1049,6 +1052,72 @@ struct TSHttp2Priority {
int32_t stream_dependency;
};
+// Wrapper class that provides controlled access to client hello data
+class TSClientHello
+{
+public:
+ class TSExtensionTypeList
+ {
+ public:
+ TSExtensionTypeList(void *ch) : _ch(ch) {}
+
+ class Iterator
+ {
+ public:
+ Iterator(const void *ite);
+ Iterator &operator++();
+ bool operator==(const Iterator &b) const;
+ int operator*() const;
+
+ private:
+ char _real_iterator[24];
+ };
+
+ Iterator begin();
+ Iterator end();
+
+ private:
+ void *_ch;
+ };
+
+ TSClientHello(void *ch) : _client_hello(ch) {}
+
+ ~TSClientHello() = default;
+
+ explicit
+ operator bool() const
+ {
+ return _client_hello != nullptr;
+ }
+
+ bool is_available() const;
+
+ uint16_t get_version() const;
+
+ const uint8_t *get_cipher_suites() const;
+
+ size_t get_cipher_suites_len() const;
+
+ // Returns an iterable container of extension type IDs
+ // This abstracts the difference between BoringSSL (extensions buffer) and
OpenSSL (extension_ids array)
+ TSExtensionTypeList
+ get_extension_types() const
+ {
+ return TSExtensionTypeList(_client_hello);
+ }
+
+ // Internal accessor for API implementation
+ void *
+ _get_internal() const
+ {
+ return _client_hello;
+ }
+
+private:
+ void *_client_hello;
+};
+static_assert(std::is_trivially_copyable_v<TSClientHello> == true);
+
using TSFile = struct tsapi_file *;
using TSMLoc = struct tsapi_mloc *;
diff --git a/include/ts/ts.h b/include/ts/ts.h
index c63febb360..9b62d64fe8 100644
--- a/include/ts/ts.h
+++ b/include/ts/ts.h
@@ -1334,6 +1334,52 @@ int TSVConnIsSsl(TSVConn sslp);
int TSVConnProvidedSslCert(TSVConn sslp);
const char *TSVConnSslSniGet(TSVConn sslp, int *length);
+/**
+ Retrieve TLS Client Hello information from an SSL virtual connection.
+
+ This function extracts TLS Client Hello data from a TLS handshake.
+ The returned object provides access to version, cipher suites, and
extensions
+ in a way that is portable across both BoringSSL and OpenSSL
implementations.
+
+ IMPORTANT: This function must be called during the
TS_SSL_CLIENT_HELLO_HOOK.
+ The underlying SSL context may not be available at other hooks,
particularly
+ for BoringSSL where the SSL_CLIENT_HELLO structure is only valid during
+ specific callback functions. Calling this function outside of the client
+ hello hook may result in unavailable object being returned.
+
+ @param[in] sslp The SSL virtual connection handle. Must not be nullptr.
+ @return A TSClientHello object containing Client Hello data.
+
+ @see TSClientHelloExtensionGet
+ */
+TSClientHello TSVConnClientHelloGet(TSVConn sslp);
+
+/**
+ Retrieve a specific TLS extension from the Client Hello.
+
+ This function looks up a TLS extension by its type (e.g., 0x10 for ALPN,
+ 0x00 for SNI) and returns a pointer to its data. The lookup is performed
+ using SSL library-specific functions that work with both BoringSSL and
+ OpenSSL without requiring conditional compilation in the plugin.
+
+ The returned buffer is still owned by the underlying SSL context and must
+ not be freed by the caller. The buffer is valid only in the condition where
+ you can get a TSClientHello object from an SSL virtual connection.
+
+ @param[in] ch The Client Hello object obtained from
TSVConnClientHelloGet().
+ @param[in] type The TLS extension type to retrieve.
+ @param[out] out Pointer to receive the extension data buffer. Must not be
nullptr.
+ @param[out] outlen Pointer to receive the length of the extension data in
bytes.
+ Must not be nullptr.
+
+ @return TS_SUCCESS if the extension was found and retrieved successfully.
+ TS_ERROR if the extension is not present, or if any parameter is
nullptr,
+ or if an error occurred during lookup.
+
+ @see TSVConnClientHelloGet
+ */
+TSReturnCode TSClientHelloExtensionGet(TSClientHello ch, unsigned int type,
const unsigned char **out, size_t *outlen);
+
TSSslSession TSSslSessionGet(const TSSslSessionID *session_id);
int TSSslSessionGetBuffer(const TSSslSessionID *session_id, char
*buffer, int *len_ptr);
TSReturnCode TSSslSessionInsert(const TSSslSessionID *session_id, TSSslSession
add_session, TSSslConnection ssl_conn);
diff --git a/plugins/experimental/ja4_fingerprint/README.md
b/plugins/experimental/ja4_fingerprint/README.md
index d45ddf0078..3de07907d2 100644
--- a/plugins/experimental/ja4_fingerprint/README.md
+++ b/plugins/experimental/ja4_fingerprint/README.md
@@ -21,6 +21,8 @@ The technical specification of the algorithm is available
[here](https://github.
These changes were made to simplify the plugin as much as possible. The
missing features are useful and may be implemented in the future.
+This plugin works with BoringSSL as well.
+
## Logging and Debugging
To get debug information in the traffic log, enable the debug tag
`ja4_fingerprint`.
diff --git a/plugins/experimental/ja4_fingerprint/plugin.cc
b/plugins/experimental/ja4_fingerprint/plugin.cc
index 16419d4566..216171280f 100644
--- a/plugins/experimental/ja4_fingerprint/plugin.cc
+++ b/plugins/experimental/ja4_fingerprint/plugin.cc
@@ -54,13 +54,13 @@ static void reserve_user_arg();
static bool create_log_file();
static void register_hooks();
static int handle_client_hello(TSCont cont, TSEvent event, void
*edata);
-static std::string get_fingerprint(SSL *ssl);
char *get_IP(sockaddr const *s_sockaddr, char
res[INET6_ADDRSTRLEN]);
static void log_fingerprint(JA4_data const *data);
-static std::uint16_t get_version(SSL *ssl);
-static std::string get_first_ALPN(SSL *ssl);
-static void add_ciphers(JA4::TLSClientHelloSummary &summary, SSL
*ssl);
-static void add_extensions(JA4::TLSClientHelloSummary &summary,
SSL *ssl);
+static std::string get_fingerprint(TSClientHello ch);
+static std::uint16_t get_version(TSClientHello ch);
+static std::string get_first_ALPN(TSClientHello ch);
+static void add_ciphers(JA4::TLSClientHelloSummary &summary,
TSClientHello ch);
+static void add_extensions(JA4::TLSClientHelloSummary &summary,
TSClientHello ch);
static std::string hash_with_SHA256(std::string_view sv);
static int handle_read_request_hdr(TSCont cont, TSEvent event,
void *edata);
static void append_JA4_headers(TSCont cont, TSHttpTxn txnp,
std::string const *fingerprint);
@@ -77,7 +77,6 @@ constexpr std::string_view JA4_VIA_HEADER{"x-ja4-via"};
constexpr unsigned int EXT_ALPN{0x10};
constexpr unsigned int EXT_SUPPORTED_VERSIONS{0x2b};
-constexpr int SSL_SUCCESS{1};
DbgCtl dbg_ctl{PLUGIN_NAME};
@@ -198,13 +197,16 @@ handle_client_hello(TSCont /* cont ATS_UNUSED */, TSEvent
event, void *edata)
// We ignore the event, but we don't want to reject the connection.
return TS_SUCCESS;
}
- TSVConn const ssl_vc{static_cast<TSVConn>(edata)};
- TSSslConnection const ssl{TSVConnSslConnectionGet(ssl_vc)};
- if (nullptr == ssl) {
- Dbg(dbg_ctl, "Could not get SSL object.");
+
+ TSVConn const ssl_vc{static_cast<TSVConn>(edata)};
+
+ TSClientHello ch = TSVConnClientHelloGet(ssl_vc);
+
+ if (!ch) {
+ Dbg(dbg_ctl, "Could not get TSClientHello object.");
} else {
auto data{std::make_unique<JA4_data>()};
- data->fingerprint = get_fingerprint(reinterpret_cast<SSL *>(ssl));
+ data->fingerprint = get_fingerprint(ch);
get_IP(TSNetVConnRemoteAddrGet(ssl_vc), data->IP_addr);
log_fingerprint(data.get());
// The VCONN_CLOSE handler is now responsible for freeing the resource.
@@ -215,14 +217,14 @@ handle_client_hello(TSCont /* cont ATS_UNUSED */, TSEvent
event, void *edata)
}
std::string
-get_fingerprint(SSL *ssl)
+get_fingerprint(TSClientHello ch)
{
JA4::TLSClientHelloSummary summary{};
summary.protocol = JA4::Protocol::TLS;
- summary.TLS_version = get_version(ssl);
- summary.ALPN = get_first_ALPN(ssl);
- add_ciphers(summary, ssl);
- add_extensions(summary, ssl);
+ 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;
}
@@ -264,49 +266,52 @@ log_fingerprint(JA4_data const *data)
}
std::uint16_t
-get_version(SSL *ssl)
+get_version(TSClientHello ch)
{
unsigned char const *buf{};
std::size_t buflen{};
- if (SSL_SUCCESS == SSL_client_hello_get0_ext(ssl, EXT_SUPPORTED_VERSIONS,
&buf, &buflen)) {
+ if (TS_SUCCESS == TSClientHelloExtensionGet(ch, EXT_SUPPORTED_VERSIONS,
&buf, &buflen)) {
std::uint16_t max_version{0};
- for (std::size_t i{1}; i < buflen; i += 2) {
- std::uint16_t version{make_word(buf[i - 1], buf[i])};
- if ((!JA4::is_GREASE(version)) && version > max_version) {
+ size_t n_versions = buf[0];
+ for (size_t i = 1; i + 1 < buflen && i < (n_versions * 2) + 1; i += 2) {
+ std::uint16_t version = (buf[i] << 8) | buf[i + 1];
+ if (!JA4::is_GREASE(version) && version > max_version) {
max_version = version;
}
}
return max_version;
} else {
Dbg(dbg_ctl, "No supported_versions extension... using legacy version.");
- return SSL_client_hello_get0_legacy_version(ssl);
+ return ch.get_version();
}
}
std::string
-get_first_ALPN(SSL *ssl)
+get_first_ALPN(TSClientHello ch)
{
unsigned char const *buf{};
std::size_t buflen{};
std::string result{""};
- if (SSL_SUCCESS == SSL_client_hello_get0_ext(ssl, EXT_ALPN, &buf, &buflen)) {
+ 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;
}
void
-add_ciphers(JA4::TLSClientHelloSummary &summary, SSL *ssl)
+add_ciphers(JA4::TLSClientHelloSummary &summary, TSClientHello ch)
{
- unsigned char const *buf{};
- std::size_t buflen{SSL_client_hello_get0_ciphers(ssl, &buf)};
+ const uint8_t *buf = ch.get_cipher_suites();
+ size_t buflen = ch.get_cipher_suites_len();
+
if (buflen > 0) {
- for (std::size_t i{1}; i < buflen; i += 2) {
- summary.add_cipher(make_word(buf[i], buf[i - 1]));
+ 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.");
@@ -314,16 +319,11 @@ add_ciphers(JA4::TLSClientHelloSummary &summary, SSL *ssl)
}
void
-add_extensions(JA4::TLSClientHelloSummary &summary, SSL *ssl)
+add_extensions(JA4::TLSClientHelloSummary &summary, TSClientHello ch)
{
- int *buf{};
- std::size_t buflen{};
- if (SSL_SUCCESS == SSL_client_hello_get1_extensions_present(ssl, &buf,
&buflen)) {
- for (std::size_t i{1}; i < buflen; i += 2) {
- summary.add_extension(make_word(buf[i], buf[i - 1]));
- }
+ for (auto ext_type : ch.get_extension_types()) {
+ summary.add_extension(ext_type);
}
- OPENSSL_free(buf);
}
std::string
@@ -443,7 +443,6 @@ handle_vconn_close(TSCont /* cont ATS_UNUSED */, TSEvent
event, void *edata)
// We ignore the event, but we don't want to reject the connection.
return TS_SUCCESS;
}
-
TSVConn const ssl_vc{static_cast<TSVConn>(edata)};
delete static_cast<std::string *>(TSUserArgGet(ssl_vc,
*get_user_arg_index()));
TSUserArgSet(ssl_vc, *get_user_arg_index(), nullptr);
diff --git a/src/api/InkAPI.cc b/src/api/InkAPI.cc
index 1a4b734e28..529ff6d202 100644
--- a/src/api/InkAPI.cc
+++ b/src/api/InkAPI.cc
@@ -31,6 +31,7 @@
#include "iocore/net/NetVConnection.h"
#include "iocore/net/NetHandler.h"
#include "iocore/net/UDPNet.h"
+#include "tscore/ink_config.h"
#include "tscore/ink_platform.h"
#include "tscore/ink_base64.h"
#include "tscore/Encoding.h"
@@ -67,6 +68,7 @@
#include "iocore/net/SSLAPIHooks.h"
#include "iocore/net/SSLDiags.h"
#include "iocore/net/TLSBasicSupport.h"
+#include "iocore/net/TLSSNISupport.h"
#include "iocore/eventsystem/ConfigProcessor.h"
#include "proxy/Plugin.h"
#include "proxy/logging/LogObject.h"
@@ -7920,6 +7922,37 @@ TSVConnSslSniGet(TSVConn sslp, int *length)
return server_name;
}
+TSClientHello
+TSVConnClientHelloGet(TSVConn sslp)
+{
+ NetVConnection *netvc = reinterpret_cast<NetVConnection *>(sslp);
+ if (netvc == nullptr) {
+ return nullptr;
+ }
+
+ if (auto snis = netvc->get_service<TLSSNISupport>(); snis) {
+ TLSSNISupport::ClientHello *client_hello = snis->get_client_hello();
+ if (client_hello == nullptr) {
+ return nullptr;
+ }
+
+ // Wrap the raw object in the accessor and return
+ return TSClientHello(client_hello);
+ }
+
+ return nullptr;
+}
+
+TSReturnCode
+TSClientHelloExtensionGet(TSClientHello ch, unsigned int type, const unsigned
char **out, size_t *outlen)
+{
+ if (static_cast<TLSSNISupport::ClientHello
*>(ch._get_internal())->getExtension(type, out, outlen) == 1) {
+ return TS_SUCCESS;
+ }
+
+ return TS_ERROR;
+}
+
TSSslVerifyCTX
TSVConnSslVerifyCTXGet(TSVConn sslp)
{
@@ -9158,3 +9191,72 @@ TSLogAddrUnmarshal(char **buf, char *dest, int len)
return {-1, -1};
}
+
+bool
+TSClientHello::is_available() const
+{
+ return static_cast<bool>(*this);
+}
+
+uint16_t
+TSClientHello::get_version() const
+{
+ return static_cast<TLSSNISupport::ClientHello
*>(_client_hello)->getVersion();
+}
+
+const uint8_t *
+TSClientHello::get_cipher_suites() const
+{
+ return reinterpret_cast<const uint8_t
*>(static_cast<TLSSNISupport::ClientHello
*>(_client_hello)->getCipherSuites().data());
+}
+
+size_t
+TSClientHello::get_cipher_suites_len() const
+{
+ return static_cast<TLSSNISupport::ClientHello
*>(_client_hello)->getCipherSuites().length();
+}
+
+TSClientHello::TSExtensionTypeList::Iterator::Iterator(const void *ite)
+{
+ static_assert(sizeof(_real_iterator) >=
sizeof(TLSSNISupport::ClientHello::ExtensionIdIterator));
+
+ ink_assert(_real_iterator);
+ ink_assert(ite);
+ memcpy(_real_iterator, ite,
sizeof(TLSSNISupport::ClientHello::ExtensionIdIterator));
+}
+
+TSClientHello::TSExtensionTypeList::Iterator
+TSClientHello::TSExtensionTypeList::begin()
+{
+ ink_assert(_ch);
+ auto ch = static_cast<TLSSNISupport::ClientHello *>(_ch);
+ auto ite = ch->begin();
+ // The temporal pointer is for the memcpy in the constructor. It's only used
in the constructor.
+ return TSClientHello::TSExtensionTypeList::Iterator(&ite);
+}
+
+TSClientHello::TSExtensionTypeList::Iterator
+TSClientHello::TSExtensionTypeList::end()
+{
+ auto ite = static_cast<TLSSNISupport::ClientHello *>(_ch)->end();
+ // The temporal pointer is for the memcpy in the constructor. It's only used
in the constructor.
+ return TSClientHello::TSExtensionTypeList::Iterator(&ite);
+}
+
+TSClientHello::TSExtensionTypeList::Iterator &
+TSClientHello::TSExtensionTypeList::Iterator::operator++()
+{
+ ++(*reinterpret_cast<TLSSNISupport::ClientHello::ExtensionIdIterator
*>(_real_iterator));
+ return *this;
+}
+
+bool
+TSClientHello::TSExtensionTypeList::Iterator::operator==(const
TSClientHello::TSExtensionTypeList::Iterator &b) const
+{
+ return memcmp(_real_iterator, b._real_iterator, sizeof(_real_iterator)) == 0;
+}
+int
+TSClientHello::TSExtensionTypeList::Iterator::operator*() const
+{
+ return *(*reinterpret_cast<const
TLSSNISupport::ClientHello::ExtensionIdIterator *>(_real_iterator));
+}
diff --git a/src/iocore/net/TLSSNISupport.cc b/src/iocore/net/TLSSNISupport.cc
index ee5e4a8c44..9b56c9129b 100644
--- a/src/iocore/net/TLSSNISupport.cc
+++ b/src/iocore/net/TLSSNISupport.cc
@@ -50,6 +50,13 @@ TLSSNISupport::getInstance(SSL *ssl)
return static_cast<TLSSNISupport *>(SSL_get_ex_data(ssl, _ex_data_index));
}
+// In TLSSNISupport.h
+TLSSNISupport::ClientHello *
+TLSSNISupport::get_client_hello() const
+{
+ return this->_ch;
+}
+
void
TLSSNISupport::bind(SSL *ssl, TLSSNISupport *snis)
{
@@ -95,6 +102,9 @@ TLSSNISupport::perform_sni_action(SSL &ssl)
void
TLSSNISupport::on_client_hello(ClientHello &client_hello)
{
+ // Save local copy for later use;
+ _ch = &client_hello;
+
const char *servername = nullptr;
const unsigned char *p;
size_t remaining, len;
@@ -203,3 +213,95 @@ TLSSNISupport::ClientHello::getExtension(int type, const
uint8_t **out, size_t *
return SSL_early_callback_ctx_extension_get(this->_chc, type, out, outlen);
#endif
}
+
+uint16_t
+TLSSNISupport::ClientHello::getVersion()
+{
+#if HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
+ // Get legacy version (OpenSSL doesn't expose the direct version field from
client hello)
+ return SSL_client_hello_get0_legacy_version(_chc);
+#elif HAVE_SSL_CTX_SET_SELECT_CERTIFICATE_CB
+ return _chc->version;
+#endif
+}
+
+std::string_view
+TLSSNISupport::ClientHello::getCipherSuites()
+{
+#if HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
+ const unsigned char *cipher_buf = nullptr;
+ size_t cipher_buf_len = SSL_client_hello_get0_ciphers(_chc,
&cipher_buf);
+ return {reinterpret_cast<const char *>(cipher_buf), cipher_buf_len};
+#elif HAVE_SSL_CTX_SET_SELECT_CERTIFICATE_CB
+ return {reinterpret_cast<const char *>(_chc->cipher_suites),
_chc->cipher_suites_len};
+#endif
+}
+
+TLSSNISupport::ClientHello::ExtensionIdIterator
+TLSSNISupport::ClientHello::begin()
+{
+ ink_assert(_chc);
+#if HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
+ if (_ext_ids == nullptr) {
+ SSL_client_hello_get1_extensions_present(_chc, &_ext_ids, &_ext_len);
+ }
+ return ExtensionIdIterator(_ext_ids, _ext_len, 0);
+#elif HAVE_SSL_CTX_SET_SELECT_CERTIFICATE_CB
+ return ExtensionIdIterator(_chc->extensions, _chc->extensions_len, 0);
+#endif
+}
+
+TLSSNISupport::ClientHello::ExtensionIdIterator
+TLSSNISupport::ClientHello::end()
+{
+ ink_assert(_chc);
+#if HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
+ if (_ext_ids == nullptr) {
+ SSL_client_hello_get1_extensions_present(_chc, &_ext_ids, &_ext_len);
+ }
+ return ExtensionIdIterator(_ext_ids, _ext_len, _ext_len);
+#elif HAVE_SSL_CTX_SET_SELECT_CERTIFICATE_CB
+ return ExtensionIdIterator(_chc->extensions, _chc->extensions_len,
_chc->extensions_len);
+#endif
+}
+
+TLSSNISupport::ClientHello::~ClientHello()
+{
+#if HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
+ if (_ext_ids) {
+ OPENSSL_free(_ext_ids);
+ }
+#elif HAVE_SSL_CTX_SET_SELECT_CERTIFICATE_CB
+ // Nothing to do
+#endif
+}
+
+TLSSNISupport::ClientHello::ExtensionIdIterator &
+TLSSNISupport::ClientHello::ExtensionIdIterator::operator++()
+{
+#if HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
+ _offset++;
+#elif HAVE_SSL_CTX_SET_SELECT_CERTIFICATE_CB
+ uint16_t ext_len = (_extensions[_offset + 2] << 8) + _extensions[_offset +
3];
+ _offset += 2 + 2 + ext_len;
+ ink_assert(_offset <= _ext_len);
+#endif
+ return *this;
+}
+
+bool
+TLSSNISupport::ClientHello::ExtensionIdIterator::operator==(const
ExtensionIdIterator &b) const
+{
+ return _extensions == b._extensions && _offset == b._offset;
+}
+
+int
+TLSSNISupport::ClientHello::ExtensionIdIterator::operator*() const
+{
+ ink_assert(_offset < _ext_len);
+#if HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
+ return _extensions[_offset];
+#elif HAVE_SSL_CTX_SET_SELECT_CERTIFICATE_CB
+ return (_extensions[_offset] << 8) + _extensions[_offset + 1];
+#endif
+}