From 786fbaafd13154235a4174eb9fa58efcacd8af94 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Tue, 6 Jan 2026 15:56:35 +1100 Subject: [PATCH 01/18] got client hello routed to plugins --- cmake/ExperimentalPlugins.cmake | 10 +- include/iocore/net/TLSSNISupport.h | 11 +- include/ts/apidefs.h.in | 1 + include/ts/ts.h | 5 +- .../experimental/ja4_fingerprint/plugin.cc | 161 +++++++++++------- src/api/InkAPI.cc | 17 ++ src/iocore/net/SSLUtils.cc | 2 + src/iocore/net/TLSSNISupport.cc | 20 +++ 8 files changed, 152 insertions(+), 75 deletions(-) diff --git a/cmake/ExperimentalPlugins.cmake b/cmake/ExperimentalPlugins.cmake index 487e0bfed16..74347efde92 100644 --- a/cmake/ExperimentalPlugins.cmake +++ b/cmake/ExperimentalPlugins.cmake @@ -42,13 +42,9 @@ auto_option(HTTP_STATS FEATURE_VAR BUILD_HTTP_STATS DEFAULT ${_DEFAULT}) auto_option(ICAP FEATURE_VAR BUILD_ICAP DEFAULT ${_DEFAULT}) auto_option(INLINER FEATURE_VAR BUILD_INLINER DEFAULT ${_DEFAULT}) auto_option( - JA4_FINGERPRINT - FEATURE_VAR - BUILD_JA4_FINGERPRINT - VAR_DEPENDS - HAVE_SSL_CTX_SET_CLIENT_HELLO_CB - DEFAULT - ${_DEFAULT} + JA4_FINGERPRINT FEATURE_VAR BUILD_JA4_FINGERPRINT VAR_DEPENDS + # HAVE_SSL_CTX_SET_CLIENT_HELLO_CB + DEFAULT ${_DEFAULT} ) auto_option( MAGICK diff --git a/include/iocore/net/TLSSNISupport.h b/include/iocore/net/TLSSNISupport.h index 6897cce36a4..f56223d23b0 100644 --- a/include/iocore/net/TLSSNISupport.h +++ b/include/iocore/net/TLSSNISupport.h @@ -43,7 +43,8 @@ class TLSSNISupport /** * @return 1 if successful */ - int getExtension(int type, const uint8_t **out, size_t *outlen); + int getExtension(int type, const uint8_t **out, size_t *outlen); + ClientHelloContainer get_client_hello_container(); private: ClientHelloContainer _chc; @@ -55,8 +56,9 @@ class TLSSNISupport static TLSSNISupport *getInstance(SSL *ssl); static void bind(SSL *ssl, TLSSNISupport *snis); static void unbind(SSL *ssl); - - int perform_sni_action(SSL &ssl); + int perform_sni_action(SSL &ssl); + ClientHelloContainer get_client_hello_container() const; + void set_client_hello_container(ClientHelloContainer container); // Callback functions for OpenSSL libraries /** Process a CLIENT_HELLO from a client. @@ -114,5 +116,6 @@ class TLSSNISupport // Null-terminated string, or nullptr if there is no SNI server name. std::unique_ptr _sni_server_name; - void _set_sni_server_name_buffer(std::string_view name); + void _set_sni_server_name_buffer(std::string_view name); + ClientHelloContainer _chc = nullptr; }; diff --git a/include/ts/apidefs.h.in b/include/ts/apidefs.h.in index 078ce0eb69c..dda12fa0db6 100644 --- a/include/ts/apidefs.h.in +++ b/include/ts/apidefs.h.in @@ -1080,6 +1080,7 @@ using TSHostLookupResult = struct tsapi_hostlookupresult *; using TSAIOCallback = struct tsapi_aiocallback *; using TSAcceptor = struct tsapi_net_accept *; using TSRemapPluginInfo = struct tsapi_remap_plugin_info *; +using TSClientHello = struct tsapi_clienthello *; using TSFetchSM = struct tsapi_fetchsm *; diff --git a/include/ts/ts.h b/include/ts/ts.h index 3227f1cf18d..892d58a0c6f 100644 --- a/include/ts/ts.h +++ b/include/ts/ts.h @@ -1331,8 +1331,9 @@ TSReturnCode TSVConnProtocolEnable(TSVConn connp, const char *protocol_name); int TSVConnIsSsl(TSVConn sslp); /* Returns 1 if a certificate was provided in the TLS handshake, 0 otherwise. */ -int TSVConnProvidedSslCert(TSVConn sslp); -const char *TSVConnSslSniGet(TSVConn sslp, int *length); +int TSVConnProvidedSslCert(TSVConn sslp); +const char *TSVConnSslSniGet(TSVConn sslp, int *length); +TSClientHello TSVConnClientHelloGet(TSVConn sslp); TSSslSession TSSslSessionGet(const TSSslSessionID *session_id); int TSSslSessionGetBuffer(const TSSslSessionID *session_id, char *buffer, int *len_ptr); diff --git a/plugins/experimental/ja4_fingerprint/plugin.cc b/plugins/experimental/ja4_fingerprint/plugin.cc index ada54ed1c98..149a3b2c387 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -53,11 +53,12 @@ static bool create_log_file(); static void register_hooks(); static int handle_client_hello(TSCont cont, TSEvent event, void *edata); static std::string get_fingerprint(SSL *ssl); +static std::string get_fingerprint(SSL_CLIENT_HELLO *ssl); char *get_IP(sockaddr const *s_sockaddr, char res[INET6_ADDRSTRLEN]); static void log_fingerprint(JA4_data const *data); static std::uint16_t get_version(SSL *ssl); static std::string get_first_ALPN(SSL *ssl); -static void add_ciphers(JA4::TLSClientHelloSummary &summary, SSL *ssl); +static void add_ciphers(JA4::TLSClientHelloSummary &summary, SSL_CLIENT_HELLO *ssl); static void add_extensions(JA4::TLSClientHelloSummary &summary, SSL *ssl); static std::string hash_with_SHA256(std::string_view sv); static int handle_read_request_hdr(TSCont cont, TSEvent event, void *edata); @@ -163,8 +164,11 @@ handle_client_hello(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata) // We ignore the event, but we don't want to reject the connection. return TS_SUCCESS; } - TSVConn const ssl_vc{static_cast(edata)}; - TSSslConnection const ssl{TSVConnSslConnectionGet(ssl_vc)}; + TSVConn const ssl_vc{static_cast(edata)}; + TSSslConnection const ssl{TSVConnSslConnectionGet(ssl_vc)}; + TSClientHello ssl_client_hello = TSVConnClientHelloGet(ssl_vc); + const SSL_CLIENT_HELLO *client_hello = reinterpret_cast(ssl_client_hello); + if (nullptr == ssl) { Dbg(dbg_ctl, "Could not get SSL object."); } else { @@ -180,14 +184,27 @@ handle_client_hello(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata) } std::string -get_fingerprint(SSL *ssl) +get_fingerprint(SSL_CLIENT_HELLO *ssl) { JA4::TLSClientHelloSummary summary{}; summary.protocol = JA4::Protocol::TLS; - summary.TLS_version = get_version(ssl); - summary.ALPN = get_first_ALPN(ssl); + summary.TLS_version = ssl->; + // summary.ALPN = get_first_ALPN(ssl); add_ciphers(summary, ssl); - add_extensions(summary, ssl); + // add_extensions(summary, ssl); + std::string result{JA4::make_JA4_fingerprint(summary, hash_with_SHA256)}; + return result; +} + +std::string +get_fingerprint(SSL *ssl) +{ + JA4::TLSClientHelloSummary summary{}; + summary.protocol = JA4::Protocol::TLS; + // summary.TLS_version = get_version(ssl); + // summary.ALPN = get_first_ALPN(ssl); + add_ciphers(summary, ssl); + // add_extensions(summary, ssl); std::string result{JA4::make_JA4_fingerprint(summary, hash_with_SHA256)}; return result; } @@ -228,68 +245,88 @@ log_fingerprint(JA4_data const *data) } } -std::uint16_t -get_version(SSL *ssl) -{ - unsigned char const *buf{}; - std::size_t buflen{}; - if (SSL_SUCCESS == SSL_client_hello_get0_ext(ssl, EXT_SUPPORTED_VERSIONS, &buf, &buflen)) { - std::uint16_t max_version{0}; - for (std::size_t i{1}; i < buflen; i += 2) { - std::uint16_t version{make_word(buf[i - 1], buf[i])}; - if ((!JA4::is_GREASE(version)) && version > max_version) { - max_version = version; - } - } - return max_version; - } else { - Dbg(dbg_ctl, "No supported_versions extension... using legacy version."); - return SSL_client_hello_get0_legacy_version(ssl); - } -} - -std::string -get_first_ALPN(SSL *ssl) -{ - unsigned char const *buf{}; - std::size_t buflen{}; - std::string result{""}; - if (SSL_SUCCESS == SSL_client_hello_get0_ext(ssl, EXT_ALPN, &buf, &buflen)) { - // The first two bytes are a 16bit encoding of the total length. - unsigned char first_ALPN_length{buf[2]}; - TSAssert(buflen > 4); - TSAssert(0 != first_ALPN_length); - result.assign(&buf[3], (&buf[3]) + first_ALPN_length); - } - return result; -} +// std::uint16_t +// get_version(SSL *ssl) +// { +// unsigned char const *buf{}; +// std::size_t buflen{}; +// if (SSL_SUCCESS == SSL_client_hello_get0_ext(ssl, EXT_SUPPORTED_VERSIONS, &buf, &buflen)) { +// std::uint16_t max_version{0}; +// for (std::size_t i{1}; i < buflen; i += 2) { +// std::uint16_t version{make_word(buf[i - 1], buf[i])}; +// if ((!JA4::is_GREASE(version)) && version > max_version) { +// max_version = version; +// } +// } +// return max_version; +// } else { +// Dbg(dbg_ctl, "No supported_versions extension... using legacy version."); +// return SSL_client_hello_get0_legacy_version(ssl); +// } +// } + +// std::string +// get_first_ALPN(SSL *ssl) +// { +// unsigned char const *buf{}; +// std::size_t buflen{}; +// std::string result{""}; +// if (SSL_SUCCESS == SSL_client_hello_get0_ext(ssl, EXT_ALPN, &buf, &buflen)) { +// // The first two bytes are a 16bit encoding of the total length. +// unsigned char first_ALPN_length{buf[2]}; +// TSAssert(buflen > 4); +// TSAssert(0 != first_ALPN_length); +// result.assign(&buf[3], (&buf[3]) + first_ALPN_length); +// } +// return result; +// } void -add_ciphers(JA4::TLSClientHelloSummary &summary, SSL *ssl) +add_ciphers(JA4::TLSClientHelloSummary &summary, SSL_CLIENT_HELLO *client_hello) { - unsigned char const *buf{}; - std::size_t buflen{SSL_client_hello_get0_ciphers(ssl, &buf)}; - if (buflen > 0) { - for (std::size_t i{1}; i < buflen; i += 2) { - summary.add_cipher(make_word(buf[i], buf[i - 1])); - } - } else { - Dbg(dbg_ctl, "Failed to get ciphers."); - } -} + const uint8_t *ciphers = client_hello->cipher_suites; + size_t len = client_hello->cipher_suites_len; -void -add_extensions(JA4::TLSClientHelloSummary &summary, SSL *ssl) -{ - int *buf{}; - std::size_t buflen{}; - if (SSL_SUCCESS == SSL_client_hello_get1_extensions_present(ssl, &buf, &buflen)) { - for (std::size_t i{1}; i < buflen; i += 2) { - summary.add_extension(make_word(buf[i], buf[i - 1])); + for (size_t i = 0; i + 1 < len; i += 2) { + uint16_t cipher_value = (ciphers[i] << 8) | ciphers[i + 1]; + summary.add_extension(cipher_value); + + const SSL_CIPHER *cipher = SSL_get_cipher_by_value(cipher_value); + + if (cipher != nullptr) { + const char *cipher_name = SSL_CIPHER_get_name(cipher); + Dbg(dbg_ctl, "0x%04X: %s", cipher_value, cipher_name); + } else { + Dbg(dbg_ctl, " 0x%04X: (unknown/unsupported)", cipher_value); } } - OPENSSL_free(buf); } +// void +// add_ciphers(JA4::TLSClientHelloSummary &summary, SSL *ssl) +// { +// unsigned char const *buf{}; +// std::size_t buflen{SSL_client_hello_get0_ciphers(ssl, &buf)}; +// if (buflen > 0) { +// for (std::size_t i{1}; i < buflen; i += 2) { +// summary.add_cipher(make_word(buf[i], buf[i - 1])); +// } +// } else { +// Dbg(dbg_ctl, "Failed to get ciphers."); +// } +// } + +// void +// add_extensions(JA4::TLSClientHelloSummary &summary, SSL *ssl) +// { +// int *buf{}; +// std::size_t buflen{}; +// if (SSL_SUCCESS == SSL_client_hello_get1_extensions_present(ssl, &buf, &buflen)) { +// for (std::size_t i{1}; i < buflen; i += 2) { +// summary.add_extension(make_word(buf[i], buf[i - 1])); +// } +// } +// OPENSSL_free(buf); +// } std::string hash_with_SHA256(std::string_view sv) diff --git a/src/api/InkAPI.cc b/src/api/InkAPI.cc index cabfb5309d6..b63b7e7c2b9 100644 --- a/src/api/InkAPI.cc +++ b/src/api/InkAPI.cc @@ -7890,6 +7890,23 @@ TSVConnSslSniGet(TSVConn sslp, int *length) return server_name; } +TSClientHello +TSVConnClientHelloGet(TSVConn sslp) +{ + NetVConnection *netvc = reinterpret_cast(sslp); + if (netvc == nullptr) { + return nullptr; + } + + if (auto snis = netvc->get_service(); snis) { + ClientHelloContainer client_hello = snis->get_client_hello_container(); + // Cast the pointer value directly (no const_cast needed if types match) + return reinterpret_cast(const_cast(static_cast(client_hello))); + } + + return nullptr; +} + TSSslVerifyCTX TSVConnSslVerifyCTXGet(TSVConn sslp) { diff --git a/src/iocore/net/SSLUtils.cc b/src/iocore/net/SSLUtils.cc index 0b7bc982ad4..abebf2b9565 100644 --- a/src/iocore/net/SSLUtils.cc +++ b/src/iocore/net/SSLUtils.cc @@ -302,10 +302,12 @@ ssl_client_hello_callback(const SSL_CLIENT_HELLO *client_hello) { SSL *s = client_hello->ssl; TLSSNISupport::ClientHello ch = {client_hello}; + #endif TLSSNISupport *snis = TLSSNISupport::getInstance(s); if (snis) { + snis->set_client_hello_container(ch.get_client_hello_container()); snis->on_client_hello(ch); int ret = snis->perform_sni_action(*s); if (ret != SSL_TLSEXT_ERR_OK) { diff --git a/src/iocore/net/TLSSNISupport.cc b/src/iocore/net/TLSSNISupport.cc index ee5e4a8c441..3f5574eb9ee 100644 --- a/src/iocore/net/TLSSNISupport.cc +++ b/src/iocore/net/TLSSNISupport.cc @@ -50,6 +50,25 @@ TLSSNISupport::getInstance(SSL *ssl) return static_cast(SSL_get_ex_data(ssl, _ex_data_index)); } +ClientHelloContainer +TLSSNISupport::ClientHello::get_client_hello_container() +{ + return this->_chc; +} + +// In TLSSNISupport.h +ClientHelloContainer +TLSSNISupport::get_client_hello_container() const +{ + return this->_chc; +} + +void +TLSSNISupport::set_client_hello_container(ClientHelloContainer container) +{ + this->_chc = container; +} + void TLSSNISupport::bind(SSL *ssl, TLSSNISupport *snis) { @@ -98,6 +117,7 @@ TLSSNISupport::on_client_hello(ClientHello &client_hello) const char *servername = nullptr; const unsigned char *p; size_t remaining, len; + // Parse the server name if the get extension call succeeds and there are more than 2 bytes to parse if (client_hello.getExtension(TLSEXT_TYPE_server_name, &p, &remaining) && remaining > 2) { // Parse to get to the name, originally from test/handshake_helper.c in openssl tree From 3f4e5c4915d3befe8053f2ec4c31630ef9ca6eeb Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Wed, 7 Jan 2026 10:35:21 +1100 Subject: [PATCH 02/18] Creates ja4 fingerprint with boringssl --- .../experimental/ja4_fingerprint/plugin.cc | 273 ++++++++++++------ 1 file changed, 183 insertions(+), 90 deletions(-) diff --git a/plugins/experimental/ja4_fingerprint/plugin.cc b/plugins/experimental/ja4_fingerprint/plugin.cc index 149a3b2c387..53835532726 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -52,17 +52,24 @@ static void reserve_user_arg(); static bool create_log_file(); static void register_hooks(); static int handle_client_hello(TSCont cont, TSEvent event, void *edata); -static std::string get_fingerprint(SSL *ssl); -static std::string get_fingerprint(SSL_CLIENT_HELLO *ssl); char *get_IP(sockaddr const *s_sockaddr, char res[INET6_ADDRSTRLEN]); static void log_fingerprint(JA4_data const *data); -static std::uint16_t get_version(SSL *ssl); -static std::string get_first_ALPN(SSL *ssl); -static void add_ciphers(JA4::TLSClientHelloSummary &summary, SSL_CLIENT_HELLO *ssl); -static void add_extensions(JA4::TLSClientHelloSummary &summary, SSL *ssl); -static std::string hash_with_SHA256(std::string_view sv); -static int handle_read_request_hdr(TSCont cont, TSEvent event, void *edata); -static void append_JA4_headers(TSCont cont, TSHttpTxn txnp, std::string const *fingerprint); +#ifdef OPENSSL_IS_BORINGSSL +static std::string get_fingerprint(SSL_CLIENT_HELLO *ssl); +static std::uint16_t get_version(SSL_CLIENT_HELLO *ssl); +static std::string get_first_ALPN(SSL_CLIENT_HELLO *ssl); +static void add_ciphers(JA4::TLSClientHelloSummary &summary, SSL_CLIENT_HELLO *ssl); +static void add_extensions(JA4::TLSClientHelloSummary &summary, SSL_CLIENT_HELLO *ssl); +#else +static std::string get_fingerprint(SSL *ssl); +static std::uint16_t get_version(SSL *ssl); +static std::string get_first_ALPN(SSL *ssl); +static void add_ciphers(JA4::TLSClientHelloSummary &summary, SSL *ssl); +static void add_extensions(JA4::TLSClientHelloSummary &summary, SSL *ssl); +#endif +static std::string hash_with_SHA256(std::string_view sv); +static int handle_read_request_hdr(TSCont cont, TSEvent event, void *edata); +static void append_JA4_headers(TSCont cont, TSHttpTxn txnp, std::string const *fingerprint); static void append_to_field(TSMBuffer bufp, TSMLoc hdr_loc, char const *field, int field_len, char const *value, int value_len); static int handle_vconn_close(TSCont cont, TSEvent event, void *edata); @@ -164,11 +171,24 @@ handle_client_hello(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata) // We ignore the event, but we don't want to reject the connection. return TS_SUCCESS; } - TSVConn const ssl_vc{static_cast(edata)}; - TSSslConnection const ssl{TSVConnSslConnectionGet(ssl_vc)}; - TSClientHello ssl_client_hello = TSVConnClientHelloGet(ssl_vc); - const SSL_CLIENT_HELLO *client_hello = reinterpret_cast(ssl_client_hello); + TSVConn const ssl_vc{static_cast(edata)}; + +#ifdef OPENSSL_IS_BORINGSSL + TSClientHello ssl_client_hello = TSVConnClientHelloGet(ssl_vc); + SSL_CLIENT_HELLO *client_hello = reinterpret_cast(ssl_client_hello); + if (nullptr == client_hello) { + Dbg(dbg_ctl, "Could not get SSL client hello object."); + } else { + auto data{std::make_unique()}; + data->fingerprint = get_fingerprint(client_hello); + get_IP(TSNetVConnRemoteAddrGet(ssl_vc), data->IP_addr); + log_fingerprint(data.get()); + // The VCONN_CLOSE handler is now responsible for freeing the resource. + TSUserArgSet(ssl_vc, *get_user_arg_index(), static_cast(data.release())); + } +#else + TSSslConnection const ssl{TSVConnSslConnectionGet(ssl_vc)}; if (nullptr == ssl) { Dbg(dbg_ctl, "Could not get SSL object."); } else { @@ -179,35 +199,37 @@ handle_client_hello(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata) // The VCONN_CLOSE handler is now responsible for freeing the resource. TSUserArgSet(ssl_vc, *get_user_arg_index(), static_cast(data.release())); } +#endif TSVConnReenable(ssl_vc); return TS_SUCCESS; } - +#ifdef OPENSSL_IS_BORINGSSL std::string get_fingerprint(SSL_CLIENT_HELLO *ssl) { JA4::TLSClientHelloSummary summary{}; summary.protocol = JA4::Protocol::TLS; - summary.TLS_version = ssl->; - // summary.ALPN = get_first_ALPN(ssl); + summary.TLS_version = get_version(ssl); + summary.ALPN = get_first_ALPN(ssl); add_ciphers(summary, ssl); - // add_extensions(summary, ssl); + add_extensions(summary, ssl); std::string result{JA4::make_JA4_fingerprint(summary, hash_with_SHA256)}; return result; } - +#else std::string get_fingerprint(SSL *ssl) { JA4::TLSClientHelloSummary summary{}; - summary.protocol = JA4::Protocol::TLS; - // summary.TLS_version = get_version(ssl); - // summary.ALPN = get_first_ALPN(ssl); + summary.protocol = JA4::Protocol::TLS; + summary.TLS_version = get_version(ssl); + summary.ALPN = get_first_ALPN(ssl); add_ciphers(summary, ssl); - // add_extensions(summary, ssl); + add_extensions(summary, ssl); std::string result{JA4::make_JA4_fingerprint(summary, hash_with_SHA256)}; return result; } +#endif // This implementation is copied verbatim from JA3 fingerprint to make the // potential for deduplication as obvious as possible. @@ -245,42 +267,91 @@ log_fingerprint(JA4_data const *data) } } -// std::uint16_t -// get_version(SSL *ssl) -// { -// unsigned char const *buf{}; -// std::size_t buflen{}; -// if (SSL_SUCCESS == SSL_client_hello_get0_ext(ssl, EXT_SUPPORTED_VERSIONS, &buf, &buflen)) { -// std::uint16_t max_version{0}; -// for (std::size_t i{1}; i < buflen; i += 2) { -// std::uint16_t version{make_word(buf[i - 1], buf[i])}; -// if ((!JA4::is_GREASE(version)) && version > max_version) { -// max_version = version; -// } -// } -// return max_version; -// } else { -// Dbg(dbg_ctl, "No supported_versions extension... using legacy version."); -// return SSL_client_hello_get0_legacy_version(ssl); -// } -// } - -// std::string -// get_first_ALPN(SSL *ssl) -// { -// unsigned char const *buf{}; -// std::size_t buflen{}; -// std::string result{""}; -// if (SSL_SUCCESS == SSL_client_hello_get0_ext(ssl, EXT_ALPN, &buf, &buflen)) { -// // The first two bytes are a 16bit encoding of the total length. -// unsigned char first_ALPN_length{buf[2]}; -// TSAssert(buflen > 4); -// TSAssert(0 != first_ALPN_length); -// result.assign(&buf[3], (&buf[3]) + first_ALPN_length); -// } -// return result; -// } +#ifdef OPENSSL_IS_BORINGSSL +std::uint16_t +get_version(SSL_CLIENT_HELLO *client_hello) +{ + unsigned char const *buf{}; + std::size_t buflen{}; + // If no extensions, fall back to legacy version field + if (!client_hello->extensions || client_hello->extensions_len == 0) { + return client_hello->version; + } + + if (SSL_SUCCESS == SSL_early_callback_ctx_extension_get(client_hello, EXT_SUPPORTED_VERSIONS, &buf, &buflen)) { + std::uint16_t max_version{0}; + uint8_t list_len = buf[0]; + for (size_t i = 1; i + 1 < buflen && i < list_len + 1; i += 2) { + std::uint16_t version = (buf[i] << 8) | buf[i + 1]; + if (!JA4::is_GREASE(version) && version > max_version) { + max_version = version; + } + } + return max_version; + } else { + Dbg(dbg_ctl, "No supported_versions extension... using legacy version."); + return client_hello->version; + } +} +#else +std::uint16_t +get_version(SSL *ssl) +{ + unsigned char const *buf{}; + std::size_t buflen{}; + if (SSL_SUCCESS == SSL_client_hello_get0_ext(ssl, EXT_SUPPORTED_VERSIONS, &buf, &buflen)) { + std::uint16_t max_version{0}; + for (std::size_t i{1}; i < buflen; i += 2) { + std::uint16_t version{make_word(buf[i - 1], buf[i])}; + if ((!JA4::is_GREASE(version)) && version > max_version) { + max_version = version; + } + } + return max_version; + } else { + Dbg(dbg_ctl, "No supported_versions extension... using legacy version."); + return SSL_client_hello_get0_legacy_version(ssl); + } +} +#endif + +#ifdef OPENSSL_IS_BORINGSSL +std::string +get_first_ALPN(SSL_CLIENT_HELLO *client_hello) +{ + const uint8_t *buf = nullptr; + size_t buflen = 0; + std::string result; + + bool found = SSL_early_callback_ctx_extension_get(client_hello, EXT_ALPN, &buf, &buflen); + if (found && buflen > 3) { + uint8_t first_ALPN_length = buf[2]; + if (first_ALPN_length > 0 && 3 + first_ALPN_length <= buflen) { + result.assign(reinterpret_cast(&buf[3]), first_ALPN_length); + } + } + + return result; +} +#else +std::string +get_first_ALPN(SSL *ssl) +{ + unsigned char const *buf{}; + std::size_t buflen{}; + std::string result{""}; + if (SSL_SUCCESS == SSL_client_hello_get0_ext(ssl, EXT_ALPN, &buf, &buflen)) { + // The first two bytes are a 16bit encoding of the total length. + unsigned char first_ALPN_length{buf[2]}; + TSAssert(buflen > 4); + TSAssert(0 != first_ALPN_length); + result.assign(&buf[3], (&buf[3]) + first_ALPN_length); + } + return result; +} +#endif +#ifdef OPENSSL_IS_BORINGSSL void add_ciphers(JA4::TLSClientHelloSummary &summary, SSL_CLIENT_HELLO *client_hello) { @@ -289,45 +360,67 @@ add_ciphers(JA4::TLSClientHelloSummary &summary, SSL_CLIENT_HELLO *client_hello) for (size_t i = 0; i + 1 < len; i += 2) { uint16_t cipher_value = (ciphers[i] << 8) | ciphers[i + 1]; - summary.add_extension(cipher_value); + summary.add_cipher(cipher_value); + } +} +#else +void +add_ciphers(JA4::TLSClientHelloSummary &summary, SSL *ssl) +{ + unsigned char const *buf{}; + std::size_t buflen{SSL_client_hello_get0_ciphers(ssl, &buf)}; + if (buflen > 0) { + for (std::size_t i{1}; i < buflen; i += 2) { + summary.add_cipher(make_word(buf[i], buf[i - 1])); + } + } else { + Dbg(dbg_ctl, "Failed to get ciphers."); + } +} +#endif + +#ifdef OPENSSL_IS_BORINGSSL +void +add_extensions(JA4::TLSClientHelloSummary &summary, SSL_CLIENT_HELLO *client_hello) +{ + const uint8_t *ext = client_hello->extensions; + size_t remaining = client_hello->extensions_len; + + while (remaining >= 4) { // Need at least 4 bytes (2 type + 2 length) + // Read extension type (2 bytes, big endian) + uint16_t ext_type = (ext[0] << 8) | ext[1]; - const SSL_CIPHER *cipher = SSL_get_cipher_by_value(cipher_value); + // Read extension length (2 bytes, big endian) + uint16_t ext_len = (ext[2] << 8) | ext[3]; - if (cipher != nullptr) { - const char *cipher_name = SSL_CIPHER_get_name(cipher); - Dbg(dbg_ctl, "0x%04X: %s", cipher_value, cipher_name); - } else { - Dbg(dbg_ctl, " 0x%04X: (unknown/unsupported)", cipher_value); + // Add the extension type to summary + summary.add_extension(ext_type); + + // Move to next extension + size_t total_ext_size = 4 + ext_len; // 4 bytes header + data + if (total_ext_size > remaining) { + break; // Malformed extension, stop parsing } + + ext += total_ext_size; + remaining -= total_ext_size; } } -// void -// add_ciphers(JA4::TLSClientHelloSummary &summary, SSL *ssl) -// { -// unsigned char const *buf{}; -// std::size_t buflen{SSL_client_hello_get0_ciphers(ssl, &buf)}; -// if (buflen > 0) { -// for (std::size_t i{1}; i < buflen; i += 2) { -// summary.add_cipher(make_word(buf[i], buf[i - 1])); -// } -// } else { -// Dbg(dbg_ctl, "Failed to get ciphers."); -// } -// } - -// void -// add_extensions(JA4::TLSClientHelloSummary &summary, SSL *ssl) -// { -// int *buf{}; -// std::size_t buflen{}; -// if (SSL_SUCCESS == SSL_client_hello_get1_extensions_present(ssl, &buf, &buflen)) { -// for (std::size_t i{1}; i < buflen; i += 2) { -// summary.add_extension(make_word(buf[i], buf[i - 1])); -// } -// } -// OPENSSL_free(buf); -// } +#else +void +add_extensions(JA4::TLSClientHelloSummary &summary, SSL *ssl) +{ + int *buf{}; + std::size_t buflen{}; + if (SSL_SUCCESS == SSL_client_hello_get1_extensions_present(ssl, &buf, &buflen)) { + for (std::size_t i{1}; i < buflen; i += 2) { + summary.add_extension(make_word(buf[i], buf[i - 1])); + } + } + OPENSSL_free(buf); +} +#endif std::string hash_with_SHA256(std::string_view sv) { From 5c8c8ed5aa53e160b04e3700badadb93538e1bcf Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Wed, 7 Jan 2026 11:46:37 +1100 Subject: [PATCH 03/18] cleanup a bit --- .../experimental/ja4_fingerprint/plugin.cc | 116 +++++------------- 1 file changed, 33 insertions(+), 83 deletions(-) diff --git a/plugins/experimental/ja4_fingerprint/plugin.cc b/plugins/experimental/ja4_fingerprint/plugin.cc index 53835532726..4adeda4ae6b 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -176,49 +176,32 @@ handle_client_hello(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata) #ifdef OPENSSL_IS_BORINGSSL TSClientHello ssl_client_hello = TSVConnClientHelloGet(ssl_vc); - SSL_CLIENT_HELLO *client_hello = reinterpret_cast(ssl_client_hello); - if (nullptr == client_hello) { - Dbg(dbg_ctl, "Could not get SSL client hello object."); - } else { - auto data{std::make_unique()}; - data->fingerprint = get_fingerprint(client_hello); - get_IP(TSNetVConnRemoteAddrGet(ssl_vc), data->IP_addr); - log_fingerprint(data.get()); - // The VCONN_CLOSE handler is now responsible for freeing the resource. - TSUserArgSet(ssl_vc, *get_user_arg_index(), static_cast(data.release())); - } + SSL_CLIENT_HELLO *ssl = reinterpret_cast(ssl_client_hello); #else - TSSslConnection const ssl{TSVConnSslConnectionGet(ssl_vc)}; + TSSslConnection const ssl_conn{TSVConnSslConnectionGet(ssl_vc)}; + SSL *ssl = reinterpret_cast(ssl_conn); +#endif if (nullptr == ssl) { - Dbg(dbg_ctl, "Could not get SSL object."); + Dbg(dbg_ctl, "Could not get SSL client hello object."); } else { auto data{std::make_unique()}; - data->fingerprint = get_fingerprint(reinterpret_cast(ssl)); + data->fingerprint = get_fingerprint(ssl); get_IP(TSNetVConnRemoteAddrGet(ssl_vc), data->IP_addr); log_fingerprint(data.get()); // The VCONN_CLOSE handler is now responsible for freeing the resource. TSUserArgSet(ssl_vc, *get_user_arg_index(), static_cast(data.release())); } -#endif + TSVConnReenable(ssl_vc); return TS_SUCCESS; } -#ifdef OPENSSL_IS_BORINGSSL + std::string +#ifdef OPENSSL_IS_BORINGSSL get_fingerprint(SSL_CLIENT_HELLO *ssl) -{ - JA4::TLSClientHelloSummary summary{}; - summary.protocol = JA4::Protocol::TLS; - summary.TLS_version = get_version(ssl); - summary.ALPN = get_first_ALPN(ssl); - add_ciphers(summary, ssl); - add_extensions(summary, ssl); - std::string result{JA4::make_JA4_fingerprint(summary, hash_with_SHA256)}; - return result; -} #else -std::string get_fingerprint(SSL *ssl) +#endif { JA4::TLSClientHelloSummary summary{}; summary.protocol = JA4::Protocol::TLS; @@ -229,7 +212,6 @@ get_fingerprint(SSL *ssl) std::string result{JA4::make_JA4_fingerprint(summary, hash_with_SHA256)}; return result; } -#endif // This implementation is copied verbatim from JA3 fingerprint to make the // potential for deduplication as obvious as possible. @@ -267,18 +249,24 @@ log_fingerprint(JA4_data const *data) } } -#ifdef OPENSSL_IS_BORINGSSL std::uint16_t +#ifdef OPENSSL_IS_BORINGSSL get_version(SSL_CLIENT_HELLO *client_hello) +#else +get_version(SSL *ssl) +#endif { unsigned char const *buf{}; std::size_t buflen{}; +#ifdef OPENSSL_IS_BORINGSSL // If no extensions, fall back to legacy version field if (!client_hello->extensions || client_hello->extensions_len == 0) { return client_hello->version; } - if (SSL_SUCCESS == SSL_early_callback_ctx_extension_get(client_hello, EXT_SUPPORTED_VERSIONS, &buf, &buflen)) { +#else + if (SSL_SUCCESS == SSL_client_hello_get0_ext(ssl, EXT_SUPPORTED_VERSIONS, &buf, &buflen)) { +#endif std::uint16_t max_version{0}; uint8_t list_len = buf[0]; for (size_t i = 1; i + 1 < buflen && i < list_len + 1; i += 2) { @@ -290,85 +278,53 @@ get_version(SSL_CLIENT_HELLO *client_hello) return max_version; } else { Dbg(dbg_ctl, "No supported_versions extension... using legacy version."); +#ifdef OPENSSL_IS_BORINGSSL return client_hello->version; - } -} #else -std::uint16_t -get_version(SSL *ssl) -{ - unsigned char const *buf{}; - std::size_t buflen{}; - if (SSL_SUCCESS == SSL_client_hello_get0_ext(ssl, EXT_SUPPORTED_VERSIONS, &buf, &buflen)) { - std::uint16_t max_version{0}; - for (std::size_t i{1}; i < buflen; i += 2) { - std::uint16_t version{make_word(buf[i - 1], buf[i])}; - if ((!JA4::is_GREASE(version)) && version > max_version) { - max_version = version; - } - } - return max_version; - } else { - Dbg(dbg_ctl, "No supported_versions extension... using legacy version."); return SSL_client_hello_get0_legacy_version(ssl); +#endif } } -#endif -#ifdef OPENSSL_IS_BORINGSSL std::string +#ifdef OPENSSL_IS_BORINGSSL get_first_ALPN(SSL_CLIENT_HELLO *client_hello) -{ - const uint8_t *buf = nullptr; - size_t buflen = 0; - std::string result; - - bool found = SSL_early_callback_ctx_extension_get(client_hello, EXT_ALPN, &buf, &buflen); - if (found && buflen > 3) { - uint8_t first_ALPN_length = buf[2]; - if (first_ALPN_length > 0 && 3 + first_ALPN_length <= buflen) { - result.assign(reinterpret_cast(&buf[3]), first_ALPN_length); - } - } - - return result; -} #else -std::string get_first_ALPN(SSL *ssl) +#endif { unsigned char const *buf{}; std::size_t buflen{}; std::string result{""}; +#ifdef OPENSSL_IS_BORINGSSL + if (SSL_SUCCESS == SSL_early_callback_ctx_extension_get(client_hello, EXT_ALPN, &buf, &buflen)) { +#else if (SSL_SUCCESS == SSL_client_hello_get0_ext(ssl, EXT_ALPN, &buf, &buflen)) { +#endif // The first two bytes are a 16bit encoding of the total length. unsigned char first_ALPN_length{buf[2]}; TSAssert(buflen > 4); TSAssert(0 != first_ALPN_length); result.assign(&buf[3], (&buf[3]) + first_ALPN_length); } + return result; } -#endif #ifdef OPENSSL_IS_BORINGSSL void add_ciphers(JA4::TLSClientHelloSummary &summary, SSL_CLIENT_HELLO *client_hello) { - const uint8_t *ciphers = client_hello->cipher_suites; - size_t len = client_hello->cipher_suites_len; - - for (size_t i = 0; i + 1 < len; i += 2) { - uint16_t cipher_value = (ciphers[i] << 8) | ciphers[i + 1]; - summary.add_cipher(cipher_value); - } -} + const uint8_t *buf = client_hello->cipher_suites; + size_t buflen = client_hello->cipher_suites_len; #else void add_ciphers(JA4::TLSClientHelloSummary &summary, SSL *ssl) { unsigned char const *buf{}; std::size_t buflen{SSL_client_hello_get0_ciphers(ssl, &buf)}; +#endif + if (buflen > 0) { for (std::size_t i{1}; i < buflen; i += 2) { summary.add_cipher(make_word(buf[i], buf[i - 1])); @@ -377,7 +333,6 @@ add_ciphers(JA4::TLSClientHelloSummary &summary, SSL *ssl) Dbg(dbg_ctl, "Failed to get ciphers."); } } -#endif #ifdef OPENSSL_IS_BORINGSSL void @@ -386,17 +341,12 @@ add_extensions(JA4::TLSClientHelloSummary &summary, SSL_CLIENT_HELLO *client_hel const uint8_t *ext = client_hello->extensions; size_t remaining = client_hello->extensions_len; - while (remaining >= 4) { // Need at least 4 bytes (2 type + 2 length) - // Read extension type (2 bytes, big endian) + while (remaining >= 4) { uint16_t ext_type = (ext[0] << 8) | ext[1]; // Read extension length (2 bytes, big endian) uint16_t ext_len = (ext[2] << 8) | ext[3]; - - // Add the extension type to summary summary.add_extension(ext_type); - - // Move to next extension size_t total_ext_size = 4 + ext_len; // 4 bytes header + data if (total_ext_size > remaining) { break; // Malformed extension, stop parsing @@ -406,7 +356,6 @@ add_extensions(JA4::TLSClientHelloSummary &summary, SSL_CLIENT_HELLO *client_hel remaining -= total_ext_size; } } - #else void add_extensions(JA4::TLSClientHelloSummary &summary, SSL *ssl) @@ -421,6 +370,7 @@ add_extensions(JA4::TLSClientHelloSummary &summary, SSL *ssl) OPENSSL_free(buf); } #endif + std::string hash_with_SHA256(std::string_view sv) { From 0efeade3015b25ef7cebcbd891caf2ffc0a03d26 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Wed, 7 Jan 2026 12:35:38 +1100 Subject: [PATCH 04/18] make ssl_client_hello const --- include/ts/apidefs.h.in | 2 +- .../experimental/ja4_fingerprint/plugin.cc | 26 +++++++++---------- src/api/InkAPI.cc | 3 +-- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/include/ts/apidefs.h.in b/include/ts/apidefs.h.in index dda12fa0db6..32f7221a477 100644 --- a/include/ts/apidefs.h.in +++ b/include/ts/apidefs.h.in @@ -1080,7 +1080,7 @@ using TSHostLookupResult = struct tsapi_hostlookupresult *; using TSAIOCallback = struct tsapi_aiocallback *; using TSAcceptor = struct tsapi_net_accept *; using TSRemapPluginInfo = struct tsapi_remap_plugin_info *; -using TSClientHello = struct tsapi_clienthello *; +using TSClientHello = const void *; using TSFetchSM = struct tsapi_fetchsm *; diff --git a/plugins/experimental/ja4_fingerprint/plugin.cc b/plugins/experimental/ja4_fingerprint/plugin.cc index 4adeda4ae6b..058eefd98fe 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -55,11 +55,11 @@ static int handle_client_hello(TSCont cont, TSEvent event, void * char *get_IP(sockaddr const *s_sockaddr, char res[INET6_ADDRSTRLEN]); static void log_fingerprint(JA4_data const *data); #ifdef OPENSSL_IS_BORINGSSL -static std::string get_fingerprint(SSL_CLIENT_HELLO *ssl); -static std::uint16_t get_version(SSL_CLIENT_HELLO *ssl); -static std::string get_first_ALPN(SSL_CLIENT_HELLO *ssl); -static void add_ciphers(JA4::TLSClientHelloSummary &summary, SSL_CLIENT_HELLO *ssl); -static void add_extensions(JA4::TLSClientHelloSummary &summary, SSL_CLIENT_HELLO *ssl); +static std::string get_fingerprint(const SSL_CLIENT_HELLO *ssl); +static std::uint16_t get_version(const SSL_CLIENT_HELLO *ssl); +static std::string get_first_ALPN(const SSL_CLIENT_HELLO *ssl); +static void add_ciphers(JA4::TLSClientHelloSummary &summary, const SSL_CLIENT_HELLO *ssl); +static void add_extensions(JA4::TLSClientHelloSummary &summary, const SSL_CLIENT_HELLO *ssl); #else static std::string get_fingerprint(SSL *ssl); static std::uint16_t get_version(SSL *ssl); @@ -175,14 +175,14 @@ handle_client_hello(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata) TSVConn const ssl_vc{static_cast(edata)}; #ifdef OPENSSL_IS_BORINGSSL - TSClientHello ssl_client_hello = TSVConnClientHelloGet(ssl_vc); - SSL_CLIENT_HELLO *ssl = reinterpret_cast(ssl_client_hello); + TSClientHello ssl_client_hello = TSVConnClientHelloGet(ssl_vc); + const SSL_CLIENT_HELLO *ssl = reinterpret_cast(ssl_client_hello); #else TSSslConnection const ssl_conn{TSVConnSslConnectionGet(ssl_vc)}; SSL *ssl = reinterpret_cast(ssl_conn); #endif if (nullptr == ssl) { - Dbg(dbg_ctl, "Could not get SSL client hello object."); + Dbg(dbg_ctl, "Could not get SSL object."); } else { auto data{std::make_unique()}; data->fingerprint = get_fingerprint(ssl); @@ -198,7 +198,7 @@ handle_client_hello(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata) std::string #ifdef OPENSSL_IS_BORINGSSL -get_fingerprint(SSL_CLIENT_HELLO *ssl) +get_fingerprint(const SSL_CLIENT_HELLO *ssl) #else get_fingerprint(SSL *ssl) #endif @@ -251,7 +251,7 @@ log_fingerprint(JA4_data const *data) std::uint16_t #ifdef OPENSSL_IS_BORINGSSL -get_version(SSL_CLIENT_HELLO *client_hello) +get_version(const SSL_CLIENT_HELLO *client_hello) #else get_version(SSL *ssl) #endif @@ -288,7 +288,7 @@ get_version(SSL *ssl) std::string #ifdef OPENSSL_IS_BORINGSSL -get_first_ALPN(SSL_CLIENT_HELLO *client_hello) +get_first_ALPN(const SSL_CLIENT_HELLO *client_hello) #else get_first_ALPN(SSL *ssl) #endif @@ -313,7 +313,7 @@ get_first_ALPN(SSL *ssl) #ifdef OPENSSL_IS_BORINGSSL void -add_ciphers(JA4::TLSClientHelloSummary &summary, SSL_CLIENT_HELLO *client_hello) +add_ciphers(JA4::TLSClientHelloSummary &summary, const SSL_CLIENT_HELLO *client_hello) { const uint8_t *buf = client_hello->cipher_suites; size_t buflen = client_hello->cipher_suites_len; @@ -336,7 +336,7 @@ add_ciphers(JA4::TLSClientHelloSummary &summary, SSL *ssl) #ifdef OPENSSL_IS_BORINGSSL void -add_extensions(JA4::TLSClientHelloSummary &summary, SSL_CLIENT_HELLO *client_hello) +add_extensions(JA4::TLSClientHelloSummary &summary, const SSL_CLIENT_HELLO *client_hello) { const uint8_t *ext = client_hello->extensions; size_t remaining = client_hello->extensions_len; diff --git a/src/api/InkAPI.cc b/src/api/InkAPI.cc index b63b7e7c2b9..f703e3607ab 100644 --- a/src/api/InkAPI.cc +++ b/src/api/InkAPI.cc @@ -7900,8 +7900,7 @@ TSVConnClientHelloGet(TSVConn sslp) if (auto snis = netvc->get_service(); snis) { ClientHelloContainer client_hello = snis->get_client_hello_container(); - // Cast the pointer value directly (no const_cast needed if types match) - return reinterpret_cast(const_cast(static_cast(client_hello))); + return reinterpret_cast(client_hello); } return nullptr; From 9765eb838b056b77b1907220e7561e0ec880ecf7 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Wed, 7 Jan 2026 12:39:50 +1100 Subject: [PATCH 05/18] spaces cleanup --- cmake/ExperimentalPlugins.cmake | 6 +----- plugins/experimental/ja4_fingerprint/README.md | 2 ++ plugins/experimental/ja4_fingerprint/plugin.cc | 1 - src/iocore/net/SSLUtils.cc | 1 - src/iocore/net/TLSSNISupport.cc | 1 - 5 files changed, 3 insertions(+), 8 deletions(-) diff --git a/cmake/ExperimentalPlugins.cmake b/cmake/ExperimentalPlugins.cmake index 74347efde92..c546e7ebb5f 100644 --- a/cmake/ExperimentalPlugins.cmake +++ b/cmake/ExperimentalPlugins.cmake @@ -41,11 +41,7 @@ auto_option(HOOK_TRACE FEATURE_VAR BUILD_HOOK_TRACE DEFAULT ${_DEFAULT}) auto_option(HTTP_STATS FEATURE_VAR BUILD_HTTP_STATS DEFAULT ${_DEFAULT}) auto_option(ICAP FEATURE_VAR BUILD_ICAP DEFAULT ${_DEFAULT}) auto_option(INLINER FEATURE_VAR BUILD_INLINER DEFAULT ${_DEFAULT}) -auto_option( - JA4_FINGERPRINT FEATURE_VAR BUILD_JA4_FINGERPRINT VAR_DEPENDS - # HAVE_SSL_CTX_SET_CLIENT_HELLO_CB - DEFAULT ${_DEFAULT} -) +auto_option(JA4_FINGERPRINT FEATURE_VAR BUILD_JA4_FINGERPRINT VAR_DEPENDS DEFAULT ${_DEFAULT}) auto_option( MAGICK FEATURE_VAR diff --git a/plugins/experimental/ja4_fingerprint/README.md b/plugins/experimental/ja4_fingerprint/README.md index d45ddf00785..b1b4dd55c7d 100644 --- a/plugins/experimental/ja4_fingerprint/README.md +++ b/plugins/experimental/ja4_fingerprint/README.md @@ -21,6 +21,8 @@ The technical specification of the algorithm is available [here](https://github. These changes were made to simplify the plugin as much as possible. The missing features are useful and may be implemented in the future. +Ja4 now supports boringssl + ## Logging and Debugging To get debug information in the traffic log, enable the debug tag `ja4_fingerprint`. diff --git a/plugins/experimental/ja4_fingerprint/plugin.cc b/plugins/experimental/ja4_fingerprint/plugin.cc index 058eefd98fe..f0ef4aaa5ff 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -191,7 +191,6 @@ handle_client_hello(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata) // The VCONN_CLOSE handler is now responsible for freeing the resource. TSUserArgSet(ssl_vc, *get_user_arg_index(), static_cast(data.release())); } - TSVConnReenable(ssl_vc); return TS_SUCCESS; } diff --git a/src/iocore/net/SSLUtils.cc b/src/iocore/net/SSLUtils.cc index abebf2b9565..ede8f20543f 100644 --- a/src/iocore/net/SSLUtils.cc +++ b/src/iocore/net/SSLUtils.cc @@ -302,7 +302,6 @@ ssl_client_hello_callback(const SSL_CLIENT_HELLO *client_hello) { SSL *s = client_hello->ssl; TLSSNISupport::ClientHello ch = {client_hello}; - #endif TLSSNISupport *snis = TLSSNISupport::getInstance(s); diff --git a/src/iocore/net/TLSSNISupport.cc b/src/iocore/net/TLSSNISupport.cc index 3f5574eb9ee..b4ced5d632a 100644 --- a/src/iocore/net/TLSSNISupport.cc +++ b/src/iocore/net/TLSSNISupport.cc @@ -117,7 +117,6 @@ TLSSNISupport::on_client_hello(ClientHello &client_hello) const char *servername = nullptr; const unsigned char *p; size_t remaining, len; - // Parse the server name if the get extension call succeeds and there are more than 2 bytes to parse if (client_hello.getExtension(TLSEXT_TYPE_server_name, &p, &remaining) && remaining > 2) { // Parse to get to the name, originally from test/handshake_helper.c in openssl tree From 2bc38d078ea56105161be70a8de5ad0d4668913a Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Wed, 7 Jan 2026 15:37:16 +1100 Subject: [PATCH 06/18] cleanup code --- include/ts/ts.h | 10 +- .../experimental/ja4_fingerprint/plugin.cc | 121 ++++++------------ src/api/InkAPI.cc | 24 ++++ 3 files changed, 71 insertions(+), 84 deletions(-) diff --git a/include/ts/ts.h b/include/ts/ts.h index 892d58a0c6f..c2b42435679 100644 --- a/include/ts/ts.h +++ b/include/ts/ts.h @@ -1334,11 +1334,11 @@ int TSVConnIsSsl(TSVConn sslp); int TSVConnProvidedSslCert(TSVConn sslp); const char *TSVConnSslSniGet(TSVConn sslp, int *length); TSClientHello TSVConnClientHelloGet(TSVConn sslp); - -TSSslSession TSSslSessionGet(const TSSslSessionID *session_id); -int TSSslSessionGetBuffer(const TSSslSessionID *session_id, char *buffer, int *len_ptr); -TSReturnCode TSSslSessionInsert(const TSSslSessionID *session_id, TSSslSession add_session, TSSslConnection ssl_conn); -TSReturnCode TSSslSessionRemove(const TSSslSessionID *session_id); +TSReturnCode TSVConnClientHelloExtGet(TSClientHello ch, unsigned int type, const unsigned char **out, size_t *outlen); +TSSslSession TSSslSessionGet(const TSSslSessionID *session_id); +int TSSslSessionGetBuffer(const TSSslSessionID *session_id, char *buffer, int *len_ptr); +TSReturnCode TSSslSessionInsert(const TSSslSessionID *session_id, TSSslSession add_session, TSSslConnection ssl_conn); +TSReturnCode TSSslSessionRemove(const TSSslSessionID *session_id); /* -------------------------------------------------------------------------- HTTP transactions */ diff --git a/plugins/experimental/ja4_fingerprint/plugin.cc b/plugins/experimental/ja4_fingerprint/plugin.cc index f0ef4aaa5ff..ac37757b28b 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -54,22 +54,14 @@ static void register_hooks(); static int handle_client_hello(TSCont cont, TSEvent event, void *edata); char *get_IP(sockaddr const *s_sockaddr, char res[INET6_ADDRSTRLEN]); static void log_fingerprint(JA4_data const *data); -#ifdef OPENSSL_IS_BORINGSSL -static std::string get_fingerprint(const SSL_CLIENT_HELLO *ssl); -static std::uint16_t get_version(const SSL_CLIENT_HELLO *ssl); -static std::string get_first_ALPN(const SSL_CLIENT_HELLO *ssl); -static void add_ciphers(JA4::TLSClientHelloSummary &summary, const SSL_CLIENT_HELLO *ssl); -static void add_extensions(JA4::TLSClientHelloSummary &summary, const SSL_CLIENT_HELLO *ssl); -#else -static std::string get_fingerprint(SSL *ssl); -static std::uint16_t get_version(SSL *ssl); -static std::string get_first_ALPN(SSL *ssl); -static void add_ciphers(JA4::TLSClientHelloSummary &summary, SSL *ssl); -static void add_extensions(JA4::TLSClientHelloSummary &summary, SSL *ssl); -#endif -static std::string hash_with_SHA256(std::string_view sv); -static int handle_read_request_hdr(TSCont cont, TSEvent event, void *edata); -static void append_JA4_headers(TSCont cont, TSHttpTxn txnp, std::string const *fingerprint); +static std::string get_fingerprint(TSClientHello ch); +static std::uint16_t get_version(TSClientHello ch); +static std::string get_first_ALPN(TSClientHello ch); +static void add_ciphers(JA4::TLSClientHelloSummary &summary, TSClientHello ch); +static void add_extensions(JA4::TLSClientHelloSummary &summary, TSClientHello ch); +static std::string hash_with_SHA256(std::string_view sv); +static int handle_read_request_hdr(TSCont cont, TSEvent event, void *edata); +static void append_JA4_headers(TSCont cont, TSHttpTxn txnp, std::string const *fingerprint); static void append_to_field(TSMBuffer bufp, TSMLoc hdr_loc, char const *field, int field_len, char const *value, int value_len); static int handle_vconn_close(TSCont cont, TSEvent event, void *edata); @@ -83,7 +75,6 @@ constexpr std::string_view JA4_VIA_HEADER{"x-ja4-via"}; constexpr unsigned int EXT_ALPN{0x10}; constexpr unsigned int EXT_SUPPORTED_VERSIONS{0x2b}; -constexpr int SSL_SUCCESS{1}; DbgCtl dbg_ctl{PLUGIN_NAME}; @@ -175,17 +166,16 @@ handle_client_hello(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata) TSVConn const ssl_vc{static_cast(edata)}; #ifdef OPENSSL_IS_BORINGSSL - TSClientHello ssl_client_hello = TSVConnClientHelloGet(ssl_vc); - const SSL_CLIENT_HELLO *ssl = reinterpret_cast(ssl_client_hello); + TSClientHello ch = TSVConnClientHelloGet(ssl_vc); #else TSSslConnection const ssl_conn{TSVConnSslConnectionGet(ssl_vc)}; - SSL *ssl = reinterpret_cast(ssl_conn); + TSClientHello ch = reinterpret_cast(ssl_conn); #endif - if (nullptr == ssl) { - Dbg(dbg_ctl, "Could not get SSL object."); + if (nullptr == ch) { + Dbg(dbg_ctl, "Could not get TSClientHello object."); } else { auto data{std::make_unique()}; - data->fingerprint = get_fingerprint(ssl); + data->fingerprint = get_fingerprint(ch); get_IP(TSNetVConnRemoteAddrGet(ssl_vc), data->IP_addr); log_fingerprint(data.get()); // The VCONN_CLOSE handler is now responsible for freeing the resource. @@ -196,18 +186,14 @@ handle_client_hello(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata) } std::string -#ifdef OPENSSL_IS_BORINGSSL -get_fingerprint(const SSL_CLIENT_HELLO *ssl) -#else -get_fingerprint(SSL *ssl) -#endif +get_fingerprint(TSClientHello ch) { JA4::TLSClientHelloSummary summary{}; summary.protocol = JA4::Protocol::TLS; - summary.TLS_version = get_version(ssl); - summary.ALPN = get_first_ALPN(ssl); - add_ciphers(summary, ssl); - add_extensions(summary, ssl); + summary.TLS_version = get_version(ch); + summary.ALPN = get_first_ALPN(ch); + add_ciphers(summary, ch); + add_extensions(summary, ch); std::string result{JA4::make_JA4_fingerprint(summary, hash_with_SHA256)}; return result; } @@ -249,23 +235,11 @@ log_fingerprint(JA4_data const *data) } std::uint16_t -#ifdef OPENSSL_IS_BORINGSSL -get_version(const SSL_CLIENT_HELLO *client_hello) -#else -get_version(SSL *ssl) -#endif +get_version(TSClientHello ch) { unsigned char const *buf{}; std::size_t buflen{}; -#ifdef OPENSSL_IS_BORINGSSL - // If no extensions, fall back to legacy version field - if (!client_hello->extensions || client_hello->extensions_len == 0) { - return client_hello->version; - } - if (SSL_SUCCESS == SSL_early_callback_ctx_extension_get(client_hello, EXT_SUPPORTED_VERSIONS, &buf, &buflen)) { -#else - if (SSL_SUCCESS == SSL_client_hello_get0_ext(ssl, EXT_SUPPORTED_VERSIONS, &buf, &buflen)) { -#endif + if (TS_SUCCESS == TSVConnClientHelloExtGet(ch, EXT_SUPPORTED_VERSIONS, &buf, &buflen)) { std::uint16_t max_version{0}; uint8_t list_len = buf[0]; for (size_t i = 1; i + 1 < buflen && i < list_len + 1; i += 2) { @@ -278,28 +252,20 @@ get_version(SSL *ssl) } else { Dbg(dbg_ctl, "No supported_versions extension... using legacy version."); #ifdef OPENSSL_IS_BORINGSSL - return client_hello->version; + return reinterpret_cast(ch)->version; #else - return SSL_client_hello_get0_legacy_version(ssl); + return SSL_client_hello_get0_legacy_version(reinterpret_cast(ch)); #endif } } std::string -#ifdef OPENSSL_IS_BORINGSSL -get_first_ALPN(const SSL_CLIENT_HELLO *client_hello) -#else -get_first_ALPN(SSL *ssl) -#endif +get_first_ALPN(TSClientHello ch) { unsigned char const *buf{}; std::size_t buflen{}; std::string result{""}; -#ifdef OPENSSL_IS_BORINGSSL - if (SSL_SUCCESS == SSL_early_callback_ctx_extension_get(client_hello, EXT_ALPN, &buf, &buflen)) { -#else - if (SSL_SUCCESS == SSL_client_hello_get0_ext(ssl, EXT_ALPN, &buf, &buflen)) { -#endif + if (TS_SUCCESS == TSVConnClientHelloExtGet(ch, EXT_ALPN, &buf, &buflen)) { // The first two bytes are a 16bit encoding of the total length. unsigned char first_ALPN_length{buf[2]}; TSAssert(buflen > 4); @@ -310,35 +276,36 @@ get_first_ALPN(SSL *ssl) return result; } -#ifdef OPENSSL_IS_BORINGSSL void -add_ciphers(JA4::TLSClientHelloSummary &summary, const SSL_CLIENT_HELLO *client_hello) +add_ciphers(JA4::TLSClientHelloSummary &summary, TSClientHello ch) { - const uint8_t *buf = client_hello->cipher_suites; - size_t buflen = client_hello->cipher_suites_len; +#ifdef OPENSSL_IS_BORINGSSL + const SSL_CLIENT_HELLO *client_hello = reinterpret_cast(ch); + const uint8_t *buf = client_hello->cipher_suites; + size_t buflen = client_hello->cipher_suites_len; #else -void -add_ciphers(JA4::TLSClientHelloSummary &summary, SSL *ssl) -{ - unsigned char const *buf{}; - std::size_t buflen{SSL_client_hello_get0_ciphers(ssl, &buf)}; + unsigned char const *buf = nullptr; + // Fix: Add const_cast to remove const from ch + SSL *ssl = const_cast(reinterpret_cast(ch)); + std::size_t buflen = SSL_client_hello_get0_ciphers(ssl, &buf); #endif if (buflen > 0) { - for (std::size_t i{1}; i < buflen; i += 2) { - summary.add_cipher(make_word(buf[i], buf[i - 1])); + for (std::size_t i = 0; i + 1 < buflen; i += 2) { + summary.add_cipher(make_word(buf[i], buf[i + 1])); } } else { Dbg(dbg_ctl, "Failed to get ciphers."); } } -#ifdef OPENSSL_IS_BORINGSSL void -add_extensions(JA4::TLSClientHelloSummary &summary, const SSL_CLIENT_HELLO *client_hello) +add_extensions(JA4::TLSClientHelloSummary &summary, TSClientHello ch) { - const uint8_t *ext = client_hello->extensions; - size_t remaining = client_hello->extensions_len; +#ifdef OPENSSL_IS_BORINGSSL + const SSL_CLIENT_HELLO *client_hello = reinterpret_cast(ch); + const uint8_t *ext = client_hello->extensions; + size_t remaining = client_hello->extensions_len; while (remaining >= 4) { uint16_t ext_type = (ext[0] << 8) | ext[1]; @@ -354,21 +321,17 @@ add_extensions(JA4::TLSClientHelloSummary &summary, const SSL_CLIENT_HELLO *clie ext += total_ext_size; remaining -= total_ext_size; } -} #else -void -add_extensions(JA4::TLSClientHelloSummary &summary, SSL *ssl) -{ int *buf{}; std::size_t buflen{}; - if (SSL_SUCCESS == SSL_client_hello_get1_extensions_present(ssl, &buf, &buflen)) { + if (1 == SSL_client_hello_get1_extensions_present(reinterpret_cast(ch), &buf, &buflen)) { for (std::size_t i{1}; i < buflen; i += 2) { summary.add_extension(make_word(buf[i], buf[i - 1])); } } OPENSSL_free(buf); -} #endif +} std::string hash_with_SHA256(std::string_view sv) diff --git a/src/api/InkAPI.cc b/src/api/InkAPI.cc index f703e3607ab..c5e15145265 100644 --- a/src/api/InkAPI.cc +++ b/src/api/InkAPI.cc @@ -7906,6 +7906,30 @@ TSVConnClientHelloGet(TSVConn sslp) return nullptr; } +TSReturnCode +TSVConnClientHelloExtGet(TSClientHello ch, unsigned int type, const unsigned char **out, size_t *outlen) +{ + TSReturnCode retval = TS_SUCCESS; + + if (ch == nullptr) { + return TS_ERROR; + } + +#ifdef OPENSSL_IS_BORINGSSL + const SSL_CLIENT_HELLO *client_hello = reinterpret_cast(ch); + if (SSL_early_callback_ctx_extension_get(client_hello, type, out, outlen) == 1) { + return TS_SUCCESS; + } +#else + SSL *ssl = const_cast(reinterpret_cast(ch)); + if (SSL_client_hello_get0_ext(ssl, type, out, outlen) == 1) { + return TS_SUCCESS; + } +#endif + + return retval; +} + TSSslVerifyCTX TSVConnSslVerifyCTXGet(TSVConn sslp) { From fd0380651f9a69c53ec6c1016e11e128d9ebad37 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Thu, 8 Jan 2026 11:41:56 +1100 Subject: [PATCH 07/18] more cleanup --- include/ts/apidefs.h.in | 2 +- .../experimental/ja4_fingerprint/plugin.cc | 34 ++++++++++++++++--- src/api/InkAPI.cc | 26 ++------------ 3 files changed, 33 insertions(+), 29 deletions(-) diff --git a/include/ts/apidefs.h.in b/include/ts/apidefs.h.in index 32f7221a477..aac11c2f68d 100644 --- a/include/ts/apidefs.h.in +++ b/include/ts/apidefs.h.in @@ -1080,7 +1080,7 @@ using TSHostLookupResult = struct tsapi_hostlookupresult *; using TSAIOCallback = struct tsapi_aiocallback *; using TSAcceptor = struct tsapi_net_accept *; using TSRemapPluginInfo = struct tsapi_remap_plugin_info *; -using TSClientHello = const void *; +using TSClientHello = void *; using TSFetchSM = struct tsapi_fetchsm *; diff --git a/plugins/experimental/ja4_fingerprint/plugin.cc b/plugins/experimental/ja4_fingerprint/plugin.cc index ac37757b28b..3c5d0efaca1 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -64,6 +64,7 @@ static int handle_read_request_hdr(TSCont cont, TSEvent event, vo static void append_JA4_headers(TSCont cont, TSHttpTxn txnp, std::string const *fingerprint); static void append_to_field(TSMBuffer bufp, TSMLoc hdr_loc, char const *field, int field_len, char const *value, int value_len); static int handle_vconn_close(TSCont cont, TSEvent event, void *edata); +int client_hello_ext_get(TSClientHello ch, unsigned int type, const unsigned char **out, size_t *outlen); namespace { @@ -171,6 +172,7 @@ handle_client_hello(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata) TSSslConnection const ssl_conn{TSVConnSslConnectionGet(ssl_vc)}; TSClientHello ch = reinterpret_cast(ssl_conn); #endif + if (nullptr == ch) { Dbg(dbg_ctl, "Could not get TSClientHello object."); } else { @@ -239,7 +241,7 @@ get_version(TSClientHello ch) { unsigned char const *buf{}; std::size_t buflen{}; - if (TS_SUCCESS == TSVConnClientHelloExtGet(ch, EXT_SUPPORTED_VERSIONS, &buf, &buflen)) { + if (TS_SUCCESS == client_hello_ext_get(ch, EXT_SUPPORTED_VERSIONS, &buf, &buflen)) { std::uint16_t max_version{0}; uint8_t list_len = buf[0]; for (size_t i = 1; i + 1 < buflen && i < list_len + 1; i += 2) { @@ -265,7 +267,7 @@ get_first_ALPN(TSClientHello ch) unsigned char const *buf{}; std::size_t buflen{}; std::string result{""}; - if (TS_SUCCESS == TSVConnClientHelloExtGet(ch, EXT_ALPN, &buf, &buflen)) { + if (TS_SUCCESS == client_hello_ext_get(ch, EXT_ALPN, &buf, &buflen)) { // The first two bytes are a 16bit encoding of the total length. unsigned char first_ALPN_length{buf[2]}; TSAssert(buflen > 4); @@ -286,7 +288,7 @@ add_ciphers(JA4::TLSClientHelloSummary &summary, TSClientHello ch) #else unsigned char const *buf = nullptr; // Fix: Add const_cast to remove const from ch - SSL *ssl = const_cast(reinterpret_cast(ch)); + SSL *ssl = const_cast(reinterpret_cast(ch)); std::size_t buflen = SSL_client_hello_get0_ciphers(ssl, &buf); #endif @@ -324,7 +326,7 @@ add_extensions(JA4::TLSClientHelloSummary &summary, TSClientHello ch) #else int *buf{}; std::size_t buflen{}; - if (1 == SSL_client_hello_get1_extensions_present(reinterpret_cast(ch), &buf, &buflen)) { + if (1 == SSL_client_hello_get1_extensions_present(const_cast(reinterpret_cast(ch)), &buf, &buflen)) { for (std::size_t i{1}; i < buflen; i += 2) { summary.add_extension(make_word(buf[i], buf[i - 1])); } @@ -436,3 +438,27 @@ handle_vconn_close(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata) TSVConnReenable(ssl_vc); return TS_SUCCESS; } + +int +client_hello_ext_get(TSClientHello ch, unsigned int type, const unsigned char **out, size_t *outlen) +{ + TSReturnCode retval = TS_SUCCESS; + + if (ch == nullptr) { + return TS_ERROR; + } + +#ifdef OPENSSL_IS_BORINGSSL + const SSL_CLIENT_HELLO *client_hello = reinterpret_cast(ch); + if (SSL_early_callback_ctx_extension_get(client_hello, type, out, outlen) == 1) { + return TS_SUCCESS; + } +#else + SSL *ssl = const_cast(reinterpret_cast(ch)); + if (SSL_client_hello_get0_ext(ssl, type, out, outlen) == 1) { + return TS_SUCCESS; + } +#endif + + return retval; +} diff --git a/src/api/InkAPI.cc b/src/api/InkAPI.cc index c5e15145265..b55ad7401df 100644 --- a/src/api/InkAPI.cc +++ b/src/api/InkAPI.cc @@ -7890,6 +7890,7 @@ TSVConnSslSniGet(TSVConn sslp, int *length) return server_name; } +#ifdef OPENSSL_IS_BORINGSSL TSClientHello TSVConnClientHelloGet(TSVConn sslp) { @@ -7900,36 +7901,13 @@ TSVConnClientHelloGet(TSVConn sslp) if (auto snis = netvc->get_service(); snis) { ClientHelloContainer client_hello = snis->get_client_hello_container(); - return reinterpret_cast(client_hello); + return reinterpret_cast(const_cast(client_hello)); } return nullptr; } - -TSReturnCode -TSVConnClientHelloExtGet(TSClientHello ch, unsigned int type, const unsigned char **out, size_t *outlen) -{ - TSReturnCode retval = TS_SUCCESS; - - if (ch == nullptr) { - return TS_ERROR; - } - -#ifdef OPENSSL_IS_BORINGSSL - const SSL_CLIENT_HELLO *client_hello = reinterpret_cast(ch); - if (SSL_early_callback_ctx_extension_get(client_hello, type, out, outlen) == 1) { - return TS_SUCCESS; - } -#else - SSL *ssl = const_cast(reinterpret_cast(ch)); - if (SSL_client_hello_get0_ext(ssl, type, out, outlen) == 1) { - return TS_SUCCESS; - } #endif - return retval; -} - TSSslVerifyCTX TSVConnSslVerifyCTXGet(TSVConn sslp) { From 9b76bd0d34574e7362476f169f3376fb79561675 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Thu, 8 Jan 2026 11:45:44 +1100 Subject: [PATCH 08/18] Update plugin.cc --- plugins/experimental/ja4_fingerprint/plugin.cc | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/experimental/ja4_fingerprint/plugin.cc b/plugins/experimental/ja4_fingerprint/plugin.cc index 3c5d0efaca1..b39dd8470e6 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -442,8 +442,6 @@ handle_vconn_close(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata) int client_hello_ext_get(TSClientHello ch, unsigned int type, const unsigned char **out, size_t *outlen) { - TSReturnCode retval = TS_SUCCESS; - if (ch == nullptr) { return TS_ERROR; } @@ -460,5 +458,5 @@ client_hello_ext_get(TSClientHello ch, unsigned int type, const unsigned char ** } #endif - return retval; + return TS_ERROR; } From 51ebe553ef8c821517239dab282859ddc37aa5f4 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Thu, 8 Jan 2026 12:07:00 +1100 Subject: [PATCH 09/18] Update plugin.cc --- plugins/experimental/ja4_fingerprint/plugin.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/experimental/ja4_fingerprint/plugin.cc b/plugins/experimental/ja4_fingerprint/plugin.cc index b39dd8470e6..2fe53d1d97b 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -243,7 +243,7 @@ get_version(TSClientHello ch) std::size_t buflen{}; if (TS_SUCCESS == client_hello_ext_get(ch, EXT_SUPPORTED_VERSIONS, &buf, &buflen)) { std::uint16_t max_version{0}; - uint8_t list_len = buf[0]; + size_t list_len = buf[0]; for (size_t i = 1; i + 1 < buflen && i < list_len + 1; i += 2) { std::uint16_t version = (buf[i] << 8) | buf[i + 1]; if (!JA4::is_GREASE(version) && version > max_version) { From b57f3384e1f61aa6f0fe8adce54d9a7173b0404d Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Thu, 8 Jan 2026 12:07:45 +1100 Subject: [PATCH 10/18] Update ts.h --- include/ts/ts.h | 1 - 1 file changed, 1 deletion(-) diff --git a/include/ts/ts.h b/include/ts/ts.h index c2b42435679..985f0693291 100644 --- a/include/ts/ts.h +++ b/include/ts/ts.h @@ -1334,7 +1334,6 @@ int TSVConnIsSsl(TSVConn sslp); int TSVConnProvidedSslCert(TSVConn sslp); const char *TSVConnSslSniGet(TSVConn sslp, int *length); TSClientHello TSVConnClientHelloGet(TSVConn sslp); -TSReturnCode TSVConnClientHelloExtGet(TSClientHello ch, unsigned int type, const unsigned char **out, size_t *outlen); TSSslSession TSSslSessionGet(const TSSslSessionID *session_id); int TSSslSessionGetBuffer(const TSSslSessionID *session_id, char *buffer, int *len_ptr); TSReturnCode TSSslSessionInsert(const TSSslSessionID *session_id, TSSslSession add_session, TSSslConnection ssl_conn); From 162af784db8be4f4117a8f66a9948a51279a1d46 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Thu, 8 Jan 2026 13:58:53 +1100 Subject: [PATCH 11/18] Update apidefs.h.in --- include/ts/apidefs.h.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/ts/apidefs.h.in b/include/ts/apidefs.h.in index aac11c2f68d..b6843e51153 100644 --- a/include/ts/apidefs.h.in +++ b/include/ts/apidefs.h.in @@ -1080,7 +1080,7 @@ using TSHostLookupResult = struct tsapi_hostlookupresult *; using TSAIOCallback = struct tsapi_aiocallback *; using TSAcceptor = struct tsapi_net_accept *; using TSRemapPluginInfo = struct tsapi_remap_plugin_info *; -using TSClientHello = void *; +using TSClientHello = struct tsapi_ssl_client_hello *; using TSFetchSM = struct tsapi_fetchsm *; From a42ae48cdc4d21c36067e6521c6fe5c179f4a17c Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Wed, 21 Jan 2026 10:08:03 +1100 Subject: [PATCH 12/18] Update to make more clean --- include/ts/apidefs.h.in | 14 +++- include/ts/ts.h | 10 ++- .../experimental/ja4_fingerprint/plugin.cc | 84 ++++++++----------- src/api/InkAPI.cc | 76 ++++++++++++++++- 4 files changed, 128 insertions(+), 56 deletions(-) diff --git a/include/ts/apidefs.h.in b/include/ts/apidefs.h.in index b6843e51153..4a254ced081 100644 --- a/include/ts/apidefs.h.in +++ b/include/ts/apidefs.h.in @@ -1080,7 +1080,19 @@ using TSHostLookupResult = struct tsapi_hostlookupresult *; using TSAIOCallback = struct tsapi_aiocallback *; using TSAcceptor = struct tsapi_net_accept *; using TSRemapPluginInfo = struct tsapi_remap_plugin_info *; -using TSClientHello = struct tsapi_ssl_client_hello *; + +struct tsapi_ssl_client_hello { + uint16_t version; + const uint8_t *cipher_suites; + size_t cipher_suites_len; + const uint8_t *extensions; + size_t extensions_len; + int *extension_ids; + size_t extension_ids_len; + void *ssl_ptr; +}; + +using TSClientHello = struct tsapi_ssl_client_hello *; using TSFetchSM = struct tsapi_fetchsm *; diff --git a/include/ts/ts.h b/include/ts/ts.h index 985f0693291..514470d9431 100644 --- a/include/ts/ts.h +++ b/include/ts/ts.h @@ -1334,10 +1334,12 @@ int TSVConnIsSsl(TSVConn sslp); int TSVConnProvidedSslCert(TSVConn sslp); const char *TSVConnSslSniGet(TSVConn sslp, int *length); TSClientHello TSVConnClientHelloGet(TSVConn sslp); -TSSslSession TSSslSessionGet(const TSSslSessionID *session_id); -int TSSslSessionGetBuffer(const TSSslSessionID *session_id, char *buffer, int *len_ptr); -TSReturnCode TSSslSessionInsert(const TSSslSessionID *session_id, TSSslSession add_session, TSSslConnection ssl_conn); -TSReturnCode TSSslSessionRemove(const TSSslSessionID *session_id); +void TSClientHelloDestroy(TSClientHello ch); + +TSSslSession TSSslSessionGet(const TSSslSessionID *session_id); +int TSSslSessionGetBuffer(const TSSslSessionID *session_id, char *buffer, int *len_ptr); +TSReturnCode TSSslSessionInsert(const TSSslSessionID *session_id, TSSslSession add_session, TSSslConnection ssl_conn); +TSReturnCode TSSslSessionRemove(const TSSslSessionID *session_id); /* -------------------------------------------------------------------------- HTTP transactions */ diff --git a/plugins/experimental/ja4_fingerprint/plugin.cc b/plugins/experimental/ja4_fingerprint/plugin.cc index 2fe53d1d97b..aa8c9266ba4 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -166,12 +166,7 @@ handle_client_hello(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata) TSVConn const ssl_vc{static_cast(edata)}; -#ifdef OPENSSL_IS_BORINGSSL TSClientHello ch = TSVConnClientHelloGet(ssl_vc); -#else - TSSslConnection const ssl_conn{TSVConnSslConnectionGet(ssl_vc)}; - TSClientHello ch = reinterpret_cast(ssl_conn); -#endif if (nullptr == ch) { Dbg(dbg_ctl, "Could not get TSClientHello object."); @@ -180,6 +175,8 @@ handle_client_hello(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata) data->fingerprint = get_fingerprint(ch); get_IP(TSNetVConnRemoteAddrGet(ssl_vc), data->IP_addr); log_fingerprint(data.get()); + // Clean up the TSClientHello structure + TSClientHelloDestroy(ch); // The VCONN_CLOSE handler is now responsible for freeing the resource. TSUserArgSet(ssl_vc, *get_user_arg_index(), static_cast(data.release())); } @@ -253,11 +250,7 @@ get_version(TSClientHello ch) return max_version; } else { Dbg(dbg_ctl, "No supported_versions extension... using legacy version."); -#ifdef OPENSSL_IS_BORINGSSL - return reinterpret_cast(ch)->version; -#else - return SSL_client_hello_get0_legacy_version(reinterpret_cast(ch)); -#endif + return ch->version; } } @@ -281,16 +274,8 @@ get_first_ALPN(TSClientHello ch) void add_ciphers(JA4::TLSClientHelloSummary &summary, TSClientHello ch) { -#ifdef OPENSSL_IS_BORINGSSL - const SSL_CLIENT_HELLO *client_hello = reinterpret_cast(ch); - const uint8_t *buf = client_hello->cipher_suites; - size_t buflen = client_hello->cipher_suites_len; -#else - unsigned char const *buf = nullptr; - // Fix: Add const_cast to remove const from ch - SSL *ssl = const_cast(reinterpret_cast(ch)); - std::size_t buflen = SSL_client_hello_get0_ciphers(ssl, &buf); -#endif + const uint8_t *buf = ch->cipher_suites; + size_t buflen = ch->cipher_suites_len; if (buflen > 0) { for (std::size_t i = 0; i + 1 < buflen; i += 2) { @@ -304,35 +289,28 @@ add_ciphers(JA4::TLSClientHelloSummary &summary, TSClientHello ch) void add_extensions(JA4::TLSClientHelloSummary &summary, TSClientHello ch) { -#ifdef OPENSSL_IS_BORINGSSL - const SSL_CLIENT_HELLO *client_hello = reinterpret_cast(ch); - const uint8_t *ext = client_hello->extensions; - size_t remaining = client_hello->extensions_len; - - while (remaining >= 4) { - uint16_t ext_type = (ext[0] << 8) | ext[1]; - - // Read extension length (2 bytes, big endian) - uint16_t ext_len = (ext[2] << 8) | ext[3]; - summary.add_extension(ext_type); - size_t total_ext_size = 4 + ext_len; // 4 bytes header + data - if (total_ext_size > remaining) { - break; // Malformed extension, stop parsing - } + if (ch->extensions != nullptr) { + const uint8_t *ext = ch->extensions; + size_t remaining = ch->extensions_len; + + while (remaining >= 4) { + uint16_t ext_type = (ext[0] << 8) | ext[1]; + uint16_t ext_len = (ext[2] << 8) | ext[3]; + summary.add_extension(ext_type); + size_t total_ext_size = 4 + ext_len; + if (total_ext_size > remaining) { + break; + } - ext += total_ext_size; - remaining -= total_ext_size; - } -#else - int *buf{}; - std::size_t buflen{}; - if (1 == SSL_client_hello_get1_extensions_present(const_cast(reinterpret_cast(ch)), &buf, &buflen)) { - for (std::size_t i{1}; i < buflen; i += 2) { - summary.add_extension(make_word(buf[i], buf[i - 1])); + ext += total_ext_size; + remaining -= total_ext_size; + } + } else if (ch->extension_ids != nullptr) { + // OpenSSL's extension_ids is an array of ints, each element is a complete extension ID + for (std::size_t i = 0; i < ch->extension_ids_len; i++) { + summary.add_extension(static_cast(ch->extension_ids[i])); } } - OPENSSL_free(buf); -#endif } std::string @@ -442,17 +420,25 @@ handle_vconn_close(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata) int client_hello_ext_get(TSClientHello ch, unsigned int type, const unsigned char **out, size_t *outlen) { - if (ch == nullptr) { + if (ch == nullptr || out == nullptr || outlen == nullptr) { return TS_ERROR; } #ifdef OPENSSL_IS_BORINGSSL - const SSL_CLIENT_HELLO *client_hello = reinterpret_cast(ch); + const SSL_CLIENT_HELLO *client_hello = static_cast(ch->ssl_ptr); + if (client_hello == nullptr) { + return TS_ERROR; + } + if (SSL_early_callback_ctx_extension_get(client_hello, type, out, outlen) == 1) { return TS_SUCCESS; } #else - SSL *ssl = const_cast(reinterpret_cast(ch)); + SSL *ssl = static_cast(ch->ssl_ptr); + if (ssl == nullptr) { + return TS_ERROR; + } + if (SSL_client_hello_get0_ext(ssl, type, out, outlen) == 1) { return TS_SUCCESS; } diff --git a/src/api/InkAPI.cc b/src/api/InkAPI.cc index b55ad7401df..2ec6b8be7ef 100644 --- a/src/api/InkAPI.cc +++ b/src/api/InkAPI.cc @@ -7890,7 +7890,6 @@ TSVConnSslSniGet(TSVConn sslp, int *length) return server_name; } -#ifdef OPENSSL_IS_BORINGSSL TSClientHello TSVConnClientHelloGet(TSVConn sslp) { @@ -7900,14 +7899,87 @@ TSVConnClientHelloGet(TSVConn sslp) } if (auto snis = netvc->get_service(); snis) { + // Allocate the TSClientHello structure + auto ch = new tsapi_ssl_client_hello(); + +#ifdef OPENSSL_IS_BORINGSSL + // Get the BoringSSL client hello container ClientHelloContainer client_hello = snis->get_client_hello_container(); - return reinterpret_cast(const_cast(client_hello)); + if (client_hello == nullptr) { + delete ch; + return nullptr; + } + + // Populate from BoringSSL SSL_CLIENT_HELLO structure + ch->version = client_hello->version; + ch->cipher_suites = client_hello->cipher_suites; + ch->cipher_suites_len = client_hello->cipher_suites_len; + ch->extensions = client_hello->extensions; + ch->extensions_len = client_hello->extensions_len; + ch->extension_ids = nullptr; + ch->extension_ids_len = 0; + ch->ssl_ptr = const_cast(client_hello); +#else + // Get the OpenSSL SSL* object + auto tbs = netvc->get_service(); + if (!tbs) { + delete ch; + return nullptr; + } + SSL *ssl = tbs->get_tls_handle(); + if (ssl == nullptr) { + delete ch; + return nullptr; + } + + // Get legacy version (OpenSSL doesn't expose the direct version field from client hello) + ch->version = SSL_client_hello_get0_legacy_version(ssl); + + // Get cipher suites + const unsigned char *cipher_buf = nullptr; + ch->cipher_suites_len = SSL_client_hello_get0_ciphers(ssl, &cipher_buf); + ch->cipher_suites = cipher_buf; + + // For OpenSSL, we can't get direct access to the raw extensions buffer + // Instead, get the list of extension IDs + ch->extensions = nullptr; + ch->extensions_len = 0; + int *ext_ids = nullptr; + size_t ext_count; + if (SSL_client_hello_get1_extensions_present(ssl, &ext_ids, &ext_count) == 1) { + ch->extension_ids = ext_ids; + ch->extension_ids_len = ext_count; + } else { + ch->extension_ids = nullptr; + ch->extension_ids_len = 0; + } + ch->ssl_ptr = ssl; +#endif + + return ch; } return nullptr; } + +void +TSClientHelloDestroy(TSClientHello ch) +{ + if (ch == nullptr) { + return; + } + +#ifndef OPENSSL_IS_BORINGSSL + // For OpenSSL, we need to free the extension IDs array that was allocated + // by SSL_client_hello_get1_extensions_present + if (ch->extension_ids != nullptr) { + OPENSSL_free(ch->extension_ids); + } #endif + delete ch; +} + TSSslVerifyCTX TSVConnSslVerifyCTXGet(TSVConn sslp) { From 19bbc064d7ebe2c6d8c533b29f3f9451f49b6ec7 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Wed, 28 Jan 2026 14:20:10 +1100 Subject: [PATCH 13/18] Update data --- include/ts/apidefs.h.in | 94 ++++++++++++++++--- include/ts/ts.h | 76 ++++++++++++++- .../experimental/ja4_fingerprint/plugin.cc | 57 +++-------- src/api/InkAPI.cc | 59 ++++++++---- 4 files changed, 207 insertions(+), 79 deletions(-) diff --git a/include/ts/apidefs.h.in b/include/ts/apidefs.h.in index 4a254ced081..d2c8f7d67d6 100644 --- a/include/ts/apidefs.h.in +++ b/include/ts/apidefs.h.in @@ -1044,6 +1044,86 @@ struct TSHttp2Priority { * or -1 if the stream has no dependency. */ int32_t stream_dependency; }; +/** + * A structure for SSL Client Hello data + */ +struct tsapi_ssl_client_hello { + uint16_t version{0}; + const uint8_t *cipher_suites{nullptr}; + size_t cipher_suites_len{0}; + const uint8_t *extensions{nullptr}; + size_t extensions_len{0}; + int *extension_ids{nullptr}; + size_t extension_ids_len{0}; + void *ssl_ptr{nullptr}; +}; + +// Wrapper class that provides controlled access to client hello data +class TSClientHelloImpl +{ +public: + TSClientHelloImpl(tsapi_ssl_client_hello *ch) : _ssl_client_hello(ch) {} + + ~TSClientHelloImpl() { delete _ssl_client_hello; } + + uint16_t + get_version() const + { + return _ssl_client_hello->version; + } + + const uint8_t * + get_cipher_suites() const + { + return _ssl_client_hello->cipher_suites; + } + + size_t + get_cipher_suites_len() const + { + return _ssl_client_hello->cipher_suites_len; + } + + const uint8_t * + get_extensions() const + { + return _ssl_client_hello->extensions; + } + + size_t + get_extensions_len() const + { + return _ssl_client_hello->extensions_len; + } + + const int * + get_extension_ids() const + { + return _ssl_client_hello->extension_ids; + } + + size_t + get_extension_ids_len() const + { + return _ssl_client_hello->extension_ids_len; + } + + void * + get_ssl_ptr() const + { + return _ssl_client_hello->ssl_ptr; + } + + // Internal accessor for API implementation + tsapi_ssl_client_hello * + _get_internal() const + { + return _ssl_client_hello; + } + +private: + tsapi_ssl_client_hello *_ssl_client_hello; +}; using TSFile = struct tsapi_file *; @@ -1080,19 +1160,7 @@ using TSHostLookupResult = struct tsapi_hostlookupresult *; using TSAIOCallback = struct tsapi_aiocallback *; using TSAcceptor = struct tsapi_net_accept *; using TSRemapPluginInfo = struct tsapi_remap_plugin_info *; - -struct tsapi_ssl_client_hello { - uint16_t version; - const uint8_t *cipher_suites; - size_t cipher_suites_len; - const uint8_t *extensions; - size_t extensions_len; - int *extension_ids; - size_t extension_ids_len; - void *ssl_ptr; -}; - -using TSClientHello = struct tsapi_ssl_client_hello *; +using TSClientHello = TSClientHelloImpl *; using TSFetchSM = struct tsapi_fetchsm *; diff --git a/include/ts/ts.h b/include/ts/ts.h index 514470d9431..519dbf16f79 100644 --- a/include/ts/ts.h +++ b/include/ts/ts.h @@ -1331,10 +1331,80 @@ TSReturnCode TSVConnProtocolEnable(TSVConn connp, const char *protocol_name); int TSVConnIsSsl(TSVConn sslp); /* Returns 1 if a certificate was provided in the TLS handshake, 0 otherwise. */ -int TSVConnProvidedSslCert(TSVConn sslp); -const char *TSVConnSslSniGet(TSVConn sslp, int *length); +int TSVConnProvidedSslCert(TSVConn sslp); +const char *TSVConnSslSniGet(TSVConn sslp, int *length); + +/** + Retrieve TLS Client Hello information from an SSL virtual connection. + + This function extracts TLS Client Hello data from a TLS handshake. + The returned object provides access to version, cipher suites, and extensions + in a way that is portable across both BoringSSL and OpenSSL implementations. + + IMPORTANT: This function must be called during the TS_SSL_CLIENT_HELLO_HOOK. + The underlying SSL context may not be available at other hooks, particularly + for BoringSSL where the SSL_CLIENT_HELLO structure is only valid during + specific callback functions. Calling this function outside of the client + hello hook may result in nullptr being returned. + + For BoringSSL, the Client Hello data is copied from the SSL_CLIENT_HELLO + structure. For OpenSSL, cipher suites and extension IDs are extracted using + SSL_client_hello_get0_* functions. + + Memory Management: The caller must call TSClientHelloDestroy() to free the + returned object when it is no longer needed. Failure to do so will result + in memory leaks, especially for OpenSSL which allocates memory for the + extension IDs array. + + @param sslp The SSL virtual connection handle. Must not be nullptr. + @return Pointer to TSClientHello object containing Client Hello data, or + nullptr if the client hello is not available or if an error occurs. + + @see TSClientHelloDestroy + @see TSClientHelloExtensionGet + */ TSClientHello TSVConnClientHelloGet(TSVConn sslp); -void TSClientHelloDestroy(TSClientHello ch); +/** + Destroys a Client Hello object and frees associated memory. + + This function must be called to properly free a TSClientHello object + obtained from TSVConnClientHelloGet(). It handles SSL library-specific + cleanup, including freeing the extension IDs array allocated by OpenSSL's + SSL_client_hello_get1_extensions_present() function. + + @param ch The Client Hello object to destroy. + + @see TSVConnClientHelloGet + */ +void TSClientHelloDestroy(TSClientHello ch); + +/** + Retrieve a specific TLS extension from the Client Hello. + + This function looks up a TLS extension by its type (e.g., 0x10 for ALPN, + 0x00 for SNI) and returns a pointer to its data. The lookup is performed + using SSL library-specific functions that work with both BoringSSL and + OpenSSL without requiring conditional compilation in the plugin. + + The returned buffer is still owned by the underlying SSL context and must + not be freed by the caller. The buffer is valid only as long as the + TSClientHello object has not been destroyed. + + @param ch The Client Hello object obtained from TSVConnClientHelloGet(). + Must not be nullptr. + @param type The TLS extension type to retrieve. + @param out Pointer to receive the extension data buffer. Must not be nullptr. + @param outlen Pointer to receive the length of the extension data in bytes. + Must not be nullptr. + + @return TS_SUCCESS if the extension was found and retrieved successfully. + TS_ERROR if the extension is not present, or if any parameter is nullptr, + or if an error occurred during lookup. + + @see TSVConnClientHelloGet + @see TSClientHelloDestroy + */ +TSReturnCode TSClientHelloExtensionGet(TSClientHello ch, unsigned int type, const unsigned char **out, size_t *outlen); TSSslSession TSSslSessionGet(const TSSslSessionID *session_id); int TSSslSessionGetBuffer(const TSSslSessionID *session_id, char *buffer, int *len_ptr); diff --git a/plugins/experimental/ja4_fingerprint/plugin.cc b/plugins/experimental/ja4_fingerprint/plugin.cc index aa8c9266ba4..44eb09daadb 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -64,7 +64,6 @@ static int handle_read_request_hdr(TSCont cont, TSEvent event, vo static void append_JA4_headers(TSCont cont, TSHttpTxn txnp, std::string const *fingerprint); static void append_to_field(TSMBuffer bufp, TSMLoc hdr_loc, char const *field, int field_len, char const *value, int value_len); static int handle_vconn_close(TSCont cont, TSEvent event, void *edata); -int client_hello_ext_get(TSClientHello ch, unsigned int type, const unsigned char **out, size_t *outlen); namespace { @@ -238,7 +237,7 @@ get_version(TSClientHello ch) { unsigned char const *buf{}; std::size_t buflen{}; - if (TS_SUCCESS == client_hello_ext_get(ch, EXT_SUPPORTED_VERSIONS, &buf, &buflen)) { + if (TS_SUCCESS == TSClientHelloExtensionGet(ch, EXT_SUPPORTED_VERSIONS, &buf, &buflen)) { std::uint16_t max_version{0}; size_t list_len = buf[0]; for (size_t i = 1; i + 1 < buflen && i < list_len + 1; i += 2) { @@ -250,7 +249,7 @@ get_version(TSClientHello ch) return max_version; } else { Dbg(dbg_ctl, "No supported_versions extension... using legacy version."); - return ch->version; + return ch->get_version(); } } @@ -260,7 +259,7 @@ get_first_ALPN(TSClientHello ch) unsigned char const *buf{}; std::size_t buflen{}; std::string result{""}; - if (TS_SUCCESS == client_hello_ext_get(ch, EXT_ALPN, &buf, &buflen)) { + if (TS_SUCCESS == TSClientHelloExtensionGet(ch, EXT_ALPN, &buf, &buflen)) { // The first two bytes are a 16bit encoding of the total length. unsigned char first_ALPN_length{buf[2]}; TSAssert(buflen > 4); @@ -274,8 +273,8 @@ get_first_ALPN(TSClientHello ch) void add_ciphers(JA4::TLSClientHelloSummary &summary, TSClientHello ch) { - const uint8_t *buf = ch->cipher_suites; - size_t buflen = ch->cipher_suites_len; + const uint8_t *buf = ch->get_cipher_suites(); + size_t buflen = ch->get_cipher_suites_len(); if (buflen > 0) { for (std::size_t i = 0; i + 1 < buflen; i += 2) { @@ -289,9 +288,10 @@ add_ciphers(JA4::TLSClientHelloSummary &summary, TSClientHello ch) void add_extensions(JA4::TLSClientHelloSummary &summary, TSClientHello ch) { - if (ch->extensions != nullptr) { - const uint8_t *ext = ch->extensions; - size_t remaining = ch->extensions_len; + // For BoringSSL, we have direct access to the extensions buffer + if (ch->get_extensions() != nullptr) { + const uint8_t *ext = ch->get_extensions(); + size_t remaining = ch->get_extensions_len(); while (remaining >= 4) { uint16_t ext_type = (ext[0] << 8) | ext[1]; @@ -305,10 +305,12 @@ add_extensions(JA4::TLSClientHelloSummary &summary, TSClientHello ch) ext += total_ext_size; remaining -= total_ext_size; } - } else if (ch->extension_ids != nullptr) { + } + // For OpenSSL, we use the extension IDs array + else if (ch->get_extension_ids() != nullptr) { // OpenSSL's extension_ids is an array of ints, each element is a complete extension ID - for (std::size_t i = 0; i < ch->extension_ids_len; i++) { - summary.add_extension(static_cast(ch->extension_ids[i])); + for (std::size_t i = 0; i < ch->get_extension_ids_len(); i++) { + summary.add_extension(static_cast(ch->get_extension_ids()[i])); } } } @@ -409,40 +411,9 @@ handle_vconn_close(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata) // We ignore the event, but we don't want to reject the connection. return TS_SUCCESS; } - TSVConn const ssl_vc{static_cast(edata)}; delete static_cast(TSUserArgGet(ssl_vc, *get_user_arg_index())); TSUserArgSet(ssl_vc, *get_user_arg_index(), nullptr); TSVConnReenable(ssl_vc); return TS_SUCCESS; } - -int -client_hello_ext_get(TSClientHello ch, unsigned int type, const unsigned char **out, size_t *outlen) -{ - if (ch == nullptr || out == nullptr || outlen == nullptr) { - return TS_ERROR; - } - -#ifdef OPENSSL_IS_BORINGSSL - const SSL_CLIENT_HELLO *client_hello = static_cast(ch->ssl_ptr); - if (client_hello == nullptr) { - return TS_ERROR; - } - - if (SSL_early_callback_ctx_extension_get(client_hello, type, out, outlen) == 1) { - return TS_SUCCESS; - } -#else - SSL *ssl = static_cast(ch->ssl_ptr); - if (ssl == nullptr) { - return TS_ERROR; - } - - if (SSL_client_hello_get0_ext(ssl, type, out, outlen) == 1) { - return TS_SUCCESS; - } -#endif - - return TS_ERROR; -} diff --git a/src/api/InkAPI.cc b/src/api/InkAPI.cc index 2ec6b8be7ef..4beb642b066 100644 --- a/src/api/InkAPI.cc +++ b/src/api/InkAPI.cc @@ -7899,7 +7899,6 @@ TSVConnClientHelloGet(TSVConn sslp) } if (auto snis = netvc->get_service(); snis) { - // Allocate the TSClientHello structure auto ch = new tsapi_ssl_client_hello(); #ifdef OPENSSL_IS_BORINGSSL @@ -7916,8 +7915,6 @@ TSVConnClientHelloGet(TSVConn sslp) ch->cipher_suites_len = client_hello->cipher_suites_len; ch->extensions = client_hello->extensions; ch->extensions_len = client_hello->extensions_len; - ch->extension_ids = nullptr; - ch->extension_ids_len = 0; ch->ssl_ptr = const_cast(client_hello); #else // Get the OpenSSL SSL* object @@ -7936,27 +7933,24 @@ TSVConnClientHelloGet(TSVConn sslp) ch->version = SSL_client_hello_get0_legacy_version(ssl); // Get cipher suites - const unsigned char *cipher_buf = nullptr; - ch->cipher_suites_len = SSL_client_hello_get0_ciphers(ssl, &cipher_buf); - ch->cipher_suites = cipher_buf; + const unsigned char *cipher_buf = nullptr; + size_t cipher_buf_len = SSL_client_hello_get0_ciphers(ssl, &cipher_buf); + ch->cipher_suites = cipher_buf; + ch->cipher_suites_len = cipher_buf_len; // For OpenSSL, we can't get direct access to the raw extensions buffer // Instead, get the list of extension IDs - ch->extensions = nullptr; - ch->extensions_len = 0; - int *ext_ids = nullptr; + int *ext_ids = nullptr; size_t ext_count; if (SSL_client_hello_get1_extensions_present(ssl, &ext_ids, &ext_count) == 1) { ch->extension_ids = ext_ids; ch->extension_ids_len = ext_count; - } else { - ch->extension_ids = nullptr; - ch->extension_ids_len = 0; } ch->ssl_ptr = ssl; #endif - return ch; + // Wrap the POD structure in the wrapper class and return + return new TSClientHelloImpl(ch); } return nullptr; @@ -7965,21 +7959,46 @@ TSVConnClientHelloGet(TSVConn sslp) void TSClientHelloDestroy(TSClientHello ch) { - if (ch == nullptr) { - return; - } - #ifndef OPENSSL_IS_BORINGSSL // For OpenSSL, we need to free the extension IDs array that was allocated // by SSL_client_hello_get1_extensions_present - if (ch->extension_ids != nullptr) { - OPENSSL_free(ch->extension_ids); + if (ch->get_extension_ids() != nullptr) { + OPENSSL_free(const_cast(ch->get_extension_ids())); } #endif - delete ch; } +TSReturnCode +TSClientHelloExtensionGet(TSClientHello ch, unsigned int type, const unsigned char **out, size_t *outlen) +{ + if (ch == nullptr || out == nullptr || outlen == nullptr) { + return TS_ERROR; + } + +#ifdef OPENSSL_IS_BORINGSSL + const SSL_CLIENT_HELLO *client_hello = static_cast(ch->get_ssl_ptr()); + if (client_hello == nullptr) { + return TS_ERROR; + } + + if (SSL_early_callback_ctx_extension_get(client_hello, type, out, outlen) == 1) { + return TS_SUCCESS; + } +#else + SSL *ssl = static_cast(ch->get_ssl_ptr()); + if (ssl == nullptr) { + return TS_ERROR; + } + + if (SSL_client_hello_get0_ext(ssl, type, out, outlen) == 1) { + return TS_SUCCESS; + } +#endif + + return TS_ERROR; +} + TSSslVerifyCTX TSVConnSslVerifyCTXGet(TSVConn sslp) { From 4dc5af82ee14a2e57c762afc3d873798f586e770 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Thu, 29 Jan 2026 11:27:01 +1100 Subject: [PATCH 14/18] address comments --- doc/admin-guide/plugins/index.en.rst | 4 + .../plugins/ja4_fingerprint.en.rst | 209 ++++++++++++++++++ include/ts/apidefs.h.in | 46 +++- .../experimental/ja4_fingerprint/plugin.cc | 26 +-- src/api/InkAPI.cc | 7 +- 5 files changed, 259 insertions(+), 33 deletions(-) create mode 100644 doc/admin-guide/plugins/ja4_fingerprint.en.rst diff --git a/doc/admin-guide/plugins/index.en.rst b/doc/admin-guide/plugins/index.en.rst index 8de06d97242..251647a40b2 100644 --- a/doc/admin-guide/plugins/index.en.rst +++ b/doc/admin-guide/plugins/index.en.rst @@ -177,6 +177,7 @@ directory of the |TS| source tree. Experimental plugins can be compiled by passi Header Frequency Hook Trace ICAP + JA4 Fingerprint Maxmind ACL Memcache Memory Profile @@ -228,6 +229,9 @@ directory of the |TS| source tree. Experimental plugins can be compiled by passi :doc:`ICAP ` Pass response data to external server for further processing using the ICAP protocol. +:doc:`JA4 Fingerprint ` + Calculates JA4 Fingerprints for incoming TLS traffic. + :doc:`MaxMind ACL ` ACL based on the maxmind geo databases (GeoIP2 mmdb and libmaxminddb) diff --git a/doc/admin-guide/plugins/ja4_fingerprint.en.rst b/doc/admin-guide/plugins/ja4_fingerprint.en.rst new file mode 100644 index 00000000000..b31bd85a1bd --- /dev/null +++ b/doc/admin-guide/plugins/ja4_fingerprint.en.rst @@ -0,0 +1,209 @@ +.. Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + +.. include:: ../../common.defs + +.. _admin-plugins-ja4-fingerprint: + +JA4 Fingerprint Plugin +********************** + +Description +=========== + +The JA4 Fingerprint plugin generates TLS client fingerprints based on the JA4 +algorithm designed by John Althouse. JA4 is the successor to the JA3 +fingerprinting algorithm and provides improved client identification for TLS +connections. + +A JA4 fingerprint uniquely identifies TLS clients based on characteristics of +their TLS ClientHello messages, including: + +* TLS version +* ALPN (Application-Layer Protocol Negotiation) preferences +* Cipher suites offered +* TLS extensions present + +This information can be used for: + +* Client identification and tracking +* Bot detection and mitigation +* Security analytics and threat intelligence +* Understanding client TLS implementation patterns + +How It Works +============ + +The plugin intercepts TLS ClientHello messages during the TLS handshake and +generates a JA4 fingerprint consisting of three sections separated by underscores: + +**Section a (unhashed)**: Basic information about the client including: + + * Protocol (``t`` for TCP, ``q`` for QUIC) + * TLS version + * SNI (Server Name Indication) status + * Number of cipher suites + * Number of extensions + * First ALPN value + +**Section b (hashed)**: A SHA-256 hash of the sorted cipher suite list + +**Section c (hashed)**: A SHA-256 hash of the sorted extension list + +Example fingerprint:: + + t13d1516h2_8daaf6152771_b186095e22b6 + +Key Differences from JA3 +------------------------- + +* Cipher suites and extensions are sorted before hashing for consistency +* SNI and ALPN information is included in the fingerprint +* More resistant to fingerprint randomization + +Plugin Configuration +==================== + +The plugin operates as a global plugin and has no configuration options. + +To enable the plugin, add the following line to :file:`plugin.config`:: + + ja4_fingerprint.so + +No additional parameters are required or supported. + +Plugin Behavior +=============== + +When loaded, the plugin will: + +1. **Capture TLS ClientHello**: Intercepts all incoming TLS connections during + the ClientHello phase + +2. **Generate Fingerprint**: Calculates the JA4 fingerprint from the + ClientHello data + +3. **Log to File**: Writes the fingerprint and client IP address to + ``ja4_fingerprint.log`` + +4. **Add HTTP Headers**: Injects the following headers into subsequent HTTP + requests on the same connection: + + * ``ja4``: Contains the JA4 fingerprint + * ``x-ja4-via``: Contains the proxy name (from ``proxy.config.proxy_name``) + +Log Output +========== + +The plugin writes to :file:`ja4_fingerprint.log` in the Traffic Server log +directory (typically ``/var/log/trafficserver/``). + +**Log Format**:: + + [timestamp] Client IP: JA4: + +**Example**:: + + [Jan 29 10:15:23.456] Client IP: 192.168.1.100 JA4: t13d1516h2_8daaf6152771_b186095e22b6 + [Jan 29 10:15:24.123] Client IP: 10.0.0.50 JA4: t13d1715h2_8daaf6152771_02713d6af862 + +Using JA4 Headers in Origin Requests +===================================== + +Origin servers can access the JA4 fingerprint through the injected HTTP header. +This allows the origin to: + +* Make access control decisions based on client fingerprints +* Log fingerprints for security analysis +* Track client populations and TLS implementation patterns + +The ``x-ja4-via`` header allows origin servers to track which Traffic Server +proxy handled the request when multiple proxies are deployed. + +Debugging +========= + +To enable debug logging for the plugin, set the following in :file:`records.yaml`:: + + records: + diags: + debug: + enabled: 1 + tags: ja4_fingerprint + +Debug output will appear in :file:`diags.log` and includes: + +* ClientHello processing events +* Fingerprint generation details +* Header injection operations + +Requirements +============ + +* Traffic Server must be built with TLS support (OpenSSL or BoringSSL) +* The plugin operates on all TLS connections + +Configuration Settings +====================== + +The plugin requires the ``proxy.config.proxy_name`` setting to be configured +for the ``x-ja4-via`` header. If not set, the plugin will log an error and use +"unknown" as the proxy name. + +To set the proxy name in :file:`records.yaml`:: + + records: + proxy: + config: + proxy_name: proxy01 + +Limitations +=========== + +* The plugin only operates in global mode (no per-remap configuration) +* Logging cannot be disabled +* Raw (unhashed) cipher and extension lists are not logged +* Non-TLS connections do not generate fingerprints + +See Also +======== + +* JA4 Technical Specification: https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4.md +* JA4 is licensed under the BSD 3-Clause license + +Example Configuration +===================== + +Complete example configuration for enabling JA4 fingerprinting: + +**plugin.config**:: + + ja4_fingerprint.so + +**records.yaml**:: + + records: + proxy: + config: + proxy_name: proxy-01 + diags: + debug: + enabled: 1 + tags: ja4_fingerprint + +After restarting Traffic Server, the plugin will begin fingerprinting TLS +connections and logging to ``ja4_fingerprint.log``. diff --git a/include/ts/apidefs.h.in b/include/ts/apidefs.h.in index d2c8f7d67d6..44a7395c6f4 100644 --- a/include/ts/apidefs.h.in +++ b/include/ts/apidefs.h.in @@ -43,6 +43,8 @@ */ #include +#include +#include #include #include #include @@ -1062,9 +1064,9 @@ struct tsapi_ssl_client_hello { class TSClientHelloImpl { public: - TSClientHelloImpl(tsapi_ssl_client_hello *ch) : _ssl_client_hello(ch) {} + TSClientHelloImpl(std::unique_ptr ch) : _ssl_client_hello(std::move(ch)) {} - ~TSClientHelloImpl() { delete _ssl_client_hello; } + ~TSClientHelloImpl() = default; uint16_t get_version() const @@ -1114,15 +1116,51 @@ public: return _ssl_client_hello->ssl_ptr; } + // Returns a vector of extension type IDs + // This abstracts the difference between BoringSSL (extensions buffer) and OpenSSL (extension_ids array) + std::vector + get_extension_types() const + { + std::vector result; + + // For BoringSSL, parse the extensions buffer + if (_ssl_client_hello->extensions != nullptr) { + const uint8_t *ext = _ssl_client_hello->extensions; + size_t remaining = _ssl_client_hello->extensions_len; + + while (remaining >= 4) { + uint16_t ext_type = (ext[0] << 8) | ext[1]; + uint16_t ext_len = (ext[2] << 8) | ext[3]; + size_t total_ext_size = 4 + ext_len; + + result.push_back(ext_type); + + if (total_ext_size > remaining) { + break; + } + ext += total_ext_size; + remaining -= total_ext_size; + } + } + // For OpenSSL, use the extension IDs array + else if (_ssl_client_hello->extension_ids != nullptr) { + for (size_t i = 0; i < _ssl_client_hello->extension_ids_len; i++) { + result.push_back(static_cast(_ssl_client_hello->extension_ids[i])); + } + } + + return result; + } + // Internal accessor for API implementation tsapi_ssl_client_hello * _get_internal() const { - return _ssl_client_hello; + return _ssl_client_hello.get(); } private: - tsapi_ssl_client_hello *_ssl_client_hello; + std::unique_ptr _ssl_client_hello; }; using TSFile = struct tsapi_file *; diff --git a/plugins/experimental/ja4_fingerprint/plugin.cc b/plugins/experimental/ja4_fingerprint/plugin.cc index 44eb09daadb..5ddc4cbefdd 100644 --- a/plugins/experimental/ja4_fingerprint/plugin.cc +++ b/plugins/experimental/ja4_fingerprint/plugin.cc @@ -288,30 +288,8 @@ add_ciphers(JA4::TLSClientHelloSummary &summary, TSClientHello ch) void add_extensions(JA4::TLSClientHelloSummary &summary, TSClientHello ch) { - // For BoringSSL, we have direct access to the extensions buffer - if (ch->get_extensions() != nullptr) { - const uint8_t *ext = ch->get_extensions(); - size_t remaining = ch->get_extensions_len(); - - while (remaining >= 4) { - uint16_t ext_type = (ext[0] << 8) | ext[1]; - uint16_t ext_len = (ext[2] << 8) | ext[3]; - summary.add_extension(ext_type); - size_t total_ext_size = 4 + ext_len; - if (total_ext_size > remaining) { - break; - } - - ext += total_ext_size; - remaining -= total_ext_size; - } - } - // For OpenSSL, we use the extension IDs array - else if (ch->get_extension_ids() != nullptr) { - // OpenSSL's extension_ids is an array of ints, each element is a complete extension ID - for (std::size_t i = 0; i < ch->get_extension_ids_len(); i++) { - summary.add_extension(static_cast(ch->get_extension_ids()[i])); - } + for (auto ext_type : ch->get_extension_types()) { + summary.add_extension(ext_type); } } diff --git a/src/api/InkAPI.cc b/src/api/InkAPI.cc index 4beb642b066..c3e05227585 100644 --- a/src/api/InkAPI.cc +++ b/src/api/InkAPI.cc @@ -7899,13 +7899,12 @@ TSVConnClientHelloGet(TSVConn sslp) } if (auto snis = netvc->get_service(); snis) { - auto ch = new tsapi_ssl_client_hello(); + auto ch = std::make_unique(); #ifdef OPENSSL_IS_BORINGSSL // Get the BoringSSL client hello container ClientHelloContainer client_hello = snis->get_client_hello_container(); if (client_hello == nullptr) { - delete ch; return nullptr; } @@ -7920,12 +7919,10 @@ TSVConnClientHelloGet(TSVConn sslp) // Get the OpenSSL SSL* object auto tbs = netvc->get_service(); if (!tbs) { - delete ch; return nullptr; } SSL *ssl = tbs->get_tls_handle(); if (ssl == nullptr) { - delete ch; return nullptr; } @@ -7950,7 +7947,7 @@ TSVConnClientHelloGet(TSVConn sslp) #endif // Wrap the POD structure in the wrapper class and return - return new TSClientHelloImpl(ch); + return new TSClientHelloImpl(std::move(ch)); } return nullptr; From 4a51fac21f418e492209db0ccb14b5771e727b08 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Thu, 29 Jan 2026 11:40:52 +1100 Subject: [PATCH 15/18] Update ja4_fingerprint.en.rst --- doc/admin-guide/plugins/ja4_fingerprint.en.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/admin-guide/plugins/ja4_fingerprint.en.rst b/doc/admin-guide/plugins/ja4_fingerprint.en.rst index b31bd85a1bd..b8e5e5e37ad 100644 --- a/doc/admin-guide/plugins/ja4_fingerprint.en.rst +++ b/doc/admin-guide/plugins/ja4_fingerprint.en.rst @@ -109,7 +109,7 @@ When loaded, the plugin will: Log Output ========== -The plugin writes to :file:`ja4_fingerprint.log` in the Traffic Server log +The plugin writes to ``ja4_fingerprint.log`` in the Traffic Server log directory (typically ``/var/log/trafficserver/``). **Log Format**:: From b2868d9f3bd61ee52db422c2a65d3283766aa585 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Thu, 29 Jan 2026 14:13:11 +1100 Subject: [PATCH 16/18] Add docs --- .../functions/TSVConnClientHelloGet.en.rst | 59 +++++++++++++++++++ .../api/types/TSClientHello.en.rst | 43 ++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 doc/developer-guide/api/functions/TSVConnClientHelloGet.en.rst create mode 100644 doc/developer-guide/api/types/TSClientHello.en.rst diff --git a/doc/developer-guide/api/functions/TSVConnClientHelloGet.en.rst b/doc/developer-guide/api/functions/TSVConnClientHelloGet.en.rst new file mode 100644 index 00000000000..cafa50d199f --- /dev/null +++ b/doc/developer-guide/api/functions/TSVConnClientHelloGet.en.rst @@ -0,0 +1,59 @@ +.. Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed + with this work for additional information regarding copyright + ownership. The ASF licenses this file to you under the Apache + License, Version 2.0 (the "License"); you may not use this file + except in compliance with the License. You may obtain a copy of + the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied. See the License for the specific language governing + permissions and limitations under the License. + +.. include:: ../../../common.defs + +.. default-domain:: cpp + +TSVConnClientHelloGet +********************* + +Synopsis +======== + +.. code-block:: cpp + + #include + +.. function:: TSClientHello TSVConnClientHelloGet(TSVConn sslp) +.. function:: void TSClientHelloDestroy(TSClientHello ch) +.. function:: TSReturnCode TSClientHelloExtensionGet(TSClientHello ch, unsigned int type, const unsigned char **out, size_t *outlen) + +Description +=========== + +:func:`TSVConnClientHelloGet` retrieves ClientHello message data from the TLS +virtual connection :arg:`sslp`. This function is typically called from the +:enumerator:`TS_EVENT_SSL_CLIENT_HELLO` hook. Returns ``nullptr`` if +:arg:`sslp` is invalid or not a TLS connection. + +The caller must call :func:`TSClientHelloDestroy` to free the returned object. + +:func:`TSClientHelloDestroy` frees the :type:`TSClientHello` object :arg:`ch`. + +:func:`TSClientHelloExtensionGet` retrieves extension data for the specified +:arg:`type` (e.g., ``0x10`` for ALPN). Returns :enumerator:`TS_SUCCESS` if +found, :enumerator:`TS_ERROR` otherwise. The returned pointer in :arg:`out` is +valid only while :arg:`ch` exists. + +Types +===== + +.. type:: TSClientHello + + Opaque type representing a TLS ClientHello message. This is an opaque handle + that provides access to ClientHello data via accessor methods. + diff --git a/doc/developer-guide/api/types/TSClientHello.en.rst b/doc/developer-guide/api/types/TSClientHello.en.rst new file mode 100644 index 00000000000..e3d2f4fcf4f --- /dev/null +++ b/doc/developer-guide/api/types/TSClientHello.en.rst @@ -0,0 +1,43 @@ +.. Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed + with this work for additional information regarding copyright + ownership. The ASF licenses this file to you under the Apache + License, Version 2.0 (the "License"); you may not use this file + except in compliance with the License. You may obtain a copy of + the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied. See the License for the specific language governing + permissions and limitations under the License. + +.. include:: ../../../common.defs + +.. default-domain:: cpp + +TSClientHello +************* + +Synopsis +======== + +.. code-block:: cpp + + #include + +.. type:: TSClientHello + + +Description +=========== + +:type:`TSClientHello` is an opaque handle to a TLS ClientHello message sent by +a client during the TLS handshake. It provides access to the client's TLS +version, cipher suites, and extensions. + +Objects of this type are obtained via :func:`TSVConnClientHelloGet` and must +be freed using :func:`TSClientHelloDestroy`. The implementation abstracts +differences between OpenSSL and BoringSSL to provide a consistent interface. From b9e13514d7c4584a709d16440b260a55e252ea58 Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Thu, 29 Jan 2026 14:19:10 +1100 Subject: [PATCH 17/18] Update TSVConnClientHelloGet.en.rst --- doc/developer-guide/api/functions/TSVConnClientHelloGet.en.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/developer-guide/api/functions/TSVConnClientHelloGet.en.rst b/doc/developer-guide/api/functions/TSVConnClientHelloGet.en.rst index cafa50d199f..0837482dfa0 100644 --- a/doc/developer-guide/api/functions/TSVConnClientHelloGet.en.rst +++ b/doc/developer-guide/api/functions/TSVConnClientHelloGet.en.rst @@ -37,7 +37,7 @@ Description :func:`TSVConnClientHelloGet` retrieves ClientHello message data from the TLS virtual connection :arg:`sslp`. This function is typically called from the -:enumerator:`TS_EVENT_SSL_CLIENT_HELLO` hook. Returns ``nullptr`` if +``TS_EVENT_SSL_CLIENT_HELLO`` hook. Returns ``nullptr`` if :arg:`sslp` is invalid or not a TLS connection. The caller must call :func:`TSClientHelloDestroy` to free the returned object. From 3019dd7fe1143388ccfcee079c878879dfd3b38d Mon Sep 17 00:00:00 2001 From: Jasmine Emanouel Date: Thu, 29 Jan 2026 14:25:33 +1100 Subject: [PATCH 18/18] 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. -