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>
202 lines
4.9 KiB
Go
202 lines
4.9 KiB
Go
package parser
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestIsHTTP1RequestTrue(t *testing.T) {
|
|
cases := [][]byte{
|
|
[]byte("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"),
|
|
[]byte("POST /api/data HTTP/1.1\r\n"),
|
|
[]byte("PUT /resource HTTP/1.0\r\n"),
|
|
[]byte("HEAD /ping HTTP/1.1\r\n"),
|
|
[]byte("DELETE /item/1 HTTP/1.1\r\n"),
|
|
}
|
|
for _, c := range cases {
|
|
if !IsHTTP1Request(c) {
|
|
t.Errorf("attendu true pour %q", c[:20])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIsHTTP1RequestFalse(t *testing.T) {
|
|
cases := [][]byte{
|
|
[]byte("HTTP/1.1 200 OK\r\n"),
|
|
[]byte(H2Magic),
|
|
[]byte("INVALID /path HTTP/1.1\r\n"),
|
|
[]byte{0x16, 0x03, 0x01}, // TLS handshake
|
|
}
|
|
for _, c := range cases {
|
|
if IsHTTP1Request(c) {
|
|
t.Errorf("attendu false pour %q", c[:min(20, len(c))])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseHTTP1RequestBasic(t *testing.T) {
|
|
raw := "GET /path/to/resource HTTP/1.1\r\n" +
|
|
"Host: example.com\r\n" +
|
|
"User-Agent: Go-http-client/1.1\r\n" +
|
|
"Accept: */*\r\n" +
|
|
"\r\n"
|
|
req := ParseHTTP1Request([]byte(raw))
|
|
if req == nil {
|
|
t.Fatal("attendu non-nil")
|
|
}
|
|
if req.Method != "GET" {
|
|
t.Errorf("method: attendu GET, obtenu %q", req.Method)
|
|
}
|
|
if req.Path != "/path/to/resource" {
|
|
t.Errorf("path: %q", req.Path)
|
|
}
|
|
if req.Protocol != "HTTP/1.1" {
|
|
t.Errorf("protocol: %q", req.Protocol)
|
|
}
|
|
if len(req.Headers) != 3 {
|
|
t.Errorf("nb headers: attendu 3, obtenu %d", len(req.Headers))
|
|
}
|
|
if req.Headers[0] != "Host" {
|
|
t.Errorf("premier header: attendu Host, obtenu %q", req.Headers[0])
|
|
}
|
|
expected := "Host;User-Agent;Accept"
|
|
if req.HeaderSig != expected {
|
|
t.Errorf("HeaderSig: attendu %q, obtenu %q", expected, req.HeaderSig)
|
|
}
|
|
}
|
|
|
|
func TestParseHTTP1RequestWithQueryString(t *testing.T) {
|
|
raw := "GET /search?q=ebpf&page=2 HTTP/1.1\r\nHost: test.com\r\n\r\n"
|
|
req := ParseHTTP1Request([]byte(raw))
|
|
if req == nil {
|
|
t.Fatal("attendu non-nil")
|
|
}
|
|
if req.Path != "/search" {
|
|
t.Errorf("path: attendu /search, obtenu %q", req.Path)
|
|
}
|
|
if req.Query != "q=ebpf&page=2" {
|
|
t.Errorf("query: %q", req.Query)
|
|
}
|
|
}
|
|
|
|
func TestParseHTTP1RequestHTTP10(t *testing.T) {
|
|
raw := "GET / HTTP/1.0\r\nHost: legacy.com\r\n\r\n"
|
|
req := ParseHTTP1Request([]byte(raw))
|
|
if req == nil {
|
|
t.Fatal("attendu non-nil")
|
|
}
|
|
if req.Protocol != "HTTP/1.0" {
|
|
t.Errorf("protocol: %q", req.Protocol)
|
|
}
|
|
}
|
|
|
|
func TestParseHTTP1RequestPostBody(t *testing.T) {
|
|
raw := "POST /api/submit HTTP/1.1\r\n" +
|
|
"Host: api.example.com\r\n" +
|
|
"Content-Type: application/json\r\n" +
|
|
"Content-Length: 42\r\n" +
|
|
"\r\n" +
|
|
`{"key":"value"}`
|
|
req := ParseHTTP1Request([]byte(raw))
|
|
if req == nil {
|
|
t.Fatal("attendu non-nil")
|
|
}
|
|
if req.Method != "POST" {
|
|
t.Errorf("method: %q", req.Method)
|
|
}
|
|
if len(req.Headers) != 3 {
|
|
t.Errorf("nb headers: attendu 3, obtenu %d", len(req.Headers))
|
|
}
|
|
}
|
|
|
|
func TestParseHTTP1RequestInvalid(t *testing.T) {
|
|
cases := [][]byte{
|
|
[]byte("HTTP/1.1 200 OK\r\n"),
|
|
[]byte("NOTAMETHOD /path HTTP/1.1\r\n"),
|
|
[]byte(""),
|
|
{0xFF, 0xFE, 0xFD},
|
|
}
|
|
for _, c := range cases {
|
|
req := ParseHTTP1Request(c)
|
|
if req != nil {
|
|
t.Errorf("attendu nil pour %q, obtenu non-nil", c)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIsHTTP1Response(t *testing.T) {
|
|
if !IsHTTP1Response([]byte("HTTP/1.1 200 OK\r\n")) {
|
|
t.Error("attendu true pour HTTP/1.1 200")
|
|
}
|
|
if !IsHTTP1Response([]byte("HTTP/1.0 404 Not Found\r\n")) {
|
|
t.Error("attendu true pour HTTP/1.0 404")
|
|
}
|
|
if IsHTTP1Response([]byte("GET / HTTP/1.1\r\n")) {
|
|
t.Error("attendu false pour une requête")
|
|
}
|
|
}
|
|
|
|
func TestParseHTTP1Response(t *testing.T) {
|
|
cases := []struct {
|
|
raw string
|
|
code int
|
|
}{
|
|
{"HTTP/1.1 200 OK\r\n", 200},
|
|
{"HTTP/1.0 404 Not Found\r\n", 404},
|
|
{"HTTP/1.1 301 Moved Permanently\r\n", 301},
|
|
{"HTTP/1.1 500 Internal Server Error\r\n", 500},
|
|
}
|
|
for _, tc := range cases {
|
|
resp := ParseHTTP1Response([]byte(tc.raw))
|
|
if resp == nil {
|
|
t.Errorf("attendu non-nil pour %q", tc.raw)
|
|
continue
|
|
}
|
|
if resp.StatusCode != tc.code {
|
|
t.Errorf("code attendu %d, obtenu %d pour %q", tc.code, resp.StatusCode, tc.raw)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseHTTP1ResponseInvalid(t *testing.T) {
|
|
cases := []string{
|
|
"GET / HTTP/1.1",
|
|
"HTTP/1.1 99 Continue", // hors plage 100-599
|
|
"",
|
|
}
|
|
for _, tc := range cases {
|
|
resp := ParseHTTP1Response([]byte(tc))
|
|
if resp != nil && (resp.StatusCode < 100 || resp.StatusCode > 599) {
|
|
t.Errorf("code invalide %d pour %q", resp.StatusCode, tc)
|
|
}
|
|
}
|
|
}
|
|
|
|
// min retourne le minimum de deux entiers (helper pour les tests).
|
|
func min(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
func TestHTTP1HeaderOrderSignature(t *testing.T) {
|
|
raw := "GET / HTTP/1.1\r\n" +
|
|
"Accept: text/html\r\n" +
|
|
"Accept-Encoding: gzip\r\n" +
|
|
"Connection: keep-alive\r\n" +
|
|
"Host: example.com\r\n" +
|
|
"\r\n"
|
|
req := ParseHTTP1Request([]byte(raw))
|
|
if req == nil {
|
|
t.Fatal("attendu non-nil")
|
|
}
|
|
parts := strings.Split(req.HeaderSig, ";")
|
|
if len(parts) != 4 {
|
|
t.Errorf("attendu 4 headers dans la signature, obtenu %d: %q", len(parts), req.HeaderSig)
|
|
}
|
|
if parts[0] != "Accept" {
|
|
t.Errorf("premier header sig: attendu Accept, obtenu %q", parts[0])
|
|
}
|
|
}
|