Files
ja4-platform/docs/services/ja4ebpf.md
Jacquin Antoine f0c8fe81c6 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>
2026-04-16 01:49:26 +02:00

19 KiB

ja4ebpf — Agent eBPF CO-RE

Description

ja4ebpf est l'agent de collecte de données de la plateforme ja4-platform. C'est un binaire Go unique qui utilise eBPF CO-RE (Compile Once — Run Everywhere) pour observer passivement le trafic réseau d'un serveur Linux, sans modifier ni interrompre le serveur web cible (Apache, Nginx, Varnish, HAProxy, ou tout processus utilisant OpenSSL/BoringSSL).

Il capture simultanément les métadonnées réseau L3/L4 (TCP SYN), les paramètres TLS L5 (ClientHello), et le contenu applicatif L7 (requêtes HTTP déchiffrées), corrèle le tout en mémoire par la clé src_ip:src_port, et envoie les données vers ClickHouse en batch.

Architecture interne

  +-----------------+    TC ingress                +--------------------+
  |  réseau entrant  |---------------------------->|  bpf/tc_capture.c  |
  |                 |                             |                    |
  |  SYN packet     |  --> pb_tcp_syn  (perf)    |  Programme eBPF    |
  |  TLS ClientHello|  --> pb_tls_hello (perf)   |  CO-RE             |
  |  HTTP port 80   |  --> pb_http_plain (perf)  |                    |
  +-----------------+                             +--------------------+
                                                          |
  +-----------------+    uprobe SSL_read/SSL_write  +--------------------+
  |  serveur web    |--------------------------->  |  bpf/uprobe_ssl.c  |
  |  (OpenSSL)      |                             |                    |
  |  flux déchiffré |  --> pb_ssl_data  (perf)    |  Programme eBPF    |
  |  accept4 events |  --> pb_accept   (perf)    |  CO-RE             |
  +-----------------+                             +--------------------+
                                                          |
                                              +-----------v-----------+
                                              |     Go userspace       |
                                              |                        |
                                              |  internal/loader/      |
                                              |  - PerfEvent readers    |
                                              |  - 5 goroutines        |
                                              |                        |
                                              |  internal/parser/      |
                                              |  - JA4/JA3 calculator   |
                                              |  - H2ConnState (HPACK) |
                                              |  - HTTP/1.1 parser     |
                                              |                        |
                                              |  internal/dispatcher/  |
                                              |  - Magic Bytes router  |
                                              |                        |
                                              |  internal/correlation/ |
                                              |  - 256-shard manager   |
                                              |  - AcceptCache          |
                                              |  - GC 100ms            |
                                              |  - timeout 500ms       |
                                              |                        |
                                              |  internal/writer/      |
                                              |  - ClickHouse batch    |
                                              +----------+-------------+
                                                         |
                                              INSERT batch TCP :9000
                                                         |
                                                         v
                                              +----------------------+
                                              |  ja4_logs.           |
                                              |  http_logs_raw       |
                                              +----------------------+

Hooks eBPF

TC ingress — Couches L3/L4/L5

Le programme bpf/tc_capture.c est attaché aux interfaces réseau via TC (Traffic Control) en ingress (clsact qdisc). Par défaut, il s'attache sur toutes les interfaces UP (sauf loopback). Il s'exécute pour chaque paquet entrant :

Filtrage kernel-side :

  • Ports autorisés : la map allowed_ports (HASH) filtre par port destination/source. Seuls les ports configurés (défaut : 80, 443) sont traités.
  • IP/CIDR ignorés : la map ignored_src (LPM_TRIE) ignore le trafic provenant des CIDR configurés. Le lookup utilise le format {prefixlen, data[4]} en network byte order.

Paquets TCP SYN : extraction des options TCP et métadonnées IP depuis les headers du paquet.

  • bpf_skb_load_bytes() pour lire les options TCP depuis le skb
  • Émis via pb_tcp_syn (PerfEventArray)

