- Use two separate //go:generate directives (Ja4Tc for tc_capture.c, Ja4Ssl
for uprobe_ssl.c) to avoid duplicate LICENSE symbol and multi-file clang issue
- Update loader.go to hold tcObjs/sslObjs separately with correct field names:
UprobeSslSetFd, UprobeSslReadEntry, UretprobeSslReadExit,
KprobeAccept4Entry, KretprobeAccept4Exit
- Add systemd-rpm-macros to all three RPM build stages (el8/el9/el10)
so that %{_unitdir} macro resolves correctly
- RPMs now build successfully for el8, el9, el10
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
12 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 tout en mémoire par la clé src_ip:src_port, et envoie les données vers ClickHouse en batch.
Architecture interne
+-----------------+ TC ingress (XDP/TC) +--------------------+
| réseau entrant |-------------------------->| bpf/tc_capture.c |
| | | |
| SYN packet | --> rb_tcp_syn (16 MB) | Programme eBPF |
| TLS ClientHello| --> rb_tls_hello (16 MB) | CO-RE |
| HTTP port 80 | --> rb_http_plain (32 MB)| |
+-----------------+ +--------------------+
|
+-----------------+ uprobe SSL_read +--------------------+
| serveur web |--------------------------> | bpf/uprobe_ssl.c |
| (OpenSSL) | | |
| flux déchiffré | --> rb_ssl_data (64 MB) | Programme eBPF |
+-----------------+ | CO-RE |
+--------------------+
|
+-----------v-----------+
| Go userspace |
| |
| internal/loader/ |
| - RingBuffer readers |
| - 5 goroutines |
| |
| internal/parser/ |
| - JA4 calculator |
| - H2 preface parser |
| - HTTP/1.1 parser |
| |
| internal/dispatcher/ |
| - Magic Bytes router |
| |
| internal/correlation/ |
| - 256-shard manager |
| - 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é à l'interface réseau via TC (Traffic Control) en ingress. Il s'exécute pour chaque paquet entrant :
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- Envoyé dans le RingBuffer
rb_tcp_syn(16 MB)
ClientHello TLS : détection du type 0x16 (Handshake) et sous-type 0x01 (ClientHello).
bpf_skb_load_bytes()pour capturer 512 octets du payload- Envoyé dans le RingBuffer
rb_tls_hello(16 MB)
HTTP en clair (port 80/8080) : pour les connexions non chiffrées.
- SYN/FIN/RST exclus (uniquement les segments porteurs de données)
- Jusqu'à 4096 octets via
bpf_skb_load_bytes() - Envoyé dans le RingBuffer
rb_http_plain(32 MB)
Uprobe SSL_read — Couche L7
Les uprobes s'attachent dynamiquement à SSL_read (ou SSL_read_ex) dans le processus du serveur web. Elles s'exécutent après le déchiffrement TLS, capturant le flux HTTP en clair dans rb_ssl_data (64 MB).
La clé de corrélation src_ip:src_port est extraite depuis la structure SSL* → file descriptor → socket noyau via bpf_probe_read_kernel().
Kprobe accept4
Un kprobe sur accept4 peuple rb_accept (4 MB) avec le tuple (src_ip, src_port, fd, pid_tgid), permettant d'associer chaque fd SSL à une connexion TCP.
Corrélation in-memory
Le gestionnaire internal/correlation maintient un état par connexion :
| Mécanisme | Valeur |
|---|---|
| Sharding | 256 buckets (hash src_ip:src_port) |
| Timeout orphelin | 500 ms (→ correlated=0) |
| Détection Slowloris | 10 s (export partiel) |
| GC interval | 100 ms |
| Keep-Alive | max_keepalives incrémenté par requête |
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é)
Fingerprinting HTTP/2 passif : après détection du preface PRI * HTTP/2.0..., le parser itère les frames :
- Frame
SETTINGS(type0x04) : extraction des 7 paramètres, valeur-1si absent - Frame
WINDOW_UPDATE(type0x08, stream 0) : incrément de fenêtre connexion - Frame
HEADERS(type0x01) : ordre des pseudo-headers (m,a,s,p)
Données collectées
L3/L4 (TCP SYN)
| Champ | Description |
|---|---|
src_ip, src_port |
Clé de corrélation |
ttl |
Time To Live initial |
df_bit |
Don't Fragment bit |
ip_id |
IP Identification (0 = Linux/VPN/spoofé) |
window_size |
Taille fenêtre TCP SYN |
window_scale |
Option Window Scale (RFC 1323) |
mss |
Maximum Segment Size |
tcp_options |
Options TCP brutes (40 octets max) |
tcp_jitter_variance |
Variance jitter inter-SYN |
syn_timing_cv |
Délai SYN→ClientHello (ns) |
L5 (TLS ClientHello)
| Champ | Description |
|---|---|
tls_version |
Version TLS |
ciphers |
Liste suites cryptographiques |
extensions |
Liste extensions TLS |
elliptic_curves |
Courbes elliptiques supportées |
point_formats |
Formats de points EC |
alpn |
ALPN list (h2, http/1.1, ...) |
sni |
Server Name Indication |
ja4 |
Empreinte JA4 calculée Go-side |
ja4t |
Empreinte JA4T (TCP) calculée Go-side |
L7 HTTP/1.1
| Champ | Description |
|---|---|
method |
Méthode HTTP |
path |
Chemin |
query_string |
Paramètres query |
http_version |
HTTP/1.0 ou HTTP/1.1 |
headers_raw |
En-têtes dans leur ordre d'émission |
header_order_signature |
Hash de l'ordre |
status_code |
Code de statut |
response_size |
Taille réponse (octets) |
duration_ms |
Durée requête |
timestamp_ns |
Horodatage ns absolu |
L7 HTTP/2 (preface client)
| Champ | Description |
|---|---|
h2_header_table_size |
SETTINGS ID 1 (-1 si absent) |
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 |
Flag PRIORITY dans HEADERS frame |
h2_pseudo_order |
Ordre pseudo-headers (ex: m,a,s,p) |
eBPF CO-RE
| Aspect | Détail |
|---|---|
| Compilateur | clang + llvm |
| Target | bpf (architecture BPF 64 bits) |
| BTF source | /sys/kernel/btf/vmlinux (disponible RHEL 8+) |
| Relocations | cilium/ebpf résout automatiquement les offsets struct |
| Embed | go:generate génère bpf_bpfel.go avec bytecode embarqué |
| Compatibilité | Rocky/RHEL Linux 8, 9, 10 (kernel 4.18+) |
Configuration
# /etc/ja4ebpf/config.yml
interface: eth0
target_binary: /usr/sbin/httpd
clickhouse:
dsn: clickhouse://data_writer:pwd@localhost:9000/ja4_logs
table: http_logs_raw
batch_size: 500
flush_interval_ms: 200
correlation:
session_timeout_ms: 500
slowloris_threshold_s: 10
gc_interval_ms: 100
Variables d'environnement
| Variable | Défaut | Description |
|---|---|---|
JA4EBPF_INTERFACE |
eth0 |
Interface réseau |
JA4EBPF_TARGET_BINARY |
/usr/sbin/httpd |
Binaire à hooker (uprobe SSL_read) |
JA4EBPF_CLICKHOUSE_DSN |
— | DSN ClickHouse |
JA4EBPF_BATCH_SIZE |
500 |
Taille des batchs d'insertion |
JA4EBPF_FLUSH_INTERVAL_MS |
200 |
Intervalle de flush (ms) |
JA4EBPF_SESSION_TIMEOUT_MS |
500 |
Timeout session orpheline |
JA4EBPF_SLOWLORIS_THRESHOLD_S |
10 |
Seuil détection Slowloris (s) |
Build
# Build complet (bytecode eBPF + binaire Go) — Docker Rocky Linux
make build-ja4ebpf
# Tests unitaires (nécessite NET_RAW/NET_ADMIN/BPF capabilities)
make test-ja4ebpf
# Build RPMs el8/el9/el10
make rpm-ja4ebpf
# → services/ja4ebpf/dist/rpm/el{8,9,10}/
Structure du code
services/ja4ebpf/
├── bpf/
│ ├── bpf_types.h # Structs C partagées + déclarations maps eBPF
│ ├── tc_capture.c # Programme TC ingress (L3/L4/L5 + HTTP plain)
│ └── uprobe_ssl.c # Programme uprobe SSL_read (L7 déchiffré)
├── cmd/ja4ebpf/
│ └── main.go # Point d'entrée : 5 goroutines consumer
├── internal/
│ ├── loader/
│ │ └── loader.go # Chargement eBPF + RingBuffer readers + désérialisation
│ ├── parser/
│ │ ├── ja4.go # Calcul empreintes JA4 / JA4T
│ │ ├── http2.go # Parser HTTP/2 preface (SETTINGS, WINDOW_UPDATE, HEADERS)
│ │ └── http1.go # Parser HTTP/1.1
│ ├── dispatcher/
│ │ └── dispatcher.go # Routeur Magic Bytes (ProtoHTTP1/2/Unknown)
│ ├── correlation/
│ │ └── manager.go # Gestionnaire sessions 256-shard
│ └── writer/
│ └── writer.go # Writer ClickHouse (batch + retry)
├── packaging/
│ ├── rpm/ja4ebpf.spec # Spec RPM (el8/el9/el10)
│ └── systemd/ja4ebpf.service # Unit systemd
├── Dockerfile # Image de production
├── Dockerfile.tests # Image de tests
├── Dockerfile.package # Build RPM multi-distro (5 stages)
└── 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_PERFMON |
Accès perf events pour les uprobes |
LimitMEMLOCK=infinity est requis pour le mlock() des maps eBPF.
Tests d'intégration
Stacks Docker Compose testant l'agent contre différents serveurs web :
make test-integration # Apache httpd (référence)
make test-nginx # Nginx
make test-nginx-varnish # Nginx + Varnish (reverse proxy)
make test-hitch-varnish # Hitch (TLS) + Varnish
make test-all-stacks # Les 4 stacks en séquence