Skip to content
Merged
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
1 change: 1 addition & 0 deletions bpf/configs.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ volatile const u8 enable_pkt_translation_tracking = 0;
volatile const u8 enable_ipsec = 0;
volatile const u8 enable_openssl_tracking = 0;
volatile const u8 enable_directflows_ringbuf = 0;
volatile const u8 enable_tls_usage_tracking = 0;
#endif //__CONFIGS_H__
37 changes: 33 additions & 4 deletions bpf/flows.c
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
*/
#include "dns_tracker.h"

/*
* Defines the TLS tracker,
*/
#include "tls_tracker.h"

/*
* Defines an rtt tracker,
* which runs inside flow_monitor. Is optional.
Expand Down Expand Up @@ -86,8 +91,8 @@ static __always_inline int add_observed_intf(flow_metrics *value, pkt_info *pkt,
}

static __always_inline void update_existing_flow(flow_metrics *aggregate_flow, pkt_info *pkt,
u64 len, u32 sampling, u32 if_index,
u8 direction) {
u64 len, u32 sampling, u32 if_index, u8 direction,
tls_info *tls) {
// Count only packets seen from the same interface as previously to avoid duplicate counts
int maxReached = 0;
bpf_spin_lock(&aggregate_flow->lock);
Expand All @@ -98,6 +103,21 @@ static __always_inline void update_existing_flow(flow_metrics *aggregate_flow, p
aggregate_flow->flags |= pkt->flags;
aggregate_flow->dscp = pkt->dscp;
aggregate_flow->sampling = sampling;
if (tls->hello_version > 0 && aggregate_flow->ssl_version != tls->hello_version) {
if (aggregate_flow->ssl_version == 0) {
aggregate_flow->ssl_version = tls->hello_version;
} else {
// Inconsistency: different client/server hello received with different versions
aggregate_flow->misc_flags |= MISC_FLAGS_SSL_MISMATCH;
}
}
if (tls->cipher_suite > 0 && tls->type == TLSTRACKER_BF_SERVER_HELLO) {
aggregate_flow->tls_cipher_suite = tls->cipher_suite;
}
if (tls->key_share > 0 && tls->type == TLSTRACKER_BF_SERVER_HELLO) {
aggregate_flow->tls_key_share = tls->key_share;
}
aggregate_flow->tls_types |= tls->type;
} else if (if_index != 0) {
// Only add info that we've seen this interface (we can also update end time & flags)
aggregate_flow->end_mono_time_ts = pkt->current_ts;
Expand Down Expand Up @@ -186,9 +206,14 @@ static inline int flow_monitor(struct __sk_buff *skb, u8 direction) {
if (enable_dns_tracking) {
dns_errno = track_dns_packet(skb, &pkt);
}

tls_info tls;
__builtin_memset(&tls, 0, sizeof(tls));
track_tls(skb, pkt.id->transport_protocol, pkt.l4_hdr, pkt.flags, &tls);
flow_metrics *aggregate_flow = (flow_metrics *)bpf_map_lookup_elem(&aggregated_flows, &id);
if (aggregate_flow != NULL) {
update_existing_flow(aggregate_flow, &pkt, len, flow_sampling, skb->ifindex, direction);
update_existing_flow(aggregate_flow, &pkt, len, flow_sampling, skb->ifindex, direction,
&tls);
} else {
// Key does not exist in the map, and will need to create a new entry.
flow_metrics new_flow;
Expand All @@ -205,6 +230,10 @@ static inline int flow_monitor(struct __sk_buff *skb, u8 direction) {
new_flow.sampling = flow_sampling;
__builtin_memcpy(new_flow.dst_mac, eth->h_dest, ETH_ALEN);
__builtin_memcpy(new_flow.src_mac, eth->h_source, ETH_ALEN);
new_flow.ssl_version = tls.hello_version;
new_flow.tls_cipher_suite = tls.cipher_suite;
new_flow.tls_key_share = tls.key_share;
new_flow.tls_types = tls.type;

long ret = bpf_map_update_elem(&aggregated_flows, &id, &new_flow, BPF_NOEXIST);
if (ret != 0) {
Expand All @@ -213,7 +242,7 @@ static inline int flow_monitor(struct __sk_buff *skb, u8 direction) {
(flow_metrics *)bpf_map_lookup_elem(&aggregated_flows, &id);
if (aggregate_flow != NULL) {
update_existing_flow(aggregate_flow, &pkt, len, flow_sampling, skb->ifindex,
direction);
direction, &tls);
} else {
if (trace_messages) {
bpf_printk("failed to update an exising flow\n");
Expand Down
287 changes: 287 additions & 0 deletions bpf/tls_tracker.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
/*
TLS tracker detects TLS packets and extracts information such as version or cipher suite.
*/
#ifndef __TLS_TRACKER_H__
#define __TLS_TRACKER_H__

