feat: pipeline L7 HTTP complet + infrastructure tests VM
Correctifs pipeline L7 (uprobe SSL_read) :
- uprobe_ssl.c : ssl_set_fd ne retourne plus tôt quand fd_conn_map est
vide (accept4 non disponible en Docker). Sauvegarde ssl_ptr→{fd,0,0}
pour permettre le fallback /proc côté Go.
- main.go : consumeSSLEvents reécrit avec routeur magic-bytes complet :
* HTTP/2 preface → extraction SETTINGS + conversion correlation.HTTP2Settings
* HTTP/1.x requête → method, path, query, headers, header_order_sig
* HTTP/1.x réponse → status_code
* Fallback /proc/<tgid>/fd/<fd> quand src_ip=0 (accept4 absent)
- writer/clickhouse.go : export header_order_signature ajouté
Nouveaux packages :
- internal/parser/http1.go : parseur HTTP/1.x (IsHTTP1Request,
ParseHTTP1Request, IsHTTP1Response, ParseHTTP1Response)
- internal/parser/http1_test.go : 11 tests unitaires (28 total passent)
- internal/procutil/proc_lookup.go : résolution fd→IP via /proc avec cache
TTL 5s (FDCache). Supporte /proc/PID/net/tcp et tcp6, IPv4-mappé IPv6.
Infrastructure tests VM (tests/vm/) :
- Vagrantfile : VM Rocky Linux 9 KVM, 4 CPU / 4 GB RAM
- provision.sh : installation toolchain eBPF + Go + Docker + nginx
- run-tests-vm.sh : suite de test complète dans la VM (L3/L4+TLS+L7)
- README.md : guide d'installation et d'utilisation
- Makefile : cibles vm-up, vm-down, vm-ssh, test-vm-nginx, test-vm-all,
vm-rebuild-ja4ebpf
Corrections stack Docker :
- Dockerfiles nginx/apache/nginx-varnish/hitch-varnish : suppression des
références à shared/go/ja4common/ (répertoire supprimé)
- clickhouse-init.sh : restauré depuis git, seed anubis_ua_rules obsolète
supprimé (table REGEXP_TREE supprimée du schéma)
- traffic-gen : ajout HTTP/1.0 (http.client) et HTTP/2 (httpx)
- verify_db.py : script de vérification 35 checks (L3/L4/TLS/L7/corrélation)
- run-stack-tests.sh : phase 6 verify_db ajoutée
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@ -16,11 +16,16 @@ import (
|
||||
"github.com/antitbone/ja4/ja4ebpf/internal/correlation"
|
||||
"github.com/antitbone/ja4/ja4ebpf/internal/loader"
|
||||
"github.com/antitbone/ja4/ja4ebpf/internal/parser"
|
||||
"github.com/antitbone/ja4/ja4ebpf/internal/procutil"
|
||||
"github.com/antitbone/ja4/ja4ebpf/internal/writer"
|
||||
"github.com/cilium/ebpf/ringbuf"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// fdCache résout les associations fd → IP:port via /proc quand accept4 n'est pas disponible.
|
||||
// Durée de vie d'une entrée : 5 secondes (suffisant pour une requête HTTP).
|
||||
var fdCache = procutil.NewFDCache(5 * time.Second)
|
||||
|
||||
// Config décrit la configuration complète du démon ja4ebpf.
|
||||
// Chargée depuis un fichier YAML et enrichie par les variables d'environnement
|
||||
// avec le préfixe JA4EBPF_.
|
||||
@ -127,9 +132,9 @@ func main() {
|
||||
// Continuer sans uprobes SSL (capture L3/L4 toujours active)
|
||||
}
|
||||
|
||||
// --- 4. Attachement kprobes accept4 ---
|
||||
// --- 4. Attachement tracepoints accept4 (sys_enter/exit_accept4) ---
|
||||
if err := ldr.AttachAcceptProbe(); err != nil {
|
||||
log.Printf("[ja4ebpf] avertissement kprobe accept4: %v", err)
|
||||
log.Printf("[ja4ebpf] avertissement tracepoint accept4: %v", err)
|
||||
}
|
||||
|
||||
// --- 5. Gestionnaire de sessions ---
|
||||
@ -312,7 +317,8 @@ func consumeTLSEvents(ctx context.Context, rd *ringbuf.Reader, mgr *correlation.
|
||||
}
|
||||
|
||||
// consumeSSLEvents lit les données SSL déchiffrées depuis le ring buffer.
|
||||
// Détecte le préambule HTTP/2 et extrait les paramètres SETTINGS.
|
||||
// Parse les requêtes HTTP/1.x et détecte le préambule HTTP/2.
|
||||
// Quand src_ip=0 (accept4 non disponible), tente un lookup /proc pour retrouver l'IP du client.
|
||||
func consumeSSLEvents(ctx context.Context, rd *ringbuf.Reader, mgr *correlation.Manager) {
|
||||
for {
|
||||
select {
|
||||
@ -335,10 +341,12 @@ func consumeSSLEvents(ctx context.Context, rd *ringbuf.Reader, mgr *correlation.
|
||||
continue
|
||||
}
|
||||
|
||||
pidTgid := binary.LittleEndian.Uint64(data[0:8])
|
||||
fd := binary.LittleEndian.Uint32(data[8:12])
|
||||
srcIPRaw := binary.LittleEndian.Uint32(data[12:16])
|
||||
srcPort := binary.LittleEndian.Uint16(data[16:18])
|
||||
|
||||
// data_len à l'offset 4112 (8+4+4+2 + data[4096] = offset 18, data_len à 18+4096)
|
||||
// data[4096] commence à offset 18, data_len à offset 4114
|
||||
if len(data) < 4118 {
|
||||
continue
|
||||
}
|
||||
@ -346,8 +354,31 @@ func consumeSSLEvents(ctx context.Context, rd *ringbuf.Reader, mgr *correlation.
|
||||
if dataLen > 4096 {
|
||||
dataLen = 4096
|
||||
}
|
||||
if dataLen == 0 {
|
||||
continue
|
||||
}
|
||||
sslData := data[18 : 18+dataLen]
|
||||
|
||||
// --- Fallback /proc quand accept4 n'a pas fourni l'IP ---
|
||||
if srcIPRaw == 0 && fd != 0 {
|
||||
tgid := uint32(pidTgid >> 32)
|
||||
if tgid == 0 {
|
||||
tgid = uint32(pidTgid) // fallback: utiliser le TID si TGID=0
|
||||
}
|
||||
if ip, port, lookupErr := fdCache.Lookup(tgid, fd); lookupErr == nil {
|
||||
ipv4 := ip.To4()
|
||||
if ipv4 != nil {
|
||||
srcIPRaw = uint32(ipv4[0])<<24 | uint32(ipv4[1])<<16 | uint32(ipv4[2])<<8 | uint32(ipv4[3])
|
||||
srcPort = port
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ignorer les événements sans IP identifiable (ex: connexions locales non HTTP)
|
||||
if srcIPRaw == 0 && srcPort == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var key correlation.SessionKey
|
||||
key.SrcIP[0] = byte(srcIPRaw >> 24)
|
||||
key.SrcIP[1] = byte(srcIPRaw >> 16)
|
||||
@ -355,25 +386,82 @@ func consumeSSLEvents(ctx context.Context, rd *ringbuf.Reader, mgr *correlation.
|
||||
key.SrcIP[3] = byte(srcIPRaw)
|
||||
key.SrcPort = srcPort
|
||||
|
||||
// Détecter le préambule HTTP/2
|
||||
// === Routeur Magic Bytes ===
|
||||
|
||||
if parser.DetectH2Preface(sslData) {
|
||||
// HTTP/2 : extraire les paramètres SETTINGS depuis la préface
|
||||
afterPreface := sslData
|
||||
if len(afterPreface) > parser.H2MagicPrefaceLen() {
|
||||
afterPreface = sslData[parser.H2MagicPrefaceLen():]
|
||||
}
|
||||
_, err := parser.ParseH2ClientPreface(afterPreface)
|
||||
if err == nil {
|
||||
mgr.Update(key, func(s *correlation.SessionState) {
|
||||
if len(s.Requests) == 0 {
|
||||
s.Requests = append(s.Requests, correlation.HTTPRequest{
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
}
|
||||
if s.TLS != nil {
|
||||
s.Correlated = true
|
||||
}
|
||||
})
|
||||
h2settings, err := parser.ParseH2ClientPreface(afterPreface)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
mgr.Update(key, func(s *correlation.SessionState) {
|
||||
req := correlation.HTTPRequest{
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
if h2settings != nil {
|
||||
req.HTTP2Settings = &correlation.HTTP2Settings{
|
||||
HeaderTableSize: h2settings.HeaderTableSize,
|
||||
EnablePush: h2settings.EnablePush,
|
||||
MaxConcurrentStreams: h2settings.MaxConcurrentStreams,
|
||||
InitialWindowSize: h2settings.InitialWindowSize,
|
||||
MaxFrameSize: h2settings.MaxFrameSize,
|
||||
MaxHeaderListSize: h2settings.MaxHeaderListSize,
|
||||
UnknownSettings: h2settings.UnknownSettings,
|
||||
WindowUpdateIncrement: h2settings.WindowUpdateIncrement,
|
||||
PseudoHeaderOrder: h2settings.PseudoHeaderOrder,
|
||||
}
|
||||
}
|
||||
if len(s.Requests) == 0 {
|
||||
s.Requests = append(s.Requests, req)
|
||||
}
|
||||
if s.TLS != nil {
|
||||
s.Correlated = true
|
||||
}
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if parser.IsHTTP1Request(sslData) {
|
||||
// HTTP/1.x : parser la requête
|
||||
req := parser.ParseHTTP1Request(sslData)
|
||||
if req == nil {
|
||||
continue
|
||||
}
|
||||
mgr.Update(key, func(s *correlation.SessionState) {
|
||||
s.Requests = append(s.Requests, correlation.HTTPRequest{
|
||||
Timestamp: time.Now(),
|
||||
Method: req.Method,
|
||||
Path: req.Path,
|
||||
QueryString: req.Query,
|
||||
HeaderOrder: req.Headers,
|
||||
HeaderOrderSig: req.HeaderSig,
|
||||
})
|
||||
if s.TLS != nil {
|
||||
s.Correlated = true
|
||||
}
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if parser.IsHTTP1Response(sslData) {
|
||||
// Réponse HTTP/1.x : extraire le code de statut
|
||||
resp := parser.ParseHTTP1Response(sslData)
|
||||
if resp == nil {
|
||||
continue
|
||||
}
|
||||
mgr.Update(key, func(s *correlation.SessionState) {
|
||||
// Mettre à jour le code de statut de la dernière requête
|
||||
if len(s.Requests) > 0 {
|
||||
last := &s.Requests[len(s.Requests)-1]
|
||||
if last.StatusCode == 0 {
|
||||
last.StatusCode = resp.StatusCode
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user