ClientHello TLS : détection du type 0x16 (Handshake) et sous-type 0x01 (ClientHello).

  • bpf_skb_load_bytes() avec tailles en cascade (1024 → 512 → 256) pour capturer SNI et extensions
  • Le struct tls_hello_event place le payload à l'offset 0 (compatible kernel 4.18)
  • Émis via pb_tls_hello (PerfEventArray)

HTTP en clair (port 80/8080) : pour les connexions non chiffrées.

  • SYN/FIN/RST exclus (uniquement les segments porteurs de données)
  • bpf_skb_load_bytes() avec tailles en cascade (512 → 256 → 128 → 64)
  • Le struct http_plain_event place le payload à l'offset 0
  • Émis via pb_http_plain (PerfEventArray)

Uprobes SSL_read/SSL_write — Couche L7

Les uprobes s'attachent dynamiquement aux fonctions OpenSSL dans libssl.so :

Hook Type Rôle
SSL_set_fd uprobe Associe SSL*fd, peuple ssl_conn_map
SSL_read uprobe + uretprobe Capture les requêtes du client (direction=0)
SSL_write uprobe + uretprobe Capture les réponses du serveur (direction=1)

Corrélation fd → src_ip:src_port (3 niveaux de priorité) :

  1. ssl_conn_map[ssl_ptr] — si SSL_set_fd a été appelé et que fd_conn_map[fd] contient l'IP (via accept4)
  2. accept_map[{pid_tgid, fd}] — cache accept4 (tracepoint kernel)
  3. Fallback /proc/<pid>/net/tcp — lecture depuis l'espace utilisateur (moins fiable)

Les données sont émises via pb_ssl_data (PerfEventArray) avec un flag direction (0=client→serveur, 1=serveur→client).

Tracepoints accept4

Les tracepoints sys_enter_accept4 / sys_exit_accept4 (plus stables que les kprobes) capturent le tuple (src_ip, src_port, fd, pid_tgid) pour chaque nouvelle connexion acceptée. Ils peuplent :

  • accept_map[{pid_tgid, fd}] — pour corrélation SSL côté BPF
  • fd_conn_map[fd] — pour SSL_set_fd
  • pb_accept (PerfEventArray) — pour corrélation SSL côté Go (AcceptCache)

Maps eBPF résumé

Map Type Rôle
allowed_ports HASH (key=u16, val=u8) Ports TCP autorisés (peuplée depuis Go)
ignored_src LPM_TRIE (key={prefixlen, data[4]}, val=u8) CIDR/IP sources à ignorer (peuplée depuis Go)
tc_stats PERCPU_ARRAY (7 compteurs) Statistiques de debug BPF
ssl_conn_map HASH (key=ssl_ptr, val=ssl_conn_info) Association SSL* → fd + IP
fd_conn_map HASH (key=fd, val=ssl_conn_info) Association fd → IP (depuis accept4)
accept_map HASH (key={pid_tgid,fd}, val=accept_event) Cache accept4 côté BPF
ssl_args_map HASH (key=pid_tgid, val=ssl_read_args) Sauvegarde arguments SSL_read/Write entry
__tls_buf PERCPU_ARRAY (1 entrée) Buffer temp > 512o (stack eBPF limit)
__http_buf PERCPU_ARRAY (1 entrée) Buffer temp HTTP plain
__ssl_buf PERCPU_ARRAY (1 entrée) Buffer temp SSL data

Corrélation in-memory

Le gestionnaire internal/correlation maintient un état par connexion :

Mécanisme Valeur
Sharding 256 buckets (hash XOR src_ip ^ src_port)
Timeout orphelin 500 ms (→ session exportée vers ClickHouse)
Détection Slowloris 10 s (export partiel)
GC interval 100 ms
Keep-Alive len(Requests) incrémenté par requête

AcceptCache

Le AcceptCache map {tgid, fd} → SessionKey + dstIP + dstPort avec TTL 10s. Il est peuplé par les événements accept4 et consulté par le consommateur SSL quand ssl_conn_map a src_ip=0. Purge automatique toutes les 30s.

FDCache

