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:
toto
2026-04-12 02:37:00 +02:00
parent 9734e21fe3
commit f85a10b012
21 changed files with 1868 additions and 74 deletions

View File

@ -0,0 +1,146 @@
// Package parser fournit les parseurs pour les protocoles HTTP/1.x, HTTP/2 et TLS.
package parser
import (
"bytes"
"strings"
)
// HTTP1Request représente une requête HTTP/1.x parsée depuis le flux déchiffré.
type HTTP1Request struct {
Method string // méthode HTTP (GET, POST, …)
Path string // chemin (sans query string)
Query string // query string (sans le '?')
Protocol string // "HTTP/1.0" ou "HTTP/1.1"
Headers []string // noms des en-têtes dans l'ordre exact d'arrivée
HeaderSig string // signature : noms joints par ";"
}
// HTTP1Response représente le début d'une réponse HTTP/1.x (status line).
type HTTP1Response struct {
StatusCode int
}
// knownMethods est la liste des méthodes HTTP/1.x reconnues.
var knownMethods = []string{
"GET", "POST", "PUT", "DELETE", "HEAD",
"OPTIONS", "PATCH", "CONNECT", "TRACE",
}
// IsHTTP1Request retourne true si les premiers octets ressemblent à une
// requête HTTP/1.x (commence par une méthode reconnue suivi d'un espace).
func IsHTTP1Request(data []byte) bool {
for _, m := range knownMethods {
if bytes.HasPrefix(data, []byte(m+" ")) {
return true
}
}
return false
}
// IsHTTP1Response retourne true si les premiers octets ressemblent à une
// réponse HTTP/1.x ("HTTP/1.").
func IsHTTP1Response(data []byte) bool {
return bytes.HasPrefix(data, []byte("HTTP/1."))
}
// ParseHTTP1Request extrait les champs d'une requête HTTP/1.x depuis un buffer brut.
// Retourne nil sans erreur si le buffer ne contient pas de requête complète.
func ParseHTTP1Request(data []byte) *HTTP1Request {
// Localiser la fin de la request-line + headers (double CRLF)
headerEnd := bytes.Index(data, []byte("\r\n\r\n"))
if headerEnd < 0 {
headerEnd = len(data)
}
text := string(data[:headerEnd])
lines := strings.Split(text, "\r\n")
if len(lines) == 0 {
return nil
}
// Parser la request-line : "METHOD path HTTP/x.y"
requestLine := lines[0]
parts := strings.SplitN(requestLine, " ", 3)
if len(parts) < 2 {
return nil
}
method := parts[0]
rawPath := parts[1]
protocol := "HTTP/1.1"
if len(parts) == 3 {
protocol = parts[2]
}
// Valider la méthode
validMethod := false
for _, m := range knownMethods {
if method == m {
validMethod = true
break
}
}
if !validMethod {
return nil
}
// Séparer path et query string
path := rawPath
query := ""
if idx := strings.Index(rawPath, "?"); idx >= 0 {
path = rawPath[:idx]
query = rawPath[idx+1:]
}
// Extraire les noms d'en-têtes dans l'ordre
headers := make([]string, 0, len(lines)-1)
for _, line := range lines[1:] {
if line == "" {
break
}
if colon := strings.Index(line, ":"); colon > 0 {
name := strings.TrimSpace(line[:colon])
if name != "" {
headers = append(headers, name)
}
}
}
sig := strings.Join(headers, ";")
return &HTTP1Request{
Method: method,
Path: path,
Query: query,
Protocol: protocol,
Headers: headers,
HeaderSig: sig,
}
}
// ParseHTTP1Response extrait le code de statut d'une réponse HTTP/1.x.
// Retourne nil si le buffer n'est pas une réponse HTTP/1.x reconnaissable.
func ParseHTTP1Response(data []byte) *HTTP1Response {
if !IsHTTP1Response(data) {
return nil
}
// Status-line : "HTTP/1.1 200 OK\r\n..."
line := data
if idx := bytes.IndexByte(data, '\n'); idx >= 0 {
line = data[:idx]
}
parts := strings.SplitN(strings.TrimRight(string(line), "\r\n"), " ", 3)
if len(parts) < 2 {
return nil
}
code := 0
for _, c := range parts[1] {
if c < '0' || c > '9' {
break
}
code = code*10 + int(c-'0')
}
if code < 100 || code > 599 {
return nil
}
return &HTTP1Response{StatusCode: code}
}