- TC ingress hook captures TCP SYN (L3/L4) and TLS ClientHello - Uprobes on SSL_read/SSL_set_fd capture decrypted TLS data - Kprobes on accept4 correlate socket FDs to client IP:port - JA4 fingerprint computed from parsed TLS ClientHello - HTTP/2 SETTINGS and WINDOW_UPDATE extracted from decrypted streams - Session manager with sharded map (256 shards) and GC goroutine - Slowloris detection: sessions with no requests after 10s threshold - ClickHouse batch writer to ja4_logs.http_logs_raw (raw_json) - All tests pass: 17 parser + 10 correlation tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
158 lines
5.4 KiB
Go
158 lines
5.4 KiB
Go
package parser_test
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/antitbone/ja4/ja4ebpf/internal/parser"
|
|
)
|
|
|
|
func TestDetectH2PrefaceTrue(t *testing.T) {
|
|
preface := []byte("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n")
|
|
data := append(preface, 0x00, 0x00) // données supplémentaires
|
|
|
|
if !parser.DetectH2Preface(data) {
|
|
t.Error("H2Magic non détecté dans un buffer valide")
|
|
}
|
|
}
|
|
|
|
func TestDetectH2PrefaceFalse(t *testing.T) {
|
|
if parser.DetectH2Preface([]byte("GET / HTTP/1.1\r\n")) {
|
|
t.Error("détection faux positif HTTP/1.1 comme HTTP/2")
|
|
}
|
|
}
|
|
|
|
func TestDetectH2PrefaceTooShort(t *testing.T) {
|
|
if parser.DetectH2Preface([]byte("PRI *")) {
|
|
t.Error("détection sur buffer trop court")
|
|
}
|
|
}
|
|
|
|
func TestH2MagicPrefaceLen(t *testing.T) {
|
|
if parser.H2MagicPrefaceLen() != 24 {
|
|
t.Errorf("longueur préambule HTTP/2 attendue 24, obtenue %d", parser.H2MagicPrefaceLen())
|
|
}
|
|
}
|
|
|
|
func TestParseH2ClientPrefaceSettingsEmpty(t *testing.T) {
|
|
// Frame SETTINGS vide (longueur 0, aucun paramètre) sur stream 0
|
|
frame := buildH2Frame(0x4, 0x0, 0, []byte{})
|
|
settings, err := parser.ParseH2ClientPreface(frame)
|
|
if err != nil {
|
|
t.Fatalf("ParseH2ClientPreface: %v", err)
|
|
}
|
|
if settings == nil {
|
|
t.Fatal("settings ne doit pas être nil")
|
|
}
|
|
// Tous les champs doivent être -1 (absent)
|
|
if settings.HeaderTableSize != -1 {
|
|
t.Errorf("HeaderTableSize: attendu -1, obtenu %d", settings.HeaderTableSize)
|
|
}
|
|
if settings.InitialWindowSize != -1 {
|
|
t.Errorf("InitialWindowSize: attendu -1, obtenu %d", settings.InitialWindowSize)
|
|
}
|
|
}
|
|
|
|
func TestParseH2ClientPrefaceSettingsWithValues(t *testing.T) {
|
|
// Frame SETTINGS avec INITIAL_WINDOW_SIZE=65536 et MAX_CONCURRENT_STREAMS=100
|
|
settingsPayload := []byte{
|
|
0x00, 0x04, 0x00, 0x01, 0x00, 0x00, // INITIAL_WINDOW_SIZE = 65536
|
|
0x00, 0x03, 0x00, 0x00, 0x00, 0x64, // MAX_CONCURRENT_STREAMS = 100
|
|
}
|
|
frame := buildH2Frame(0x4, 0x0, 0, settingsPayload)
|
|
|
|
settings, err := parser.ParseH2ClientPreface(frame)
|
|
if err != nil {
|
|
t.Fatalf("ParseH2ClientPreface: %v", err)
|
|
}
|
|
|
|
if settings.InitialWindowSize != 65536 {
|
|
t.Errorf("InitialWindowSize: attendu 65536, obtenu %d", settings.InitialWindowSize)
|
|
}
|
|
if settings.MaxConcurrentStreams != 100 {
|
|
t.Errorf("MaxConcurrentStreams: attendu 100, obtenu %d", settings.MaxConcurrentStreams)
|
|
}
|
|
// Les paramètres non présents restent à -1
|
|
if settings.HeaderTableSize != -1 {
|
|
t.Errorf("HeaderTableSize non fourni: attendu -1, obtenu %d", settings.HeaderTableSize)
|
|
}
|
|
}
|
|
|
|
func TestParseH2ClientPrefaceWindowUpdate(t *testing.T) {
|
|
// Frame WINDOW_UPDATE sur stream 0 avec incrément = 1073741824
|
|
wuPayload := []byte{0x40, 0x00, 0x00, 0x00} // 0x40000000 = 1073741824
|
|
frame := buildH2Frame(0x8, 0x0, 0, wuPayload)
|
|
|
|
settings, err := parser.ParseH2ClientPreface(frame)
|
|
if err != nil {
|
|
t.Fatalf("ParseH2ClientPreface: %v", err)
|
|
}
|
|
if settings.WindowUpdateIncrement != 1073741824 {
|
|
t.Errorf("WindowUpdateIncrement: attendu 1073741824, obtenu %d", settings.WindowUpdateIncrement)
|
|
}
|
|
}
|
|
|
|
func TestParseH2ClientPrefaceCombined(t *testing.T) {
|
|
// SETTINGS + WINDOW_UPDATE combinés (comme envoyé par curl/h2)
|
|
settingsPayload := []byte{
|
|
0x00, 0x01, 0x00, 0x00, 0x10, 0x00, // HEADER_TABLE_SIZE = 4096
|
|
0x00, 0x04, 0x00, 0x00, 0xff, 0xff, // INITIAL_WINDOW_SIZE = 65535
|
|
}
|
|
wuPayload := []byte{0x00, 0x0f, 0x00, 0x01} // WINDOW_UPDATE incr = 983041
|
|
|
|
frames := buildH2Frame(0x4, 0x0, 0, settingsPayload)
|
|
frames = append(frames, buildH2Frame(0x8, 0x0, 0, wuPayload)...)
|
|
|
|
settings, err := parser.ParseH2ClientPreface(frames)
|
|
if err != nil {
|
|
t.Fatalf("ParseH2ClientPreface: %v", err)
|
|
}
|
|
if settings.HeaderTableSize != 4096 {
|
|
t.Errorf("HeaderTableSize: attendu 4096, obtenu %d", settings.HeaderTableSize)
|
|
}
|
|
if settings.InitialWindowSize != 65535 {
|
|
t.Errorf("InitialWindowSize: attendu 65535, obtenu %d", settings.InitialWindowSize)
|
|
}
|
|
if settings.WindowUpdateIncrement != 983041 {
|
|
t.Errorf("WindowUpdateIncrement: attendu 983041, obtenu %d", settings.WindowUpdateIncrement)
|
|
}
|
|
}
|
|
|
|
func TestParseH2ClientPrefaceEmpty(t *testing.T) {
|
|
// Données vides : doit retourner sans erreur, settings avec valeurs par défaut (-1)
|
|
settings, err := parser.ParseH2ClientPreface([]byte{})
|
|
if err != nil {
|
|
t.Fatalf("ParseH2ClientPreface sur vide: %v", err)
|
|
}
|
|
if settings == nil {
|
|
t.Error("settings ne doit pas être nil même pour données vides")
|
|
}
|
|
if settings.HeaderTableSize != -1 {
|
|
t.Errorf("HeaderTableSize: attendu -1 par défaut, obtenu %d", settings.HeaderTableSize)
|
|
}
|
|
}
|
|
|
|
func TestParseH2ClientPrefaceTruncatedFrame(t *testing.T) {
|
|
// Frame tronquée : en-tête complet mais payload incomplet
|
|
truncated := []byte{0x00, 0x00, 0x06, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x01} // payload tronqué
|
|
settings, err := parser.ParseH2ClientPreface(truncated)
|
|
if err != nil {
|
|
t.Fatalf("ParseH2ClientPreface sur frame tronquée: %v (doit tolérer)", err)
|
|
}
|
|
// Les paramètres restent à -1 car le payload est incomplet
|
|
_ = settings
|
|
}
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
|
|
// buildH2Frame construit une frame HTTP/2 brute (en-tête 9 octets + payload).
|
|
func buildH2Frame(frameType, flags uint8, streamID uint32, payload []byte) []byte {
|
|
l := len(payload)
|
|
frame := []byte{
|
|
byte(l >> 16), byte(l >> 8), byte(l), // longueur sur 3 octets
|
|
frameType, flags,
|
|
byte(streamID >> 24), byte(streamID >> 16), byte(streamID >> 8), byte(streamID),
|
|
}
|
|
return append(frame, payload...)
|
|
}
|