**Apache HTTP capture via apr_socket_recv** : - Uprobe sur libapr-1.so.0 (Apache Portable Runtime) - Compatible tous kernels 4.18+ (CentOS 8, Rocky 9/10) - Configuration unifiée : servers: ["nginx", "apache"] **nginx HTTP capture validation multi-kernel** : - Kretprobe __x64_sys_recvfrom validé sur CentOS 8 (4.18) - Rocky 9 (5.14) et Rocky 10 (6.12) confirmés - Contourne limitation tracepoint sys_exit_recvfrom **Documentation** : - docs/TEST_BUILD_STACK.md : stack complète test/build (VMs, Docker, RPMs) - services/ja4ebpf/docs/APACHE_HTTP_VALIDATION.md : validation Apache - services/ja4ebpf/docs/NGINX_MULTI_KERNEL_VALIDATION.md : validation nginx - docs/architecture.md + docs/services/ja4ebpf.md mis à jour **Tests unitaires Apache** : - internal/loader/apache_test.go : tests libapr, paths, structures BPF - internal/correlation/apache_test.go : tests corrélation HTTP Apache **Packaging** : - RPM spec mis à jour (version 0.3.0-1, changelog complet) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
506 lines
25 KiB
Markdown
506 lines
25 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 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 |
|
|
+-----------------+ +--------------------+
|
|
|
|
|
+-----------------+ uprobe HTTP (nginx/Apache) +-------------------+
|
|
| nginx recvfrom | --> pb_nginx_http (perf) | bpf/uprobe_nginx.c|
|
|
| Apache apr_recv| --> pb_apache_http (perf) | bpf/uprobe_apache.c|
|
|
+-----------------+ +--------------------+
|
|
|
|
|
+-----------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) |
|
|
|
|
### Tracepoints/Kretprobe HTTP serveurs web (Nginx, Apache)
|
|
|
|
#### 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 |
|
|
|
|
**Filtrage par PID nginx** : La map `nginx_pid_map` ne permet que les processus nginx identifiés via `/proc/<pid>/cmdline`.
|
|
|
|
#### Apache httpd HTTP en clair
|
|
Les hooks `uprobe_apr_socket_recv` (entry) / `uretprobe_apr_socket_recv` (return) capturent les appels à la fonction `apr_socket_recv` d'Apache Portable Runtime pour capturer le trafic HTTP en clair complet :
|
|
|
|
| Hook | Type | État | Rôle |
|
|
|------|------|------|------|
|
|
| `uprobe/apr_socket_recv` | uprobe | ✅ Fonctionnel | Sauvegarde buf_ptr et len depuis arguments |
|
|
| `uretprobe/apr_socket_recv` | uretprobe | ✅ Fonctionnel | Capture les données lues + émet vers pb_apache_http |
|
|
|
|
**Cible** : `libapr-1.so.0` (Apache Portable Runtime)
|
|
**Chemin automatique** : `/usr/lib64/libapr-1.so.0` (RHEL/CentOS/Rocky/Alma 8/9/10)
|
|
|
|
**Avantages** :
|
|
- Universelle : Fonctionne sur tous les kernels 4.18+ (pas de dépendance tracepoint)
|
|
- Fiable : Capture directe au niveau application Apache
|
|
- Performant : Un seul uprobe par processus Apache
|
|
|
|
**Filtrage par PID Apache** : La map `apache_http_pid_map` ne permet que les processus Apache httpd identifiés via `/proc/<pid>/cmdline`.
|
|
|
|
**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
|
|
|
|
```yaml
|
|
# /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
|
|
|
|
# Configuration des uprobes pour capture HTTP serveurs web
|
|
uprobes:
|
|
enabled: true
|
|
# Liste des serveurs à monitorer : "nginx", "apache", ou les deux
|
|
servers:
|
|
- nginx
|
|
- apache
|
|
# Nombre de tentatives d'attachement (processus peuvent démarrer après ja4ebpf)
|
|
max_retries: 30
|
|
retry_interval_sec: 2
|
|
|
|
# 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
|
|
|
|
```bash
|
|
# Build complet (bytecode eBPF + binaire Go) — Docker Rocky Linux
|
|
make build
|
|
|
|
# Build RPMs (multi-distro el8/el9/el10)
|
|
make rpm-ja4ebpf
|
|
|
|
# Tests unitaires (exécutés dans le conteneur de build)
|
|
make test
|
|
```
|
|
|
|
**Note** : La compilation eBPF nécessite clang/llvm et s'effectue dans un conteneur Docker Rocky Linux, pas sur le système hôte.
|
|
|
|
## 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
|
|
│ ├── uprobe_nginx.c # Programme uprobes nginx HTTP (recvfrom)
|
|
│ └── uprobe_apache.c # Programme uprobes Apache HTTP (apr_socket_recv)
|
|
├── cmd/ja4ebpf/
|
|
│ ├── main.go # Point d'entrée : 5 goroutines consumer + config
|
|
│ ├── apache_test.go # Tests Apache (PID detection, libapr paths, corrélation)
|
|
│ └── 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)
|
|
│ │ ├── ja4nginx_x86_bpfel.go# Bytecode nginx embarqué (généré par bpf2go)
|
|
│ │ └── ja4apache_x86_bpfel.go# Bytecode Apache 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 Apache via apr_socket_recv — VALIDÉ (2026-04-20)
|
|
|
|
**Solution implémentée** : Uprobe sur `apr_socket_recv` dans `libapr-1.so.0` (Apache Portable Runtime).
|
|
|
|
**Détails** : Contrairement à nginx qui utilise `recvfrom()`, Apache event MPM utilise les fonctions APR pour les I/O réseau. L'uprobe sur `apr_socket_recv` capture les données HTTP directement au niveau application.
|
|
|
|
**Validation** :
|
|
- ✅ CentOS 8 (kernel 4.18) : 2 événements HTTP capturés
|
|
- ✅ Rocky 10 (kernel 6.12) : 1 événement HTTP capturé
|
|
- ✅ Universelle sur kernels 4.18+ (pas de dépendance tracepoint)
|
|
- ✅ Rapport de validation : `services/ja4ebpf/docs/APACHE_HTTP_VALIDATION.md`
|
|
|
|
### ✅ HTTP Nginx via recvfrom — VALIDÉ multi-kernels (2026-04-20)
|
|
|
|
**Solution implémentée** : Kretprobe sur `__x64_sys_recvfrom`.
|
|
|
|
**Validation** :
|
|
- ✅ CentOS 8 (kernel 4.18) : kretprobe attaché (prog 835)
|
|
- ✅ Rocky 9 (kernel 5.14) : capture HTTP complète validée
|
|
- ✅ Rocky 10 (kernel 6.12) : kretprobe attaché (prog 909)
|
|
- ✅ Universelle sur kernels 4.18+ (x86_64)
|
|
- ✅ Rapport de validation : `services/ja4ebpf/docs/NGINX_MULTI_KERNEL_VALIDATION.md`
|
|
|
|
**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 |
|
|
| `apache_http_pid_map` | HASH (key=u32, val=u8) | Filtrage apr_socket_recv par PID Apache |
|
|
| `apr_socket_recv_args_map` | HASH (key=pid_tgid, val=apr_socket_recv_args) | Sauvegarde arguments apr_socket_recv 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 |
|
|
| `__apache_buf` | PERCPU_ARRAY (1 entrée) | Buffer temp Apache 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. |