#include "utils.h"

#define TLSTRACKER_UNKNOWN -1
#define TLSTRACKER_NOTLS -2
#define TLSTRACKER_MATCHED 0

#define CONTENT_TYPE_CHANGE_CIPHER 0x14
#define CONTENT_TYPE_ALERT 0x15
#define CONTENT_TYPE_HANDSHAKE 0x16
#define CONTENT_TYPE_APP_DATA 0x17
Comment thread
jotak marked this conversation as resolved.

#define TLSTRACKER_BF_CLIENT_HELLO 0x01
#define TLSTRACKER_BF_SERVER_HELLO 0x02
#define TLSTRACKER_BF_OTHER_HANDSHAKE 0x04
#define TLSTRACKER_BF_CHANGE_CIPHER 0x08
#define TLSTRACKER_BF_ALERT 0x10
#define TLSTRACKER_BF_APP_DATA 0x20

// https://www.rfc-editor.org/rfc/rfc5246
struct tls_record {
u8 content_type; // handshake, alert, change cipher, app data
u16 version;
} __attribute__((packed));

struct tls_handshake_header {
u8 type; // client hello, server hello ...
} __attribute__((packed));

struct tls_extension_header {
u16 type;
u16 len;
} __attribute__((packed));

static inline int tls_read_client_hello(struct __sk_buff *skb, u32 offset, tls_info *tls) {
u16 handshake_version;
if (bpf_skb_load_bytes(skb, offset, &handshake_version, sizeof(handshake_version)) < 0) {
// Returning unknown because it could still be a FINISHED encrypted message
return TLSTRACKER_UNKNOWN;
}
handshake_version = bpf_ntohs(handshake_version);
offset += 2;
// Accept only 0300 (ssl v3), 0301 (tls 1.0), 0302 (tls 1.1) or 0303 (tls 1.2 or 1.3)
if (handshake_version < 0x0300 || handshake_version > 0x0303) {
return TLSTRACKER_UNKNOWN;
}
tls->type = TLSTRACKER_BF_CLIENT_HELLO;
if (handshake_version == 0x0303) {
// Check extensions to discriminate 1.2 and 1.3
u8 session_len, compr_len;
u16 cipher_len, exts_len;
offset += 32; // skip random
// Read session
if (bpf_skb_load_bytes(skb, offset, &session_len, sizeof(session_len)) < 0) {
return TLSTRACKER_UNKNOWN;
}
offset += 1 + session_len;
// Read cipher suites
if (bpf_skb_load_bytes(skb, offset, &cipher_len, sizeof(cipher_len)) < 0) {
return TLSTRACKER_UNKNOWN;
}
offset += 2 + bpf_ntohs(cipher_len);
// Read compression
if (bpf_skb_load_bytes(skb, offset, &compr_len, sizeof(compr_len)) < 0) {
return TLSTRACKER_UNKNOWN;
}
offset += 1 + compr_len;
// Read extensions
Comment on lines +53 to +73
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It would be good to be able to see pre TLS-1.2 connections here even if it's not expected in recent environments. That's especially useful for auditing legacy TLS usage.

Something around those lines should work:

Suggested change
if (handshake_version == 0x0303) {
// Check extensions to discriminate 1.2 and 1.3
u8 session_len, compr_len;
u16 cipher_len, exts_len;
offset += 32; /*skip random*/
// Read session
if (bpf_skb_load_bytes(skb, offset, &session_len, sizeof(session_len)) < 0) {
return TLSTRACKER_UNKNOWN;
}
offset += 1 + session_len;
// Read cipher suites
if (bpf_skb_load_bytes(skb, offset, &cipher_len, sizeof(cipher_len)) < 0) {
return TLSTRACKER_UNKNOWN;
}
offset += 2 + bpf_ntohs(cipher_len);
// Read compression
if (bpf_skb_load_bytes(skb, offset, &compr_len, sizeof(compr_len)) < 0) {
return TLSTRACKER_UNKNOWN;
}
offset += 1 + compr_len;
// Read extensions
u8 session_len;
offset += 32; /*skip random*/
// Read session
if (bpf_skb_load_bytes(skb, offset, &session_len, sizeof(session_len)) < 0) {
return TLSTRACKER_UNKNOWN;
}
offset += 1 + session_len;
// Read cipher suite (same position in all versions)
if (bpf_skb_load_bytes(skb, offset, &tls->cipher_suite, sizeof(tls->cipher_suite)) < 0) {
return TLSTRACKER_UNKNOWN;
}
tls->cipher_suite = bpf_ntohs(tls->cipher_suite);
offset += 3; // Skip also compression (1B)
if (handshake_version == 0x0303) {
// Check extensions to discriminate 1.2 and 1.3
u16 exts_len;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Pre-1.2 should already be handled - before 1.2, the TLS version was not provided through an extension, it was in the handshake version field (read from lines 41-46 above).
So it would skip the whole if (handshake_version == 0x0303) block and return correctly with the handshake version.

if (bpf_skb_load_bytes(skb, offset, &exts_len, sizeof(exts_len)) < 0) {
return TLSTRACKER_UNKNOWN;
}
exts_len = bpf_ntohs(exts_len);
offset += 2;
u16 ext_offset = 0;
// Read up to 30 extensions
for (int i = 0; i < 30; i++) {
if (ext_offset >= exts_len) {
break;
}
struct tls_extension_header ext_hdr;
if (bpf_skb_load_bytes(skb, offset + ext_offset, &ext_hdr, sizeof(ext_hdr)) < 0) {
return TLSTRACKER_UNKNOWN;
}
ext_hdr.type = bpf_ntohs(ext_hdr.type);
ext_hdr.len = bpf_ntohs(ext_hdr.len);
ext_offset += 4; // 2 bytes for type, 2 bytes for length
if (ext_hdr.type == 0x002b) {
// Supported Versions
u16 supportedversions_offset =
1; // skip supported versions length (u8), it's always ext_hdr.len-1
// Read up to 5 versions
for (int j = 0; j < 5; j++) {
if (supportedversions_offset >= ext_hdr.len) {
break;
}
u16 version;
if (bpf_skb_load_bytes(skb, offset + ext_offset + supportedversions_offset,
&version, sizeof(version)) < 0) {
return TLSTRACKER_UNKNOWN;
}
version = bpf_ntohs(version);
if (version > tls->hello_version) {
tls->hello_version = version;
}
supportedversions_offset += 2;
}
// Stop reading here
return TLSTRACKER_MATCHED;
}
ext_offset += ext_hdr.len;
}
}
tls->hello_version = handshake_version;
return TLSTRACKER_MATCHED;
}

static inline int tls_read_server_hello(struct __sk_buff *skb, u32 offset, tls_info *tls) {
u16 handshake_version;
if (bpf_skb_load_bytes(skb, offset, &handshake_version, sizeof(handshake_version)) < 0) {
// Returning unknown because it could still be a FINISHED encrypted message
return TLSTRACKER_UNKNOWN;
}
handshake_version = bpf_ntohs(handshake_version);
offset += 2;
// Accept only 0300 (ssl v3), 0301 (tls 1.0), 0302 (tls 1.1) or 0303 (tls 1.2 or 1.3)
if (handshake_version < 0x0300 || handshake_version > 0x0303) {
return TLSTRACKER_UNKNOWN;
}
tls->type = TLSTRACKER_BF_SERVER_HELLO;
if (handshake_version == 0x0303) {
// Check extensions to discriminate 1.2 and 1.3
u8 session_len;
u16 exts_len;
offset += 32; // skip random
// Read session
if (bpf_skb_load_bytes(skb, offset, &session_len, sizeof(session_len)) < 0) {
return TLSTRACKER_UNKNOWN;
}
offset += 1 + session_len;
// Read cipher suites
if (bpf_skb_load_bytes(skb, offset, &tls->cipher_suite, sizeof(tls->cipher_suite)) < 0) {
return TLSTRACKER_UNKNOWN;
}
tls->cipher_suite = bpf_ntohs(tls->cipher_suite);
offset += 3; // Skip also compression (1B)
// Read extensions
if (bpf_skb_load_bytes(skb, offset, &exts_len, sizeof(exts_len)) < 0) {
return TLSTRACKER_UNKNOWN;
}
exts_len = bpf_ntohs(exts_len);
offset += 2;
u16 ext_offset = 0;
u8 ext_read = 0;
// Read up to 30 extensions
for (int i = 0; i < 30; i++) {
if (ext_offset >= exts_len) {
break;
}
struct tls_extension_header ext_hdr;
if (bpf_skb_load_bytes(skb, offset + ext_offset, &ext_hdr, sizeof(ext_hdr)) < 0) {
return TLSTRACKER_UNKNOWN;
}
ext_hdr.type = bpf_ntohs(ext_hdr.type);
ext_hdr.len = bpf_ntohs(ext_hdr.len);
ext_offset += 4; // 2 bytes for type, 2 bytes for length
if (ext_hdr.type == 0x002b) {
// Supported Versions: single version expected
u16 version;
if (bpf_skb_load_bytes(skb, offset + ext_offset, &version, sizeof(version)) < 0) {
return TLSTRACKER_UNKNOWN;
}
tls->hello_version = bpf_ntohs(version);
ext_read++;
} else if (ext_hdr.type == 0x0033) {
// Key Share Extension (TLS 1.3)
// The Server Hello Key Share contains:
// Group (2 bytes) + Key Exchange Length (2 bytes) + Key Exchange Data
u16 key_share;
if (bpf_skb_load_bytes(skb, offset + ext_offset, &key_share, sizeof(key_share)) <
0) {
return TLSTRACKER_UNKNOWN;
}
tls->key_share = bpf_ntohs(key_share);
ext_read++;
}
if (ext_read == 2) {
// Return after 0x002b and 0x0033 are read
return TLSTRACKER_MATCHED;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Something else that would be nice to pull out of the extensions is the key share. This would tell users if the server is using classical or hybrid key exchange algorithms, which is useful for finding out which applications are PQC-ready (e.g., looking for X25519 + Kyber768).

We might need to do something like:

if (ext_hdr.type == 0x002b) {
    // Supported Versions: single version expected
    u16 version;
    if (bpf_skb_load_bytes(skb, offset + ext_offset, &version, sizeof(version)) < 0) {
        return TLSTRACKER_UNKNOWN;
    }
    tls->version = bpf_ntohs(version);
} 
else if (ext_hdr.type == 0x0033) {
    // Key Share Extension (TLS 1.3)
    // The Server Hello Key Share contains:
    // Group (2 bytes) + Key Exchange Length (2 bytes) + Key Exchange Data
    u16 selected_group;
    if (bpf_skb_load_bytes(skb, offset + ext_offset, &selected_group, sizeof(selected_group)) < 0) {
        // not sure what would be best to return here?
        return KEYSHARE_UNKNOWN;
    }
    tls->key_share_group = bpf_ntohs(selected_group);
}

The tls_info would need to be updated to relay that information though.

Thoughts?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

yep, definitely, if that's useful to figure out PQC-readiness, it's something that we want here
I'll add it, thanks for the hint!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

done here a9c6e61

ext_offset += ext_hdr.len;
}
}
tls->hello_version = handshake_version;
return TLSTRACKER_MATCHED;
}

static inline int track_tls_tcp(struct __sk_buff *skb, void *l4_hdr, tls_info *tls) {
void *data_end = (void *)(long)skb->data_end;

struct tcphdr *tcp = (struct tcphdr *)l4_hdr;
if (!tcp || ((void *)tcp + sizeof(*tcp) > data_end)) {
return TLSTRACKER_NOTLS;
}

u8 len = tcp->doff * sizeof(u32);
if (!len) {
return TLSTRACKER_NOTLS;
}

struct tls_record rec;
u32 offset = (long)l4_hdr - (long)skb->data + len;

if ((bpf_skb_load_bytes(skb, offset, &rec, sizeof(rec))) < 0) {
return TLSTRACKER_NOTLS;
}
// Record header = 5 bytes: 1 for content type, 2 for version, 2 for following message length (which we don't need to keep)
offset += 5;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can you please put a comment to explain this ?

rec.version = bpf_ntohs(rec.version);

// Accept only 0300, 0301, 0302 or 0303
// Note that for compatibility reasons, versions cannot be trusted here:
// TLS 1.2 or 1.3 packets can be disguised as 1.0, hence further analysis is required
if (rec.version < 0x0300 || rec.version > 0x0303) {
return TLSTRACKER_NOTLS;
}

switch (rec.content_type) {
case CONTENT_TYPE_HANDSHAKE: {
// Handshakes should have the handshake header, except if it's a FINISHED msg, which is encrypted.
// In both cases, there should be sufficient data to fill tls_handshake_header; but we can't assume it's valid
struct tls_handshake_header handshake;
if (bpf_skb_load_bytes(skb, offset, &handshake, sizeof(handshake)) < 0) {
return TLSTRACKER_NOTLS;
}
// Handshake header = 4 bytes: 1 for handshake type, 3 for following length (which we don't need to keep)
offset += 4;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same here. A comment would help.


// From now on, if we fail to read what we expect, this was either not a TLS packet, or a FINISHED message.
switch (handshake.type) {
case 0x01:
return tls_read_client_hello(skb, offset, tls);
case 0x02:
return tls_read_server_hello(skb, offset, tls);
case 0x10:
case 0x0b:
case 0x0c:
case 0x0e:
// Still sounds like a valid handshake, assume it is
tls->type = TLSTRACKER_BF_OTHER_HANDSHAKE;
return TLSTRACKER_MATCHED;
}
// Either not TLS, or FINISHED
return TLSTRACKER_UNKNOWN;
}
case CONTENT_TYPE_CHANGE_CIPHER:
tls->type = TLSTRACKER_BF_CHANGE_CIPHER;
return TLSTRACKER_MATCHED;
case CONTENT_TYPE_ALERT:
tls->type = TLSTRACKER_BF_ALERT;
return TLSTRACKER_MATCHED;
case CONTENT_TYPE_APP_DATA:
tls->type = TLSTRACKER_BF_APP_DATA;
return TLSTRACKER_MATCHED;
}
return TLSTRACKER_NOTLS;
}

// Extract TLS info
static inline int track_tls(struct __sk_buff *skb, u8 proto, void *l4_hdr, u8 flags,
tls_info *tls) {
if (enable_tls_usage_tracking == 0) {
return TLSTRACKER_UNKNOWN;
}
if (proto == IPPROTO_TCP && flags & 0x10) {
// TCP ACK
return track_tls_tcp(skb, l4_hdr, tls);
}
// TODO: UDP/QUIC
return TLSTRACKER_UNKNOWN;
}

#endif // __TLS_TRACKER_H__
Loading
Loading