- 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>
293 lines
12 KiB
Markdown
293 lines
12 KiB
Markdown
# 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
|
|
```
|