From d2c0e223ac7346153c7b09f65633cb01e32bc248 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Tue, 6 Jan 2026 15:56:35 +1100 Subject: [PATCH 01/25] 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 52313acfddf..59949dff727 100644 --- a/cmake/ExperimentalPlugins.cmake +++ b/cmake/ExperimentalPlugins.cmake @@ -43,13 +43,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 c979ac8e94c..8a55a3f3e5c 100644 --- a/include/ts/apidefs.h.in +++ b/include/ts/apidefs.h.in @@ -1084,6 +1084,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 c63febb3607..24a886cd192 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 16419d45668..56518a14c0c 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -55,11 +55,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); @@ -198,8 +199,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 { @@ -215,14 +219,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; } @@ -263,68 +280,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 1a4b734e285..911c53c5870 100644 --- a/src/api/InkAPI.cc +++ b/src/api/InkAPI.cc @@ -7920,6 +7920,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 6d262c1ca6b..3f895ff3c7a 100644 --- a/src/iocore/net/SSLUtils.cc +++ b/src/iocore/net/SSLUtils.cc @@ -304,10 +304,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 553849627870d7fac49b8fb0f8b26c7fa0d54e36 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Wed, 7 Jan 2026 10:35:21 +1100 Subject: [PATCH 02/25] 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 56518a14c0c..e174d81d93e 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -54,17 +54,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); @@ -199,11 +206,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 { @@ -214,35 +234,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. @@ -280,42 +302,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) { @@ -324,45 +395,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 2c6a0d094c2c270cb22b6de6f20e356a3b228efc Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Wed, 7 Jan 2026 11:46:37 +1100 Subject: [PATCH 03/25] 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 e174d81d93e..b76356d3aee 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -211,49 +211,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; @@ -264,7 +247,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. @@ -302,18 +284,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) { @@ -325,85 +313,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])); @@ -412,7 +368,6 @@ add_ciphers(JA4::TLSClientHelloSummary &summary, SSL *ssl) Dbg(dbg_ctl, "Failed to get ciphers."); } } -#endif #ifdef OPENSSL_IS_BORINGSSL void @@ -421,17 +376,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 @@ -441,7 +391,6 @@ add_extensions(JA4::TLSClientHelloSummary &summary, SSL_CLIENT_HELLO *client_hel remaining -= total_ext_size; } } - #else void add_extensions(JA4::TLSClientHelloSummary &summary, SSL *ssl) @@ -456,6 +405,7 @@ add_extensions(JA4::TLSClientHelloSummary &summary, SSL *ssl) OPENSSL_free(buf); } #endif + std::string hash_with_SHA256(std::string_view sv) { From 2dff963c87d352ebd31e730243c0878edf0b4e5c Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Wed, 7 Jan 2026 12:35:38 +1100 Subject: [PATCH 04/25] 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 8a55a3f3e5c..e6d032167bb 100644 --- a/include/ts/apidefs.h.in +++ b/include/ts/apidefs.h.in @@ -1084,7 +1084,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 b76356d3aee..de7d06b3342 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -57,11 +57,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); @@ -210,14 +210,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); @@ -233,7 +233,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 @@ -286,7 +286,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 @@ -323,7 +323,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 @@ -348,7 +348,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; @@ -371,7 +371,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 911c53c5870..419aab8920b 100644 --- a/src/api/InkAPI.cc +++ b/src/api/InkAPI.cc @@ -7930,8 +7930,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 fec3ceec2d8050585a8186a4ae66cf00ad854d70 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Wed, 7 Jan 2026 12:39:50 +1100 Subject: [PATCH 05/25] 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 59949dff727..764571546c1 100644 --- a/cmake/ExperimentalPlugins.cmake +++ b/cmake/ExperimentalPlugins.cmake @@ -42,11 +42,7 @@ auto_option(HOOK_TRACE FEATURE_VAR BUILD_HOOK_TRACE DEFAULT ${_DEFAULT}) auto_option(HTTP_STATS FEATURE_VAR BUILD_HTTP_STATS DEFAULT ${_DEFAULT}) auto_option(ICAP FEATURE_VAR BUILD_ICAP DEFAULT ${_DEFAULT}) auto_option(INLINER FEATURE_VAR BUILD_INLINER DEFAULT ${_DEFAULT}) -auto_option( - JA4_FINGERPRINT FEATURE_VAR BUILD_JA4_FINGERPRINT VAR_DEPENDS - # HAVE_SSL_CTX_SET_CLIENT_HELLO_CB - DEFAULT ${_DEFAULT} -) +auto_option(JA4_FINGERPRINT FEATURE_VAR BUILD_JA4_FINGERPRINT VAR_DEPENDS DEFAULT ${_DEFAULT}) auto_option( MAGICK FEATURE_VAR diff --git a/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 de7d06b3342..077f99625d7 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -226,7 +226,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 3f895ff3c7a..d7703cd37f8 100644 --- a/src/iocore/net/SSLUtils.cc +++ b/src/iocore/net/SSLUtils.cc @@ -304,7 +304,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 56209204e8e22fe43fcc50466f7b7bc9d082e3f5 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Wed, 7 Jan 2026 15:37:16 +1100 Subject: [PATCH 06/25] 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 24a886cd192..ced0d44f032 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 077f99625d7..65a1c31237a 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -56,22 +56,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); @@ -85,7 +77,6 @@ constexpr std::string_view JA4_VIA_HEADER{"x-ja4-via"}; constexpr unsigned int EXT_ALPN{0x10}; constexpr unsigned int EXT_SUPPORTED_VERSIONS{0x2b}; -constexpr int SSL_SUCCESS{1}; DbgCtl dbg_ctl{PLUGIN_NAME}; @@ -210,17 +201,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. @@ -231,18 +221,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; } @@ -284,23 +270,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) { @@ -313,28 +287,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); @@ -345,35 +311,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]; @@ -389,21 +356,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 419aab8920b..73d8a309e69 100644 --- a/src/api/InkAPI.cc +++ b/src/api/InkAPI.cc @@ -7936,6 +7936,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 766465741b624b097f0a478437b99d12b397b912 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Thu, 8 Jan 2026 11:41:56 +1100 Subject: [PATCH 07/25] 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 e6d032167bb..429ab5fad48 100644 --- a/include/ts/apidefs.h.in +++ b/include/ts/apidefs.h.in @@ -1084,7 +1084,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 65a1c31237a..643e24769bc 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -66,6 +66,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 { @@ -206,6 +207,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 { @@ -274,7 +276,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) { @@ -300,7 +302,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); @@ -321,7 +323,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 @@ -359,7 +361,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])); } @@ -492,3 +494,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 73d8a309e69..67019101a21 100644 --- a/src/api/InkAPI.cc +++ b/src/api/InkAPI.cc @@ -7920,6 +7920,7 @@ TSVConnSslSniGet(TSVConn sslp, int *length) return server_name; } +#ifdef OPENSSL_IS_BORINGSSL TSClientHello TSVConnClientHelloGet(TSVConn sslp) { @@ -7930,36 +7931,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 e42e776a75f5df7bc0e2bc950b7693bcdb310298 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Thu, 8 Jan 2026 11:45:44 +1100 Subject: [PATCH 08/25] 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 643e24769bc..05f05e91f5c 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -498,8 +498,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; } @@ -516,5 +514,5 @@ client_hello_ext_get(TSClientHello ch, unsigned int type, const unsigned char ** } #endif - return retval; + return TS_ERROR; } From 08168c71f2183f44daf640e9cd675a43833da12f Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Thu, 8 Jan 2026 12:07:00 +1100 Subject: [PATCH 09/25] 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 05f05e91f5c..09dc696ca07 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -278,7 +278,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 23fe04753a83ab7280b0148e729128dcf62663a9 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Thu, 8 Jan 2026 12:07:45 +1100 Subject: [PATCH 10/25] 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 ced0d44f032..45c835e5d9f 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 7ee0bcf23ef87247446e04265681744c9d3dbb12 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Thu, 8 Jan 2026 13:58:53 +1100 Subject: [PATCH 11/25] 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 429ab5fad48..0e62e1ffcf3 100644 --- a/include/ts/apidefs.h.in +++ b/include/ts/apidefs.h.in @@ -1084,7 +1084,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 70a568f6c4ee2cb2e134dc9deb309284745a8c42 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Wed, 21 Jan 2026 10:08:03 +1100 Subject: [PATCH 12/25] 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 0e62e1ffcf3..f51d89e1a54 100644 --- a/include/ts/apidefs.h.in +++ b/include/ts/apidefs.h.in @@ -1084,7 +1084,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 45c835e5d9f..e93ad76ddc4 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 09dc696ca07..980bff919b5 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -201,12 +201,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."); @@ -215,6 +210,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())); } @@ -288,11 +285,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; } } @@ -316,16 +309,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) { @@ -339,35 +324,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 @@ -498,17 +476,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 67019101a21..386cef4687a 100644 --- a/src/api/InkAPI.cc +++ b/src/api/InkAPI.cc @@ -7920,7 +7920,6 @@ TSVConnSslSniGet(TSVConn sslp, int *length) return server_name; } -#ifdef OPENSSL_IS_BORINGSSL TSClientHello TSVConnClientHelloGet(TSVConn sslp) { @@ -7930,14 +7929,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 333d697cd9d0df554730cd263c36c2ef4e86ac35 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Wed, 28 Jan 2026 14:20:10 +1100 Subject: [PATCH 13/25] 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 f51d89e1a54..a6e066ccde1 100644 --- a/include/ts/apidefs.h.in +++ b/include/ts/apidefs.h.in @@ -1048,6 +1048,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 *; @@ -1084,19 +1164,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 e93ad76ddc4..07ca5ad3c13 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 980bff919b5..e0294b403f1 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -66,7 +66,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 { @@ -273,7 +272,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) { @@ -285,7 +284,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(); } } @@ -295,7 +294,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); @@ -309,8 +308,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) { @@ -324,9 +323,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]; @@ -340,10 +340,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])); } } } @@ -465,40 +467,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 386cef4687a..93733127407 100644 --- a/src/api/InkAPI.cc +++ b/src/api/InkAPI.cc @@ -7929,7 +7929,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 @@ -7946,8 +7945,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 @@ -7966,27 +7963,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; @@ -7995,21 +7989,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 576fa4f642719146c0ece56dd423726a48da86bd Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Thu, 29 Jan 2026 11:27:01 +1100 Subject: [PATCH 14/25] 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 1def2223a78..5379c9a72e9 100644 --- a/doc/admin-guide/plugins/index.en.rst +++ b/doc/admin-guide/plugins/index.en.rst @@ -178,6 +178,7 @@ directory of the |TS| source tree. Experimental plugins can be compiled by passi Header Frequency Hook Trace ICAP + JA4 Fingerprint Maxmind ACL Memcache Memory Profile @@ -232,6 +233,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 a6e066ccde1..1e4c134aa8c 100644 --- a/include/ts/apidefs.h.in +++ b/include/ts/apidefs.h.in @@ -43,6 +43,8 @@ */ #include +#include +#include #include #include #include @@ -1066,9 +1068,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 @@ -1118,15 +1120,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 e0294b403f1..6097852ab4c 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -323,30 +323,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 93733127407..a0526d713f4 100644 --- a/src/api/InkAPI.cc +++ b/src/api/InkAPI.cc @@ -7929,13 +7929,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; } @@ -7950,12 +7949,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; } @@ -7980,7 +7977,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 360526f9d344bb7d865d0e810475ac5ffccc5a71 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Thu, 29 Jan 2026 11:40:52 +1100 Subject: [PATCH 15/25] 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 30db0c3864f7dc6f871d9ee4247a0280d489b68a Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Thu, 29 Jan 2026 14:13:11 +1100 Subject: [PATCH 16/25] 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 432e4bacdd1947d45835a12f32bc7148e284b29e Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Thu, 29 Jan 2026 14:19:10 +1100 Subject: [PATCH 17/25] 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 74510d97fd752c9398fcbe774f3b8267753f15d5 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Thu, 29 Jan 2026 14:25:33 +1100 Subject: [PATCH 18/25] 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 5629d59af87362eb1d042e33c5357bdeeb062a3f Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Tue, 3 Feb 2026 14:22:22 +1100 Subject: [PATCH 19/25] 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 1e4c134aa8c..68cd9ce81b1 100644 --- a/include/ts/apidefs.h.in +++ b/include/ts/apidefs.h.in @@ -1068,6 +1068,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; @@ -1120,12 +1123,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 d76df15c2a344f755b4593e9b5712f073e0e8105 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Tue, 3 Feb 2026 15:45:42 +1100 Subject: [PATCH 20/25] 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 c8d37e0f53bc14e047b01a2356029d8d249fd2e9 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Tue, 3 Feb 2026 15:51:51 +1100 Subject: [PATCH 21/25] 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 From 293f873790d300a9060e076e693786e15a604666 Mon Sep 17 00:00:00 2001 From: Masakazu Kitajo Date: Tue, 24 Feb 2026 09:30:23 -0700 Subject: [PATCH 22/25] Eliminate heap allocations --- .../functions/TSVConnClientHelloGet.en.rst | 9 +- .../api/types/TSClientHello.en.rst | 11 +- include/iocore/net/TLSSNISupport.h | 52 +++++- include/ts/apidefs.h.in | 123 ++++---------- include/ts/ts.h | 28 +--- .../experimental/ja4_fingerprint/plugin.cc | 12 +- src/api/InkAPI.cc | 154 +++++++++--------- src/iocore/net/SSLUtils.cc | 1 - src/iocore/net/TLSSNISupport.cc | 122 ++++++++++++-- 9 files changed, 279 insertions(+), 233 deletions(-) diff --git a/doc/developer-guide/api/functions/TSVConnClientHelloGet.en.rst b/doc/developer-guide/api/functions/TSVConnClientHelloGet.en.rst index 0f953ff4c08..acfd96a91d2 100644 --- a/doc/developer-guide/api/functions/TSVConnClientHelloGet.en.rst +++ b/doc/developer-guide/api/functions/TSVConnClientHelloGet.en.rst @@ -29,15 +29,14 @@ Synopsis #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`. Returns ``nullptr`` if :arg:`sslp` is invalid -or not a TLS connection. +virtual connection :arg:`sslp`. Returns a :type:`TSClientHello` always. The availability +of the returned object must be checked before use. .. important:: @@ -45,10 +44,6 @@ or not a TLS connection. 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. - -: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 diff --git a/doc/developer-guide/api/types/TSClientHello.en.rst b/doc/developer-guide/api/types/TSClientHello.en.rst index 1a73be769c1..9bead1f97cd 100644 --- a/doc/developer-guide/api/types/TSClientHello.en.rst +++ b/doc/developer-guide/api/types/TSClientHello.en.rst @@ -42,15 +42,20 @@ Description 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. +The implementation abstracts differences between OpenSSL and BoringSSL to +provide a consistent interface. Accessor Methods ================ The following methods are available to access ClientHello data: +.. function:: bool is_available() const + + Returns whether the object contains valid values. As long as + :func:`TSVConnClientHelloGet` is called for a TLS connection, the return + value should be `true`. + .. function:: uint16_t get_version() const Returns the TLS version from the ClientHello message. diff --git a/include/iocore/net/TLSSNISupport.h b/include/iocore/net/TLSSNISupport.h index f56223d23b0..2ae45490c9e 100644 --- a/include/iocore/net/TLSSNISupport.h +++ b/include/iocore/net/TLSSNISupport.h @@ -23,6 +23,7 @@ */ #pragma once +#include "tscore/ink_config.h" #include "tscore/ink_memory.h" #include "SSLTypes.h" @@ -40,14 +41,51 @@ class TLSSNISupport { public: ClientHello(ClientHelloContainer chc) : _chc(chc) {} + ~ClientHello(); + + class ExtensionIdIterator + { + public: +#if HAVE_SSL_CTX_SET_CLIENT_HELLO_CB + ExtensionIdIterator(int *ids, size_t len, size_t offset) : _extensions(ids), _ext_len(len), _offset(offset) {} +#elif HAVE_SSL_CTX_SET_SELECT_CERTIFICATE_CB + ExtensionIdIterator(const uint8_t *extensions, size_t len, size_t offset) + : _extensions(extensions), _ext_len(len), _offset(offset) + { + } +#endif + ~ExtensionIdIterator(); + + ExtensionIdIterator &operator++(); + bool operator==(const ExtensionIdIterator &b) const; + int operator*() const; + + private: +#if HAVE_SSL_CTX_SET_CLIENT_HELLO_CB + int *_extensions; +#elif HAVE_SSL_CTX_SET_SELECT_CERTIFICATE_CB + const uint8_t *_extensions; +#endif + size_t _ext_len; + size_t _offset; + }; + + uint16_t getVersion(); + std::string_view getCipherSuites(); + ExtensionIdIterator begin(); + ExtensionIdIterator end(); + /** * @return 1 if successful */ - int getExtension(int type, const uint8_t **out, size_t *outlen); - ClientHelloContainer get_client_hello_container(); + int getExtension(int type, const uint8_t **out, size_t *outlen); private: ClientHelloContainer _chc; +#if HAVE_SSL_CTX_SET_CLIENT_HELLO_CB + int *_ext_ids = nullptr; + size_t _ext_len; +#endif }; virtual ~TLSSNISupport() = default; @@ -56,9 +94,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); - ClientHelloContainer get_client_hello_container() const; - void set_client_hello_container(ClientHelloContainer container); + + int perform_sni_action(SSL &ssl); + ClientHello *get_client_hello() const; // Callback functions for OpenSSL libraries /** Process a CLIENT_HELLO from a client. @@ -116,6 +154,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); - ClientHelloContainer _chc = nullptr; + void _set_sni_server_name_buffer(std::string_view name); + ClientHello *_ch; }; diff --git a/include/ts/apidefs.h.in b/include/ts/apidefs.h.in index 68cd9ce81b1..cba01896583 100644 --- a/include/ts/apidefs.h.in +++ b/include/ts/apidefs.h.in @@ -1050,124 +1050,70 @@ 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 +class TSClientHello { public: - // Type alias for extension type list - using TSExtensionTypeList = std::vector; + class TSExtensionTypeList + { + public: + TSExtensionTypeList(void *ch) : _ch(ch) {} - TSClientHelloImpl(std::unique_ptr ch) : _ssl_client_hello(std::move(ch)) {} + class Iterator + { + public: + Iterator(const void *ite); + Iterator &operator++(); + bool operator==(const Iterator &b) const; + int operator*() const; - ~TSClientHelloImpl() = default; + private: + char _real_iterator[24]; + }; - uint16_t - get_version() const - { - return _ssl_client_hello->version; - } + Iterator begin(); + Iterator end(); - const uint8_t * - get_cipher_suites() const - { - return _ssl_client_hello->cipher_suites; - } + private: + void *_ch; + }; - size_t - get_cipher_suites_len() const - { - return _ssl_client_hello->cipher_suites_len; - } + TSClientHello(void *ch) : _client_hello(ch) {} - const uint8_t * - get_extensions() const - { - return _ssl_client_hello->extensions; - } + ~TSClientHello() = default; - size_t - get_extensions_len() const + explicit + operator bool() const { - return _ssl_client_hello->extensions_len; + return _client_hello != nullptr; } - const int * - get_extension_ids() const - { - return _ssl_client_hello->extension_ids; - } + bool is_available() const; - size_t - get_extension_ids_len() const - { - return _ssl_client_hello->extension_ids_len; - } + uint16_t get_version() const; - void * - get_ssl_ptr() const - { - return _ssl_client_hello->ssl_ptr; - } + const uint8_t *get_cipher_suites() const; + + size_t get_cipher_suites_len() const; // Returns an iterable container of extension type IDs // This abstracts the difference between BoringSSL (extensions buffer) and OpenSSL (extension_ids array) TSExtensionTypeList get_extension_types() const { - TSExtensionTypeList 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; + return TSExtensionTypeList(_client_hello); } // Internal accessor for API implementation - tsapi_ssl_client_hello * + void * _get_internal() const { - return _ssl_client_hello.get(); + return _client_hello; } private: - std::unique_ptr _ssl_client_hello; + void *_client_hello; }; using TSFile = struct tsapi_file *; @@ -1205,7 +1151,6 @@ 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 = TSClientHelloImpl *; using TSFetchSM = struct tsapi_fetchsm *; diff --git a/include/ts/ts.h b/include/ts/ts.h index 07ca5ad3c13..20c4ad4e84b 100644 --- a/include/ts/ts.h +++ b/include/ts/ts.h @@ -1351,32 +1351,12 @@ const char *TSVConnSslSniGet(TSVConn sslp, int *length); 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. + @return A TSClientHello object containing Client Hello data. - @see TSClientHelloDestroy @see TSClientHelloExtensionGet */ TSClientHello TSVConnClientHelloGet(TSVConn sslp); -/** - 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. @@ -1387,11 +1367,10 @@ void TSClientHelloDestroy(TSClientHello ch); 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. + not be freed by the caller. The buffer is valid only in the condition where + you can get a TSClientHello object from an SSL virtual connection. @param 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. @@ -1402,7 +1381,6 @@ void TSClientHelloDestroy(TSClientHello ch); 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); diff --git a/plugins/experimental/ja4_fingerprint/plugin.cc b/plugins/experimental/ja4_fingerprint/plugin.cc index 6097852ab4c..fe6252435f2 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -202,15 +202,13 @@ handle_client_hello(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata) TSClientHello ch = TSVConnClientHelloGet(ssl_vc); - if (nullptr == ch) { + if (!ch) { Dbg(dbg_ctl, "Could not get TSClientHello object."); } else { auto data{std::make_unique()}; 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())); } @@ -284,7 +282,7 @@ get_version(TSClientHello ch) return max_version; } else { Dbg(dbg_ctl, "No supported_versions extension... using legacy version."); - return ch->get_version(); + return ch.get_version(); } } @@ -308,8 +306,8 @@ get_first_ALPN(TSClientHello ch) void add_ciphers(JA4::TLSClientHelloSummary &summary, TSClientHello ch) { - const uint8_t *buf = ch->get_cipher_suites(); - size_t buflen = ch->get_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) { @@ -323,7 +321,7 @@ add_ciphers(JA4::TLSClientHelloSummary &summary, TSClientHello ch) void add_extensions(JA4::TLSClientHelloSummary &summary, TSClientHello ch) { - for (auto ext_type : ch->get_extension_types()) { + 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 a0526d713f4..529ff6d2029 100644 --- a/src/api/InkAPI.cc +++ b/src/api/InkAPI.cc @@ -31,6 +31,7 @@ #include "iocore/net/NetVConnection.h" #include "iocore/net/NetHandler.h" #include "iocore/net/UDPNet.h" +#include "tscore/ink_config.h" #include "tscore/ink_platform.h" #include "tscore/ink_base64.h" #include "tscore/Encoding.h" @@ -67,6 +68,7 @@ #include "iocore/net/SSLAPIHooks.h" #include "iocore/net/SSLDiags.h" #include "iocore/net/TLSBasicSupport.h" +#include "iocore/net/TLSSNISupport.h" #include "iocore/eventsystem/ConfigProcessor.h" #include "proxy/Plugin.h" #include "proxy/logging/LogObject.h" @@ -7929,99 +7931,24 @@ TSVConnClientHelloGet(TSVConn sslp) } if (auto snis = netvc->get_service(); snis) { - auto ch = std::make_unique(); - -#ifdef OPENSSL_IS_BORINGSSL - // Get the BoringSSL client hello container - ClientHelloContainer client_hello = snis->get_client_hello_container(); + TLSSNISupport::ClientHello *client_hello = snis->get_client_hello(); if (client_hello == nullptr) { 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->ssl_ptr = const_cast(client_hello); -#else - // Get the OpenSSL SSL* object - auto tbs = netvc->get_service(); - if (!tbs) { - return nullptr; - } - SSL *ssl = tbs->get_tls_handle(); - if (ssl == nullptr) { - 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; - 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 - 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; - } - ch->ssl_ptr = ssl; -#endif - - // Wrap the POD structure in the wrapper class and return - return new TSClientHelloImpl(std::move(ch)); + // Wrap the raw object in the accessor and return + return TSClientHello(client_hello); } return nullptr; } -void -TSClientHelloDestroy(TSClientHello ch) -{ -#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->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) { + if (static_cast(ch._get_internal())->getExtension(type, out, outlen) == 1) { return TS_SUCCESS; } -#endif return TS_ERROR; } @@ -9264,3 +9191,72 @@ TSLogAddrUnmarshal(char **buf, char *dest, int len) return {-1, -1}; } + +bool +TSClientHello::is_available() const +{ + return static_cast(*this); +} + +uint16_t +TSClientHello::get_version() const +{ + return static_cast(_client_hello)->getVersion(); +} + +const uint8_t * +TSClientHello::get_cipher_suites() const +{ + return reinterpret_cast(static_cast(_client_hello)->getCipherSuites().data()); +} + +size_t +TSClientHello::get_cipher_suites_len() const +{ + return static_cast(_client_hello)->getCipherSuites().length(); +} + +TSClientHello::TSExtensionTypeList::Iterator::Iterator(const void *ite) +{ + static_assert(sizeof(_real_iterator) >= sizeof(TLSSNISupport::ClientHello::ExtensionIdIterator)); + + ink_assert(_real_iterator); + ink_assert(ite); + memcpy(_real_iterator, ite, sizeof(TLSSNISupport::ClientHello::ExtensionIdIterator)); +} + +TSClientHello::TSExtensionTypeList::Iterator +TSClientHello::TSExtensionTypeList::begin() +{ + ink_assert(_ch); + auto ch = static_cast(_ch); + auto ite = ch->begin(); + // The temporal pointer is for the memcpy in the constructor. It's only used in the constructor. + return TSClientHello::TSExtensionTypeList::Iterator(&ite); +} + +TSClientHello::TSExtensionTypeList::Iterator +TSClientHello::TSExtensionTypeList::end() +{ + auto ite = static_cast(_ch)->end(); + // The temporal pointer is for the memcpy in the constructor. It's only used in the constructor. + return TSClientHello::TSExtensionTypeList::Iterator(&ite); +} + +TSClientHello::TSExtensionTypeList::Iterator & +TSClientHello::TSExtensionTypeList::Iterator::operator++() +{ + ++(*reinterpret_cast(_real_iterator)); + return *this; +} + +bool +TSClientHello::TSExtensionTypeList::Iterator::operator==(const TSClientHello::TSExtensionTypeList::Iterator &b) const +{ + return memcmp(_real_iterator, b._real_iterator, sizeof(_real_iterator)) == 0; +} +int +TSClientHello::TSExtensionTypeList::Iterator::operator*() const +{ + return *(*reinterpret_cast(_real_iterator)); +} diff --git a/src/iocore/net/SSLUtils.cc b/src/iocore/net/SSLUtils.cc index d7703cd37f8..6d262c1ca6b 100644 --- a/src/iocore/net/SSLUtils.cc +++ b/src/iocore/net/SSLUtils.cc @@ -308,7 +308,6 @@ ssl_client_hello_callback(const SSL_CLIENT_HELLO *client_hello) 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 b4ced5d632a..13515e5c6b0 100644 --- a/src/iocore/net/TLSSNISupport.cc +++ b/src/iocore/net/TLSSNISupport.cc @@ -50,23 +50,11 @@ 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) +TLSSNISupport::ClientHello * +TLSSNISupport::get_client_hello() const { - this->_chc = container; + return this->_ch; } void @@ -114,6 +102,9 @@ TLSSNISupport::perform_sni_action(SSL &ssl) void TLSSNISupport::on_client_hello(ClientHello &client_hello) { + // Save local copy for later use; + _ch = &client_hello; + const char *servername = nullptr; const unsigned char *p; size_t remaining, len; @@ -222,3 +213,104 @@ TLSSNISupport::ClientHello::getExtension(int type, const uint8_t **out, size_t * return SSL_early_callback_ctx_extension_get(this->_chc, type, out, outlen); #endif } + +uint16_t +TLSSNISupport::ClientHello::getVersion() +{ +#if HAVE_SSL_CTX_SET_CLIENT_HELLO_CB + // Get legacy version (OpenSSL doesn't expose the direct version field from client hello) + return SSL_client_hello_get0_legacy_version(_chc); +#elif HAVE_SSL_CTX_SET_SELECT_CERTIFICATE_CB + return _chc->version; +#endif +} + +std::string_view +TLSSNISupport::ClientHello::getCipherSuites() +{ +#if HAVE_SSL_CTX_SET_CLIENT_HELLO_CB + const unsigned char *cipher_buf = nullptr; + size_t cipher_buf_len = SSL_client_hello_get0_ciphers(_chc, &cipher_buf); + return {reinterpret_cast(cipher_buf), cipher_buf_len}; +#elif HAVE_SSL_CTX_SET_SELECT_CERTIFICATE_CB + return {reinterpret_cast(_chc->cipher_suites), _chc->cipher_suites_len}; +#endif +} + +TLSSNISupport::ClientHello::ExtensionIdIterator +TLSSNISupport::ClientHello::begin() +{ + ink_assert(_chc); +#if HAVE_SSL_CTX_SET_CLIENT_HELLO_CB + if (_ext_ids == nullptr) { + SSL_client_hello_get1_extensions_present(_chc, &_ext_ids, &_ext_len); + } + return ExtensionIdIterator(_ext_ids, _ext_len, 0); +#elif HAVE_SSL_CTX_SET_SELECT_CERTIFICATE_CB + return ExtensionIdIterator(_chc->extensions, _chc->extensions_len, 0); +#endif +} + +TLSSNISupport::ClientHello::ExtensionIdIterator +TLSSNISupport::ClientHello::end() +{ + ink_assert(_chc); +#if HAVE_SSL_CTX_SET_CLIENT_HELLO_CB + if (_ext_ids == nullptr) { + SSL_client_hello_get1_extensions_present(_chc, &_ext_ids, &_ext_len); + } + return ExtensionIdIterator(_ext_ids, _ext_len, _ext_len); +#elif HAVE_SSL_CTX_SET_SELECT_CERTIFICATE_CB + return ExtensionIdIterator(_chc->extensions, _chc->extensions_len, _chc->extensions_len); +#endif +} + +TLSSNISupport::ClientHello::~ClientHello() +{ +#if HAVE_SSL_CTX_SET_CLIENT_HELLO_CB + if (_ext_ids) { + OPENSSL_free(_ext_ids); + } +#elif HAVE_SSL_CTX_SET_SELECT_CERTIFICATE_CB + // Nothing to do +#endif +} + +TLSSNISupport::ClientHello::ExtensionIdIterator::~ExtensionIdIterator() +{ + _extensions = nullptr; + _ext_len = 0; + _offset = 0; +} + +TLSSNISupport::ClientHello::ExtensionIdIterator & +TLSSNISupport::ClientHello::ExtensionIdIterator::operator++() +{ +#if HAVE_SSL_CTX_SET_CLIENT_HELLO_CB + _offset++; +#elif HAVE_SSL_CTX_SET_SELECT_CERTIFICATE_CB + uint16_t ext_len = (_extensions[_offset + 2] << 8) + _extensions[_offset + 3]; + _offset += 2 + 2 + ext_len; + ink_assert(_offset <= _ext_len); +#endif + return *this; +} + +bool +TLSSNISupport::ClientHello::ExtensionIdIterator::operator==(const ExtensionIdIterator &b) const +{ + return _extensions == b._extensions && _offset == b._offset; +} + +int +TLSSNISupport::ClientHello::ExtensionIdIterator::operator*() const +{ + if (_offset == _ext_len) { + throw std::out_of_range{"Invalid offset"}; + } +#if HAVE_SSL_CTX_SET_CLIENT_HELLO_CB + return _extensions[_offset]; +#elif HAVE_SSL_CTX_SET_SELECT_CERTIFICATE_CB + return (_extensions[_offset] << 8) + _extensions[_offset + 1]; +#endif +} From 9c6174eccd7943e1976935784d60168a113c350f Mon Sep 17 00:00:00 2001 From: Masakazu Kitajo Date: Thu, 26 Feb 2026 13:26:30 -0700 Subject: [PATCH 23/25] Address Copilot comments --- .../api/types/TSClientHello.en.rst | 22 +------------------ include/iocore/net/TLSSNISupport.h | 4 ++-- include/ts/apidefs.h.in | 1 + .../experimental/ja4_fingerprint/plugin.cc | 4 ++-- src/iocore/net/TLSSNISupport.cc | 11 +--------- 5 files changed, 7 insertions(+), 35 deletions(-) diff --git a/doc/developer-guide/api/types/TSClientHello.en.rst b/doc/developer-guide/api/types/TSClientHello.en.rst index 9bead1f97cd..ec4872ec121 100644 --- a/doc/developer-guide/api/types/TSClientHello.en.rst +++ b/doc/developer-guide/api/types/TSClientHello.en.rst @@ -69,26 +69,6 @@ The following methods are available to access ClientHello data: 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:: TSClientHello::TSExtensionTypeList get_extension_types() const Returns an iterable container of extension type IDs present in the ClientHello. @@ -96,7 +76,7 @@ The following methods are available to access ClientHello data: buffer) and OpenSSL (which uses an extension_ids array), providing a consistent interface regardless of the SSL library in use. -.. function:: void* get_ssl_ptr() const +.. function:: void* _get_internal() const Returns the underlying SSL pointer. This is an internal accessor for advanced use cases. diff --git a/include/iocore/net/TLSSNISupport.h b/include/iocore/net/TLSSNISupport.h index 2ae45490c9e..c5b74adedfa 100644 --- a/include/iocore/net/TLSSNISupport.h +++ b/include/iocore/net/TLSSNISupport.h @@ -54,7 +54,7 @@ class TLSSNISupport { } #endif - ~ExtensionIdIterator(); + ~ExtensionIdIterator() = default; ExtensionIdIterator &operator++(); bool operator==(const ExtensionIdIterator &b) const; @@ -155,5 +155,5 @@ class TLSSNISupport std::unique_ptr _sni_server_name; void _set_sni_server_name_buffer(std::string_view name); - ClientHello *_ch; + ClientHello *_ch = nullptr; }; diff --git a/include/ts/apidefs.h.in b/include/ts/apidefs.h.in index cba01896583..c1075afc74e 100644 --- a/include/ts/apidefs.h.in +++ b/include/ts/apidefs.h.in @@ -1115,6 +1115,7 @@ public: private: void *_client_hello; }; +static_assert(std::is_trivially_copyable_v == true); using TSFile = struct tsapi_file *; diff --git a/plugins/experimental/ja4_fingerprint/plugin.cc b/plugins/experimental/ja4_fingerprint/plugin.cc index fe6252435f2..216171280fa 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -272,8 +272,8 @@ get_version(TSClientHello ch) std::size_t 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) { + size_t n_versions = buf[0]; + for (size_t i = 1; i + 1 < buflen && i < (n_versions * 2) + 1; i += 2) { std::uint16_t version = (buf[i] << 8) | buf[i + 1]; if (!JA4::is_GREASE(version) && version > max_version) { max_version = version; diff --git a/src/iocore/net/TLSSNISupport.cc b/src/iocore/net/TLSSNISupport.cc index 13515e5c6b0..9b56c9129be 100644 --- a/src/iocore/net/TLSSNISupport.cc +++ b/src/iocore/net/TLSSNISupport.cc @@ -276,13 +276,6 @@ TLSSNISupport::ClientHello::~ClientHello() #endif } -TLSSNISupport::ClientHello::ExtensionIdIterator::~ExtensionIdIterator() -{ - _extensions = nullptr; - _ext_len = 0; - _offset = 0; -} - TLSSNISupport::ClientHello::ExtensionIdIterator & TLSSNISupport::ClientHello::ExtensionIdIterator::operator++() { @@ -305,9 +298,7 @@ TLSSNISupport::ClientHello::ExtensionIdIterator::operator==(const ExtensionIdIte int TLSSNISupport::ClientHello::ExtensionIdIterator::operator*() const { - if (_offset == _ext_len) { - throw std::out_of_range{"Invalid offset"}; - } + ink_assert(_offset < _ext_len); #if HAVE_SSL_CTX_SET_CLIENT_HELLO_CB return _extensions[_offset]; #elif HAVE_SSL_CTX_SET_SELECT_CERTIFICATE_CB From 5ad174524a4a64da94f41575e1bb359d5e7c2640 Mon Sep 17 00:00:00 2001 From: Masakazu Kitajo Date: Mon, 2 Mar 2026 19:02:07 -0700 Subject: [PATCH 24/25] Address Copilot comments --- doc/developer-guide/api/types/TSClientHello.en.rst | 5 +++-- include/iocore/net/TLSSNISupport.h | 4 ++-- include/ts/apidefs.h.in | 1 + include/ts/ts.h | 6 +----- plugins/experimental/ja4_fingerprint/README.md | 2 +- 5 files changed, 8 insertions(+), 10 deletions(-) diff --git a/doc/developer-guide/api/types/TSClientHello.en.rst b/doc/developer-guide/api/types/TSClientHello.en.rst index ec4872ec121..a9fbcc981b4 100644 --- a/doc/developer-guide/api/types/TSClientHello.en.rst +++ b/doc/developer-guide/api/types/TSClientHello.en.rst @@ -78,5 +78,6 @@ The following methods are available to access ClientHello data: .. function:: void* _get_internal() const - Returns the underlying SSL pointer. This is an internal accessor for advanced use - cases. + Returns a pointer to internal implementation data. This is an internal accessor for advanced use + cases. This accessor is not part of the stable public API, and plugins must not cast or rely + on the returned pointer type. diff --git a/include/iocore/net/TLSSNISupport.h b/include/iocore/net/TLSSNISupport.h index c5b74adedfa..22f3d751a8b 100644 --- a/include/iocore/net/TLSSNISupport.h +++ b/include/iocore/net/TLSSNISupport.h @@ -66,8 +66,8 @@ class TLSSNISupport #elif HAVE_SSL_CTX_SET_SELECT_CERTIFICATE_CB const uint8_t *_extensions; #endif - size_t _ext_len; - size_t _offset; + size_t _ext_len = 0; + size_t _offset = 0; }; uint16_t getVersion(); diff --git a/include/ts/apidefs.h.in b/include/ts/apidefs.h.in index c1075afc74e..86aff5202fd 100644 --- a/include/ts/apidefs.h.in +++ b/include/ts/apidefs.h.in @@ -49,6 +49,7 @@ #include #include #include +#include /** Apply printf format string compile-time argument checking to a function. * diff --git a/include/ts/ts.h b/include/ts/ts.h index 20c4ad4e84b..974e75aa310 100644 --- a/include/ts/ts.h +++ b/include/ts/ts.h @@ -1345,11 +1345,7 @@ const char *TSVConnSslSniGet(TSVConn sslp, int *length); 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. + hello hook may result in unavailable object being returned. @param sslp The SSL virtual connection handle. Must not be nullptr. @return A TSClientHello object containing Client Hello data. diff --git a/plugins/experimental/ja4_fingerprint/README.md b/plugins/experimental/ja4_fingerprint/README.md index b1b4dd55c7d..3de07907d2f 100644 --- a/plugins/experimental/ja4_fingerprint/README.md +++ b/plugins/experimental/ja4_fingerprint/README.md @@ -21,7 +21,7 @@ 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 +This plugin works with BoringSSL as well. ## Logging and Debugging From a87095bd5ba95cb579e780a1a1907ceaea1e5cb7 Mon Sep 17 00:00:00 2001 From: Masakazu Kitajo Date: Tue, 3 Mar 2026 16:30:56 -0700 Subject: [PATCH 25/25] Update doxygen comments --- include/ts/ts.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/include/ts/ts.h b/include/ts/ts.h index 974e75aa310..9b62d64fe8c 100644 --- a/include/ts/ts.h +++ b/include/ts/ts.h @@ -1347,7 +1347,7 @@ const char *TSVConnSslSniGet(TSVConn sslp, int *length); specific callback functions. Calling this function outside of the client hello hook may result in unavailable object being returned. - @param sslp The SSL virtual connection handle. Must not be nullptr. + @param[in] sslp The SSL virtual connection handle. Must not be nullptr. @return A TSClientHello object containing Client Hello data. @see TSClientHelloExtensionGet @@ -1366,10 +1366,10 @@ TSClientHello TSVConnClientHelloGet(TSVConn sslp); not be freed by the caller. The buffer is valid only in the condition where you can get a TSClientHello object from an SSL virtual connection. - @param ch The Client Hello object obtained from TSVConnClientHelloGet(). - @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. + @param[in] ch The Client Hello object obtained from TSVConnClientHelloGet(). + @param[in] type The TLS extension type to retrieve. + @param[out] out Pointer to receive the extension data buffer. Must not be nullptr. + @param[out] outlen Pointer to receive the length of the extension data in bytes. Must not be nullptr. @return TS_SUCCESS if the extension was found and retrieved successfully.