Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 1 addition & 9 deletions cmake/ExperimentalPlugins.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +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
Expand Down
11 changes: 7 additions & 4 deletions include/iocore/net/TLSSNISupport.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -114,5 +116,6 @@ class TLSSNISupport
// Null-terminated string, or nullptr if there is no SNI server name.
std::unique_ptr<char[]> _sni_server_name;

void _set_sni_server_name_buffer(std::string_view name);
void _set_sni_server_name_buffer(std::string_view name);
ClientHelloContainer _chc = nullptr;
};
13 changes: 13 additions & 0 deletions include/ts/apidefs.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -1081,6 +1081,19 @@ 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;
};
Comment on lines +1084 to +1093
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's initialize these here (nullptr, 0).

Also: as a tweak on this, what do you think of making these private and adding public getters for these such that we can lazily load them as they are requested? Subsequent requests can then return the populated (cached) values if the same value is asked for twice. Currently, the caller has to pay for the population of all of these even though they might not need them all.

Copy link
Member

@maskit maskit Jan 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lazy load would be tricky. Best we can do is probably read and cache everything in TSVConnClientHelloGet.

As I commented on somewhere, SSL_CLIENT_HELLO is only available during BoringSSL callback functions are called. So TSVConnClientHelloGet needs to be called on certain hooks (this should be documented). This plugin seems fine since TSVConnClientHelloGet and TSClientHelloDestroy are called on TS_SSL_CLIENT_HELLO_HOOK, but if another plugin needs information from Client Hello later on another hook, everything needs to be deep copied when TSVConnClientHelloGet is called.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Public getters would be nice even if we don't do lazy land. Those would enable removing ifdef from the plugin code.


using TSClientHello = struct tsapi_ssl_client_hello *;

using TSFetchSM = struct tsapi_fetchsm *;

using TSThreadFunc = void *(*)(void *data);
Expand Down
6 changes: 4 additions & 2 deletions include/ts/ts.h
Original file line number Diff line number Diff line change
Expand Up @@ -1331,8 +1331,10 @@ 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);
void TSClientHelloDestroy(TSClientHello ch);

TSSslSession TSSslSessionGet(const TSSslSessionID *session_id);
int TSSslSessionGetBuffer(const TSSslSessionID *session_id, char *buffer, int *len_ptr);
Expand Down
2 changes: 2 additions & 0 deletions plugins/experimental/ja4_fingerprint/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
124 changes: 88 additions & 36 deletions plugins/experimental/ja4_fingerprint/plugin.cc
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,19 @@ static void reserve_user_arg();
static bool create_log_file();
static void register_hooks();
static int handle_client_hello(TSCont cont, TSEvent event, void *edata);
static std::string get_fingerprint(SSL *ssl);
char *get_IP(sockaddr const *s_sockaddr, char res[INET6_ADDRSTRLEN]);
static void log_fingerprint(JA4_data const *data);
static std::uint16_t get_version(SSL *ssl);
static std::string get_first_ALPN(SSL *ssl);
static void add_ciphers(JA4::TLSClientHelloSummary &summary, SSL *ssl);
static void add_extensions(JA4::TLSClientHelloSummary &summary, SSL *ssl);
static std::string get_fingerprint(TSClientHello ch);
static std::uint16_t get_version(TSClientHello ch);
static std::string get_first_ALPN(TSClientHello ch);
static void add_ciphers(JA4::TLSClientHelloSummary &summary, TSClientHello ch);
static void add_extensions(JA4::TLSClientHelloSummary &summary, TSClientHello ch);
static std::string hash_with_SHA256(std::string_view sv);
static int handle_read_request_hdr(TSCont cont, TSEvent event, void *edata);
static void append_JA4_headers(TSCont cont, TSHttpTxn txnp, std::string const *fingerprint);
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
{
Expand All @@ -75,7 +76,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};

Expand Down Expand Up @@ -163,15 +163,20 @@ handle_client_hello(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata)
// We ignore the event, but we don't want to reject the connection.
return TS_SUCCESS;
}
TSVConn const ssl_vc{static_cast<TSVConn>(edata)};
TSSslConnection const ssl{TSVConnSslConnectionGet(ssl_vc)};
if (nullptr == ssl) {
Dbg(dbg_ctl, "Could not get SSL object.");

TSVConn const ssl_vc{static_cast<TSVConn>(edata)};

TSClientHello ch = TSVConnClientHelloGet(ssl_vc);

if (nullptr == ch) {
Dbg(dbg_ctl, "Could not get TSClientHello object.");
} else {
auto data{std::make_unique<JA4_data>()};
data->fingerprint = get_fingerprint(reinterpret_cast<SSL *>(ssl));
data->fingerprint = get_fingerprint(ch);
get_IP(TSNetVConnRemoteAddrGet(ssl_vc), data->IP_addr);
log_fingerprint(data.get());
// 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<void *>(data.release()));
}
Expand All @@ -180,14 +185,14 @@ handle_client_hello(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata)
}

