Update all documentation to reflect the resolved HTTP nginx capture issue via kretprobe on __x64_sys_recvfrom. Changes: - README.md: Update HTTP status table showing kretprobe is now working - docs/services/ja4ebpf.md: Replace tracepoint with kretprobe in hooks table, mark issue as resolved with validation reference - docs/architecture.md: Clarify TC HTTP plain capture is packet-level only Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
22 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_eventplace 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_eventplace 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) |
Tracepoints/Kretprobe recvfrom (Nginx HTTP en clair)
Les hooks sys_enter_recvfrom / sys_exit_recvfrom capturent les appels système recvfrom() du serveur Nginx pour capturer le trafic HTTP en clair complet :
| Hook | Type | État | Rôle |
|---|---|---|---|
tp_syscalls_sys_enter_recvfrom |
tracepoint | ✅ Fonctionnel | Sauvegarde les arguments recvfrom (sockfd, buf_ptr, len) |
tp_sys_exit_recvfrom |
kretprobe | ✅ Fonctionnel | Capture les données lues + émet vers pb_ginx_http |
Note : Le kretprobe sur __x64_sys_recvfrom remplace le tracepoint sys_exit_recvfrom qui échouait avec "permission denied" sur Rocky Linux 9+.
Filtrage par PID nginx : La map nginx_pid_map ne permet que les processus nginx identifiés via /proc/<pid>/cmdline.
Corrélation fd → src_ip:src_port (3 niveaux de priorité) :
ssl_conn_map[ssl_ptr]— siSSL_set_fda été appelé et quefd_conn_map[fd]contient l'IP (via accept4)accept_map[{pid_tgid, fd}]— cache accept4 (tracepoint kernel)- 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é BPFfd_conn_map[fd]— pour SSL_set_fdpb_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 SETTINGSh2_pseudo_order: notation abrégée (ex:m,a,s,ppour: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
Problèmes connus
✅ HTTP Nginx via recvfrom — RÉSOLU (2026-04-20)
Solution implémentée : Remplacement du tracepoint sys_exit_recvfrom par un kretprobe sur __x64_sys_recvfrom.
Détails : Le tracepoint exit échouait avec "permission denied" sur Rocky Linux 9+. Le kretprobe contourne cette limitation en s'attachant directement à la fonction kernel.
Validation :
- ✅ Toutes les données HTTP capturées sans troncature (path jusqu'à 39 chars, query jusqu'à 244 chars)
- ✅ Headers custom (X-Request-ID, X-Custom-Header) complets
- ✅ Tests unitaires Go ajoutés et validés
- ✅ Rapport de validation :
services/ja4ebpf/docs/CLICKHOUSE_VALIDATION_REPORT.md
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 |
nginx_pid_map |
HASH (key=u32, val=u8) | Filtrage recvfrom par PID nginx |
nginx_read_args_map |
HASH (key=pid_tgid, val=nginx_read_args) | Sauvegarde arguments recvfrom 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 |
__nginx_buf |
PERCPU_ARRAY (1 entrée) | Buffer temp nginx HTTP |
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.