From 786fbaafd13154235a4174eb9fa58efcacd8af94 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Tue, 6 Jan 2026 15:56:35 +1100 Subject: [PATCH 01/21] got client hello routed to plugins --- cmake/ExperimentalPlugins.cmake | 10 +- include/iocore/net/TLSSNISupport.h | 11 +- include/ts/apidefs.h.in | 1 + include/ts/ts.h | 5 +- .../experimental/ja4_fingerprint/plugin.cc | 161 +++++++++++------- src/api/InkAPI.cc | 17 ++ src/iocore/net/SSLUtils.cc | 2 + src/iocore/net/TLSSNISupport.cc | 20 +++ 8 files changed, 152 insertions(+), 75 deletions(-) diff --git a/cmake/ExperimentalPlugins.cmake b/cmake/ExperimentalPlugins.cmake index 487e0bfed16..74347efde92 100644 --- a/cmake/ExperimentalPlugins.cmake +++ b/cmake/ExperimentalPlugins.cmake @@ -42,13 +42,9 @@ 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} + JA4_FINGERPRINT FEATURE_VAR BUILD_JA4_FINGERPRINT VAR_DEPENDS + # HAVE_SSL_CTX_SET_CLIENT_HELLO_CB + DEFAULT ${_DEFAULT} ) auto_option( MAGICK diff --git a/include/iocore/net/TLSSNISupport.h b/include/iocore/net/TLSSNISupport.h index 6897cce36a4..f56223d23b0 100644 --- a/include/iocore/net/TLSSNISupport.h +++ b/include/iocore/net/TLSSNISupport.h @@ -43,7 +43,8 @@ class TLSSNISupport /** * @return 1 if successful */ - int getExtension(int type, const uint8_t **out, size_t *outlen); + int getExtension(int type, const uint8_t **out, size_t *outlen); + ClientHelloContainer get_client_hello_container(); private: ClientHelloContainer _chc; @@ -55,8 +56,9 @@ class TLSSNISupport static TLSSNISupport *getInstance(SSL *ssl); static void bind(SSL *ssl, TLSSNISupport *snis); static void unbind(SSL *ssl); - - int perform_sni_action(SSL &ssl); + int perform_sni_action(SSL &ssl); + ClientHelloContainer get_client_hello_container() const; + void set_client_hello_container(ClientHelloContainer container); // Callback functions for OpenSSL libraries /** Process a CLIENT_HELLO from a client. @@ -114,5 +116,6 @@ class TLSSNISupport // Null-terminated string, or nullptr if there is no SNI server name. std::unique_ptr _sni_server_name; - void _set_sni_server_name_buffer(std::string_view name); + void _set_sni_server_name_buffer(std::string_view name); + ClientHelloContainer _chc = nullptr; }; diff --git a/include/ts/apidefs.h.in b/include/ts/apidefs.h.in index 078ce0eb69c..dda12fa0db6 100644 --- a/include/ts/apidefs.h.in +++ b/include/ts/apidefs.h.in @@ -1080,6 +1080,7 @@ using TSHostLookupResult = struct tsapi_hostlookupresult *; using TSAIOCallback = struct tsapi_aiocallback *; using TSAcceptor = struct tsapi_net_accept *; using TSRemapPluginInfo = struct tsapi_remap_plugin_info *; +using TSClientHello = struct tsapi_clienthello *; using TSFetchSM = struct tsapi_fetchsm *; diff --git a/include/ts/ts.h b/include/ts/ts.h index 3227f1cf18d..892d58a0c6f 100644 --- a/include/ts/ts.h +++ b/include/ts/ts.h @@ -1331,8 +1331,9 @@ TSReturnCode TSVConnProtocolEnable(TSVConn connp, const char *protocol_name); int TSVConnIsSsl(TSVConn sslp); /* Returns 1 if a certificate was provided in the TLS handshake, 0 otherwise. */ -int TSVConnProvidedSslCert(TSVConn sslp); -const char *TSVConnSslSniGet(TSVConn sslp, int *length); +int TSVConnProvidedSslCert(TSVConn sslp); +const char *TSVConnSslSniGet(TSVConn sslp, int *length); +TSClientHello TSVConnClientHelloGet(TSVConn sslp); TSSslSession TSSslSessionGet(const TSSslSessionID *session_id); int TSSslSessionGetBuffer(const TSSslSessionID *session_id, char *buffer, int *len_ptr); diff --git a/plugins/experimental/ja4_fingerprint/plugin.cc b/plugins/experimental/ja4_fingerprint/plugin.cc index ada54ed1c98..149a3b2c387 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -53,11 +53,12 @@ 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); +static std::string get_fingerprint(SSL_CLIENT_HELLO *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_ciphers(JA4::TLSClientHelloSummary &summary, SSL_CLIENT_HELLO *ssl); static void add_extensions(JA4::TLSClientHelloSummary &summary, SSL *ssl); static std::string hash_with_SHA256(std::string_view sv); static int handle_read_request_hdr(TSCont cont, TSEvent event, void *edata); @@ -163,8 +164,11 @@ 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(edata)}; - TSSslConnection const ssl{TSVConnSslConnectionGet(ssl_vc)}; + TSVConn const ssl_vc{static_cast(edata)}; + TSSslConnection const ssl{TSVConnSslConnectionGet(ssl_vc)}; + TSClientHello ssl_client_hello = TSVConnClientHelloGet(ssl_vc); + const SSL_CLIENT_HELLO *client_hello = reinterpret_cast(ssl_client_hello); + if (nullptr == ssl) { Dbg(dbg_ctl, "Could not get SSL object."); } else { @@ -180,14 +184,27 @@ handle_client_hello(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata) } std::string -get_fingerprint(SSL *ssl) +get_fingerprint(SSL_CLIENT_HELLO *ssl) { JA4::TLSClientHelloSummary summary{}; summary.protocol = JA4::Protocol::TLS; - summary.TLS_version = get_version(ssl); - summary.ALPN = get_first_ALPN(ssl); + summary.TLS_version = ssl->; + // summary.ALPN = get_first_ALPN(ssl); add_ciphers(summary, ssl); - add_extensions(summary, ssl); + // add_extensions(summary, ssl); + std::string result{JA4::make_JA4_fingerprint(summary, hash_with_SHA256)}; + return result; +} + +std::string +get_fingerprint(SSL *ssl) +{ + 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); std::string result{JA4::make_JA4_fingerprint(summary, hash_with_SHA256)}; return result; } @@ -228,68 +245,88 @@ log_fingerprint(JA4_data const *data) } } -std::uint16_t -get_version(SSL *ssl) -{ - unsigned char const *buf{}; - std::size_t buflen{}; - if (SSL_SUCCESS == SSL_client_hello_get0_ext(ssl, 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) { - 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); - } -} - -std::string -get_first_ALPN(SSL *ssl) -{ - unsigned char const *buf{}; - std::size_t buflen{}; - std::string result{""}; - if (SSL_SUCCESS == SSL_client_hello_get0_ext(ssl, 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; -} +// std::uint16_t +// get_version(SSL *ssl) +// { +// unsigned char const *buf{}; +// std::size_t buflen{}; +// if (SSL_SUCCESS == SSL_client_hello_get0_ext(ssl, 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) { +// 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); +// } +// } + +// std::string +// get_first_ALPN(SSL *ssl) +// { +// unsigned char const *buf{}; +// std::size_t buflen{}; +// std::string result{""}; +// if (SSL_SUCCESS == SSL_client_hello_get0_ext(ssl, 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, SSL_CLIENT_HELLO *client_hello) { - unsigned char const *buf{}; - std::size_t buflen{SSL_client_hello_get0_ciphers(ssl, &buf)}; - if (buflen > 0) { - for (std::size_t i{1}; i < buflen; i += 2) { - summary.add_cipher(make_word(buf[i], buf[i - 1])); - } - } else { - Dbg(dbg_ctl, "Failed to get ciphers."); - } -} + const uint8_t *ciphers = client_hello->cipher_suites; + size_t len = client_hello->cipher_suites_len; -void -add_extensions(JA4::TLSClientHelloSummary &summary, SSL *ssl) -{ - 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 (size_t i = 0; i + 1 < len; i += 2) { + uint16_t cipher_value = (ciphers[i] << 8) | ciphers[i + 1]; + summary.add_extension(cipher_value); + + const SSL_CIPHER *cipher = SSL_get_cipher_by_value(cipher_value); + + if (cipher != nullptr) { + const char *cipher_name = SSL_CIPHER_get_name(cipher); + Dbg(dbg_ctl, "0x%04X: %s", cipher_value, cipher_name); + } else { + Dbg(dbg_ctl, " 0x%04X: (unknown/unsupported)", cipher_value); } } - OPENSSL_free(buf); } +// void +// add_ciphers(JA4::TLSClientHelloSummary &summary, SSL *ssl) +// { +// unsigned char const *buf{}; +// std::size_t buflen{SSL_client_hello_get0_ciphers(ssl, &buf)}; +// if (buflen > 0) { +// for (std::size_t i{1}; i < buflen; i += 2) { +// summary.add_cipher(make_word(buf[i], buf[i - 1])); +// } +// } else { +// Dbg(dbg_ctl, "Failed to get ciphers."); +// } +// } + +// void +// add_extensions(JA4::TLSClientHelloSummary &summary, SSL *ssl) +// { +// 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])); +// } +// } +// OPENSSL_free(buf); +// } std::string hash_with_SHA256(std::string_view sv) diff --git a/src/api/InkAPI.cc b/src/api/InkAPI.cc index cabfb5309d6..b63b7e7c2b9 100644 --- a/src/api/InkAPI.cc +++ b/src/api/InkAPI.cc @@ -7890,6 +7890,23 @@ TSVConnSslSniGet(TSVConn sslp, int *length) return server_name; } +TSClientHello +TSVConnClientHelloGet(TSVConn sslp) +{ + NetVConnection *netvc = reinterpret_cast(sslp); + if (netvc == nullptr) { + return nullptr; + } + + if (auto snis = netvc->get_service(); snis) { + ClientHelloContainer client_hello = snis->get_client_hello_container(); + // Cast the pointer value directly (no const_cast needed if types match) + return reinterpret_cast(const_cast(static_cast(client_hello))); + } + + return nullptr; +} + TSSslVerifyCTX TSVConnSslVerifyCTXGet(TSVConn sslp) { diff --git a/src/iocore/net/SSLUtils.cc b/src/iocore/net/SSLUtils.cc index 0b7bc982ad4..abebf2b9565 100644 --- a/src/iocore/net/SSLUtils.cc +++ b/src/iocore/net/SSLUtils.cc @@ -302,10 +302,12 @@ ssl_client_hello_callback(const SSL_CLIENT_HELLO *client_hello) { SSL *s = client_hello->ssl; TLSSNISupport::ClientHello ch = {client_hello}; + #endif TLSSNISupport *snis = TLSSNISupport::getInstance(s); if (snis) { + snis->set_client_hello_container(ch.get_client_hello_container()); snis->on_client_hello(ch); int ret = snis->perform_sni_action(*s); if (ret != SSL_TLSEXT_ERR_OK) { diff --git a/src/iocore/net/TLSSNISupport.cc b/src/iocore/net/TLSSNISupport.cc index ee5e4a8c441..3f5574eb9ee 100644 --- a/src/iocore/net/TLSSNISupport.cc +++ b/src/iocore/net/TLSSNISupport.cc @@ -50,6 +50,25 @@ TLSSNISupport::getInstance(SSL *ssl) return static_cast(SSL_get_ex_data(ssl, _ex_data_index)); } +ClientHelloContainer +TLSSNISupport::ClientHello::get_client_hello_container() +{ + return this->_chc; +} + +// In TLSSNISupport.h +ClientHelloContainer +TLSSNISupport::get_client_hello_container() const +{ + return this->_chc; +} + +void +TLSSNISupport::set_client_hello_container(ClientHelloContainer container) +{ + this->_chc = container; +} + void TLSSNISupport::bind(SSL *ssl, TLSSNISupport *snis) { @@ -98,6 +117,7 @@ TLSSNISupport::on_client_hello(ClientHello &client_hello) const char *servername = nullptr; const unsigned char *p; size_t remaining, len; + // Parse the server name if the get extension call succeeds and there are more than 2 bytes to parse if (client_hello.getExtension(TLSEXT_TYPE_server_name, &p, &remaining) && remaining > 2) { // Parse to get to the name, originally from test/handshake_helper.c in openssl tree From 3f4e5c4915d3befe8053f2ec4c31630ef9ca6eeb Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Wed, 7 Jan 2026 10:35:21 +1100 Subject: [PATCH 02/21] Creates ja4 fingerprint with boringssl --- .../experimental/ja4_fingerprint/plugin.cc | 273 ++++++++++++------ 1 file changed, 183 insertions(+), 90 deletions(-) diff --git a/plugins/experimental/ja4_fingerprint/plugin.cc b/plugins/experimental/ja4_fingerprint/plugin.cc index 149a3b2c387..53835532726 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -52,17 +52,24 @@ 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); -static std::string get_fingerprint(SSL_CLIENT_HELLO *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_CLIENT_HELLO *ssl); -static void add_extensions(JA4::TLSClientHelloSummary &summary, SSL *ssl); -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); +#ifdef OPENSSL_IS_BORINGSSL +static std::string get_fingerprint(SSL_CLIENT_HELLO *ssl); +static std::uint16_t get_version(SSL_CLIENT_HELLO *ssl); +static std::string get_first_ALPN(SSL_CLIENT_HELLO *ssl); +static void add_ciphers(JA4::TLSClientHelloSummary &summary, SSL_CLIENT_HELLO *ssl); +static void add_extensions(JA4::TLSClientHelloSummary &summary, SSL_CLIENT_HELLO *ssl); +#else +static std::string get_fingerprint(SSL *ssl); +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); +#endif +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); static void append_to_field(TSMBuffer bufp, TSMLoc hdr_loc, char const *field, int field_len, char const *value, int value_len); static int handle_vconn_close(TSCont cont, TSEvent event, void *edata); @@ -164,11 +171,24 @@ 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(edata)}; - TSSslConnection const ssl{TSVConnSslConnectionGet(ssl_vc)}; - TSClientHello ssl_client_hello = TSVConnClientHelloGet(ssl_vc); - const SSL_CLIENT_HELLO *client_hello = reinterpret_cast(ssl_client_hello); + TSVConn const ssl_vc{static_cast(edata)}; + +#ifdef OPENSSL_IS_BORINGSSL + TSClientHello ssl_client_hello = TSVConnClientHelloGet(ssl_vc); + SSL_CLIENT_HELLO *client_hello = reinterpret_cast(ssl_client_hello); + if (nullptr == client_hello) { + Dbg(dbg_ctl, "Could not get SSL client hello object."); + } else { + auto data{std::make_unique()}; + data->fingerprint = get_fingerprint(client_hello); + get_IP(TSNetVConnRemoteAddrGet(ssl_vc), data->IP_addr); + log_fingerprint(data.get()); + // The VCONN_CLOSE handler is now responsible for freeing the resource. + TSUserArgSet(ssl_vc, *get_user_arg_index(), static_cast(data.release())); + } +#else + TSSslConnection const ssl{TSVConnSslConnectionGet(ssl_vc)}; if (nullptr == ssl) { Dbg(dbg_ctl, "Could not get SSL object."); } else { @@ -179,35 +199,37 @@ handle_client_hello(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata) // The VCONN_CLOSE handler is now responsible for freeing the resource. TSUserArgSet(ssl_vc, *get_user_arg_index(), static_cast(data.release())); } +#endif TSVConnReenable(ssl_vc); return TS_SUCCESS; } - +#ifdef OPENSSL_IS_BORINGSSL std::string get_fingerprint(SSL_CLIENT_HELLO *ssl) { JA4::TLSClientHelloSummary summary{}; summary.protocol = JA4::Protocol::TLS; - summary.TLS_version = ssl->; - // summary.ALPN = get_first_ALPN(ssl); + summary.TLS_version = get_version(ssl); + summary.ALPN = get_first_ALPN(ssl); add_ciphers(summary, ssl); - // add_extensions(summary, ssl); + add_extensions(summary, ssl); std::string result{JA4::make_JA4_fingerprint(summary, hash_with_SHA256)}; return result; } - +#else std::string get_fingerprint(SSL *ssl) { JA4::TLSClientHelloSummary summary{}; - summary.protocol = JA4::Protocol::TLS; - // summary.TLS_version = get_version(ssl); - // summary.ALPN = get_first_ALPN(ssl); + 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); + add_extensions(summary, ssl); std::string result{JA4::make_JA4_fingerprint(summary, hash_with_SHA256)}; return result; } +#endif // This implementation is copied verbatim from JA3 fingerprint to make the // potential for deduplication as obvious as possible. @@ -245,42 +267,91 @@ log_fingerprint(JA4_data const *data) } } -// std::uint16_t -// get_version(SSL *ssl) -// { -// unsigned char const *buf{}; -// std::size_t buflen{}; -// if (SSL_SUCCESS == SSL_client_hello_get0_ext(ssl, 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) { -// 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); -// } -// } - -// std::string -// get_first_ALPN(SSL *ssl) -// { -// unsigned char const *buf{}; -// std::size_t buflen{}; -// std::string result{""}; -// if (SSL_SUCCESS == SSL_client_hello_get0_ext(ssl, 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; -// } +#ifdef OPENSSL_IS_BORINGSSL +std::uint16_t +get_version(SSL_CLIENT_HELLO *client_hello) +{ + unsigned char const *buf{}; + std::size_t buflen{}; + // If no extensions, fall back to legacy version field + if (!client_hello->extensions || client_hello->extensions_len == 0) { + return client_hello->version; + } + + if (SSL_SUCCESS == SSL_early_callback_ctx_extension_get(client_hello, EXT_SUPPORTED_VERSIONS, &buf, &buflen)) { + std::uint16_t max_version{0}; + uint8_t list_len = buf[0]; + for (size_t i = 1; i + 1 < buflen && i < list_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 client_hello->version; + } +} +#else +std::uint16_t +get_version(SSL *ssl) +{ + unsigned char const *buf{}; + std::size_t buflen{}; + if (SSL_SUCCESS == SSL_client_hello_get0_ext(ssl, 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) { + 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); + } +} +#endif + +#ifdef OPENSSL_IS_BORINGSSL +std::string +get_first_ALPN(SSL_CLIENT_HELLO *client_hello) +{ + const uint8_t *buf = nullptr; + size_t buflen = 0; + std::string result; + + bool found = SSL_early_callback_ctx_extension_get(client_hello, EXT_ALPN, &buf, &buflen); + if (found && buflen > 3) { + uint8_t first_ALPN_length = buf[2]; + if (first_ALPN_length > 0 && 3 + first_ALPN_length <= buflen) { + result.assign(reinterpret_cast(&buf[3]), first_ALPN_length); + } + } + + return result; +} +#else +std::string +get_first_ALPN(SSL *ssl) +{ + unsigned char const *buf{}; + std::size_t buflen{}; + std::string result{""}; + if (SSL_SUCCESS == SSL_client_hello_get0_ext(ssl, 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; +} +#endif +#ifdef OPENSSL_IS_BORINGSSL void add_ciphers(JA4::TLSClientHelloSummary &summary, SSL_CLIENT_HELLO *client_hello) { @@ -289,45 +360,67 @@ add_ciphers(JA4::TLSClientHelloSummary &summary, SSL_CLIENT_HELLO *client_hello) for (size_t i = 0; i + 1 < len; i += 2) { uint16_t cipher_value = (ciphers[i] << 8) | ciphers[i + 1]; - summary.add_extension(cipher_value); + summary.add_cipher(cipher_value); + } +} +#else +void +add_ciphers(JA4::TLSClientHelloSummary &summary, SSL *ssl) +{ + unsigned char const *buf{}; + std::size_t buflen{SSL_client_hello_get0_ciphers(ssl, &buf)}; + if (buflen > 0) { + for (std::size_t i{1}; i < buflen; i += 2) { + summary.add_cipher(make_word(buf[i], buf[i - 1])); + } + } else { + Dbg(dbg_ctl, "Failed to get ciphers."); + } +} +#endif + +#ifdef OPENSSL_IS_BORINGSSL +void +add_extensions(JA4::TLSClientHelloSummary &summary, SSL_CLIENT_HELLO *client_hello) +{ + const uint8_t *ext = client_hello->extensions; + size_t remaining = client_hello->extensions_len; + + while (remaining >= 4) { // Need at least 4 bytes (2 type + 2 length) + // Read extension type (2 bytes, big endian) + uint16_t ext_type = (ext[0] << 8) | ext[1]; - const SSL_CIPHER *cipher = SSL_get_cipher_by_value(cipher_value); + // Read extension length (2 bytes, big endian) + uint16_t ext_len = (ext[2] << 8) | ext[3]; - if (cipher != nullptr) { - const char *cipher_name = SSL_CIPHER_get_name(cipher); - Dbg(dbg_ctl, "0x%04X: %s", cipher_value, cipher_name); - } else { - Dbg(dbg_ctl, " 0x%04X: (unknown/unsupported)", cipher_value); + // Add the extension type to summary + summary.add_extension(ext_type); + + // Move to next extension + size_t total_ext_size = 4 + ext_len; // 4 bytes header + data + if (total_ext_size > remaining) { + break; // Malformed extension, stop parsing } + + ext += total_ext_size; + remaining -= total_ext_size; } } -// void -// add_ciphers(JA4::TLSClientHelloSummary &summary, SSL *ssl) -// { -// unsigned char const *buf{}; -// std::size_t buflen{SSL_client_hello_get0_ciphers(ssl, &buf)}; -// if (buflen > 0) { -// for (std::size_t i{1}; i < buflen; i += 2) { -// summary.add_cipher(make_word(buf[i], buf[i - 1])); -// } -// } else { -// Dbg(dbg_ctl, "Failed to get ciphers."); -// } -// } - -// void -// add_extensions(JA4::TLSClientHelloSummary &summary, SSL *ssl) -// { -// 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])); -// } -// } -// OPENSSL_free(buf); -// } +#else +void +add_extensions(JA4::TLSClientHelloSummary &summary, SSL *ssl) +{ + 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])); + } + } + OPENSSL_free(buf); +} +#endif std::string hash_with_SHA256(std::string_view sv) { From 5c8c8ed5aa53e160b04e3700badadb93538e1bcf Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Wed, 7 Jan 2026 11:46:37 +1100 Subject: [PATCH 03/21] cleanup a bit --- .../experimental/ja4_fingerprint/plugin.cc | 116 +++++------------- 1 file changed, 33 insertions(+), 83 deletions(-) diff --git a/plugins/experimental/ja4_fingerprint/plugin.cc b/plugins/experimental/ja4_fingerprint/plugin.cc index 53835532726..4adeda4ae6b 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -176,49 +176,32 @@ handle_client_hello(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata) #ifdef OPENSSL_IS_BORINGSSL TSClientHello ssl_client_hello = TSVConnClientHelloGet(ssl_vc); - SSL_CLIENT_HELLO *client_hello = reinterpret_cast(ssl_client_hello); - if (nullptr == client_hello) { - Dbg(dbg_ctl, "Could not get SSL client hello object."); - } else { - auto data{std::make_unique()}; - data->fingerprint = get_fingerprint(client_hello); - get_IP(TSNetVConnRemoteAddrGet(ssl_vc), data->IP_addr); - log_fingerprint(data.get()); - // The VCONN_CLOSE handler is now responsible for freeing the resource. - TSUserArgSet(ssl_vc, *get_user_arg_index(), static_cast(data.release())); - } + SSL_CLIENT_HELLO *ssl = reinterpret_cast(ssl_client_hello); #else - TSSslConnection const ssl{TSVConnSslConnectionGet(ssl_vc)}; + TSSslConnection const ssl_conn{TSVConnSslConnectionGet(ssl_vc)}; + SSL *ssl = reinterpret_cast(ssl_conn); +#endif if (nullptr == ssl) { - Dbg(dbg_ctl, "Could not get SSL object."); + Dbg(dbg_ctl, "Could not get SSL client hello object."); } else { auto data{std::make_unique()}; - data->fingerprint = get_fingerprint(reinterpret_cast(ssl)); + data->fingerprint = get_fingerprint(ssl); get_IP(TSNetVConnRemoteAddrGet(ssl_vc), data->IP_addr); log_fingerprint(data.get()); // The VCONN_CLOSE handler is now responsible for freeing the resource. TSUserArgSet(ssl_vc, *get_user_arg_index(), static_cast(data.release())); } -#endif + TSVConnReenable(ssl_vc); return TS_SUCCESS; } -#ifdef OPENSSL_IS_BORINGSSL + std::string +#ifdef OPENSSL_IS_BORINGSSL get_fingerprint(SSL_CLIENT_HELLO *ssl) -{ - 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); - std::string result{JA4::make_JA4_fingerprint(summary, hash_with_SHA256)}; - return result; -} #else -std::string get_fingerprint(SSL *ssl) +#endif { JA4::TLSClientHelloSummary summary{}; summary.protocol = JA4::Protocol::TLS; @@ -229,7 +212,6 @@ get_fingerprint(SSL *ssl) std::string result{JA4::make_JA4_fingerprint(summary, hash_with_SHA256)}; return result; } -#endif // This implementation is copied verbatim from JA3 fingerprint to make the // potential for deduplication as obvious as possible. @@ -267,18 +249,24 @@ log_fingerprint(JA4_data const *data) } } -#ifdef OPENSSL_IS_BORINGSSL std::uint16_t +#ifdef OPENSSL_IS_BORINGSSL get_version(SSL_CLIENT_HELLO *client_hello) +#else +get_version(SSL *ssl) +#endif { unsigned char const *buf{}; std::size_t buflen{}; +#ifdef OPENSSL_IS_BORINGSSL // If no extensions, fall back to legacy version field if (!client_hello->extensions || client_hello->extensions_len == 0) { return client_hello->version; } - if (SSL_SUCCESS == SSL_early_callback_ctx_extension_get(client_hello, EXT_SUPPORTED_VERSIONS, &buf, &buflen)) { +#else + if (SSL_SUCCESS == SSL_client_hello_get0_ext(ssl, EXT_SUPPORTED_VERSIONS, &buf, &buflen)) { +#endif std::uint16_t max_version{0}; uint8_t list_len = buf[0]; for (size_t i = 1; i + 1 < buflen && i < list_len + 1; i += 2) { @@ -290,85 +278,53 @@ get_version(SSL_CLIENT_HELLO *client_hello) return max_version; } else { Dbg(dbg_ctl, "No supported_versions extension... using legacy version."); +#ifdef OPENSSL_IS_BORINGSSL return client_hello->version; - } -} #else -std::uint16_t -get_version(SSL *ssl) -{ - unsigned char const *buf{}; - std::size_t buflen{}; - if (SSL_SUCCESS == SSL_client_hello_get0_ext(ssl, 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) { - 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); +#endif } } -#endif -#ifdef OPENSSL_IS_BORINGSSL std::string +#ifdef OPENSSL_IS_BORINGSSL get_first_ALPN(SSL_CLIENT_HELLO *client_hello) -{ - const uint8_t *buf = nullptr; - size_t buflen = 0; - std::string result; - - bool found = SSL_early_callback_ctx_extension_get(client_hello, EXT_ALPN, &buf, &buflen); - if (found && buflen > 3) { - uint8_t first_ALPN_length = buf[2]; - if (first_ALPN_length > 0 && 3 + first_ALPN_length <= buflen) { - result.assign(reinterpret_cast(&buf[3]), first_ALPN_length); - } - } - - return result; -} #else -std::string get_first_ALPN(SSL *ssl) +#endif { unsigned char const *buf{}; std::size_t buflen{}; std::string result{""}; +#ifdef OPENSSL_IS_BORINGSSL + if (SSL_SUCCESS == SSL_early_callback_ctx_extension_get(client_hello, EXT_ALPN, &buf, &buflen)) { +#else if (SSL_SUCCESS == SSL_client_hello_get0_ext(ssl, EXT_ALPN, &buf, &buflen)) { +#endif // 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; } -#endif #ifdef OPENSSL_IS_BORINGSSL void add_ciphers(JA4::TLSClientHelloSummary &summary, SSL_CLIENT_HELLO *client_hello) { - const uint8_t *ciphers = client_hello->cipher_suites; - size_t len = client_hello->cipher_suites_len; - - for (size_t i = 0; i + 1 < len; i += 2) { - uint16_t cipher_value = (ciphers[i] << 8) | ciphers[i + 1]; - summary.add_cipher(cipher_value); - } -} + const uint8_t *buf = client_hello->cipher_suites; + size_t buflen = client_hello->cipher_suites_len; #else void add_ciphers(JA4::TLSClientHelloSummary &summary, SSL *ssl) { unsigned char const *buf{}; std::size_t buflen{SSL_client_hello_get0_ciphers(ssl, &buf)}; +#endif + if (buflen > 0) { for (std::size_t i{1}; i < buflen; i += 2) { summary.add_cipher(make_word(buf[i], buf[i - 1])); @@ -377,7 +333,6 @@ add_ciphers(JA4::TLSClientHelloSummary &summary, SSL *ssl) Dbg(dbg_ctl, "Failed to get ciphers."); } } -#endif #ifdef OPENSSL_IS_BORINGSSL void @@ -386,17 +341,12 @@ add_extensions(JA4::TLSClientHelloSummary &summary, SSL_CLIENT_HELLO *client_hel const uint8_t *ext = client_hello->extensions; size_t remaining = client_hello->extensions_len; - while (remaining >= 4) { // Need at least 4 bytes (2 type + 2 length) - // Read extension type (2 bytes, big endian) + while (remaining >= 4) { uint16_t ext_type = (ext[0] << 8) | ext[1]; // Read extension length (2 bytes, big endian) uint16_t ext_len = (ext[2] << 8) | ext[3]; - - // Add the extension type to summary summary.add_extension(ext_type); - - // Move to next extension size_t total_ext_size = 4 + ext_len; // 4 bytes header + data if (total_ext_size > remaining) { break; // Malformed extension, stop parsing @@ -406,7 +356,6 @@ add_extensions(JA4::TLSClientHelloSummary &summary, SSL_CLIENT_HELLO *client_hel remaining -= total_ext_size; } } - #else void add_extensions(JA4::TLSClientHelloSummary &summary, SSL *ssl) @@ -421,6 +370,7 @@ add_extensions(JA4::TLSClientHelloSummary &summary, SSL *ssl) OPENSSL_free(buf); } #endif + std::string hash_with_SHA256(std::string_view sv) { From 0efeade3015b25ef7cebcbd891caf2ffc0a03d26 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Wed, 7 Jan 2026 12:35:38 +1100 Subject: [PATCH 04/21] make ssl_client_hello const --- include/ts/apidefs.h.in | 2 +- .../experimental/ja4_fingerprint/plugin.cc | 26 +++++++++---------- src/api/InkAPI.cc | 3 +-- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/include/ts/apidefs.h.in b/include/ts/apidefs.h.in index dda12fa0db6..32f7221a477 100644 --- a/include/ts/apidefs.h.in +++ b/include/ts/apidefs.h.in @@ -1080,7 +1080,7 @@ using TSHostLookupResult = struct tsapi_hostlookupresult *; using TSAIOCallback = struct tsapi_aiocallback *; using TSAcceptor = struct tsapi_net_accept *; using TSRemapPluginInfo = struct tsapi_remap_plugin_info *; -using TSClientHello = struct tsapi_clienthello *; +using TSClientHello = const void *; using TSFetchSM = struct tsapi_fetchsm *; diff --git a/plugins/experimental/ja4_fingerprint/plugin.cc b/plugins/experimental/ja4_fingerprint/plugin.cc index 4adeda4ae6b..058eefd98fe 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -55,11 +55,11 @@ static int handle_client_hello(TSCont cont, TSEvent event, void * char *get_IP(sockaddr const *s_sockaddr, char res[INET6_ADDRSTRLEN]); static void log_fingerprint(JA4_data const *data); #ifdef OPENSSL_IS_BORINGSSL -static std::string get_fingerprint(SSL_CLIENT_HELLO *ssl); -static std::uint16_t get_version(SSL_CLIENT_HELLO *ssl); -static std::string get_first_ALPN(SSL_CLIENT_HELLO *ssl); -static void add_ciphers(JA4::TLSClientHelloSummary &summary, SSL_CLIENT_HELLO *ssl); -static void add_extensions(JA4::TLSClientHelloSummary &summary, SSL_CLIENT_HELLO *ssl); +static std::string get_fingerprint(const SSL_CLIENT_HELLO *ssl); +static std::uint16_t get_version(const SSL_CLIENT_HELLO *ssl); +static std::string get_first_ALPN(const SSL_CLIENT_HELLO *ssl); +static void add_ciphers(JA4::TLSClientHelloSummary &summary, const SSL_CLIENT_HELLO *ssl); +static void add_extensions(JA4::TLSClientHelloSummary &summary, const SSL_CLIENT_HELLO *ssl); #else static std::string get_fingerprint(SSL *ssl); static std::uint16_t get_version(SSL *ssl); @@ -175,14 +175,14 @@ handle_client_hello(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata) TSVConn const ssl_vc{static_cast(edata)}; #ifdef OPENSSL_IS_BORINGSSL - TSClientHello ssl_client_hello = TSVConnClientHelloGet(ssl_vc); - SSL_CLIENT_HELLO *ssl = reinterpret_cast(ssl_client_hello); + TSClientHello ssl_client_hello = TSVConnClientHelloGet(ssl_vc); + const SSL_CLIENT_HELLO *ssl = reinterpret_cast(ssl_client_hello); #else TSSslConnection const ssl_conn{TSVConnSslConnectionGet(ssl_vc)}; SSL *ssl = reinterpret_cast(ssl_conn); #endif if (nullptr == ssl) { - Dbg(dbg_ctl, "Could not get SSL client hello object."); + Dbg(dbg_ctl, "Could not get SSL object."); } else { auto data{std::make_unique()}; data->fingerprint = get_fingerprint(ssl); @@ -198,7 +198,7 @@ handle_client_hello(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata) std::string #ifdef OPENSSL_IS_BORINGSSL -get_fingerprint(SSL_CLIENT_HELLO *ssl) +get_fingerprint(const SSL_CLIENT_HELLO *ssl) #else get_fingerprint(SSL *ssl) #endif @@ -251,7 +251,7 @@ log_fingerprint(JA4_data const *data) std::uint16_t #ifdef OPENSSL_IS_BORINGSSL -get_version(SSL_CLIENT_HELLO *client_hello) +get_version(const SSL_CLIENT_HELLO *client_hello) #else get_version(SSL *ssl) #endif @@ -288,7 +288,7 @@ get_version(SSL *ssl) std::string #ifdef OPENSSL_IS_BORINGSSL -get_first_ALPN(SSL_CLIENT_HELLO *client_hello) +get_first_ALPN(const SSL_CLIENT_HELLO *client_hello) #else get_first_ALPN(SSL *ssl) #endif @@ -313,7 +313,7 @@ get_first_ALPN(SSL *ssl) #ifdef OPENSSL_IS_BORINGSSL void -add_ciphers(JA4::TLSClientHelloSummary &summary, SSL_CLIENT_HELLO *client_hello) +add_ciphers(JA4::TLSClientHelloSummary &summary, const SSL_CLIENT_HELLO *client_hello) { const uint8_t *buf = client_hello->cipher_suites; size_t buflen = client_hello->cipher_suites_len; @@ -336,7 +336,7 @@ add_ciphers(JA4::TLSClientHelloSummary &summary, SSL *ssl) #ifdef OPENSSL_IS_BORINGSSL void -add_extensions(JA4::TLSClientHelloSummary &summary, SSL_CLIENT_HELLO *client_hello) +add_extensions(JA4::TLSClientHelloSummary &summary, const SSL_CLIENT_HELLO *client_hello) { const uint8_t *ext = client_hello->extensions; size_t remaining = client_hello->extensions_len; diff --git a/src/api/InkAPI.cc b/src/api/InkAPI.cc index b63b7e7c2b9..f703e3607ab 100644 --- a/src/api/InkAPI.cc +++ b/src/api/InkAPI.cc @@ -7900,8 +7900,7 @@ TSVConnClientHelloGet(TSVConn sslp) if (auto snis = netvc->get_service(); snis) { ClientHelloContainer client_hello = snis->get_client_hello_container(); - // Cast the pointer value directly (no const_cast needed if types match) - return reinterpret_cast(const_cast(static_cast(client_hello))); + return reinterpret_cast(client_hello); } return nullptr; From 9765eb838b056b77b1907220e7561e0ec880ecf7 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Wed, 7 Jan 2026 12:39:50 +1100 Subject: [PATCH 05/21] spaces cleanup --- cmake/ExperimentalPlugins.cmake | 6 +----- plugins/experimental/ja4_fingerprint/README.md | 2 ++ plugins/experimental/ja4_fingerprint/plugin.cc | 1 - src/iocore/net/SSLUtils.cc | 1 - src/iocore/net/TLSSNISupport.cc | 1 - 5 files changed, 3 insertions(+), 8 deletions(-) diff --git a/cmake/ExperimentalPlugins.cmake b/cmake/ExperimentalPlugins.cmake index 74347efde92..c546e7ebb5f 100644 --- a/cmake/ExperimentalPlugins.cmake +++ b/cmake/ExperimentalPlugins.cmake @@ -41,11 +41,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/plugins/experimental/ja4_fingerprint/README.md b/plugins/experimental/ja4_fingerprint/README.md index d45ddf00785..b1b4dd55c7d 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. +Ja4 now supports boringssl + ## 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 058eefd98fe..f0ef4aaa5ff 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -191,7 +191,6 @@ handle_client_hello(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata) // The VCONN_CLOSE handler is now responsible for freeing the resource. TSUserArgSet(ssl_vc, *get_user_arg_index(), static_cast(data.release())); } - TSVConnReenable(ssl_vc); return TS_SUCCESS; } diff --git a/src/iocore/net/SSLUtils.cc b/src/iocore/net/SSLUtils.cc index abebf2b9565..ede8f20543f 100644 --- a/src/iocore/net/SSLUtils.cc +++ b/src/iocore/net/SSLUtils.cc @@ -302,7 +302,6 @@ ssl_client_hello_callback(const SSL_CLIENT_HELLO *client_hello) { SSL *s = client_hello->ssl; TLSSNISupport::ClientHello ch = {client_hello}; - #endif TLSSNISupport *snis = TLSSNISupport::getInstance(s); diff --git a/src/iocore/net/TLSSNISupport.cc b/src/iocore/net/TLSSNISupport.cc index 3f5574eb9ee..b4ced5d632a 100644 --- a/src/iocore/net/TLSSNISupport.cc +++ b/src/iocore/net/TLSSNISupport.cc @@ -117,7 +117,6 @@ TLSSNISupport::on_client_hello(ClientHello &client_hello) const char *servername = nullptr; const unsigned char *p; size_t remaining, len; - // Parse the server name if the get extension call succeeds and there are more than 2 bytes to parse if (client_hello.getExtension(TLSEXT_TYPE_server_name, &p, &remaining) && remaining > 2) { // Parse to get to the name, originally from test/handshake_helper.c in openssl tree From 2bc38d078ea56105161be70a8de5ad0d4668913a Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Wed, 7 Jan 2026 15:37:16 +1100 Subject: [PATCH 06/21] cleanup code --- include/ts/ts.h | 10 +- .../experimental/ja4_fingerprint/plugin.cc | 121 ++++++------------ src/api/InkAPI.cc | 24 ++++ 3 files changed, 71 insertions(+), 84 deletions(-) diff --git a/include/ts/ts.h b/include/ts/ts.h index 892d58a0c6f..c2b42435679 100644 --- a/include/ts/ts.h +++ b/include/ts/ts.h @@ -1334,11 +1334,11 @@ int TSVConnIsSsl(TSVConn sslp); int TSVConnProvidedSslCert(TSVConn sslp); const char *TSVConnSslSniGet(TSVConn sslp, int *length); TSClientHello TSVConnClientHelloGet(TSVConn sslp); - -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); -TSReturnCode TSSslSessionRemove(const TSSslSessionID *session_id); +TSReturnCode TSVConnClientHelloExtGet(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); +TSReturnCode TSSslSessionRemove(const TSSslSessionID *session_id); /* -------------------------------------------------------------------------- HTTP transactions */ diff --git a/plugins/experimental/ja4_fingerprint/plugin.cc b/plugins/experimental/ja4_fingerprint/plugin.cc index f0ef4aaa5ff..ac37757b28b 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -54,22 +54,14 @@ static void register_hooks(); static int handle_client_hello(TSCont cont, TSEvent event, void *edata); char *get_IP(sockaddr const *s_sockaddr, char res[INET6_ADDRSTRLEN]); static void log_fingerprint(JA4_data const *data); -#ifdef OPENSSL_IS_BORINGSSL -static std::string get_fingerprint(const SSL_CLIENT_HELLO *ssl); -static std::uint16_t get_version(const SSL_CLIENT_HELLO *ssl); -static std::string get_first_ALPN(const SSL_CLIENT_HELLO *ssl); -static void add_ciphers(JA4::TLSClientHelloSummary &summary, const SSL_CLIENT_HELLO *ssl); -static void add_extensions(JA4::TLSClientHelloSummary &summary, const SSL_CLIENT_HELLO *ssl); -#else -static std::string get_fingerprint(SSL *ssl); -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); -#endif -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); +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); static void append_to_field(TSMBuffer bufp, TSMLoc hdr_loc, char const *field, int field_len, char const *value, int value_len); static int handle_vconn_close(TSCont cont, TSEvent event, void *edata); @@ -83,7 +75,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}; @@ -175,17 +166,16 @@ handle_client_hello(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata) TSVConn const ssl_vc{static_cast(edata)}; #ifdef OPENSSL_IS_BORINGSSL - TSClientHello ssl_client_hello = TSVConnClientHelloGet(ssl_vc); - const SSL_CLIENT_HELLO *ssl = reinterpret_cast(ssl_client_hello); + TSClientHello ch = TSVConnClientHelloGet(ssl_vc); #else TSSslConnection const ssl_conn{TSVConnSslConnectionGet(ssl_vc)}; - SSL *ssl = reinterpret_cast(ssl_conn); + TSClientHello ch = reinterpret_cast(ssl_conn); #endif - if (nullptr == ssl) { - Dbg(dbg_ctl, "Could not get SSL object."); + if (nullptr == ch) { + Dbg(dbg_ctl, "Could not get TSClientHello object."); } else { auto data{std::make_unique()}; - data->fingerprint = get_fingerprint(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. @@ -196,18 +186,14 @@ handle_client_hello(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata) } std::string -#ifdef OPENSSL_IS_BORINGSSL -get_fingerprint(const SSL_CLIENT_HELLO *ssl) -#else -get_fingerprint(SSL *ssl) -#endif +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; } @@ -249,23 +235,11 @@ log_fingerprint(JA4_data const *data) } std::uint16_t -#ifdef OPENSSL_IS_BORINGSSL -get_version(const SSL_CLIENT_HELLO *client_hello) -#else -get_version(SSL *ssl) -#endif +get_version(TSClientHello ch) { unsigned char const *buf{}; std::size_t buflen{}; -#ifdef OPENSSL_IS_BORINGSSL - // If no extensions, fall back to legacy version field - if (!client_hello->extensions || client_hello->extensions_len == 0) { - return client_hello->version; - } - if (SSL_SUCCESS == SSL_early_callback_ctx_extension_get(client_hello, EXT_SUPPORTED_VERSIONS, &buf, &buflen)) { -#else - if (SSL_SUCCESS == SSL_client_hello_get0_ext(ssl, EXT_SUPPORTED_VERSIONS, &buf, &buflen)) { -#endif + if (TS_SUCCESS == TSVConnClientHelloExtGet(ch, EXT_SUPPORTED_VERSIONS, &buf, &buflen)) { std::uint16_t max_version{0}; uint8_t list_len = buf[0]; for (size_t i = 1; i + 1 < buflen && i < list_len + 1; i += 2) { @@ -278,28 +252,20 @@ get_version(SSL *ssl) } else { Dbg(dbg_ctl, "No supported_versions extension... using legacy version."); #ifdef OPENSSL_IS_BORINGSSL - return client_hello->version; + return reinterpret_cast(ch)->version; #else - return SSL_client_hello_get0_legacy_version(ssl); + return SSL_client_hello_get0_legacy_version(reinterpret_cast(ch)); #endif } } std::string -#ifdef OPENSSL_IS_BORINGSSL -get_first_ALPN(const SSL_CLIENT_HELLO *client_hello) -#else -get_first_ALPN(SSL *ssl) -#endif +get_first_ALPN(TSClientHello ch) { unsigned char const *buf{}; std::size_t buflen{}; std::string result{""}; -#ifdef OPENSSL_IS_BORINGSSL - if (SSL_SUCCESS == SSL_early_callback_ctx_extension_get(client_hello, EXT_ALPN, &buf, &buflen)) { -#else - if (SSL_SUCCESS == SSL_client_hello_get0_ext(ssl, EXT_ALPN, &buf, &buflen)) { -#endif + if (TS_SUCCESS == TSVConnClientHelloExtGet(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); @@ -310,35 +276,36 @@ get_first_ALPN(SSL *ssl) return result; } -#ifdef OPENSSL_IS_BORINGSSL void -add_ciphers(JA4::TLSClientHelloSummary &summary, const SSL_CLIENT_HELLO *client_hello) +add_ciphers(JA4::TLSClientHelloSummary &summary, TSClientHello ch) { - const uint8_t *buf = client_hello->cipher_suites; - size_t buflen = client_hello->cipher_suites_len; +#ifdef OPENSSL_IS_BORINGSSL + const SSL_CLIENT_HELLO *client_hello = reinterpret_cast(ch); + const uint8_t *buf = client_hello->cipher_suites; + size_t buflen = client_hello->cipher_suites_len; #else -void -add_ciphers(JA4::TLSClientHelloSummary &summary, SSL *ssl) -{ - unsigned char const *buf{}; - std::size_t buflen{SSL_client_hello_get0_ciphers(ssl, &buf)}; + unsigned char const *buf = nullptr; + // Fix: Add const_cast to remove const from ch + SSL *ssl = const_cast(reinterpret_cast(ch)); + std::size_t buflen = SSL_client_hello_get0_ciphers(ssl, &buf); #endif 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."); } } -#ifdef OPENSSL_IS_BORINGSSL void -add_extensions(JA4::TLSClientHelloSummary &summary, const SSL_CLIENT_HELLO *client_hello) +add_extensions(JA4::TLSClientHelloSummary &summary, TSClientHello ch) { - const uint8_t *ext = client_hello->extensions; - size_t remaining = client_hello->extensions_len; +#ifdef OPENSSL_IS_BORINGSSL + const SSL_CLIENT_HELLO *client_hello = reinterpret_cast(ch); + const uint8_t *ext = client_hello->extensions; + size_t remaining = client_hello->extensions_len; while (remaining >= 4) { uint16_t ext_type = (ext[0] << 8) | ext[1]; @@ -354,21 +321,17 @@ add_extensions(JA4::TLSClientHelloSummary &summary, const SSL_CLIENT_HELLO *clie ext += total_ext_size; remaining -= total_ext_size; } -} #else -void -add_extensions(JA4::TLSClientHelloSummary &summary, SSL *ssl) -{ int *buf{}; std::size_t buflen{}; - if (SSL_SUCCESS == SSL_client_hello_get1_extensions_present(ssl, &buf, &buflen)) { + if (1 == SSL_client_hello_get1_extensions_present(reinterpret_cast(ch), &buf, &buflen)) { for (std::size_t i{1}; i < buflen; i += 2) { summary.add_extension(make_word(buf[i], buf[i - 1])); } } OPENSSL_free(buf); -} #endif +} std::string hash_with_SHA256(std::string_view sv) diff --git a/src/api/InkAPI.cc b/src/api/InkAPI.cc index f703e3607ab..c5e15145265 100644 --- a/src/api/InkAPI.cc +++ b/src/api/InkAPI.cc @@ -7906,6 +7906,30 @@ TSVConnClientHelloGet(TSVConn sslp) return nullptr; } +TSReturnCode +TSVConnClientHelloExtGet(TSClientHello ch, unsigned int type, const unsigned char **out, size_t *outlen) +{ + TSReturnCode retval = TS_SUCCESS; + + if (ch == nullptr) { + return TS_ERROR; + } + +#ifdef OPENSSL_IS_BORINGSSL + const SSL_CLIENT_HELLO *client_hello = reinterpret_cast(ch); + if (SSL_early_callback_ctx_extension_get(client_hello, type, out, outlen) == 1) { + return TS_SUCCESS; + } +#else + SSL *ssl = const_cast(reinterpret_cast(ch)); + if (SSL_client_hello_get0_ext(ssl, type, out, outlen) == 1) { + return TS_SUCCESS; + } +#endif + + return retval; +} + TSSslVerifyCTX TSVConnSslVerifyCTXGet(TSVConn sslp) { From fd0380651f9a69c53ec6c1016e11e128d9ebad37 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Thu, 8 Jan 2026 11:41:56 +1100 Subject: [PATCH 07/21] more cleanup --- include/ts/apidefs.h.in | 2 +- .../experimental/ja4_fingerprint/plugin.cc | 34 ++++++++++++++++--- src/api/InkAPI.cc | 26 ++------------ 3 files changed, 33 insertions(+), 29 deletions(-) diff --git a/include/ts/apidefs.h.in b/include/ts/apidefs.h.in index 32f7221a477..aac11c2f68d 100644 --- a/include/ts/apidefs.h.in +++ b/include/ts/apidefs.h.in @@ -1080,7 +1080,7 @@ using TSHostLookupResult = struct tsapi_hostlookupresult *; using TSAIOCallback = struct tsapi_aiocallback *; using TSAcceptor = struct tsapi_net_accept *; using TSRemapPluginInfo = struct tsapi_remap_plugin_info *; -using TSClientHello = const void *; +using TSClientHello = void *; using TSFetchSM = struct tsapi_fetchsm *; diff --git a/plugins/experimental/ja4_fingerprint/plugin.cc b/plugins/experimental/ja4_fingerprint/plugin.cc index ac37757b28b..3c5d0efaca1 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -64,6 +64,7 @@ static int handle_read_request_hdr(TSCont cont, TSEvent event, vo static void append_JA4_headers(TSCont cont, TSHttpTxn txnp, std::string const *fingerprint); static void append_to_field(TSMBuffer bufp, TSMLoc hdr_loc, char const *field, int field_len, char const *value, int value_len); static int handle_vconn_close(TSCont cont, TSEvent event, void *edata); +int client_hello_ext_get(TSClientHello ch, unsigned int type, const unsigned char **out, size_t *outlen); namespace { @@ -171,6 +172,7 @@ handle_client_hello(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata) TSSslConnection const ssl_conn{TSVConnSslConnectionGet(ssl_vc)}; TSClientHello ch = reinterpret_cast(ssl_conn); #endif + if (nullptr == ch) { Dbg(dbg_ctl, "Could not get TSClientHello object."); } else { @@ -239,7 +241,7 @@ get_version(TSClientHello ch) { unsigned char const *buf{}; std::size_t buflen{}; - if (TS_SUCCESS == TSVConnClientHelloExtGet(ch, EXT_SUPPORTED_VERSIONS, &buf, &buflen)) { + if (TS_SUCCESS == client_hello_ext_get(ch, EXT_SUPPORTED_VERSIONS, &buf, &buflen)) { std::uint16_t max_version{0}; uint8_t list_len = buf[0]; for (size_t i = 1; i + 1 < buflen && i < list_len + 1; i += 2) { @@ -265,7 +267,7 @@ get_first_ALPN(TSClientHello ch) unsigned char const *buf{}; std::size_t buflen{}; std::string result{""}; - if (TS_SUCCESS == TSVConnClientHelloExtGet(ch, EXT_ALPN, &buf, &buflen)) { + if (TS_SUCCESS == client_hello_ext_get(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); @@ -286,7 +288,7 @@ add_ciphers(JA4::TLSClientHelloSummary &summary, TSClientHello ch) #else unsigned char const *buf = nullptr; // Fix: Add const_cast to remove const from ch - SSL *ssl = const_cast(reinterpret_cast(ch)); + SSL *ssl = const_cast(reinterpret_cast(ch)); std::size_t buflen = SSL_client_hello_get0_ciphers(ssl, &buf); #endif @@ -324,7 +326,7 @@ add_extensions(JA4::TLSClientHelloSummary &summary, TSClientHello ch) #else int *buf{}; std::size_t buflen{}; - if (1 == SSL_client_hello_get1_extensions_present(reinterpret_cast(ch), &buf, &buflen)) { + if (1 == SSL_client_hello_get1_extensions_present(const_cast(reinterpret_cast(ch)), &buf, &buflen)) { for (std::size_t i{1}; i < buflen; i += 2) { summary.add_extension(make_word(buf[i], buf[i - 1])); } @@ -436,3 +438,27 @@ handle_vconn_close(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata) TSVConnReenable(ssl_vc); return TS_SUCCESS; } + +int +client_hello_ext_get(TSClientHello ch, unsigned int type, const unsigned char **out, size_t *outlen) +{ + TSReturnCode retval = TS_SUCCESS; + + if (ch == nullptr) { + return TS_ERROR; + } + +#ifdef OPENSSL_IS_BORINGSSL + const SSL_CLIENT_HELLO *client_hello = reinterpret_cast(ch); + if (SSL_early_callback_ctx_extension_get(client_hello, type, out, outlen) == 1) { + return TS_SUCCESS; + } +#else + SSL *ssl = const_cast(reinterpret_cast(ch)); + if (SSL_client_hello_get0_ext(ssl, type, out, outlen) == 1) { + return TS_SUCCESS; + } +#endif + + return retval; +} diff --git a/src/api/InkAPI.cc b/src/api/InkAPI.cc index c5e15145265..b55ad7401df 100644 --- a/src/api/InkAPI.cc +++ b/src/api/InkAPI.cc @@ -7890,6 +7890,7 @@ TSVConnSslSniGet(TSVConn sslp, int *length) return server_name; } +#ifdef OPENSSL_IS_BORINGSSL TSClientHello TSVConnClientHelloGet(TSVConn sslp) { @@ -7900,36 +7901,13 @@ TSVConnClientHelloGet(TSVConn sslp) if (auto snis = netvc->get_service(); snis) { ClientHelloContainer client_hello = snis->get_client_hello_container(); - return reinterpret_cast(client_hello); + return reinterpret_cast(const_cast(client_hello)); } return nullptr; } - -TSReturnCode -TSVConnClientHelloExtGet(TSClientHello ch, unsigned int type, const unsigned char **out, size_t *outlen) -{ - TSReturnCode retval = TS_SUCCESS; - - if (ch == nullptr) { - return TS_ERROR; - } - -#ifdef OPENSSL_IS_BORINGSSL - const SSL_CLIENT_HELLO *client_hello = reinterpret_cast(ch); - if (SSL_early_callback_ctx_extension_get(client_hello, type, out, outlen) == 1) { - return TS_SUCCESS; - } -#else - SSL *ssl = const_cast(reinterpret_cast(ch)); - if (SSL_client_hello_get0_ext(ssl, type, out, outlen) == 1) { - return TS_SUCCESS; - } #endif - return retval; -} - TSSslVerifyCTX TSVConnSslVerifyCTXGet(TSVConn sslp) { From 9b76bd0d34574e7362476f169f3376fb79561675 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Thu, 8 Jan 2026 11:45:44 +1100 Subject: [PATCH 08/21] Update plugin.cc --- plugins/experimental/ja4_fingerprint/plugin.cc | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/experimental/ja4_fingerprint/plugin.cc b/plugins/experimental/ja4_fingerprint/plugin.cc index 3c5d0efaca1..b39dd8470e6 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -442,8 +442,6 @@ handle_vconn_close(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata) int client_hello_ext_get(TSClientHello ch, unsigned int type, const unsigned char **out, size_t *outlen) { - TSReturnCode retval = TS_SUCCESS; - if (ch == nullptr) { return TS_ERROR; } @@ -460,5 +458,5 @@ client_hello_ext_get(TSClientHello ch, unsigned int type, const unsigned char ** } #endif - return retval; + return TS_ERROR; } From 51ebe553ef8c821517239dab282859ddc37aa5f4 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Thu, 8 Jan 2026 12:07:00 +1100 Subject: [PATCH 09/21] Update plugin.cc --- plugins/experimental/ja4_fingerprint/plugin.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/experimental/ja4_fingerprint/plugin.cc b/plugins/experimental/ja4_fingerprint/plugin.cc index b39dd8470e6..2fe53d1d97b 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -243,7 +243,7 @@ get_version(TSClientHello ch) std::size_t buflen{}; if (TS_SUCCESS == client_hello_ext_get(ch, EXT_SUPPORTED_VERSIONS, &buf, &buflen)) { std::uint16_t max_version{0}; - uint8_t list_len = buf[0]; + size_t list_len = buf[0]; for (size_t i = 1; i + 1 < buflen && i < list_len + 1; i += 2) { std::uint16_t version = (buf[i] << 8) | buf[i + 1]; if (!JA4::is_GREASE(version) && version > max_version) { From b57f3384e1f61aa6f0fe8adce54d9a7173b0404d Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Thu, 8 Jan 2026 12:07:45 +1100 Subject: [PATCH 10/21] Update ts.h --- include/ts/ts.h | 1 - 1 file changed, 1 deletion(-) diff --git a/include/ts/ts.h b/include/ts/ts.h index c2b42435679..985f0693291 100644 --- a/include/ts/ts.h +++ b/include/ts/ts.h @@ -1334,7 +1334,6 @@ int TSVConnIsSsl(TSVConn sslp); int TSVConnProvidedSslCert(TSVConn sslp); const char *TSVConnSslSniGet(TSVConn sslp, int *length); TSClientHello TSVConnClientHelloGet(TSVConn sslp); -TSReturnCode TSVConnClientHelloExtGet(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); From 162af784db8be4f4117a8f66a9948a51279a1d46 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Thu, 8 Jan 2026 13:58:53 +1100 Subject: [PATCH 11/21] Update apidefs.h.in --- include/ts/apidefs.h.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/ts/apidefs.h.in b/include/ts/apidefs.h.in index aac11c2f68d..b6843e51153 100644 --- a/include/ts/apidefs.h.in +++ b/include/ts/apidefs.h.in @@ -1080,7 +1080,7 @@ using TSHostLookupResult = struct tsapi_hostlookupresult *; using TSAIOCallback = struct tsapi_aiocallback *; using TSAcceptor = struct tsapi_net_accept *; using TSRemapPluginInfo = struct tsapi_remap_plugin_info *; -using TSClientHello = void *; +using TSClientHello = struct tsapi_ssl_client_hello *; using TSFetchSM = struct tsapi_fetchsm *; From a42ae48cdc4d21c36067e6521c6fe5c179f4a17c Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Wed, 21 Jan 2026 10:08:03 +1100 Subject: [PATCH 12/21] Update to make more clean --- include/ts/apidefs.h.in | 14 +++- include/ts/ts.h | 10 ++- .../experimental/ja4_fingerprint/plugin.cc | 84 ++++++++----------- src/api/InkAPI.cc | 76 ++++++++++++++++- 4 files changed, 128 insertions(+), 56 deletions(-) diff --git a/include/ts/apidefs.h.in b/include/ts/apidefs.h.in index b6843e51153..4a254ced081 100644 --- a/include/ts/apidefs.h.in +++ b/include/ts/apidefs.h.in @@ -1080,7 +1080,19 @@ using TSHostLookupResult = struct tsapi_hostlookupresult *; using TSAIOCallback = struct tsapi_aiocallback *; using TSAcceptor = struct tsapi_net_accept *; using TSRemapPluginInfo = struct tsapi_remap_plugin_info *; -using TSClientHello = struct tsapi_ssl_client_hello *; + +struct tsapi_ssl_client_hello { + uint16_t version; + const uint8_t *cipher_suites; + size_t cipher_suites_len; + const uint8_t *extensions; + size_t extensions_len; + int *extension_ids; + size_t extension_ids_len; + void *ssl_ptr; +}; + +using TSClientHello = struct tsapi_ssl_client_hello *; using TSFetchSM = struct tsapi_fetchsm *; diff --git a/include/ts/ts.h b/include/ts/ts.h index 985f0693291..514470d9431 100644 --- a/include/ts/ts.h +++ b/include/ts/ts.h @@ -1334,10 +1334,12 @@ int TSVConnIsSsl(TSVConn sslp); int TSVConnProvidedSslCert(TSVConn sslp); const char *TSVConnSslSniGet(TSVConn sslp, int *length); TSClientHello TSVConnClientHelloGet(TSVConn sslp); -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); -TSReturnCode TSSslSessionRemove(const TSSslSessionID *session_id); +void TSClientHelloDestroy(TSClientHello ch); + +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); +TSReturnCode TSSslSessionRemove(const TSSslSessionID *session_id); /* -------------------------------------------------------------------------- HTTP transactions */ diff --git a/plugins/experimental/ja4_fingerprint/plugin.cc b/plugins/experimental/ja4_fingerprint/plugin.cc index 2fe53d1d97b..aa8c9266ba4 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -166,12 +166,7 @@ handle_client_hello(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata) TSVConn const ssl_vc{static_cast(edata)}; -#ifdef OPENSSL_IS_BORINGSSL TSClientHello ch = TSVConnClientHelloGet(ssl_vc); -#else - TSSslConnection const ssl_conn{TSVConnSslConnectionGet(ssl_vc)}; - TSClientHello ch = reinterpret_cast(ssl_conn); -#endif if (nullptr == ch) { Dbg(dbg_ctl, "Could not get TSClientHello object."); @@ -180,6 +175,8 @@ handle_client_hello(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata) data->fingerprint = get_fingerprint(ch); get_IP(TSNetVConnRemoteAddrGet(ssl_vc), data->IP_addr); log_fingerprint(data.get()); + // Clean up the TSClientHello structure + TSClientHelloDestroy(ch); // The VCONN_CLOSE handler is now responsible for freeing the resource. TSUserArgSet(ssl_vc, *get_user_arg_index(), static_cast(data.release())); } @@ -253,11 +250,7 @@ get_version(TSClientHello ch) return max_version; } else { Dbg(dbg_ctl, "No supported_versions extension... using legacy version."); -#ifdef OPENSSL_IS_BORINGSSL - return reinterpret_cast(ch)->version; -#else - return SSL_client_hello_get0_legacy_version(reinterpret_cast(ch)); -#endif + return ch->version; } } @@ -281,16 +274,8 @@ get_first_ALPN(TSClientHello ch) void add_ciphers(JA4::TLSClientHelloSummary &summary, TSClientHello ch) { -#ifdef OPENSSL_IS_BORINGSSL - const SSL_CLIENT_HELLO *client_hello = reinterpret_cast(ch); - const uint8_t *buf = client_hello->cipher_suites; - size_t buflen = client_hello->cipher_suites_len; -#else - unsigned char const *buf = nullptr; - // Fix: Add const_cast to remove const from ch - SSL *ssl = const_cast(reinterpret_cast(ch)); - std::size_t buflen = SSL_client_hello_get0_ciphers(ssl, &buf); -#endif + const uint8_t *buf = ch->cipher_suites; + size_t buflen = ch->cipher_suites_len; if (buflen > 0) { for (std::size_t i = 0; i + 1 < buflen; i += 2) { @@ -304,35 +289,28 @@ add_ciphers(JA4::TLSClientHelloSummary &summary, TSClientHello ch) void add_extensions(JA4::TLSClientHelloSummary &summary, TSClientHello ch) { -#ifdef OPENSSL_IS_BORINGSSL - const SSL_CLIENT_HELLO *client_hello = reinterpret_cast(ch); - const uint8_t *ext = client_hello->extensions; - size_t remaining = client_hello->extensions_len; - - while (remaining >= 4) { - uint16_t ext_type = (ext[0] << 8) | ext[1]; - - // Read extension length (2 bytes, big endian) - uint16_t ext_len = (ext[2] << 8) | ext[3]; - summary.add_extension(ext_type); - size_t total_ext_size = 4 + ext_len; // 4 bytes header + data - if (total_ext_size > remaining) { - break; // Malformed extension, stop parsing - } + if (ch->extensions != nullptr) { + const uint8_t *ext = ch->extensions; + size_t remaining = ch->extensions_len; + + while (remaining >= 4) { + uint16_t ext_type = (ext[0] << 8) | ext[1]; + uint16_t ext_len = (ext[2] << 8) | ext[3]; + summary.add_extension(ext_type); + size_t total_ext_size = 4 + ext_len; + if (total_ext_size > remaining) { + break; + } - ext += total_ext_size; - remaining -= total_ext_size; - } -#else - int *buf{}; - std::size_t buflen{}; - if (1 == SSL_client_hello_get1_extensions_present(const_cast(reinterpret_cast(ch)), &buf, &buflen)) { - for (std::size_t i{1}; i < buflen; i += 2) { - summary.add_extension(make_word(buf[i], buf[i - 1])); + ext += total_ext_size; + remaining -= total_ext_size; + } + } else if (ch->extension_ids != nullptr) { + // OpenSSL's extension_ids is an array of ints, each element is a complete extension ID + for (std::size_t i = 0; i < ch->extension_ids_len; i++) { + summary.add_extension(static_cast(ch->extension_ids[i])); } } - OPENSSL_free(buf); -#endif } std::string @@ -442,17 +420,25 @@ handle_vconn_close(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata) int client_hello_ext_get(TSClientHello ch, unsigned int type, const unsigned char **out, size_t *outlen) { - if (ch == nullptr) { + if (ch == nullptr || out == nullptr || outlen == nullptr) { return TS_ERROR; } #ifdef OPENSSL_IS_BORINGSSL - const SSL_CLIENT_HELLO *client_hello = reinterpret_cast(ch); + const SSL_CLIENT_HELLO *client_hello = static_cast(ch->ssl_ptr); + if (client_hello == nullptr) { + return TS_ERROR; + } + if (SSL_early_callback_ctx_extension_get(client_hello, type, out, outlen) == 1) { return TS_SUCCESS; } #else - SSL *ssl = const_cast(reinterpret_cast(ch)); + SSL *ssl = static_cast(ch->ssl_ptr); + if (ssl == nullptr) { + return TS_ERROR; + } + if (SSL_client_hello_get0_ext(ssl, type, out, outlen) == 1) { return TS_SUCCESS; } diff --git a/src/api/InkAPI.cc b/src/api/InkAPI.cc index b55ad7401df..2ec6b8be7ef 100644 --- a/src/api/InkAPI.cc +++ b/src/api/InkAPI.cc @@ -7890,7 +7890,6 @@ TSVConnSslSniGet(TSVConn sslp, int *length) return server_name; } -#ifdef OPENSSL_IS_BORINGSSL TSClientHello TSVConnClientHelloGet(TSVConn sslp) { @@ -7900,14 +7899,87 @@ TSVConnClientHelloGet(TSVConn sslp) } if (auto snis = netvc->get_service(); snis) { + // Allocate the TSClientHello structure + auto ch = new tsapi_ssl_client_hello(); + +#ifdef OPENSSL_IS_BORINGSSL + // Get the BoringSSL client hello container ClientHelloContainer client_hello = snis->get_client_hello_container(); - return reinterpret_cast(const_cast(client_hello)); + if (client_hello == nullptr) { + delete ch; + return nullptr; + } + + // Populate from BoringSSL SSL_CLIENT_HELLO structure + ch->version = client_hello->version; + ch->cipher_suites = client_hello->cipher_suites; + ch->cipher_suites_len = client_hello->cipher_suites_len; + ch->extensions = client_hello->extensions; + ch->extensions_len = client_hello->extensions_len; + ch->extension_ids = nullptr; + ch->extension_ids_len = 0; + ch->ssl_ptr = const_cast(client_hello); +#else + // Get the OpenSSL SSL* object + auto tbs = netvc->get_service(); + if (!tbs) { + delete ch; + return nullptr; + } + SSL *ssl = tbs->get_tls_handle(); + if (ssl == nullptr) { + delete ch; + return nullptr; + } + + // Get legacy version (OpenSSL doesn't expose the direct version field from client hello) + ch->version = SSL_client_hello_get0_legacy_version(ssl); + + // Get cipher suites + const unsigned char *cipher_buf = nullptr; + ch->cipher_suites_len = SSL_client_hello_get0_ciphers(ssl, &cipher_buf); + ch->cipher_suites = cipher_buf; + + // For OpenSSL, we can't get direct access to the raw extensions buffer + // Instead, get the list of extension IDs + ch->extensions = nullptr; + ch->extensions_len = 0; + int *ext_ids = nullptr; + size_t ext_count; + if (SSL_client_hello_get1_extensions_present(ssl, &ext_ids, &ext_count) == 1) { + ch->extension_ids = ext_ids; + ch->extension_ids_len = ext_count; + } else { + ch->extension_ids = nullptr; + ch->extension_ids_len = 0; + } + ch->ssl_ptr = ssl; +#endif + + return ch; } return nullptr; } + +void +TSClientHelloDestroy(TSClientHello ch) +{ + if (ch == nullptr) { + return; + } + +#ifndef OPENSSL_IS_BORINGSSL + // For OpenSSL, we need to free the extension IDs array that was allocated + // by SSL_client_hello_get1_extensions_present + if (ch->extension_ids != nullptr) { + OPENSSL_free(ch->extension_ids); + } #endif + delete ch; +} + TSSslVerifyCTX TSVConnSslVerifyCTXGet(TSVConn sslp) { From 19bbc064d7ebe2c6d8c533b29f3f9451f49b6ec7 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Wed, 28 Jan 2026 14:20:10 +1100 Subject: [PATCH 13/21] Update data --- include/ts/apidefs.h.in | 94 ++++++++++++++++--- include/ts/ts.h | 76 ++++++++++++++- .../experimental/ja4_fingerprint/plugin.cc | 57 +++-------- src/api/InkAPI.cc | 59 ++++++++---- 4 files changed, 207 insertions(+), 79 deletions(-) diff --git a/include/ts/apidefs.h.in b/include/ts/apidefs.h.in index 4a254ced081..d2c8f7d67d6 100644 --- a/include/ts/apidefs.h.in +++ b/include/ts/apidefs.h.in @@ -1044,6 +1044,86 @@ struct TSHttp2Priority { * or -1 if the stream has no dependency. */ int32_t stream_dependency; }; +/** + * A structure for SSL Client Hello data + */ +struct tsapi_ssl_client_hello { + uint16_t version{0}; + const uint8_t *cipher_suites{nullptr}; + size_t cipher_suites_len{0}; + const uint8_t *extensions{nullptr}; + size_t extensions_len{0}; + int *extension_ids{nullptr}; + size_t extension_ids_len{0}; + void *ssl_ptr{nullptr}; +}; + +// Wrapper class that provides controlled access to client hello data +class TSClientHelloImpl +{ +public: + TSClientHelloImpl(tsapi_ssl_client_hello *ch) : _ssl_client_hello(ch) {} + + ~TSClientHelloImpl() { delete _ssl_client_hello; } + + uint16_t + get_version() const + { + return _ssl_client_hello->version; + } + + const uint8_t * + get_cipher_suites() const + { + return _ssl_client_hello->cipher_suites; + } + + size_t + get_cipher_suites_len() const + { + return _ssl_client_hello->cipher_suites_len; + } + + const uint8_t * + get_extensions() const + { + return _ssl_client_hello->extensions; + } + + size_t + get_extensions_len() const + { + return _ssl_client_hello->extensions_len; + } + + const int * + get_extension_ids() const + { + return _ssl_client_hello->extension_ids; + } + + size_t + get_extension_ids_len() const + { + return _ssl_client_hello->extension_ids_len; + } + + void * + get_ssl_ptr() const + { + return _ssl_client_hello->ssl_ptr; + } + + // Internal accessor for API implementation + tsapi_ssl_client_hello * + _get_internal() const + { + return _ssl_client_hello; + } + +private: + tsapi_ssl_client_hello *_ssl_client_hello; +}; using TSFile = struct tsapi_file *; @@ -1080,19 +1160,7 @@ using TSHostLookupResult = struct tsapi_hostlookupresult *; using TSAIOCallback = struct tsapi_aiocallback *; using TSAcceptor = struct tsapi_net_accept *; using TSRemapPluginInfo = struct tsapi_remap_plugin_info *; - -struct tsapi_ssl_client_hello { - uint16_t version; - const uint8_t *cipher_suites; - size_t cipher_suites_len; - const uint8_t *extensions; - size_t extensions_len; - int *extension_ids; - size_t extension_ids_len; - void *ssl_ptr; -}; - -using TSClientHello = struct tsapi_ssl_client_hello *; +using TSClientHello = TSClientHelloImpl *; using TSFetchSM = struct tsapi_fetchsm *; diff --git a/include/ts/ts.h b/include/ts/ts.h index 514470d9431..519dbf16f79 100644 --- a/include/ts/ts.h +++ b/include/ts/ts.h @@ -1331,10 +1331,80 @@ TSReturnCode TSVConnProtocolEnable(TSVConn connp, const char *protocol_name); int TSVConnIsSsl(TSVConn sslp); /* Returns 1 if a certificate was provided in the TLS handshake, 0 otherwise. */ -int TSVConnProvidedSslCert(TSVConn sslp); -const char *TSVConnSslSniGet(TSVConn sslp, int *length); +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 nullptr being returned. + + For BoringSSL, the Client Hello data is copied from the SSL_CLIENT_HELLO + structure. For OpenSSL, cipher suites and extension IDs are extracted using + SSL_client_hello_get0_* functions. + + Memory Management: The caller must call TSClientHelloDestroy() to free the + returned object when it is no longer needed. Failure to do so will result + in memory leaks, especially for OpenSSL which allocates memory for the + extension IDs array. + + @param sslp The SSL virtual connection handle. Must not be nullptr. + @return Pointer to TSClientHello object containing Client Hello data, or + nullptr if the client hello is not available or if an error occurs. + + @see TSClientHelloDestroy + @see TSClientHelloExtensionGet + */ TSClientHello TSVConnClientHelloGet(TSVConn sslp); -void TSClientHelloDestroy(TSClientHello ch); +/** + Destroys a Client Hello object and frees associated memory. + + This function must be called to properly free a TSClientHello object + obtained from TSVConnClientHelloGet(). It handles SSL library-specific + cleanup, including freeing the extension IDs array allocated by OpenSSL's + SSL_client_hello_get1_extensions_present() function. + + @param ch The Client Hello object to destroy. + + @see TSVConnClientHelloGet + */ +void TSClientHelloDestroy(TSClientHello ch); + +/** + 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 as long as the + TSClientHello object has not been destroyed. + + @param ch The Client Hello object obtained from TSVConnClientHelloGet(). + Must not be nullptr. + @param type The TLS extension type to retrieve. + @param out Pointer to receive the extension data buffer. Must not be nullptr. + @param 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 + @see TSClientHelloDestroy + */ +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); diff --git a/plugins/experimental/ja4_fingerprint/plugin.cc b/plugins/experimental/ja4_fingerprint/plugin.cc index aa8c9266ba4..44eb09daadb 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -64,7 +64,6 @@ static int handle_read_request_hdr(TSCont cont, TSEvent event, vo static void append_JA4_headers(TSCont cont, TSHttpTxn txnp, std::string const *fingerprint); static void append_to_field(TSMBuffer bufp, TSMLoc hdr_loc, char const *field, int field_len, char const *value, int value_len); static int handle_vconn_close(TSCont cont, TSEvent event, void *edata); -int client_hello_ext_get(TSClientHello ch, unsigned int type, const unsigned char **out, size_t *outlen); namespace { @@ -238,7 +237,7 @@ get_version(TSClientHello ch) { unsigned char const *buf{}; std::size_t buflen{}; - if (TS_SUCCESS == client_hello_ext_get(ch, EXT_SUPPORTED_VERSIONS, &buf, &buflen)) { + if (TS_SUCCESS == TSClientHelloExtensionGet(ch, EXT_SUPPORTED_VERSIONS, &buf, &buflen)) { std::uint16_t max_version{0}; size_t list_len = buf[0]; for (size_t i = 1; i + 1 < buflen && i < list_len + 1; i += 2) { @@ -250,7 +249,7 @@ get_version(TSClientHello ch) return max_version; } else { Dbg(dbg_ctl, "No supported_versions extension... using legacy version."); - return ch->version; + return ch->get_version(); } } @@ -260,7 +259,7 @@ get_first_ALPN(TSClientHello ch) unsigned char const *buf{}; std::size_t buflen{}; std::string result{""}; - if (TS_SUCCESS == client_hello_ext_get(ch, 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); @@ -274,8 +273,8 @@ get_first_ALPN(TSClientHello ch) void add_ciphers(JA4::TLSClientHelloSummary &summary, TSClientHello ch) { - const uint8_t *buf = ch->cipher_suites; - size_t buflen = ch->cipher_suites_len; + 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) { @@ -289,9 +288,10 @@ add_ciphers(JA4::TLSClientHelloSummary &summary, TSClientHello ch) void add_extensions(JA4::TLSClientHelloSummary &summary, TSClientHello ch) { - if (ch->extensions != nullptr) { - const uint8_t *ext = ch->extensions; - size_t remaining = ch->extensions_len; + // For BoringSSL, we have direct access to the extensions buffer + if (ch->get_extensions() != nullptr) { + const uint8_t *ext = ch->get_extensions(); + size_t remaining = ch->get_extensions_len(); while (remaining >= 4) { uint16_t ext_type = (ext[0] << 8) | ext[1]; @@ -305,10 +305,12 @@ add_extensions(JA4::TLSClientHelloSummary &summary, TSClientHello ch) ext += total_ext_size; remaining -= total_ext_size; } - } else if (ch->extension_ids != nullptr) { + } + // For OpenSSL, we use the extension IDs array + else if (ch->get_extension_ids() != nullptr) { // OpenSSL's extension_ids is an array of ints, each element is a complete extension ID - for (std::size_t i = 0; i < ch->extension_ids_len; i++) { - summary.add_extension(static_cast(ch->extension_ids[i])); + for (std::size_t i = 0; i < ch->get_extension_ids_len(); i++) { + summary.add_extension(static_cast(ch->get_extension_ids()[i])); } } } @@ -409,40 +411,9 @@ 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(edata)}; delete static_cast(TSUserArgGet(ssl_vc, *get_user_arg_index())); TSUserArgSet(ssl_vc, *get_user_arg_index(), nullptr); TSVConnReenable(ssl_vc); return TS_SUCCESS; } - -int -client_hello_ext_get(TSClientHello ch, unsigned int type, const unsigned char **out, size_t *outlen) -{ - if (ch == nullptr || out == nullptr || outlen == nullptr) { - return TS_ERROR; - } - -#ifdef OPENSSL_IS_BORINGSSL - const SSL_CLIENT_HELLO *client_hello = static_cast(ch->ssl_ptr); - if (client_hello == nullptr) { - return TS_ERROR; - } - - if (SSL_early_callback_ctx_extension_get(client_hello, type, out, outlen) == 1) { - return TS_SUCCESS; - } -#else - SSL *ssl = static_cast(ch->ssl_ptr); - if (ssl == nullptr) { - return TS_ERROR; - } - - if (SSL_client_hello_get0_ext(ssl, type, out, outlen) == 1) { - return TS_SUCCESS; - } -#endif - - return TS_ERROR; -} diff --git a/src/api/InkAPI.cc b/src/api/InkAPI.cc index 2ec6b8be7ef..4beb642b066 100644 --- a/src/api/InkAPI.cc +++ b/src/api/InkAPI.cc @@ -7899,7 +7899,6 @@ TSVConnClientHelloGet(TSVConn sslp) } if (auto snis = netvc->get_service(); snis) { - // Allocate the TSClientHello structure auto ch = new tsapi_ssl_client_hello(); #ifdef OPENSSL_IS_BORINGSSL @@ -7916,8 +7915,6 @@ TSVConnClientHelloGet(TSVConn sslp) ch->cipher_suites_len = client_hello->cipher_suites_len; ch->extensions = client_hello->extensions; ch->extensions_len = client_hello->extensions_len; - ch->extension_ids = nullptr; - ch->extension_ids_len = 0; ch->ssl_ptr = const_cast(client_hello); #else // Get the OpenSSL SSL* object @@ -7936,27 +7933,24 @@ TSVConnClientHelloGet(TSVConn sslp) ch->version = SSL_client_hello_get0_legacy_version(ssl); // Get cipher suites - const unsigned char *cipher_buf = nullptr; - ch->cipher_suites_len = SSL_client_hello_get0_ciphers(ssl, &cipher_buf); - ch->cipher_suites = cipher_buf; + const unsigned char *cipher_buf = nullptr; + size_t cipher_buf_len = SSL_client_hello_get0_ciphers(ssl, &cipher_buf); + ch->cipher_suites = cipher_buf; + ch->cipher_suites_len = cipher_buf_len; // For OpenSSL, we can't get direct access to the raw extensions buffer // Instead, get the list of extension IDs - ch->extensions = nullptr; - ch->extensions_len = 0; - int *ext_ids = nullptr; + int *ext_ids = nullptr; size_t ext_count; if (SSL_client_hello_get1_extensions_present(ssl, &ext_ids, &ext_count) == 1) { ch->extension_ids = ext_ids; ch->extension_ids_len = ext_count; - } else { - ch->extension_ids = nullptr; - ch->extension_ids_len = 0; } ch->ssl_ptr = ssl; #endif - return ch; + // Wrap the POD structure in the wrapper class and return + return new TSClientHelloImpl(ch); } return nullptr; @@ -7965,21 +7959,46 @@ TSVConnClientHelloGet(TSVConn sslp) void TSClientHelloDestroy(TSClientHello ch) { - if (ch == nullptr) { - return; - } - #ifndef OPENSSL_IS_BORINGSSL // For OpenSSL, we need to free the extension IDs array that was allocated // by SSL_client_hello_get1_extensions_present - if (ch->extension_ids != nullptr) { - OPENSSL_free(ch->extension_ids); + if (ch->get_extension_ids() != nullptr) { + OPENSSL_free(const_cast(ch->get_extension_ids())); } #endif - delete ch; } +TSReturnCode +TSClientHelloExtensionGet(TSClientHello ch, unsigned int type, const unsigned char **out, size_t *outlen) +{ + if (ch == nullptr || out == nullptr || outlen == nullptr) { + return TS_ERROR; + } + +#ifdef OPENSSL_IS_BORINGSSL + const SSL_CLIENT_HELLO *client_hello = static_cast(ch->get_ssl_ptr()); + if (client_hello == nullptr) { + return TS_ERROR; + } + + if (SSL_early_callback_ctx_extension_get(client_hello, type, out, outlen) == 1) { + return TS_SUCCESS; + } +#else + SSL *ssl = static_cast(ch->get_ssl_ptr()); + if (ssl == nullptr) { + return TS_ERROR; + } + + if (SSL_client_hello_get0_ext(ssl, type, out, outlen) == 1) { + return TS_SUCCESS; + } +#endif + + return TS_ERROR; +} + TSSslVerifyCTX TSVConnSslVerifyCTXGet(TSVConn sslp) { From 4dc5af82ee14a2e57c762afc3d873798f586e770 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Thu, 29 Jan 2026 11:27:01 +1100 Subject: [PATCH 14/21] address comments --- doc/admin-guide/plugins/index.en.rst | 4 + .../plugins/ja4_fingerprint.en.rst | 209 ++++++++++++++++++ include/ts/apidefs.h.in | 46 +++- .../experimental/ja4_fingerprint/plugin.cc | 26 +-- src/api/InkAPI.cc | 7 +- 5 files changed, 259 insertions(+), 33 deletions(-) create mode 100644 doc/admin-guide/plugins/ja4_fingerprint.en.rst diff --git a/doc/admin-guide/plugins/index.en.rst b/doc/admin-guide/plugins/index.en.rst index 8de06d97242..251647a40b2 100644 --- a/doc/admin-guide/plugins/index.en.rst +++ b/doc/admin-guide/plugins/index.en.rst @@ -177,6 +177,7 @@ directory of the |TS| source tree. Experimental plugins can be compiled by passi Header Frequency Hook Trace ICAP + JA4 Fingerprint Maxmind ACL Memcache Memory Profile @@ -228,6 +229,9 @@ directory of the |TS| source tree. Experimental plugins can be compiled by passi :doc:`ICAP ` Pass response data to external server for further processing using the ICAP protocol. +:doc:`JA4 Fingerprint ` + Calculates JA4 Fingerprints for incoming TLS traffic. + :doc:`MaxMind ACL ` 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 00000000000..b31bd85a1bd --- /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 :file:`ja4_fingerprint.log` in the Traffic Server log +directory (typically ``/var/log/trafficserver/``). + +**Log Format**:: + + [timestamp] Client IP: JA4: + +**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/include/ts/apidefs.h.in b/include/ts/apidefs.h.in index d2c8f7d67d6..44a7395c6f4 100644 --- a/include/ts/apidefs.h.in +++ b/include/ts/apidefs.h.in @@ -43,6 +43,8 @@ */ #include +#include +#include #include #include #include @@ -1062,9 +1064,9 @@ struct tsapi_ssl_client_hello { class TSClientHelloImpl { public: - TSClientHelloImpl(tsapi_ssl_client_hello *ch) : _ssl_client_hello(ch) {} + TSClientHelloImpl(std::unique_ptr ch) : _ssl_client_hello(std::move(ch)) {} - ~TSClientHelloImpl() { delete _ssl_client_hello; } + ~TSClientHelloImpl() = default; uint16_t get_version() const @@ -1114,15 +1116,51 @@ public: return _ssl_client_hello->ssl_ptr; } + // Returns a vector of extension type IDs + // This abstracts the difference between BoringSSL (extensions buffer) and OpenSSL (extension_ids array) + std::vector + get_extension_types() const + { + std::vector result; + + // For BoringSSL, parse the extensions buffer + if (_ssl_client_hello->extensions != nullptr) { + const uint8_t *ext = _ssl_client_hello->extensions; + size_t remaining = _ssl_client_hello->extensions_len; + + while (remaining >= 4) { + uint16_t ext_type = (ext[0] << 8) | ext[1]; + uint16_t ext_len = (ext[2] << 8) | ext[3]; + size_t total_ext_size = 4 + ext_len; + + result.push_back(ext_type); + + if (total_ext_size > remaining) { + break; + } + ext += total_ext_size; + remaining -= total_ext_size; + } + } + // For OpenSSL, use the extension IDs array + else if (_ssl_client_hello->extension_ids != nullptr) { + for (size_t i = 0; i < _ssl_client_hello->extension_ids_len; i++) { + result.push_back(static_cast(_ssl_client_hello->extension_ids[i])); + } + } + + return result; + } + // Internal accessor for API implementation tsapi_ssl_client_hello * _get_internal() const { - return _ssl_client_hello; + return _ssl_client_hello.get(); } private: - tsapi_ssl_client_hello *_ssl_client_hello; + std::unique_ptr _ssl_client_hello; }; using TSFile = struct tsapi_file *; diff --git a/plugins/experimental/ja4_fingerprint/plugin.cc b/plugins/experimental/ja4_fingerprint/plugin.cc index 44eb09daadb..5ddc4cbefdd 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -288,30 +288,8 @@ add_ciphers(JA4::TLSClientHelloSummary &summary, TSClientHello ch) void add_extensions(JA4::TLSClientHelloSummary &summary, TSClientHello ch) { - // For BoringSSL, we have direct access to the extensions buffer - if (ch->get_extensions() != nullptr) { - const uint8_t *ext = ch->get_extensions(); - size_t remaining = ch->get_extensions_len(); - - while (remaining >= 4) { - uint16_t ext_type = (ext[0] << 8) | ext[1]; - uint16_t ext_len = (ext[2] << 8) | ext[3]; - summary.add_extension(ext_type); - size_t total_ext_size = 4 + ext_len; - if (total_ext_size > remaining) { - break; - } - - ext += total_ext_size; - remaining -= total_ext_size; - } - } - // For OpenSSL, we use the extension IDs array - else if (ch->get_extension_ids() != nullptr) { - // OpenSSL's extension_ids is an array of ints, each element is a complete extension ID - for (std::size_t i = 0; i < ch->get_extension_ids_len(); i++) { - summary.add_extension(static_cast(ch->get_extension_ids()[i])); - } + for (auto ext_type : ch->get_extension_types()) { + summary.add_extension(ext_type); } } diff --git a/src/api/InkAPI.cc b/src/api/InkAPI.cc index 4beb642b066..c3e05227585 100644 --- a/src/api/InkAPI.cc +++ b/src/api/InkAPI.cc @@ -7899,13 +7899,12 @@ TSVConnClientHelloGet(TSVConn sslp) } if (auto snis = netvc->get_service(); snis) { - auto ch = new tsapi_ssl_client_hello(); + auto ch = std::make_unique(); #ifdef OPENSSL_IS_BORINGSSL // Get the BoringSSL client hello container ClientHelloContainer client_hello = snis->get_client_hello_container(); if (client_hello == nullptr) { - delete ch; return nullptr; } @@ -7920,12 +7919,10 @@ TSVConnClientHelloGet(TSVConn sslp) // Get the OpenSSL SSL* object auto tbs = netvc->get_service(); if (!tbs) { - delete ch; return nullptr; } SSL *ssl = tbs->get_tls_handle(); if (ssl == nullptr) { - delete ch; return nullptr; } @@ -7950,7 +7947,7 @@ TSVConnClientHelloGet(TSVConn sslp) #endif // Wrap the POD structure in the wrapper class and return - return new TSClientHelloImpl(ch); + return new TSClientHelloImpl(std::move(ch)); } return nullptr; From 4a51fac21f418e492209db0ccb14b5771e727b08 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Thu, 29 Jan 2026 11:40:52 +1100 Subject: [PATCH 15/21] Update ja4_fingerprint.en.rst --- doc/admin-guide/plugins/ja4_fingerprint.en.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/admin-guide/plugins/ja4_fingerprint.en.rst b/doc/admin-guide/plugins/ja4_fingerprint.en.rst index b31bd85a1bd..b8e5e5e37ad 100644 --- a/doc/admin-guide/plugins/ja4_fingerprint.en.rst +++ b/doc/admin-guide/plugins/ja4_fingerprint.en.rst @@ -109,7 +109,7 @@ When loaded, the plugin will: Log Output ========== -The plugin writes to :file:`ja4_fingerprint.log` in the Traffic Server log +The plugin writes to ``ja4_fingerprint.log`` in the Traffic Server log directory (typically ``/var/log/trafficserver/``). **Log Format**:: From b2868d9f3bd61ee52db422c2a65d3283766aa585 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Thu, 29 Jan 2026 14:13:11 +1100 Subject: [PATCH 16/21] Add docs --- .../functions/TSVConnClientHelloGet.en.rst | 59 +++++++++++++++++++ .../api/types/TSClientHello.en.rst | 43 ++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 doc/developer-guide/api/functions/TSVConnClientHelloGet.en.rst create mode 100644 doc/developer-guide/api/types/TSClientHello.en.rst 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 00000000000..cafa50d199f --- /dev/null +++ b/doc/developer-guide/api/functions/TSVConnClientHelloGet.en.rst @@ -0,0 +1,59 @@ +.. 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 + +.. function:: TSClientHello TSVConnClientHelloGet(TSVConn sslp) +.. function:: void TSClientHelloDestroy(TSClientHello ch) +.. 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`. This function is typically called from the +:enumerator:`TS_EVENT_SSL_CLIENT_HELLO` hook. Returns ``nullptr`` if +:arg:`sslp` is invalid or not a TLS connection. + +The caller must call :func:`TSClientHelloDestroy` to free the returned object. + +:func:`TSClientHelloDestroy` frees the :type:`TSClientHello` object :arg:`ch`. + +: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. + +Types +===== + +.. type:: TSClientHello + + Opaque type representing a TLS ClientHello message. This is an opaque handle + that provides access to ClientHello data via accessor methods. + 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 00000000000..e3d2f4fcf4f --- /dev/null +++ b/doc/developer-guide/api/types/TSClientHello.en.rst @@ -0,0 +1,43 @@ +.. 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 + +.. type:: TSClientHello + + +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. + +Objects of this type are obtained via :func:`TSVConnClientHelloGet` and must +be freed using :func:`TSClientHelloDestroy`. The implementation abstracts +differences between OpenSSL and BoringSSL to provide a consistent interface. From b9e13514d7c4584a709d16440b260a55e252ea58 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Thu, 29 Jan 2026 14:19:10 +1100 Subject: [PATCH 17/21] Update TSVConnClientHelloGet.en.rst --- doc/developer-guide/api/functions/TSVConnClientHelloGet.en.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/developer-guide/api/functions/TSVConnClientHelloGet.en.rst b/doc/developer-guide/api/functions/TSVConnClientHelloGet.en.rst index cafa50d199f..0837482dfa0 100644 --- a/doc/developer-guide/api/functions/TSVConnClientHelloGet.en.rst +++ b/doc/developer-guide/api/functions/TSVConnClientHelloGet.en.rst @@ -37,7 +37,7 @@ Description :func:`TSVConnClientHelloGet` retrieves ClientHello message data from the TLS virtual connection :arg:`sslp`. This function is typically called from the -:enumerator:`TS_EVENT_SSL_CLIENT_HELLO` hook. Returns ``nullptr`` if +``TS_EVENT_SSL_CLIENT_HELLO`` hook. Returns ``nullptr`` if :arg:`sslp` is invalid or not a TLS connection. The caller must call :func:`TSClientHelloDestroy` to free the returned object. From 3019dd7fe1143388ccfcee079c878879dfd3b38d Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Thu, 29 Jan 2026 14:25:33 +1100 Subject: [PATCH 18/21] Update TSVConnClientHelloGet.en.rst --- .../api/functions/TSVConnClientHelloGet.en.rst | 9 --------- 1 file changed, 9 deletions(-) diff --git a/doc/developer-guide/api/functions/TSVConnClientHelloGet.en.rst b/doc/developer-guide/api/functions/TSVConnClientHelloGet.en.rst index 0837482dfa0..5d8b1b8757d 100644 --- a/doc/developer-guide/api/functions/TSVConnClientHelloGet.en.rst +++ b/doc/developer-guide/api/functions/TSVConnClientHelloGet.en.rst @@ -48,12 +48,3 @@ The caller must call :func:`TSClientHelloDestroy` to free the returned object. :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. - -Types -===== - -.. type:: TSClientHello - - Opaque type representing a TLS ClientHello message. This is an opaque handle - that provides access to ClientHello data via accessor methods. - From 0e9867e1d06566d453ef24c3f73a08a05e40ab24 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Tue, 3 Feb 2026 14:22:22 +1100 Subject: [PATCH 19/21] Address comments --- .../functions/TSVConnClientHelloGet.en.rst | 11 ++-- .../api/types/TSClientHello.en.rst | 53 +++++++++++++++++++ include/ts/apidefs.h.in | 9 ++-- 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/doc/developer-guide/api/functions/TSVConnClientHelloGet.en.rst b/doc/developer-guide/api/functions/TSVConnClientHelloGet.en.rst index 5d8b1b8757d..0f953ff4c08 100644 --- a/doc/developer-guide/api/functions/TSVConnClientHelloGet.en.rst +++ b/doc/developer-guide/api/functions/TSVConnClientHelloGet.en.rst @@ -36,9 +36,14 @@ Description =========== :func:`TSVConnClientHelloGet` retrieves ClientHello message data from the TLS -virtual connection :arg:`sslp`. This function is typically called from the -``TS_EVENT_SSL_CLIENT_HELLO`` hook. Returns ``nullptr`` if -:arg:`sslp` is invalid or not a TLS connection. +virtual connection :arg:`sslp`. Returns ``nullptr`` if :arg:`sslp` is invalid +or not a TLS connection. + +.. 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. The caller must call :func:`TSClientHelloDestroy` to free the returned object. diff --git a/doc/developer-guide/api/types/TSClientHello.en.rst b/doc/developer-guide/api/types/TSClientHello.en.rst index e3d2f4fcf4f..0a9d8bb742b 100644 --- a/doc/developer-guide/api/types/TSClientHello.en.rst +++ b/doc/developer-guide/api/types/TSClientHello.en.rst @@ -41,3 +41,56 @@ version, cipher suites, and extensions. Objects of this type are obtained via :func:`TSVConnClientHelloGet` and must be freed using :func:`TSClientHelloDestroy`. 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:: 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:: const uint8_t* get_extensions() const + + Returns a pointer to the extensions buffer (BoringSSL format). The length is + available via :func:`get_extensions_len()`. May return ``nullptr`` if using + OpenSSL. + +.. function:: size_t get_extensions_len() const + + Returns the length of the extensions buffer in bytes. + +.. function:: const int* get_extension_ids() const + + Returns a pointer to the extension IDs array (OpenSSL format). The length is + available via :func:`get_extension_ids_len()`. May return ``nullptr`` if using + BoringSSL. + +.. function:: size_t get_extension_ids_len() const + + Returns the number of extension IDs in the array. + +.. function:: 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. + + Note: :type:`TSExtensionTypeList` is currently implemented as ``std::vector``, + but this may be optimized in future versions to use a custom iterator without copying. + +.. function:: void* get_ssl_ptr() const + + Returns the underlying SSL pointer. This is an internal accessor for advanced use + cases. diff --git a/include/ts/apidefs.h.in b/include/ts/apidefs.h.in index 44a7395c6f4..3c98c4dcdc9 100644 --- a/include/ts/apidefs.h.in +++ b/include/ts/apidefs.h.in @@ -1064,6 +1064,9 @@ struct tsapi_ssl_client_hello { class TSClientHelloImpl { public: + // Type alias for extension type list + using TSExtensionTypeList = std::vector; + TSClientHelloImpl(std::unique_ptr ch) : _ssl_client_hello(std::move(ch)) {} ~TSClientHelloImpl() = default; @@ -1116,12 +1119,12 @@ public: return _ssl_client_hello->ssl_ptr; } - // Returns a vector of extension type IDs + // Returns an iterable container of extension type IDs // This abstracts the difference between BoringSSL (extensions buffer) and OpenSSL (extension_ids array) - std::vector + TSExtensionTypeList get_extension_types() const { - std::vector result; + TSExtensionTypeList result; // For BoringSSL, parse the extensions buffer if (_ssl_client_hello->extensions != nullptr) { From 84ada4846e6edcf5b5e1bc57ed4b38980e76a689 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Tue, 3 Feb 2026 15:45:42 +1100 Subject: [PATCH 20/21] Update TSClientHello.en.rst --- doc/developer-guide/api/types/TSClientHello.en.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/developer-guide/api/types/TSClientHello.en.rst b/doc/developer-guide/api/types/TSClientHello.en.rst index 0a9d8bb742b..295a8c0749a 100644 --- a/doc/developer-guide/api/types/TSClientHello.en.rst +++ b/doc/developer-guide/api/types/TSClientHello.en.rst @@ -30,6 +30,10 @@ Synopsis .. type:: TSClientHello +.. type:: TSClientHelloImpl::TSExtensionTypeList + + A type alias for an iterable container of extension type IDs. + Description =========== @@ -80,16 +84,13 @@ The following methods are available to access ClientHello data: Returns the number of extension IDs in the array. -.. function:: TSExtensionTypeList get_extension_types() const +.. function:: TSClientHelloImpl::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. - Note: :type:`TSExtensionTypeList` is currently implemented as ``std::vector``, - but this may be optimized in future versions to use a custom iterator without copying. - .. function:: void* get_ssl_ptr() const Returns the underlying SSL pointer. This is an internal accessor for advanced use From ccb6775901b691e787035cfceacdcfa778aca514 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Tue, 3 Feb 2026 15:51:51 +1100 Subject: [PATCH 21/21] Update TSClientHello.en.rst --- doc/developer-guide/api/types/TSClientHello.en.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/developer-guide/api/types/TSClientHello.en.rst b/doc/developer-guide/api/types/TSClientHello.en.rst index 295a8c0749a..1a73be769c1 100644 --- a/doc/developer-guide/api/types/TSClientHello.en.rst +++ b/doc/developer-guide/api/types/TSClientHello.en.rst @@ -30,7 +30,7 @@ Synopsis .. type:: TSClientHello -.. type:: TSClientHelloImpl::TSExtensionTypeList +.. type:: TSClientHello::TSExtensionTypeList A type alias for an iterable container of extension type IDs. @@ -84,7 +84,7 @@ The following methods are available to access ClientHello data: Returns the number of extension IDs in the array. -.. function:: TSClientHelloImpl::TSExtensionTypeList get_extension_types() const +.. 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