Replace TrafficAutoEncoder (MSE reconstruction scoring) with TrafficNormalizingFlow (RealNVP via FrEIA, 4 affine coupling blocks, anomaly score = -log p(x)) for mathematically rigorous density estimation. Add SessionTransformer module producing 32-dimensional sequence embeddings from raw HTTP request sequences (path, method, timing) via a lightweight TransformerEncoder, replacing path_transition_entropy and cadence_cv features. Update thesis documentation sections 2.4.2b and 3.8 accordingly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
25 KiB
3. Architecture de détection multi-couches
3.1 Vue d'ensemble du pipeline
┌──────────────────────────────────────────────────────────────────┐
│ SOURCES DE DONNÉES │
├───────────────────────────┬──────────────────────────────────────┤
│ TC ingress (XDP/TC) │ uprobe SSL_read │
│ Couches L3/L4/L5 │ Couche L7 HTTP déchiffré │
│ │ │
│ │ │
│ réseau XDP/TC → │ Go Magic Bytes dispatcher → │
│ - SYN : TTL, IP-ID, DF, │ HTTP/1.1 : method, path, query, │
│ MSS, Window, Scale │ headers (bruts + ordre), │
│ - TLS ClientHello : │ status, taille, durée_ms, │
│ JA4, ALPN, SNI │ timestamp_ns (nanoseconde) │
│ - HTTP port 80/8080 │ HTTP/2 (depuis preface client) : │
│ │ ordre pseudo-headers │
│ │ WINDOW_UPDATE, PRIORITY flag, │
│ │ SETTINGS (7 params individuels), │
│ │ WINDOW_UPDATE, PRIORITY flag, │
│ │ ordre pseudo-headers │
└───────────────────────────┴──────────────────────────────────────┘
│ │
└─────────────┬─────────────┘
│
┌─────────▼─────────┐
│ Corrélation in-memory │
│ (ja4ebpf) │
│ Clé: src_ip:port │
│ Keep-Alive multi-│
│ request tracking │
│ Timeout orphelin │
│ 500ms │
│ L3/L4 ou L7 seul │
└─────────┬─────────┘
│
┌─────────▼─────────────────────────────┐
│ ClickHouse │
│ │
│ ja4_logs (brut, TTL 2h) │
│ ┌─────────────────────────────────┐ │
│ │ 13 fichiers SQL (00→12) │ │
│ │ AggregatingMergeTree views │ │
│ │ view_ai_features_1h │ │
│ │ view_thesis_features_1h │ │
│ │ agg_path_sequences_1h │ │
│ │ agg_request_timing_1h │ │
│ │ agg_resource_cascade_1h │ │
│ └─────────────────────────────────┘ │
│ ja4_processing (agrégé, TTL 7j) │
└─────────┬─────────────────────────────┘
│
┌─────────▼─────────────────────────────┐
│ bot_detector │
│ (16 modules Python, │
│ 4 800 lignes) │
│ │
│ Cycle 300s: │
│ ┌──────────────────────────────────┐ │
│ │ 1. Chargement features ClickHouse│ │
│ │ 2. dict lookups (IP, JA4, ASN) │ │
│ │ 3. browser_matcher scoring │ │
│ │ 3b. dynamic H2 profiling scoring │ │
│ │ 4. EIF bifurqué (complet/appli) │ │
│ │ 5. NF log-likelihood scoring │ │
│ │ 6. XGBoost probabilité │ │
│ │ 7. Fusion LR fusion │ │
│ │ 8. HDBSCAN clustering (NF latent) │ │
│ │ 9. Écriture résultats ClickHouse │ │
│ └──────────────────────────────────┘ │
└─────────┬─────────────────────────────┘
│
┌─────────▼─────────────────────────────┐
│ Dashboard │
│ (16 pages, 37 routes API) │
│ │
│ - Carte IP mondiale temps réel │
│ - Heatmaps temporelles │
│ - Top bots par famille │
│ - SHAP / ExIFFI feature importance │
│ - HDBSCAN cluster viewer │
│ - Graphes de flottes NetworkX │
└────────────────────────────────────────┘
Composants clés :
uprobe eBPF : mécanisme du noyau Linux permettant d'attacher dynamiquement une sonde eBPF à n'importe quelle fonction en espace utilisateur (par exemple SSL_read d'OpenSSL/BoringSSL), en utilisant l'infrastructure de breakpoints du noyau. L'uprobe intercepte l'appel à l'entrée ou à la sortie de la fonction cible, copie ses arguments ou sa valeur de retour dans un ring buffer eBPF, et rend le contrôle immédiatement — sans modifier le code source du processus cible, sans le mettre en pause, et sans nécessiter de recompilation. Dans ja4ebpf, l'uprobe sur SSL_read capture le buffer de données déchiffrées avant qu'il soit consommé par le serveur web (Apache, Nginx, Varnish, HAProxy), rendant l'interception totalement transparente côté applicatif.
AggregatingMergeTree (ClickHouse) : moteur de table ClickHouse spécialisé pour l'agrégation incrémentale. Contrairement à un merge ordinaire (qui fusionne des lignes brutes), il fusionne des états d'agrégation partiels (AggregateFunction columns) — permettant à SUM, AVG, uniqCombined, etc. d'être calculés correctement même si les données arrivent dans le désordre. Cela permet des vues matérialisées en temps réel à faible latence : chaque cycle d'insertion met à jour l'état partiel, et la vue finale est calculée à la demande en fusionnant tous les états partiels.
3.2 Couche L3 — IP et paquets
La couche L3 (couche réseau IP) fournit trois catégories de signaux :
IP ID (Identification) : champ 16 bits dans l'en-tête IP servant à l'identification unique des fragments IP. Valeur 0 (ou toujours identique) indique une pile TCP sans état ou une implémentation qui génère des paquets SYN sans identifiant séquentiel. Feature ip_id_zero_ratio : ratio de connexions avec IP ID = 0 dans la fenêtre de 300 secondes. Les scanners comme Masscan génèrent systématiquement IP ID = 0.
TTL (Time to Live) : champ 8 bits décrémenté par chaque routeur traversé. Quand TTL atteint 0, le paquet est rejeté et un message ICMP TTL Exceeded est envoyé à la source. Valeurs initiales par OS : Linux = 64, Windows = 128, iOS/macOS = 64. Après K sauts, TTL_observé = TTL_initial - K.
Features : avg_ttl (TTL moyen sur la fenêtre), ttl_std (écart-type du TTL — un TTL constant indique une topologie fixe ou une manipulation). Un TTL_observé divisible par 64 ou 128 selon le chemin réseau attendu valide l'OS déclaré ; une incohérence signale un VPN ou un tunnel qui modifie le TTL.
DF bit (Don't Fragment) : bit dans l'en-tête IP interdisant la fragmentation. Quand DF=1 et que le paquet est trop grand pour un lien, les routeurs retournent ICMP Fragmentation Needed. Linux utilise typiquement DF=1 (Path MTU Discovery actif). Certaines piles VPN utilisent DF=0. Feature ip_df_variance : variance du bit DF sur une session (instabilité anormale = stack réseau inhabituel).
3.3 Couche L4 — TCP
TCP Keep-Alive et multiplexage HTTP : à ne pas confondre avec HTTP Keep-Alive (Connection: keep-alive). Le TCP Keep-Alive est un mécanisme de détection de connexions mortes au niveau du noyau (envoi de paquets ACK vides après inactivité). L'HTTP Keep-Alive, en revanche, maintient la connexion TCP ouverte pour réutilisation par plusieurs requêtes HTTP successives. ja4ebpf trace le nombre de requêtes HTTP dans chaque connexion TCP via max_keepalives dans le gestionnaire de corrélation in-memory.
Coefficient de variation (CV) : mesure adimensionnelle de variabilité, CV = σ/μ. Un CV ≈ 0 indique une régularité élevée (automatisation à timer fixe) ; un CV ≈ 1–3 indique une variabilité naturelle (humain). Applicable à syn_timing_cv (variabilité du délai SYN→ClientHello) et à cadence_cv (variabilité des intervalles inter-requêtes).
Features TCP :
| Feature | Définition | Signal |
|---|---|---|
tcp_jitter_variance |
Variance des délais inter-connexions | Faible = automatisé à cadence fixe |
syn_timing_cv |
CV du délai SYN→TLS ClientHello | < 0.1 = script, > 0.5 = humain |
no_window_scale_ratio |
Ratio connexions sans Window Scale option | > 0.5 = stack TCP minimal ou obsolète |
tcp_shared_count |
Requêtes HTTP par connexion TCP (Keep-Alive) | 1 = bot sans Keep-Alive, >100 = bot pipeline |
port_exhaustion_ratio |
Ratio de ports source proches de l'épuisement (> 60000) | Injection rapide depuis un hôte unique |
src_port_density |
Densité des ports source (plage utilisée / connexions) | Faible = réutilisation systématique |
true_window_size |
Window Size × 2^window_scale | Fingerprint OS combiné |
window_mss_ratio |
true_window_size / MSS | Ratio diagnostique : écart = configuration non-standard |
3.4 Couche L5 — TLS
Features TLS agrégées par session :
| Feature | Définition | Valeur humain | Valeur bot |
|---|---|---|---|
tls12_ratio |
Ratio connexions TLS 1.2 / total TLS | ≈ 0 (Chrome/Firefox ≥1.3 exclusivement) | Variable selon outil |
fingerprint_coherence_score |
Cohérence JA4 à travers les connexions | ≈ 1 (même navigateur = même JA4) | < 0.5 si rotation JA4 |
is_alpn_missing |
1 si ALPN absent dans ClientHello | Toujours 0 pour navigateurs | 1 pour stacks TLS minimalistes |
sni_host_mismatch |
1 si SNI ≠ en-tête HTTP Host | 0 | > 0 si domain fronting ou proxy mal configuré |
distinct_ja4_count |
Nombre de JA4 distincts dans la session | 1 (un seul navigateur) | > 3 si rotation de pile TLS |
ja4_drift_ratio |
Transitions du JA4 dominant / (segments-1) | 0 | > 0.5 si changement intentionnel |
ja3_diversity_ratio |
Distinct JA3 / distinct JA4 | ≈ 1 | > 5 si randomisation GREASE anti-JA3 |
3.5 Couche L7 — HTTP
La couche L7 constitue la couche la plus riche en features comportementales. L'agent ja4ebpf capture le flux HTTP déchiffré via un uprobe sur SSL_read (OpenSSL/BoringSSL), avec une précision nanoseconde via bpf_ktime_get_ns().
Données capturées par ja4ebpf : src_ip, src_port, timestamp_ns (nanoseconde absolu), method, path, query_string, http_version, headers_raw (en-têtes bruts dans leur ordre d'émission), header_order_signature (hash de l'ordre), status_code, response_size, duration_ms. L'horodatage nanoseconde est critique pour le calcul des features temporelles F8 (cadence_cv, lag1_autocorrelation, benford_deviation, root_to_first_asset_delay).
Fingerprinting HTTP/2 passif intégré : pour les connexions HTTP/2, l'uprobe sur SSL_read retourne un flux d'octets bruts déchiffrés qui ne respecte pas les frontières des trames HTTP/2. En raison de la fragmentation TCP et du buffering TLS, un seul appel SSL_read peut contenir un fragment partiel de trame, plusieurs trames complètes, ou une combinaison des deux. Le Go Magic Bytes dispatcher maintient donc un buffer circulaire de réassemblage par connexion (identifiée par src_ip:src_port) : il accumule les données de plusieurs appels SSL_read successifs jusqu'à ce que la logique de parsing HTTP/2 confirme qu'une trame complète est disponible (9 octets d'en-tête + longueur payload déclarée). L'identification du protocole HTTP/2 se fait en cherchant la connection preface PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n dans le buffer accumulé. Tout le parsing lourd — décodage des trames, HPACK pour les pseudo-headers — s'effectue dans l'agent Go en espace utilisateur, et non dans la VM eBPF (dont la complexité est limitée par le vérificateur noyau). Ce découplage est essentiel : la VM eBPF collecte les octets bruts le plus rapidement possible, et le parsing est réalisé de manière asynchrone côté Go sans jamais interrompre le flux applicatif du serveur web.
Une fois les trames complètes reconstituées, le parser Go en extrait les éléments suivants du preface client :
- Les 7 paramètres SETTINGS individuels (IDs 1–6 et 8), chacun stocké dans une colonne ClickHouse dédiée (
h2_header_table_size,h2_enable_push,h2_max_concurrent_streams,h2_initial_window_size,h2_max_frame_size,h2_max_header_list_size,h2_enable_connect_protocol), avec la valeur -1 pour les paramètres absents du preface client - L'incrément
h2_window_updatede la frame WINDOW_UPDATE sur la connexion (stream ID 0) - Le flag
h2_has_priorityindiquant la présence d'un champ PRIORITY dans la frame HEADERS - L'ordre des pseudo-headers
h2_pseudo_order(ex.m,a,s,p) extrait par décodage HPACK partiel de la première frame HEADERS - Le fingerprint composite
h2_fingerprintau format Akamai et la chaîne bruteh2_settings_fp
Les données H2 sont associées à la session courante via la clé src_ip:src_port dans le gestionnaire de corrélation in-memory de ja4ebpf, puis transmises en batch à ClickHouse avec les données TCP/TLS correspondantes.
Colonnes HTTP/2 dans ja4_logs.http_logs :
| Colonne ClickHouse | Type | Défaut | Description |
|---|---|---|---|
h2_fingerprint |
String | '' |
Fingerprint composite au format Akamai (ex. 1:65536,2:0,4:6291456,6:262144|15663105|0|m,a,s,p) |
h2_settings_fp |
String | '' |
Chaîne brute des entrées SETTINGS (ex. 3:100,4:65536,2:0) |
h2_header_table_size |
Int32 | -1 |
SETTINGS ID 1 — HEADER_TABLE_SIZE (octets). -1 = absent du preface |
h2_enable_push |
Int32 | -1 |
SETTINGS ID 2 — ENABLE_PUSH (0/1). -1 = absent |
h2_max_concurrent_streams |
Int32 | -1 |
SETTINGS ID 3 — MAX_CONCURRENT_STREAMS. -1 = absent |
h2_initial_window_size |
Int64 | -1 |
SETTINGS ID 4 — INITIAL_WINDOW_SIZE (octets). -1 = absent |
h2_max_frame_size |
Int32 | -1 |
SETTINGS ID 5 — MAX_FRAME_SIZE (octets). -1 = absent |
h2_max_header_list_size |
Int32 | -1 |
SETTINGS ID 6 — MAX_HEADER_LIST_SIZE (octets). -1 = absent |
h2_enable_connect_protocol |
Int32 | -1 |
SETTINGS ID 8 — ENABLE_CONNECT_PROTOCOL (RFC 8441). -1 = absent |
h2_window_update |
UInt32 | 0 |
Incrément WINDOW_UPDATE connexion (stream ID 0). 0 = absent |
h2_has_priority |
UInt8 | 0 |
1 si le flag PRIORITY est présent dans la frame HEADERS |
h2_pseudo_order |
String | '' |
Ordre des pseudo-headers (ex. m,a,s,p pour Chrome) |
La convention -1 pour les paramètres SETTINGS absents est essentielle : elle distingue un paramètre non envoyé par le client (valeur par défaut RFC implicite) d'un paramètre explicitement fixé à 0 (ex. ENABLE_PUSH = 0 signifie « Server Push désactivé » vs -1 signifie « non envoyé, le serveur utilise la valeur par défaut RFC de 1 »).
Toutes les features des familles F1–F6 et F8 proviennent de cette couche, agrégées sur des fenêtres temporelles de 300 secondes (5 minutes) par session (src_ip).
3.6 Corrélation inter-couches (ja4ebpf)
Clé de corrélation : (src_ip, src_port) — le tuple source identifie de manière unique une connexion TCP à un instant donné (une connexion TCP est identifiée par le 4-tuple src_ip:src_port:dst_ip:dst_port, mais dst_ip:dst_port étant fixes pour un serveur, le 2-tuple src suffit).
Gestion du HTTP Keep-Alive : une connexion TCP peut transporter plusieurs requêtes HTTP successives. ja4ebpf maintient un gestionnaire de corrélation in-memory organisé en 256 shards (partitionnement par hash de src_ip pour éviter la contention). Chaque requête HTTP capturée via l'uprobe SSL_read est associée à l'enregistrement TCP/TLS ouvert correspondant, et max_keepalives est incrémenté. Un GC toutes les 100 ms libère les sessions expirées.
Timeout orphelin : si aucun enregistrement réseau (L3/L4) ne peut être associé à une requête HTTP dans les 500 ms — ce qui se produit quand le trafic arrive via un CDN ou un proxy inverse établissant une nouvelle connexion TCP entre le proxy et le serveur — la session est exportée avec uniquement les données HTTP disponibles. Les champs TCP/TLS restent absents dans l'enregistrement ClickHouse, et les features correspondantes sont imputées à zéro dans le pipeline ML.
Conséquences pour le pipeline ML : pour les sessions dont les données TCP/TLS sont absentes (proxy CDN, load balancer avec terminaison TLS), le Modèle Applicatif EIF (≈ 35 features L7 uniquement) est utilisé à la place du Modèle Complet (≈ 45 features L3→L7), évitant le biais d'imputation à zéro des features réseau.
Impact sur HTTP/2 : quand has_xff=1 (en-tête X-Forwarded-For présent, indiquant un proxy CDN), les dimensions H2 du browser_matcher sont neutralisées à 0.5 (score neutre). Le CDN réouvre sa propre connexion HTTP/2 vers le serveur d'origine ; les SETTINGS HTTP/2 capturés sont ceux du CDN (Cloudflare, Akamai), pas du client original.
X-Forwarded-For : en-tête HTTP ajouté par les proxies et load balancers pour préserver l'adresse IP du client original à travers la chaîne de proxies. Format : X-Forwarded-For: client_ip, proxy1_ip, proxy2_ip. Feature has_xff détecte la présence de cet en-tête.
3.7 Agrégation temporelle et features dérivées
Cycle d'analyse : 300 secondes (5 minutes). À chaque cycle, le bot_detector :
- Interroge ClickHouse pour les features agrégées des sessions actives dans la dernière heure
- Joint
view_ai_features_1h(features L3–L6, corrélées) avecview_thesis_features_1h(features F8, temporelles) - Exécute le scoring ML
- Écrit les résultats dans
ja4_processing
Vues matérialisées ClickHouse :
| Vue | Contenu | Features extraites |
|---|---|---|
view_ai_features_1h |
Features ML principales par session/heure | F1–F7 corrélées |
view_thesis_features_1h |
Features temporelles avancées | F8 (Benford, entropie, autocorrélation) |
agg_path_sequences_1h |
Séquences de chemins visités | path_transition_entropy, path_diversity |
agg_request_timing_1h |
Timing inter-requêtes en ms | cadence_cv, lag1_autocorrelation, burst_ratio |
agg_resource_cascade_1h |
Cascade de ressources (HTML→CSS→JS→images) | root_to_first_asset_delay, asset_load_stddev |
TTL strategy :
- ja4_logs bruts : TTL 2h — les logs bruts sont trop volumineux pour une conservation longue durée. Après 2 heures, les informations utiles ont été agrégées dans les vues.
- ja4_processing agrégé : TTL 7j — les features agrégées et les résultats de scoring sont conservés 7 jours pour l'analyse de tendances, la corrélation de campagnes, et l'entraînement des modèles.
Métriques de déploiement : > 3 millions de logs ingérés, ≈ 34 000 sessions par cycle de 300 s, ≈ 777 anomalies détectées par cycle (≈ 2,3 %), cycle d'analyse 300 s.
3.8 Détection ML semi-supervisée (full pipeline)
Trifurcation du trafic
Session entrante
│
├── dict_bot_ip CIDR match ?
│ OU dict_bot_ja4 match ?
│ OU Anubis DENY ?
│ ── OUI → KNOWN_BOT (étiquette entraînement XGB)
│
├── browser_matcher score ≥ seuil ?
│ (Chrome ≥ 0.72, Firefox/Safari ≥ 0.68)
│ ET aucune signature négative ?
│ ── OUI → LEGITIMATE_BROWSER
│
├── Zone grise [0.45, seuil[ ?
│ ── OUI → score_final × (1 - 0.5 × match_score)
│ (pénalité partielle, pas de blocage)
│
├── asn_label == 'human' ?
│ ── OUI → baseline EIF training (sans étiquette bot)
│
└── Sinon → Triple-voix : EIF + NF + XGBoost + Fusion LR
Seuil adaptatif
threshold = min(percentile_5(neg_scores_history), -0.05)
La valeur percentile_5 du historique des scores négatifs (anomalies confirmées) établit un seuil qui capture au moins le top 5 % des anomalies observées récemment. Le plancher -0.05 garantit une sensibilité minimale même lors de périodes de faible activité botique. Cette adaptation évite les faux négatifs systémiques en période calme et les faux positifs en période d'activité élevée.
Scoring bifurqué
| Modèle | Features | Trafic applicable | Indicateur |
|---|---|---|---|
| EIF Complet | ≈ 45 features L3→L7 | Données L3/L4 disponibles | eif_score_full |
| EIF Applicatif | ≈ 35 features L7 | L3/L4 absentes (CDN/proxy) | eif_score_app |
| NF | Même dimensionnalité que EIF actif | Toutes sessions | nf_log_likelihood |
| XGBoost | Ensemble complet 96 features | Toutes sessions | xgb_probability |
Niveaux de sévérité
| Étiquette | Condition | Action recommandée |
|---|---|---|
| CRITICAL | score_final < -0.30 | Blocage immédiat + alerte SOC |
| HIGH | score_final < -0.15 | Blocage + journalisation |
| MEDIUM | score_final < -0.05 | CAPTCHA challenge + journalisation |
| LOW | score_final < 0 | Journalisation + surveillance accrue |
| KNOWN_BOT | Match dict_bot_ip / dict_bot_ja4 / Anubis DENY | Blocage + label entraînement |
| ANUBIS_DENY | Règle Anubis DENY explicite | Blocage + label entraînement |
| LEGITIMATE_BROWSER | browser_matcher ≥ seuil | Passage sans friction |
HDBSCAN pour le clustering de campagnes
HDBSCAN (Hierarchical Density-Based Spatial Clustering of Applications with Noise, Campello, Moulavi, Sander, 2013) est un algorithme de clustering hiérarchique basé sur la densité. Il étend DBSCAN (Density-Based Spatial Clustering of Applications with Noise) en :
- Calculant la matrice de distances de réachabilité mutuelle (mutual reachability distance) :
d_mreach(a,b) = max(core_k(a), core_k(b), d(a,b))oùcore_k(x)est la distance au k-ème voisin le plus proche - Construisant l'arbre couvrant minimal (Minimum Spanning Tree, MST) sur cette matrice
- Transformant le MST en une hiérarchie de clusters à tous les niveaux de densité
- Extrayant les clusters les plus stables par un score de persistance :
stability(C) = Σ_{x ∈ C} (λ_death(x,C) - λ_birth(C)) - Assignant les points non intégrés à la classe bruit (-1)
Avantages sur DBSCAN : gère les clusters de densités variables, ne nécessite que min_cluster_size (pas d'ε), robuste au bruit.
Application dans l'architecture : HDBSCAN est appliqué sur l'espace latent AE de 16 dimensions. Les sessions dont la représentation latente est proche correspondent à des bots utilisant le même outil ou la même configuration. Le clustering regroupe ces sessions en campagnes identifiables, permettant de :
- Détecter les campagnes distribuées sur plusieurs adresses IP
- Lier des sessions séparées par des gaps temporels (même campagne qui reprend)
- Fournir des contextes de menace au SOC (« cluster #7 = 34 IPs, JA4 identique, asset_ratio ≈ 0 »)