docs: restructure thesis into chapter files with corrected references

Split monolithic thesis into separate chapter markdown files under
docs/thesis/. Remove fabricated bibliography entries, correct inflated
claims, add GNN/Transformers section, and rename MetaLearner to Fusion LR.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jacquin Antoine
2026-04-13 13:51:38 +02:00
parent ac75ce2956
commit 0e5f94dd0d
10 changed files with 3481 additions and 0 deletions

View File

@ -0,0 +1,485 @@
[<< Sommaire](README.md) | [Suivant >>](05_features.md)
---
## 3.9 Browser Signature Detection (browser_matcher)
`[impl.]` **Statut global : capture H2, scoring statique et profiling dynamique entièrement implémentés**
Le système `browser_confidence` à 6 axes (§3.8) fournit un score agrégé utile comme feature ML, mais sa logique de bypass (seuil 0,55) manque de précision face aux outils d'évasion modernes — notamment `httpcloak`, `curl_cffi` et `python-requests` patchés — qui reproduisent les couches TLS et HTTP/1.1 sans reproduire les subtilités HTTP/2. `browser_matcher` adresse cette lacune en implémentant une correspondance structurée par famille de navigateur, fondée sur l'empreinte passive du protocole HTTP/2. Le module de scoring à 7 dimensions pondérées (`browser_matcher.py`, 497 lignes) et la base de signatures Chrome/Firefox/Safari (`browser_signatures.py`, 165 lignes) sont entièrement opérationnels. En complément, un moteur de **profiling dynamique automatique** (`profile_builder.py`, 614 lignes et `browser_matcher_dynamic.py`, 387 lignes) apprend les signatures des navigateurs à partir des logs réels via HDBSCAN, éliminant la dépendance aux signatures codées en dur et s'adaptant automatiquement aux nouvelles versions de navigateurs (§3.9.6).
### 3.9.1 Principes du fingerprinting HTTP/2 passif
#### Rappel du protocole HTTP/2
[HTTP/2 (RFC 7540)](https://www.rfc-editor.org/rfc/rfc7540) est un protocole binaire, multiplexé, qui remplace la communication textuelle d'HTTP/1.1. Ses principales caractéristiques architecturales pertinentes pour le fingerprinting sont :
- **Framing binaire** : toute communication est découpée en **frames** (trames) de type défini. Les types principaux sont SETTINGS, HEADERS, DATA, WINDOW_UPDATE, PRIORITY, PUSH_PROMISE, PING, GOAWAY.
- **Multiplexage de streams** : plusieurs échanges requête/réponse coexistent sur une seule connexion TCP, identifiés par un **stream ID** entier.
- **Compression d'en-têtes HPACK** : [RFC 7541](https://www.rfc-editor.org/rfc/rfc7541) définit HPACK, une compression d'en-têtes HTTP par table d'indexation. L'ordre des entrées dans la table statique HPACK est normalisé, mais l'ordre de sérialisation des pseudo-headers est laissé à l'implémentation.
- **Contrôle de flux** : mécanisme de fenêtres (`WINDOW_UPDATE`) limitant le débit pour éviter la saturation du récepteur.
Après terminaison TLS par le serveur web, le flux HTTP/2 est déchiffré et disponible en clair dans le contexte d'OpenSSL/BoringSSL. Le fingerprinting passif est réalisé par **ja4ebpf** via un uprobe sur `SSL_read`. Cependant, `SSL_read` retourne des octets bruts déchiffrés sans respecter les frontières des trames HTTP/2 — la fragmentation TCP et le buffering TLS signifient qu'un seul appel peut correspondre à un fragment de trame ou à plusieurs trames complètes. Le **Go Magic Bytes dispatcher** maintient un **buffer circulaire de réassemblage** par connexion, accumulant les données de plusieurs appels `SSL_read` jusqu'à ce que la logique de parsing HTTP/2 confirme qu'une trame complète est disponible (9 octets d'en-tête de trame + longueur payload). L'identification du preface `PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n` se fait dans ce buffer accumulé. Tout le parsing des trames SETTINGS, WINDOW_UPDATE et HEADERS (y compris le décodage HPACK partiel pour extraire l'ordre des pseudo-headers) s'effectue dans l'agent Go en espace utilisateur, et non dans la VM eBPF. Cette approche est agnostique au serveur web (Apache, Nginx, Varnish, HAProxy) et ne nécessite aucun module natif installé côté serveur.
#### Frame SETTINGS
La frame **SETTINGS** ([RFC 7540 §6.5](https://www.rfc-editor.org/rfc/rfc7540#section-6.5)) est envoyée par le client immédiatement après le **connection preface** (octet sequence `PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n`). Elle contient des paires clé-valeur dont les clés sont des identifiants 16 bits normalisés. L'ensemble et les valeurs de ces paramètres constituent une empreinte stable par implémentation HTTP/2.
**Table complète des paramètres SETTINGS par client (format : ID — Nom — Chrome 119+ / Firefox 90+ / Safari 15+ / curl 8.x / Go net/http)** :
| ID | Nom | Chrome 119+ | Firefox 90+ | Safari 15+ | curl 8.x | Go net/http |
|-----|-------------------------|-------------|-------------|------------|----------|-------------|
| 1 | HEADER_TABLE_SIZE | 65536 | 65536 | absent | absent | absent |
| 2 | ENABLE_PUSH | 0 | 0 | 0 | 0 | absent |
| 3 | MAX_CONCURRENT_STREAMS | absent | absent | 100 | absent | absent |
| 4 | INITIAL_WINDOW_SIZE | 6291456 | 131072 | 2097152 | 65535 | 4194304 |
| 5 | MAX_FRAME_SIZE | absent | 16384 | absent | absent | absent |
| 6 | MAX_HEADER_LIST_SIZE | 262144 | absent | absent | absent | 10485760 |
| 9 | ENABLE_CONNECT_PROTOCOL | absent | absent | 1 | absent | absent |
**Analyse discriminante** : le paramètre `INITIAL_WINDOW_SIZE` (ID 4) discrimine à lui seul les quatre grandes familles :
- **Chrome** : 6 291 456 octets (6 Mo) — valeur la plus élevée, signalant une optimisation agressive pour les connexions haut débit
- **Safari** : 2 097 152 octets (2 Mo) — intermédiaire
- **Go net/http** : 4 194 304 octets (4 Mo) — non-navigateur, valeur élevée
- **Firefox** : 131 072 octets (128 Ko) — valeur conservatrice
- **curl** : 65 535 octets (64 Ko) — valeur par défaut minimale du standard
**Explication de INITIAL_WINDOW_SIZE** : ce paramètre fixe la taille initiale de la fenêtre de contrôle de flux pour les nouveaux streams. Une grande valeur (Chrome : 6 Mo) permet au serveur d'envoyer davantage de données avant d'attendre un accusé de réception `WINDOW_UPDATE`, optimisant les connexions à haute bande passante. Une petite valeur (curl : 64 Ko) limite le débit mais réduit les exigences mémoire. Cette différence reflète les priorités de conception : Chrome est optimisé pour la performance perçue par l'utilisateur sur des réseaux modernes ; curl est un outil en ligne de commande dont la sobriété mémoire est une contrainte historique.
#### Frame WINDOW_UPDATE
La frame **WINDOW_UPDATE** ([RFC 7540 §6.9](https://www.rfc-editor.org/rfc/rfc7540#section-6.9)) met à jour le crédit de contrôle de flux au niveau de la connexion (stream ID 0) ou d'un stream particulier. Elle est envoyée par le client après le SETTINGS initial pour élargir la fenêtre de réception globale. Sa valeur constitue une signature extrêmement stable par implémentation :
| Client | Valeur WINDOW_UPDATE | Tolérance |
|-----------------|-----------------------|-----------|
| Chrome 119+ | 15 663 105 | ±1 000 |
| Firefox 90+ | 12 517 377 | ±1 000 |
| Safari 15+ | 10 420 225 | ±1 000 |
| curl 8.x | absent (0) | — |
| Python httpx | absent (0) | — |
| Go net/http | 1 073 676 289 | ±1 000 |
L'absence de WINDOW_UPDATE (valeur 0) indique avec certitude un outil non-navigateur : aucun navigateur grand public n'omet ce frame dans son handshake H2. Go net/http envoie une valeur exceptionnellement grande (≈1 Go) correspondant à l'augmentation maximale de fenêtre autorisée par le standard.
#### Ordre des pseudo-headers
HTTP/2 remplace la ligne de requête HTTP/1.1 (par exemple `GET /chemin HTTP/1.1`) par des **champs pseudo-header** préfixés par deux points. Ces pseudo-headers sont définis par [RFC 7540 §8.1.2](https://www.rfc-editor.org/rfc/rfc7540#section-8.1.2) :
- `:method` (**:m**) : verbe HTTP (GET, POST, etc.)
- `:authority` (**:a**) : équivalent de l'en-tête `Host`
- `:scheme` (**:s**) : schéma de l'URI (http, https)
- `:path` (**:p**) : chemin et query string de l'URI
Les pseudo-headers **doivent** apparaître avant les en-têtes réguliers dans les frames HEADERS. Leur ordre entre eux n'est pas imposé par le standard mais est déterminé par l'implémentation HTTP/2 et reste **stable par version de client**. Cet ordre constitue donc une signature passive supplémentaire :
| Client | Ordre observé | Notation abrégée |
|-----------------|---------------|------------------|
| Chrome 119+ | method, authority, scheme, path | `masp` |
| Firefox 90+ | method, path, scheme, authority | `mpsa` |
| Safari 15+ | method, authority, scheme, path | `masp` |
| curl 8.x | method, path, scheme, authority | `mpsa` |
Source : [Bartlett, Cloudflare 2023](https://blog.cloudflare.com/cloudflare-bot-management-machine-learning-and-more/).
**Note sur les valeurs observées vs. références Cloudflare** : les captures réelles effectuées par `ja4ebpf` montrent des écarts avec les données de référence Cloudflare pour certaines implémentations. Firefox présente un ordre `mpsa` (method, path, scheme, authority) dans nos captures, et non `mpas` (method, path, authority, scheme) comme rapporté par Cloudflare. Safari présente un ordre `masp` identique à Chrome dans nos captures, et non `mspa`. Safari envoie `INITIAL_WINDOW_SIZE=65535` et `WINDOW_UPDATE=10485760` dans nos observations, contre respectivement 2 097 152 et 10 420 225 dans les données Cloudflare. Ces écarts peuvent résulter de différences de version (nos captures Safari correspondent à WebKit ≥ 613.1.x sur macOS/iOS), de variantes de build, ou de l'évolution des piles HTTP/2 entre les mesures. Les signatures dans `browser_signatures.py` reflètent nos valeurs capturées réelles, garantissant la cohérence du scoring en production.
**Cas httpcloak** : httpcloak reproduit correctement le SETTINGS H2 de Chrome et la valeur WINDOW_UPDATE de Chrome, mais conserve l'ordre pseudo-header de curl (`mpsa`) au lieu de Chrome (`masp`). Résultat : score browser_matcher Chrome ≈ 0,60 — zone grise. La pénalité de zone grise (voir §3.9.3) réduit le score final ML sans le neutraliser totalement. Note : `mpsa` est l'ordre réel capturé pour Firefox dans notre infrastructure — httpcloak peut donc tromper partiellement la dimension pseudo-header, mais les dimensions SETTINGS et WINDOW_UPDATE révèlent l'incohérence.
#### Frames PRIORITY
Les frames **PRIORITY** ([RFC 7540 §6.3](https://www.rfc-editor.org/rfc/rfc7540#section-6.3)) établissent un arbre de dépendances pour l'ordonnancement des streams HTTP/2. Elles permettent au client de communiquer au serveur ses priorités de rendu — par exemple, les ressources CSS bloquantes reçoivent une priorité plus élevée que les images de bas de page.
Comportement par famille :
- **Firefox 90+** : envoie des frames PRIORITY de démarrage pour les streams 3, 5, 7, 9, 11, 13, formant un arbre de priorités préétabli pour les ressources anticipées
- **Chrome ≥119** : n'envoie **pas** de frames PRIORITY — migré vers [RFC 9218 PRIORITY_UPDATE](https://www.rfc-editor.org/rfc/rfc9218), qui transmet les priorités via des en-têtes HTTP plutôt que des frames dédiées
- **Safari** : n'envoie pas de frames PRIORITY
- **curl** : n'envoie pas de frames PRIORITY
- **Go net/http** : n'envoie pas de frames PRIORITY
La présence de frames PRIORITY est donc un **marqueur exclusif de Firefox**. Leur absence ne discrimine pas entre Chrome, Safari, curl et Go — elle doit être combinée avec les autres dimensions.
#### Stabilité des empreintes H2
La signature H2 de Chrome est stable depuis la version 119 (novembre 2023) jusqu'à la version 142 (2026) — plus de deux ans de stabilité. Ceci contraste avec JA4 TLS qui change à chaque mise à jour de suite de chiffrement. Cette stabilité s'explique par le fait que les paramètres H2 sont liés aux décisions d'architecture réseau du moteur de rendu Blink/Chrome, qui évoluent beaucoup moins fréquemment que la politique TLS.
### 3.9.2 Architecture du moteur browser_matcher
Le moteur browser_matcher est structuré en quatre modules Python distincts : la base de données de signatures statiques, la logique de scoring statique, le moteur de profiling dynamique hors-ligne, et le scorer dynamique temps réel.
#### Module browser_signatures.py
Ce module constitue la **base de données de signatures** par famille de navigateur. Chaque entrée est un objet structuré définissant :
```python
# Extrait de browser_signatures.py — signature Chrome
BROWSER_SIGNATURES["Chrome"] = {
"h2_settings_exact": {
1: 65536, # HEADER_TABLE_SIZE
2: 0, # ENABLE_PUSH (désactivé)
4: 6291456, # INITIAL_WINDOW_SIZE
6: 262144, # MAX_HEADER_LIST_SIZE
},
"h2_settings_forbidden_keys": [3, 5], # MAX_CONCURRENT_STREAMS et MAX_FRAME_SIZE absents
"h2_window_update": 15663105,
"h2_window_update_tolerance": 1000,
"h2_priority_frames_expected": False, # Chrome ≥119 utilise PRIORITY_UPDATE (RFC 9218)
"pseudo_header_order": "m,a,s,p",
"tls": {"ja4_families": ["Chromium", "Chrome", "Edge"], "grease_expected": True},
"headers_sec_fetch_required": True,
"headers_ch_ua_required": True,
"accept_language_required": True,
}
# Signature Firefox (valeurs capturées réelles)
BROWSER_SIGNATURES["Firefox"] = {
"h2_settings_exact": {1: 65536, 4: 131072, 5: 16384},
"h2_settings_forbidden_keys": [2, 3, 6],
"h2_window_update": 12517377,
"pseudo_header_order": "m,p,s,a", # ordre réel capturé (pas m,p,a,s)
"tls": {"ja4_families": ["Firefox"], "grease_expected": False},
"headers_ch_ua_required": False, # Firefox ne supporte pas les Client Hints
}
# Signature Safari (valeurs capturées réelles)
BROWSER_SIGNATURES["Safari"] = {
"h2_settings_exact": {1: 4096, 3: 100, 4: 65535},
"h2_settings_forbidden_keys": [2, 5, 6],
"h2_window_update": 10485760,
"pseudo_header_order": "m,a,s,p", # identique à Chrome (WU=10485760 distingue)
"tls": {"ja4_families": ["Safari"], "grease_expected": False},
"headers_sec_fetch_forbidden": True, # Présence = incohérence
}
```
Le champ `h2_settings_exact` est désormais consommé directement par `_d1_h2_settings()` via les colonnes individuelles de `view_ai_features_1h` (`h2_header_table_size`, `h2_enable_push`, `h2_initial_window_size`, `h2_max_header_list_size`, etc.). La comparaison colonne par colonne permet de scorer partiellement un fingerprint inconnu du dictionnaire mais structurellement proche (ex. variante de navigateur non encore répertoriée).
**Implémentation de `_d1_h2_settings()` — algorithme de scoring** :
```python
# Pour chaque clé attendue (h2_settings_exact) : val_col == val_attendue → 1, sinon 0
# Pour chaque clé interdite (h2_settings_forbidden_keys) : col < 0 (absent) → 1, sinon 0
direct_score = nb_vérifications_réussies / nb_total_vérifications
base = direct_score × 0.60 + dict_match × 0.30 + h2_ja4_coherence × 0.10
```
**Avantage vs l'approche dict-only** : un fingerprint légèrement modifié (ex. une variante de Chrome avec un champ SETTINGS supplémentaire) serait rejeté par le dictionnaire (dict_match=0) mais obtiendrait quand même un score D1 élevé si les paramètres clés sont corrects. Par exemple, un client envoyant `1:65536, 2:0, 4:6291456, 6:262144` (4 clés Chrome attendues) + `3:200` (clé interdite) obtiendrait direct_score = 5/6 × 0.60 = 0.50 au lieu de 0 avec le dict seul.
**Fallback** : si les colonnes individuelles sont absentes du DataFrame (compatibilité ascendante), la fonction revient au comportement original `dict_match × 0.80 + h2_ja4_coherence × 0.20`.
#### Module browser_matcher.py
Ce module implémente le **moteur de scoring**. Il calcule un score de correspondance pour chaque famille de navigateur connue, sur 7 dimensions pondérées :
**Table des 7 dimensions de scoring avec poids et logique de calcul** :
| # | Dimension | Poids | Logique de scoring |
|---|-----------|-------|--------------------|
| 1 | H2 SETTINGS match | 0,30 | **Comparaison directe par paramètre** (colonnes individuelles) : chaque clé attendue exacte → 1,0 ; chaque clé interdite absente → 1,0 ; score = proportion de vérifications réussies. Pondération : `direct × 0,60 + dict_lookup × 0,30 + ja4_coherence × 0,10`. Fallback dict-only si colonnes indisponibles |
| 2 | H2 WINDOW_UPDATE | 0,15 | `|observé attendu| ≤ tolérance` → 1,0 ; absent (=0) → 0,0 ; hors tolérance → 0,0 |
| 3 | Ordre pseudo-headers | 0,15 | Correspondance exacte → 1,0 ; absent → neutre 0,5 ; sinon 0,0 |
| 4 | Frames H2 PRIORITY | 0,10 | Présence/absence correspond à l'attendu → 1,0 ; pas de données H2 → neutre 0,5 |
| 5 | Cohérence en-têtes HTTP | 0,15 | Accept-Language (+0,25) + Sec-Fetch cohérent (+0,25) + Sec-CH-UA cohérent (+0,25) + bonus (+0,25) |
| 6 | Structure TLS | 0,10 | Famille JA4 correcte (×0,7) + TLS 1.3 (×0,3) |
| 7 | JA4 dict lookup | 0,05 | Correspondance dans `dict_browser_ja4` pour cette famille → 1,0 ; sinon 0,0 |
**Formule générale** :
```
score_family = Σ (weight_i × score_dim_i) ∈ [0, 1]
```
Le score est calculé pour chaque famille (Chrome, Firefox, Safari). Le résultat maximal et la famille correspondante sont retenus. Les patterns NON_BROWSER_SIGNATURES sont appliqués en post-traitement : si un pattern négatif est détecté, `score_family = min(score_family, 0.30)` quelle que soit la famille.
#### Pseudo-code du pipeline browser_matcher
```python
def match_browser(session: SessionFeatures) -> BrowserMatchResult:
scores = {}
for family, sig in BROWSER_SIGNATURES.items():
s = 0.0
# Dimension 1 : H2 SETTINGS
s += 0.30 * score_h2_settings(session.h2_settings, sig)
# Dimension 2 : WINDOW_UPDATE
s += 0.15 * score_window_update(session.h2_window_update, sig)
# Dimension 3 : pseudo-headers
s += 0.15 * score_pseudo_order(session.h2_pseudo_order, sig)
# Dimension 4 : PRIORITY frames
s += 0.10 * score_priority_frames(session.h2_has_priority, sig)
# Dimension 5 : en-têtes HTTP
s += 0.15 * score_headers(session.headers, sig)
# Dimension 6 : TLS structure
s += 0.10 * score_tls(session.tls, sig)
# Dimension 7 : JA4 dict
s += 0.05 * score_ja4_dict(session.ja4, sig)
scores[family] = s
# Patterns négatifs
for pattern in NON_BROWSER_SIGNATURES:
if pattern.matches(session):
for family in scores:
scores[family] = min(scores[family], 0.30)
best_family = max(scores, key=scores.get)
best_score = scores[best_family]
return BrowserMatchResult(
family=best_family if best_score >= THRESHOLDS[best_family] else "",
score_chrome=scores.get("chrome", 0.0),
score_firefox=scores.get("firefox", 0.0),
score_safari=scores.get("safari", 0.0),
match_score=best_score,
)
```
### 3.9.3 Logique de décision et intégration dans le pipeline
#### Seuils de décision par famille
Les seuils de confiance sont différenciés par famille en raison de la variabilité inter-version observée :
| Famille | Seuil de confiance | Justification |
|----------|--------------------|---------------|
| Chrome | ≥ 0,72 | Grande stabilité H2 depuis v119 ; seuil élevé car JA4 Chrome très imitée |
| Firefox | ≥ 0,68 | Frames PRIORITY discriminantes ; seuil légèrement plus bas |
| Safari | ≥ 0,68 | MAX_CONCURRENT_STREAMS=100 et ENABLE_CONNECT_PROTOCOL=1 très spécifiques |
#### Flowchart de trifurcation (ASCII)
```
Session entrante
has_xff = 1 ?
┌────┴────┐
│Oui │Non
▼ ▼
Neutraliser Dimensions H2
dim. H2 → complètes
0.5 par
défaut
│ │
└────┬────┘
Calcul score_family
(7 dimensions)
Patterns NON_BROWSER ?
┌────┴────┐
│Oui │Non
▼ ▼
Cap score Score intact
à 0.30 │
│ │
└────┬────┘
best_score ≥ seuil ?
┌────┴────────────────┐
│Oui │Non
▼ │
browser_family_detected │
= best_family ▼
│ 0.45 ≤ best_score < seuil ?
│ ┌─────┴─────┐
│ │Oui │Non
│ ▼ ▼
│ ZONE GRISE Aucun effet
│ final_score (best_score < 0.45)
× (1 - 0.5 × best_score)
│ │
└──────┬──────┘
Sortie : features browser_match_*
→ Vecteur feature ML
```
#### Traitement zone grise [0,45 ; seuil[
Lorsque le meilleur score est en zone grise (supérieur à 0,45 mais inférieur au seuil de confiance de la famille), le score ML final est atténué par :
```
final_score = final_score × (1 0.5 × match_score)
```
Cette atténuation ne neutralise pas le score ML mais signal une incertitude sur l'authenticité du navigateur déclaré. En dessous de 0,45 : aucun effet, la signature est trop faible pour être exploitée.
**Cas httpcloak illustratif** :
- H2 SETTINGS Chrome : correct → +0,30
- WINDOW_UPDATE Chrome : correct → +0,15
- Pseudo-headers : `mpsa` (curl) vs `masp` (Chrome attendu) → 2/4 positions → 0,0
- PRIORITY frames : absentes (correct pour Chrome) → +0,10
- En-têtes HTTP : sec-fetch-* absents (httpcloak) → +0,075
- TLS : JA4 Chrome-like → +0,08
- JA4 dict : correspondance → +0,05
- **Total ≈ 0,605** → zone grise [0,45 ; 0,72[ → atténuation active
#### Gestion CDN/reverse proxy (has_xff = 1)
Lorsqu'un CDN ou reverse proxy est détecté (`has_xff = 1`), le CDN rétablit sa propre connexion H2 vers le serveur Apache d'origine. Les paramètres H2 observés (SETTINGS, WINDOW_UPDATE, pseudo-headers, PRIORITY) reflètent alors le CDN, non le client final. Dans ce cas :
- Les dimensions H2 (poids total 0,70) sont **neutralisées à 0,50** (valeur neutre)
- Le poids libéré est redistribué équitablement entre la dimension en-têtes HTTP (+0,35) et la dimension TLS (+0,35)
- Seules les dimensions TLS et en-têtes HTTP subsistent, avec des pondérations ajustées
### 3.9.4 Nouvelles features dérivées
Le module `browser_matcher` génère les features suivantes dans le vecteur feature du pipeline :
| Feature | Type | Description | Statut |
|---------|------|-------------|--------|
| `browser_match_chrome` | Float32 | Score de correspondance Chrome (01) | `[impl.]` |
| `browser_match_firefox` | Float32 | Score de correspondance Firefox (01) | `[impl.]` |
| `browser_match_safari` | Float32 | Score de correspondance Safari (01) | `[impl.]` |
| `browser_match_max` | Float32 | max(chrome, firefox, safari) | `[impl.]` |
| `browser_family_detected` | String | Famille détectée ou chaîne vide | `[impl.]` |
| `h2_window_update_value` | UInt32 | Valeur WINDOW_UPDATE observée | `[impl.]` (colonne `h2_window_update` dans `http_logs`) |
| `h2_has_priority_frames` | UInt8 | 1 si frames PRIORITY présentes | `[impl.]` (colonne `h2_has_priority` dans `http_logs`) |
| `h2_pseudo_order` | String | Ordre observé (ex. `m,a,s,p`) | `[impl.]` (colonne `h2_pseudo_order` dans `http_logs`) |
| `tls_h2_family_mismatch` | UInt8 | 1 si JA4 dit Chrome mais H2 SETTINGS dit Firefox/outil | `[impl.]` (feature F4) |
Les features `h2_window_update_value`, `h2_has_priority_frames` et `h2_pseudo_order` sont désormais capturées par ja4ebpf via le parser HTTP/2 du flux SSL_read déchiffré et stockées dans des colonnes individuelles de `ja4_logs.http_logs`. De plus, chaque paramètre SETTINGS HTTP/2 dispose de sa propre colonne (`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. La feature `tls_h2_family_mismatch` est implémentée dans le vecteur feature global (famille F4 — TLS features) et se calcule à partir des données JA4 existantes et des colonnes H2 individuelles disponibles dans `ja4_logs.http_logs`. Les features `browser_match_*` sont calculées par le module `browser_matcher.py` (497 lignes) à chaque cycle d'analyse, avec les signatures statiques de `browser_signatures.py` (165 lignes) et un rechargement dynamique optionnel depuis ClickHouse toutes les 24 heures. En parallèle, le module `browser_matcher_dynamic.py` (387 lignes) scores les sessions contre les profils auto-appris de `auto_browser_profiles` (§3.9.6), fournissant une double couverture : signatures connues (statique) + signatures observées (dynamique).
### 3.9.5 Maintenance des signatures
#### Stockage et rechargement
Les signatures sont stockées dans la table ClickHouse `ja4_processing.browser_h2_signatures`, rechargée toutes les 24 heures par le module Python. Structure de la table :
```sql
CREATE TABLE ja4_processing.browser_h2_signatures
(
family String,
version_min String,
version_max String,
h2_settings_json String, -- JSON sérialisé du dict {key_id: value}
h2_settings_forbidden String, -- JSON array des key_ids interdits
h2_window_update UInt32,
h2_window_update_tolerance UInt32,
h2_priority_expected UInt8,
pseudo_header_order String, -- "m,a,s,p"
tls_json String, -- JSON des paramètres TLS
headers_required String, -- JSON array
headers_forbidden String, -- JSON array
created_at DateTime DEFAULT now(),
is_active UInt8 DEFAULT 1
)
ENGINE = ReplacingMergeTree(created_at)
ORDER BY (family, version_min);
```
#### File d'examen pour signatures inconnues
Les sessions présentant une empreinte H2 inconnue (aucune famille avec score > 0,45) mais un comportement de type navigateur (browser_confidence ≥ 0,55, présence de sec-fetch-*, TLS 1.3) sont enregistrées dans une **file d'examen** (`ja4_processing.unknown_h2_fingerprints`) pour mise à jour progressive de la base de signatures lors des nouvelles versions de navigateurs.
```sql
INSERT INTO ja4_processing.unknown_h2_fingerprints
SELECT
now() AS observed_at,
src_ip,
ja4_fingerprint,
h2_settings_json,
h2_window_update_value,
h2_pseudo_order,
h2_has_priority_frames,
browser_confidence_score
FROM current_session_features
WHERE browser_match_max < 0.45
AND browser_confidence_score >= 0.55
AND tls_version = 'TLS1.3';
```
### 3.9.6 Profiling dynamique automatique des navigateurs
`[impl.]` **Module profile_builder.py (614 lignes) + browser_matcher_dynamic.py (387 lignes)**
Le système de signatures statiques (§3.9.2) est robuste mais fragile face aux mises à jour de navigateurs : chaque changement de version peut modifier les valeurs SETTINGS ou WINDOW_UPDATE, nécessitant une mise à jour manuelle du dictionnaire. Le **profiling dynamique automatique** résout ce problème en apprenant les signatures directement à partir du trafic observé, sans intervention humaine.
#### Architecture du pipeline
Le pipeline s'articule en deux phases :
1. **Phase hors-ligne** (`profile_builder.py`, exécuté quotidiennement par cron) :
- Lit la vue `view_h2_profiling_raw` (sessions H2 filtrées des 7 derniers jours, dédupliquées par IP)
- Clusterise les sessions similaires via HDBSCAN (`min_cluster_size=1000`) sur un vecteur mixte (variables continues normalisées par StandardScaler + variables catégorielles brutes)
- Calcule les centroïdes par cluster : moyenne et tolérance (`mean + 3σ`) pour les continues, mode pour les catégorielles
- Labelise les clusters par analyse des User-Agents → familles `Auto_Chrome`, `Auto_Firefox`, `Auto_Safari`, `Auto_Unknown`
- Fusionne les clusters redondants (même famille + même `pseudo_order` + `window_update` à < 5% d'écart)
- Persiste les profils dans `ja4_processing.auto_browser_profiles` (ReplacingMergeTree, TTL 14 jours)
2. **Phase temps réel** (`browser_matcher_dynamic.py`, appelé à chaque cycle de 300s) :
- Charge les profils en mémoire au démarrage (rafraîchissement toutes les 24h)
- Pour chaque session, calcule un score de similarité pondéré contre tous les profils :
```
score = Σ (poids_i × similarité_i) × confiance_volumétrique
Pondération :
h2_window_update : 0.40 (le plus discriminant entre familles)
pseudo_order : 0.40 (invariant par version mineure)
h2_initial_window_size : 0.10
h2_has_priority : 0.10
Similarité continue : sim = max(0, 1 - |x - μ| / max(|μ|, 1))
Similarité catégorielle : sim = 1.0 si match exact, 0.0 sinon
Confiance volumétrique : conf = min(1.0, log10(count_ips + 1) / 4)
→ count_ips=10000 → confiance≈1.0 ; count_ips=100 → confiance≈0.50
```
#### Rejet rapide
Le scoring intègre deux mécanismes de rejet rapide pour éviter les calculs inutiles :
1. **Incompatibilité pseudo_order** : si la session a `pseudo_order = mpsa` (curl) mais le profil exige `masp` (Chrome), le score est immédiatement 0. Ce signal est binaire et invariant il ne peut pas être contourné sans reproduire exactement l'ordre des pseudo-headers du navigateur cible.
2. **Dépassement de tolérance** : si `|h2_window_update_session - h2_window_update_profil| > tolérance`, le score est 0. La tolérance est calculée comme `|μ| + 3σ` lors du clustering, garantissant un intervalle de confiance à 99.7%.
#### Vue ClickHouse d'extraction
La vue `view_h2_profiling_raw` (créée dans `13_h2_profiling.sql`) filtre le trafic bots évident et encode les variables catégorielles :
```sql
-- Encodage pseudo_order → UInt8
multiIf(
h2_pseudo_order = 'm,a,s,p', 1, -- Chrome/Safari
h2_pseudo_order = 'm,p,a,s', 2, -- Firefox (ancien)
h2_pseudo_order = 'm,s,p,a', 3,
h2_pseudo_order = 'm,p,s,a', 4, -- curl/Firefox
h2_pseudo_order = 'm,a,p,s', 5,
0 -- inconnu
) AS pseudo_order_id
```
Filtres appliqués : `h2_pseudo_order != ''` (HTTP/2 uniquement), `h2_window_update > 0` (exclut curl), `h2_initial_window_size NOT IN (-1, 65535)` (exclut absents et valeur typique curl), `header_user_agent != ''` (exclut scanners).
#### Cycle de vie des profils
Le profilage dynamique gère automatiquement l'obsolescence :
- **Rafraîchissement** : `last_seen_date` est mis à jour quotidiennement pour les profils dont le vecteur H2 correspond à des sessions observées dans les dernières 24h.
- **Purge** : les profils `last_seen_date < today() - 14` sont supprimés (navigateurs dépréciés, versions obsolètes). Cette TTL garantit que seules les signatures récentes sont utilisées pour le scoring.
Le TTL ClickHouse de 14 jours sur la table `auto_browser_profiles` assure une purge physique des partitions expirées lors du merge.
#### Fusion des clusters
La fusion des clusters redondants est critique pour éviter la prolifération de profils quasi-identiques. Deux clusters sont fusionnés si **les trois** conditions sont réunies :
1. Même `detected_family` (ex: `Auto_Chrome`)
2. Même `pseudo_order_mode` (même ordre des pseudo-headers)
3. `|window_update_A - window_update_B| / max(A, B) < 5%` (valeurs proches)
La fusion calcule la **moyenne pondérée** par `count_ips` :
```
nouvelle_moyenne = (mean_A × count_A + mean_B × count_B) / (count_A + count_B)
nouvelle_tolérance = max(tol_A, tol_B) # on élargit le seuil
```
#### Avantage sur les signatures statiques
Le profiling dynamique présente trois avantages décisifs :
1. **Adaptabilité** : les nouvelles versions de navigateurs sont automatiquement apprises dès qu'elles atteignent un volume suffisant (`min_cluster_size=1000` IPs uniques), sans mise à jour manuelle du dictionnaire.
2. **Robustesse** : la tolérance `mean + 3σ` s'adapte à la variance réelle observée dans la population, contrairement à la tolérance fixe de 1000 du système statique.
3. **Couverture** : les familles `Auto_Unknown` capturent les clients H2 qui ne correspondent à aucune famille connue mais qui présentent un comportement de navigateur légitime (nouvelles implémentations, clients exotiques).