feat(ja4ebpf): add multi-interface TC, LPM_TRIE ignore_src, unit tests, and fix bugs

- Add multi-interface TC attachment (default "any" = all UP interfaces)
- Add BPF LPM_TRIE map ignored_src for kernel-side CIDR filtering
- Add userspace ignore_src filtering for SSL/accept4 path via net.IPNet.Contains()
- Add AcceptCache for fd→SessionKey correlation with TTL and Close()
- Add 5 test files covering writer, procutil, dispatcher, accept_cache, and cmd
- Fix formatTCPOptions infinite loop on EOL (case 0 break→return)
- Fix pseudoOrderToShort panic on empty slice (negative cap)
- Fix AcceptCache goroutine leak (add done channel + Close())
- Update config.yml.example with interfaces, listen_ports, ignore_src
- Rewrite docs/services/ja4ebpf.md (was massively stale: XDP, RingBuffer, etc.)
- Fix stale XDP/RingBuffer references in docs/architecture.md, thesis, tls.go

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jacquin Antoine
2026-04-16 01:49:26 +02:00
parent fd84aebc44
commit f0c8fe81c6
20 changed files with 3053 additions and 1261 deletions

View File

@ -47,6 +47,28 @@ struct {
__type(value, __u64);
} tc_stats SEC(".maps");
/* Map de ports autorisés — peuplée depuis Go au démarrage.
* key = port (uint16), value = 1 (autorisé).
* Ports non présents dans la map sont ignorés. */
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 64);
__type(key, __u16);
__type(value, __u8);
} allowed_ports SEC(".maps");
/* Map LPM_TRIE des CIDR/IP sources à ignorer — peuplée depuis Go.
* key = {prefixlen, ip[4]} (8 octets), value = 1 (ignorer).
* Un lookup réussi = IP source à ignorer → return TC_ACT_OK.
* data est en network byte order (big-endian) pour correspondre à iph.saddr. */
struct {
__uint(type, BPF_MAP_TYPE_LPM_TRIE);
__uint(max_entries, 256);
__type(key, struct { __u32 prefixlen; __u8 data[4]; });
__type(value, __u8);
__uint(map_flags, BPF_F_NO_PREALLOC);
} ignored_src SEC(".maps");
#define STAT_TOTAL 0
#define STAT_IPV4 1
#define STAT_TCP 2
@ -137,6 +159,36 @@ int capture_tc(struct __sk_buff *ctx)
__u32 payload_off = ETH_HLEN + ip_hlen + tcp_hlen;
__u32 avail = 0;
__u32 zero = 0;
/* Vérification globale : port autorisé ? (SYN, TLS, HTTP)
* On autorise si dst_port OU src_port est dans allowed_ports.
* En ingress TC, les réponses ont src_port=80/443 (serveur distant)
* et dst_port=ephemeral (client local). */
__u8 *port_allowed = bpf_map_lookup_elem(&allowed_ports, &dst_port);
if (!port_allowed) {
port_allowed = bpf_map_lookup_elem(&allowed_ports, &src_port);
if (!port_allowed)
return TC_ACT_OK;
}
/* Vérification : IP source ignorée ? (LPM_TRIE lookup /32) */
struct { __u32 prefixlen; __u8 data[4]; } lpm_key = {};
lpm_key.prefixlen = 32;
/* Copier src_ip (network byte order) dans data[4] byte par byte.
* src_ip est en network byte order (big-endian) depuis iph.saddr.
* Sur x86 little-endian, il faut extraire du MSB vers le LSB
* pour que data[] soit en network byte order comme les clés Go. */
__u32 src_ip_h = bpf_ntohl(src_ip);
lpm_key.data[0] = (__u8)((src_ip_h >> 24) & 0xFF);
lpm_key.data[1] = (__u8)((src_ip_h >> 16) & 0xFF);
lpm_key.data[2] = (__u8)((src_ip_h >> 8) & 0xFF);
lpm_key.data[3] = (__u8)(src_ip_h & 0xFF);
__u8 *src_ignored = bpf_map_lookup_elem(&ignored_src, &lpm_key);
if (src_ignored)
return TC_ACT_OK;
/* ===================================================================
* TCP SYN
* ===================================================================*/
@ -160,15 +212,23 @@ int capture_tc(struct __sk_buff *ctx)
evt.timestamp_ns = bpf_ktime_get_ns();
evt.tcp_options_len = 0;
/* Copie des options TCP via bpf_skb_load_bytes avec taille constante.
* On lit MAX_TCP_OPTIONS=40 octets depuis le début des options.
* Si le paquet est trop court, l'appel échoue → options absentes. */
/* Copie des options TCP via bpf_skb_load_bytes avec cascade de tailles.
* Le vérificateur BPF exige une taille constante pour bpf_skb_load_bytes.
* On essaie 40, puis 20, puis 10 octets — le premier appel qui réussit
* donne les options disponibles (même partielles). */
__u32 opts_off = tcp_off + 20;
__u32 opts_len = tcp_hlen - 20;
if (opts_len > 0 && opts_len <= MAX_TCP_OPTIONS &&
opts_off + MAX_TCP_OPTIONS <= pkt_len) {
bpf_skb_load_bytes(ctx, opts_off, evt.tcp_options_raw, MAX_TCP_OPTIONS);
evt.tcp_options_len = (__u8)opts_len;
if (opts_len > 0 && opts_len <= MAX_TCP_OPTIONS) {
if (opts_off + 40 <= pkt_len) {
bpf_skb_load_bytes(ctx, opts_off, evt.tcp_options_raw, 40);
evt.tcp_options_len = (__u8)opts_len;
} else if (opts_off + 20 <= pkt_len) {
bpf_skb_load_bytes(ctx, opts_off, evt.tcp_options_raw, 20);
evt.tcp_options_len = (__u8)(opts_len > 20 ? 20 : opts_len);
} else if (opts_off + 10 <= pkt_len) {
bpf_skb_load_bytes(ctx, opts_off, evt.tcp_options_raw, 10);
evt.tcp_options_len = (__u8)(opts_len > 10 ? 10 : opts_len);
}
}
bpf_perf_event_output(ctx, &pb_tcp_syn, BPF_F_CURRENT_CPU,
@ -180,133 +240,132 @@ int capture_tc(struct __sk_buff *ctx)
}
/* ===================================================================
* TLS ClientHello (port 443)
* TLS ClientHello
* ===================================================================*/
if (dst_port == HTTPS_PORT) {
/* Lire les 6 premiers octets du payload pour vérifier le type TLS */
if (payload_off + 6 > pkt_len)
return TC_ACT_OK;
/* Lire les 6 premiers octets du payload pour vérifier le type TLS */
if (payload_off + 6 > pkt_len)
goto try_http;
__u8 tls_hdr[6];
bpf_skb_load_bytes(ctx, payload_off, tls_hdr, 6);
__u8 tls_hdr[6];
bpf_skb_load_bytes(ctx, payload_off, tls_hdr, 6);
if (tls_hdr[0] != TLS_CONTENT_HANDSHAKE || tls_hdr[5] != TLS_MSG_CLIENT_HELLO)
return TC_ACT_OK;
if (tls_hdr[0] != TLS_CONTENT_HANDSHAKE || tls_hdr[5] != TLS_MSG_CLIENT_HELLO)
goto try_http;
/* Avail via pkt_len (scalaire pur) */
__u32 avail = 0;
if (pkt_len > payload_off) {
avail = pkt_len - payload_off;
if (avail > MAX_TLS_PAYLOAD)
avail = MAX_TLS_PAYLOAD;
}
if (avail == 0)
return TC_ACT_OK;
/* Avail via pkt_len (scalaire pur) */
avail = 0;
if (pkt_len > payload_off) {
avail = pkt_len - payload_off;
if (avail > MAX_TLS_PAYLOAD)
avail = MAX_TLS_PAYLOAD;
}
if (avail == 0)
return TC_ACT_OK;
__u32 zero = 0;
struct tls_hello_event *tls_evt = bpf_map_lookup_elem(&__tls_buf, &zero);
if (!tls_evt)
return TC_ACT_OK;
struct tls_hello_event *tls_evt = bpf_map_lookup_elem(&__tls_buf, &zero);
if (!tls_evt)
return TC_ACT_OK;
tls_evt->src_ip = 0;
tls_evt->dst_ip = 0;
tls_evt->src_port = 0;
tls_evt->dst_port = 0;
tls_evt->payload_len = 0;
tls_evt->timestamp_ns = 0;
tls_evt->src_ip = 0;
tls_evt->dst_ip = 0;
tls_evt->src_port = 0;
tls_evt->dst_port = 0;
tls_evt->payload_len = 0;
tls_evt->timestamp_ns = 0;
tls_evt->src_ip = bpf_ntohl(src_ip);
tls_evt->dst_ip = bpf_ntohl(dst_ip);
tls_evt->src_port = src_port;
tls_evt->dst_port = dst_port;
tls_evt->timestamp_ns = bpf_ktime_get_ns();
/* Copie via bpf_skb_load_bytes avec tailles constantes en cascade.
* Kernel 4.18 ne supporte pas les tailles variables vers map values.
* On essaie 1024 puis 512 puis 256 pour capturer SNI et extensions.
* La taille réellement copiée est stockée dans payload_len. */
if (payload_off + 1024 <= pkt_len) {
bpf_skb_load_bytes(ctx, payload_off, tls_evt, 1024);
tls_evt->payload_len = 1024;
} else if (payload_off + 512 <= pkt_len) {
bpf_skb_load_bytes(ctx, payload_off, tls_evt, 512);
tls_evt->payload_len = 512;
} else if (payload_off + 256 <= pkt_len) {
bpf_skb_load_bytes(ctx, payload_off, tls_evt, 256);
tls_evt->payload_len = 256;
} else {
return TC_ACT_OK;
}
bpf_perf_event_output(ctx, &pb_tls_hello, BPF_F_CURRENT_CPU,
tls_evt, sizeof(*tls_evt));
key = STAT_TLS_SUBMIT;
cnt = bpf_map_lookup_elem(&tc_stats, &key);
if (cnt) (*cnt)++;
tls_evt->src_ip = bpf_ntohl(src_ip);
tls_evt->dst_ip = bpf_ntohl(dst_ip);
tls_evt->src_port = src_port;
tls_evt->dst_port = dst_port;
tls_evt->timestamp_ns = bpf_ktime_get_ns();
/* Copie via bpf_skb_load_bytes avec tailles constantes en cascade.
* Kernel 4.18 ne supporte pas les tailles variables vers map values.
* On essaie 1024 puis 512 puis 256 pour capturer SNI et extensions.
* La taille réellement copiée est stockée dans payload_len. */
if (payload_off + 1024 <= pkt_len) {
bpf_skb_load_bytes(ctx, payload_off, tls_evt, 1024);
tls_evt->payload_len = 1024;
} else if (payload_off + 512 <= pkt_len) {
bpf_skb_load_bytes(ctx, payload_off, tls_evt, 512);
tls_evt->payload_len = 512;
} else if (payload_off + 256 <= pkt_len) {
bpf_skb_load_bytes(ctx, payload_off, tls_evt, 256);
tls_evt->payload_len = 256;
} else {
return TC_ACT_OK;
}
bpf_perf_event_output(ctx, &pb_tls_hello, BPF_F_CURRENT_CPU,
tls_evt, sizeof(*tls_evt));
key = STAT_TLS_SUBMIT;
cnt = bpf_map_lookup_elem(&tc_stats, &key);
if (cnt) (*cnt)++;
return TC_ACT_OK;
try_http:
/* ===================================================================
* HTTP en clair (port 80 / 8080)
* HTTP en clair (ports autorisés, non-TLS)
* ===================================================================*/
if (dst_port == HTTP_PORT || dst_port == HTTP_ALT_PORT) {
if (tcp_flags & (TH_SYN | TH_FIN | TH_RST))
return TC_ACT_OK;
if (payload_off >= pkt_len)
return TC_ACT_OK;
if (tcp_flags & (TH_SYN | TH_FIN | TH_RST))
return TC_ACT_OK;
if (payload_off >= pkt_len)
return TC_ACT_OK;
/* Avail via pkt_len (scalaire pur) */
__u32 avail = 0;
if (pkt_len > payload_off) {
avail = pkt_len - payload_off;
if (avail > MAX_HTTP_PAYLOAD)
avail = MAX_HTTP_PAYLOAD;
}
if (avail == 0)
return TC_ACT_OK;
__u32 zero = 0;
struct http_plain_event *h_evt = bpf_map_lookup_elem(&__http_buf, &zero);
if (!h_evt)
return TC_ACT_OK;
h_evt->src_ip = 0;
h_evt->dst_ip = 0;
h_evt->src_port = 0;
h_evt->dst_port = 0;
h_evt->payload_len = 0;
h_evt->timestamp_ns = 0;
h_evt->src_ip = bpf_ntohl(src_ip);
h_evt->dst_ip = bpf_ntohl(dst_ip);
h_evt->src_port = src_port;
h_evt->dst_port = dst_port;
h_evt->timestamp_ns = bpf_ktime_get_ns();
/* Copie via bpf_skb_load_bytes avec tailles constantes en cascade.
* Les requêtes HTTP sont souvent < 512 octets, on descend à 256 puis 128. */
if (payload_off + 512 <= pkt_len) {
bpf_skb_load_bytes(ctx, payload_off, h_evt, 512);
h_evt->payload_len = 512;
} else if (payload_off + 256 <= pkt_len) {
bpf_skb_load_bytes(ctx, payload_off, h_evt, 256);
h_evt->payload_len = 256;
} else if (payload_off + 128 <= pkt_len) {
bpf_skb_load_bytes(ctx, payload_off, h_evt, 128);
h_evt->payload_len = 128;
} else {
return TC_ACT_OK;
}
bpf_perf_event_output(ctx, &pb_http_plain, BPF_F_CURRENT_CPU,
h_evt, sizeof(*h_evt));
key = STAT_HTTP_SUBMIT;
cnt = bpf_map_lookup_elem(&tc_stats, &key);
if (cnt) (*cnt)++;
/* Avail via pkt_len (scalaire pur) */
avail = 0;
if (pkt_len > payload_off) {
avail = pkt_len - payload_off;
if (avail > MAX_HTTP_PAYLOAD)
avail = MAX_HTTP_PAYLOAD;
}
if (avail == 0)
return TC_ACT_OK;
struct http_plain_event *h_evt = bpf_map_lookup_elem(&__http_buf, &zero);
if (!h_evt)
return TC_ACT_OK;
h_evt->src_ip = 0;
h_evt->dst_ip = 0;
h_evt->src_port = 0;
h_evt->dst_port = 0;
h_evt->payload_len = 0;
h_evt->timestamp_ns = 0;
h_evt->src_ip = bpf_ntohl(src_ip);
h_evt->dst_ip = bpf_ntohl(dst_ip);
h_evt->src_port = src_port;
h_evt->dst_port = dst_port;
h_evt->timestamp_ns = bpf_ktime_get_ns();
/* Copie via bpf_skb_load_bytes avec tailles constantes en cascade.
* Les requêtes HTTP sont souvent < 512 octets, on descend à 256, 128, 64. */
if (payload_off + 512 <= pkt_len) {
bpf_skb_load_bytes(ctx, payload_off, h_evt, 512);
h_evt->payload_len = 512;
} else if (payload_off + 256 <= pkt_len) {
bpf_skb_load_bytes(ctx, payload_off, h_evt, 256);
h_evt->payload_len = 256;
} else if (payload_off + 128 <= pkt_len) {
bpf_skb_load_bytes(ctx, payload_off, h_evt, 128);
h_evt->payload_len = 128;
} else if (payload_off + 64 <= pkt_len) {
bpf_skb_load_bytes(ctx, payload_off, h_evt, 64);
h_evt->payload_len = 64;
} else {
return TC_ACT_OK;
}
bpf_perf_event_output(ctx, &pb_http_plain, BPF_F_CURRENT_CPU,
h_evt, sizeof(*h_evt));
key = STAT_HTTP_SUBMIT;
cnt = bpf_map_lookup_elem(&tc_stats, &key);
if (cnt) (*cnt)++;
return TC_ACT_OK;
}