Le FDCache résout fd → IP:port via /proc/<pid>/net/tcp (fallback quand accept4 n'est pas disponible). TTL 5s. Utilise parseHexIPv4 / parseHexIPv6 pour décoder le format little-endian du kernel.

Filtrage ignore_src (userspace)

En plus du filtrage BPF (LPM_TRIE côté kernel), les 5 goroutines de consommation appliquent un filtrage userspace via isIgnoredIP() + net.IPNet.Contains(). Cela couvre le chemin SSL (SSL_read/SSL_write/accept4) où le BPF LPM_TRIE ne peut pas filtrer (l'IP vient de ssl_conn_map ou /proc, pas du paquet réseau).

Routeur Magic Bytes (dispatcher)

Buffer reçu (SSL data ou HTTP plain)
    |
    +-- starts with "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" --> ProtoHTTP2
    |
    +-- starts with "GET " / "POST " / "PUT " / ...      --> ProtoHTTP1
    |
    +-- partial H2 preface prefix                         --> ProtoHTTP2 (buffer)
    |
    +-- autre                                              --> ProtoUnknown (ignoré)

Parsing HTTP/2 — H2ConnState

Le H2ConnState (package internal/parser) maintient un décodeur HPACK par-connexion avec table dynamique. Il traite les frames via golang.org/x/net/http2.Framer :

Frame Action
SETTINGS Extraction des 7 paramètres (+ paramètre 0x7 JA4H2). Mise à jour de la taille de table HPACK.
WINDOW_UPDATE (stream 0) Capture de l'incrément de fenêtre connexion
HEADERS + CONTINUATION Assemblage des fragments, décodage HPACK, extraction pseudo-headers ordre
DATA Comptage des octets par stream
PRIORITY Capture des paramètres de priorité (StreamDep, Exclusive, Weight)
RST_STREAM Transition du stream vers "closed"
GOAWAY Capture LastStreamID + ErrCode
PING Détection ACK

Fingerprinting Akamai :

  • h2_fingerprint : SETTINGS[pairs]|WINDOW_UPDATE[value]|PRIORITY[0/1]|PSEUDO_ORDER[order]
  • h2_settings_fp : liste brute des paramètres SETTINGS
  • h2_pseudo_order : notation abrégée (ex: m,a,s,p pour :method, :authority, :scheme, :path)

Données collectées

L3/L4 (TCP SYN)

Champ Description
src_ip, src_port Clé de corrélation
dst_ip, dst_port Destination IP et port
ttl Time To Live initial
df_bit Don't Fragment bit
ip_id IP Identification (0 = Linux/VPN/spoofé)
ip_total_length Longueur totale IP (octets)
window_size Taille fenêtre TCP SYN
window_scale Option Window Scale (RFC 1323), 0xFF = absent
mss Maximum Segment Size, 0 = absent
tcp_options Options TCP brutes (40 octets max), noms abrégés (MSS, WS, SACK, TS, NOP)

L5 (TLS ClientHello)

Champ Description
tls_version Version TLS la plus haute annoncée (extrait des SupportedVersions)
cipher_suites Liste suites cryptographiques (hex séparées par tirets)
extensions Liste IDs extensions TLS (hex séparés par tirets)
supported_groups Groupes Diffie-Hellman supportés
ec_point_formats Formats de points elliptiques
alpn ALPN list (h2, http/1.1, ...)
sni Server Name Indication
ja4 Empreinte JA4 (FoxIO)
ja3_raw Empreinte JA3 brute (version,ciphers,exts,groups,ecfmts)
ja3_hash JA3 hash MD5

L7 HTTP/1.1

Champ Description
method Méthode HTTP
path Chemin (sans query string)
query_string Paramètres query (sans le '?')
host En-tête Host ou TLS SNI
http_version HTTP/1.0 ou HTTP/1.1
headers_raw Noms d'en-têtes joints par ";"
header_order_signature Signature de l'ordre
status_code Code de statut de la réponse
response_size Taille réponse (octets)
duration_ms Durée requête
client_headers JSON des en-têtes capturés
header_User-Agent, etc. 16 en-têtes individuels capturés

L7 HTTP/2 (preface client)

Champ Description
h2_header_table_size SETTINGS ID 1 (-1 si absent du preface)
h2_enable_push SETTINGS ID 2
h2_max_concurrent_streams SETTINGS ID 3
h2_initial_window_size SETTINGS ID 4
h2_max_frame_size SETTINGS ID 5
h2_max_header_list_size SETTINGS ID 6
h2_enable_connect_protocol SETTINGS ID 8 (RFC 8441)
h2_window_update Incrément WINDOW_UPDATE connexion
h2_has_priority 1 si une frame PRIORITY a été reçue
h2_settings_ack 1 si SETTINGS ACK reçu
h2_pseudo_order Ordre pseudo-headers (ex: m,a,s,p)
h2_fingerprint Fingerprint composite Akamai
h2_settings_fp Chaîne brute des SETTINGS

eBPF CO-RE

Aspect Détail
Compilateur clang + llvm via bpf2go
Target bpf (amd64, kernel 4.18+)
BTF source /sys/kernel/btf/vmlinux (disponible RHEL 8+)
Relocations cilium/ebpf résout automatiquement les offsets struct
Embed go:generate génère ja4tc_x86_bpfel.go + ja4ssl_x86_bpfel.go
Compatibilité Rocky/RHEL Linux 8, 9, 10 (kernel 4.18+)
IPC BPF_MAP_TYPE_PERF_EVENT_ARRAY (kernel 4.4+)
Lecture paquets bpf_skb_load_bytes() (kernel 4.5+) avec tailles constantes en cascade

Contrainte kernel 4.18 : le vérificateur BPF rejette les tailles variables vers map values. Toutes les copies de payload utilisent des tailles constantes (1024 → 512 → 256 pour TLS, 512 → 256 → 128 → 64 pour HTTP). Les structs > 512o utilisent un PERCPU_ARRAY temporaire (limite stack eBPF).

Configuration

# /etc/ja4ebpf/config.yml

# Interfaces réseau à surveiller (TC ingress).
# "any" = toutes les interfaces UP (sauf loopback).
# Ou liste explicite : ["eth0", "eth1"]
interfaces:
  - any

# Chemin vers libssl pour les uprobes SSL_read/SSL_write/SSL_set_fd
ssl_lib_path: "/usr/lib64/libssl.so.3"

# Ports TCP à surveiller (filtrage BPF côté kernel)
listen_ports:
  - 80
  - 443

# CIDR/IP sources à ignorer (filtrage BPF LPM_TRIE + filtrage userspace SSL)
# ignore_src:
#   - 10.0.0.0/8
#   - 172.16.0.0/12
#   - 192.168.0.0/16
#   - 127.0.0.1

# Mode debug
debug: false

clickhouse:
  dsn: "clickhouse://default:@127.0.0.1:9000/ja4_logs?async_insert=0"
  batch_size: 500
  flush_secs: 1

correlation:
  timeout_ms: 500
  slowloris_ms: 10000

log:
  level: "info"
  format: "json"

Variables d'environnement

Variable Défaut Description
JA4EBPF_CONFIG /etc/ja4ebpf/config.yml Chemin du fichier config
JA4EBPF_INTERFACES any Interfaces (séparées par virgule)
JA4EBPF_INTERFACE Rétrocompatibilité : une seule interface
JA4EBPF_SSL_LIB_PATH /usr/lib64/libssl.so.3 Chemin libssl
JA4EBPF_CLICKHOUSE_DSN voir config DSN ClickHouse
JA4EBPF_DEBUG false Mode debug (true, 1, yes)
JA4EBPF_LISTEN_PORTS 80,443 Ports surveillés (séparés par virgule)
JA4EBPF_IGNORE_SRC CIDR ignorés (séparés par virgule)

Build

# Compilation eBPF → Go (nécessite clang sur la machine cible)
go generate ./internal/loader/

# Build du binaire
go build ./cmd/ja4ebpf/

# Build complet (bytecode eBPF + binaire Go) — Docker Rocky Linux
make build

# Tests unitaires
go test ./...

# Build RPMs
make rpm-ja4ebpf

Structure du code

services/ja4ebpf/
├── bpf/
│   ├── bpf_types.h            # Structs C partagées + déclarations maps PerfEventArray
│   ├── headers/vmlinux.h      # Types kernel BTF (auto-généré)
│   ├── tc_capture.c           # Programme TC ingress (L3/L4/L5 + HTTP plain)
│   └── uprobe_ssl.c           # Programme uprobes SSL + tracepoints accept4
├── cmd/ja4ebpf/
│   ├── main.go                # Point d'entrée : 5 goroutines consumer + config
│   └── main_test.go           # Tests parseCIDRs, parseIgnoreNets, isIgnoredIP, parseTCPOptions
├── internal/
│   ├── loader/
│   │   ├── loader.go          # Chargement eBPF + PerfEvent readers + attachement TC/uprobes
│   │   ├── ja4tc_x86_bpfel.go # Bytecode TC embarqué (généré par bpf2go)
│   │   └── ja4ssl_x86_bpfel.go# Bytecode SSL embarqué (généré par bpf2go)
│   ├── parser/
│   │   ├── tls.go             # ParseClientHello + ComputeJA4 + ComputeJA3
│   │   ├── http1.go           # Parser HTTP/1.1 (requêtes + réponses)
│   │   ├── http2.go           # Constantes HTTP/2 + filtres en-têtes capturés
│   │   ├── h2conn.go          # H2ConnState : framer + HPACK + fingerprinting
│   │   ├── tls_test.go        # Tests JA4, JA3, SNI, extensions
│   │   ├── http1_test.go      # Tests HTTP/1.1
│   │   └── http2_test.go      # Tests H2ConnState complet
│   ├── dispatcher/
│   │   ├── dispatcher.go      # Routeur Magic Bytes (ProtoHTTP1/2/Unknown)
│   │   └── dispatcher_test.go # Tests Classify
│   ├── correlation/
│   │   ├── session.go          # Structs L3L4, TLSInfo, HTTPRequest, SessionState
│   │   ├── manager.go         # Gestionnaire sessions 256-shard + GC
│   │   ├── accept_cache.go    # AcceptCache {tgid,fd} → SessionKey (TTL 10s)
│   │   ├── correlation_test.go# Tests Manager + SessionState
│   │   └── accept_cache_test.go# Tests AcceptCache
│   ├── procutil/
│   │   ├── proc_lookup.go     # FDCache : résolution fd → IP via /proc/net/tcp
│   │   └── proc_lookup_test.go# Tests parseHexIPv4/IPv6, isIPv4MappedIPv6
│   └── writer/
│       ├── clickhouse.go      # Writer ClickHouse (batch + JSON → http_logs_raw)
│       └── clickhouse_test.go # Tests formatTCPOptions, H2 fingerprints, etc.
├── packaging/
│   ├── rpm/ja4ebpf.spec       # Spec RPM (el8/el9/el10)
│   └── systemd/ja4ebpf.service # Unit systemd
├── config.yml.example         # Exemple de configuration
├── Dockerfile                 # Image de production
├── Dockerfile.tests           # Image de tests
├── Dockerfile.package         # Build RPM multi-distro
└── Makefile

Capabilities Linux requises (SELinux Enforcing)

L'agent tourne sous l'utilisateur ja4ebpf (UID/GID 490 fixe). Les capabilities Linux accordées via AmbientCapabilities :

Capability Raison
CAP_BPF Chargement des programmes eBPF (kernel 5.8+)
CAP_SYS_ADMIN Uprobes + RHEL 8 (kernel 4.18, pré-CAP_BPF)
CAP_NET_ADMIN Attachement hooks TC ingress
CAP_NET_RAW Accès raw socket (fallback)
CAP_PERFMON Accès perf events pour les uprobes
CAP_SYS_PTRACE Résolution des offsets de fonctions pour les uprobes
CAP_DAC_READ_SEARCH Lecture /proc/<pid>/maps pour localiser libssl.so

LimitMEMLOCK=infinity est requis pour le mlock() des maps eBPF.