std::string
get_fingerprint(SSL *ssl)
get_fingerprint(TSClientHello ch)
{
JA4::TLSClientHelloSummary summary{};
summary.protocol = JA4::Protocol::TLS;
summary.TLS_version = get_version(ssl);
summary.ALPN = get_first_ALPN(ssl);
add_ciphers(summary, ssl);
add_extensions(summary, ssl);
summary.TLS_version = get_version(ch);
summary.ALPN = get_first_ALPN(ch);
add_ciphers(summary, ch);
add_extensions(summary, ch);
std::string result{JA4::make_JA4_fingerprint(summary, hash_with_SHA256)};
return result;
}
Expand Down Expand Up @@ -229,66 +234,83 @@ log_fingerprint(JA4_data const *data)
}

std::uint16_t
get_version(SSL *ssl)
get_version(TSClientHello ch)
{
unsigned char const *buf{};
std::size_t buflen{};
if (SSL_SUCCESS == SSL_client_hello_get0_ext(ssl, EXT_SUPPORTED_VERSIONS, &buf, &buflen)) {
if (TS_SUCCESS == client_hello_ext_get(ch, EXT_SUPPORTED_VERSIONS, &buf, &buflen)) {
std::uint16_t max_version{0};
for (std::size_t i{1}; i < buflen; i += 2) {
std::uint16_t version{make_word(buf[i - 1], buf[i])};
if ((!JA4::is_GREASE(version)) && version > max_version) {
size_t 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 SSL_client_hello_get0_legacy_version(ssl);
return ch->version;
}
}

std::string
get_first_ALPN(SSL *ssl)
get_first_ALPN(TSClientHello ch)
{
unsigned char const *buf{};
std::size_t buflen{};
std::string result{""};
if (SSL_SUCCESS == SSL_client_hello_get0_ext(ssl, EXT_ALPN, &buf, &buflen)) {
if (TS_SUCCESS == 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);
TSAssert(0 != first_ALPN_length);
result.assign(&buf[3], (&buf[3]) + first_ALPN_length);
}

return result;
}

void
add_ciphers(JA4::TLSClientHelloSummary &summary, SSL *ssl)
add_ciphers(JA4::TLSClientHelloSummary &summary, TSClientHello ch)
{
unsigned char const *buf{};
std::size_t buflen{SSL_client_hello_get0_ciphers(ssl, &buf)};
const uint8_t *buf = ch->cipher_suites;
size_t buflen = ch->cipher_suites_len;

if (buflen > 0) {
for (std::size_t i{1}; i < buflen; i += 2) {
summary.add_cipher(make_word(buf[i], buf[i - 1]));
for (std::size_t i = 0; i + 1 < buflen; i += 2) {
summary.add_cipher(make_word(buf[i], buf[i + 1]));
}
} else {
Dbg(dbg_ctl, "Failed to get ciphers.");
}
}

void
add_extensions(JA4::TLSClientHelloSummary &summary, SSL *ssl)
add_extensions(JA4::TLSClientHelloSummary &summary, TSClientHello ch)
{
int *buf{};
std::size_t buflen{};
if (SSL_SUCCESS == SSL_client_hello_get1_extensions_present(ssl, &buf, &buflen)) {
for (std::size_t i{1}; i < buflen; i += 2) {
summary.add_extension(make_word(buf[i], buf[i - 1]));
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 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<uint16_t>(ch->extension_ids[i]));
}
}
OPENSSL_free(buf);
}

std::string
Expand Down Expand Up @@ -394,3 +416,33 @@ 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)
{
if (ch == nullptr || out == nullptr || outlen == nullptr) {
return TS_ERROR;
}

#ifdef OPENSSL_IS_BORINGSSL
const SSL_CLIENT_HELLO *client_hello = static_cast<const SSL_CLIENT_HELLO *>(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<SSL *>(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;
}
Loading