# 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()` avec tailles en cascade (512 → 256 → 128) pour capturer SNI et extensions - La taille réellement copiée est stockée dans `payload_len` - 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) - `bpf_skb_load_bytes()` avec tailles en cascade (256 → 128 → 64) - La taille réellement copiée est stockée dans `payload_len` - 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 | | `dst_ip`, `dst_port` | Destination IP et port (extrait du SYN) | | `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 la plus haute annoncée (extrait des SupportedVersions) | | `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 (`nil` si absent du preface, omis dans le JSON) | | `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 │ │ └── session.go # Structs L3L4, TLSInfo, SessionState │ └── 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 ```