fix(ja4ebpf): split bpf2go generate into Ja4Tc + Ja4Ssl, fix RPM systemd-rpm-macros

- 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>
This commit is contained in:
toto
2026-04-11 23:21:11 +02:00
parent a1e4c1dad5
commit 3b047b680a
155 changed files with 197011 additions and 599 deletions

292
docs/services/ja4ebpf.md Normal file
View File

@ -0,0 +1,292 @@
# 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` (type `0x04`) : extraction des 7 paramètres, valeur `-1` si absent
- Frame `WINDOW_UPDATE` (type `0x08`, stream 0) : incrément de fenêtre connexion
- Frame `HEADERS` (type `0x01`) : 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
```yaml
# /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
```bash
# 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 :
```bash
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
```