feat(ja4ebpf): add SSL_write uprobe, HPACK decoder, and AcceptCache for session correlation

Add uprobe_ssl_write_entry/uretprobe_ssl_write_exit to capture server HTTP
responses via SSL_write with direction=1. Implement full HPACK decoder
(RFC 7541 static table, multi-byte integers, literal representations) for
HTTP/2 header extraction. Add AcceptCache mapping {tgid,fd}→SessionKey
from accept4 events as authoritative source for SSL correlation when BPF
ssl_conn_map has src_ip=0. Add ip_total_length to tcp_syn_event BPF struct.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jacquin Antoine
2026-04-15 03:34:43 +02:00
parent a02423fd18
commit 24306ef390
7 changed files with 847 additions and 16 deletions

View File

@ -3,6 +3,7 @@ package parser
import (
"encoding/binary"
"fmt"
"strings"
)
// H2Magic est la préface HTTP/2 client (RFC 7540 §3.5), exportée pour usage
@ -30,14 +31,15 @@ const (
h2FrameContinuation = 9
)
// Identifiants des paramètres SETTINGS (RFC 7540, §11.3).
// Identifiants des paramètres SETTINGS (RFC 7540, §11.3 + RFC 8441).
const (
h2SettingHeaderTableSize = 1
h2SettingEnablePush = 2
h2SettingMaxConcurrentStreams = 3
h2SettingInitialWindowSize = 4
h2SettingMaxFrameSize = 5
h2SettingHeaderTableSize = 1
h2SettingEnablePush = 2
h2SettingMaxConcurrentStreams = 3
h2SettingInitialWindowSize = 4
h2SettingMaxFrameSize = 5
h2SettingMaxHeaderListSize = 6
h2SettingEnableConnectProtocol = 8
)
// h2FrameHeader représente l'en-tête fixe de 9 octets d'une frame HTTP/2.
@ -83,15 +85,18 @@ func H2MagicPrefaceLen() int {
// HTTP2Settings contient les paramètres SETTINGS et WINDOW_UPDATE du client HTTP/2.
type HTTP2Settings struct {
HeaderTableSize int32 // SETTINGS_HEADER_TABLE_SIZE (-1 si absent)
EnablePush int32 // SETTINGS_ENABLE_PUSH
MaxConcurrentStreams int32 // SETTINGS_MAX_CONCURRENT_STREAMS
InitialWindowSize int32 // SETTINGS_INITIAL_WINDOW_SIZE
MaxFrameSize int32 // SETTINGS_MAX_FRAME_SIZE
MaxHeaderListSize int32 // SETTINGS_MAX_HEADER_LIST_SIZE
UnknownSettings int32 // paramètre 0x7 (JA4H2)
WindowUpdateIncrement uint32 // valeur WINDOW_UPDATE sur stream 0
PseudoHeaderOrder []string // ordre des pseudo-headers [:method, :authority, ...]
HeaderTableSize int32 // SETTINGS_HEADER_TABLE_SIZE (-1 si absent)
EnablePush int32 // SETTINGS_ENABLE_PUSH
MaxConcurrentStreams int32 // SETTINGS_MAX_CONCURRENT_STREAMS
InitialWindowSize int32 // SETTINGS_INITIAL_WINDOW_SIZE
MaxFrameSize int32 // SETTINGS_MAX_FRAME_SIZE
MaxHeaderListSize int32 // SETTINGS_MAX_HEADER_LIST_SIZE
UnknownSettings int32 // paramètre 0x7 (JA4H2)
EnableConnectProtocol int32 // SETTINGS_ENABLE_CONNECT_PROTOCOL (0x8, RFC 8441)
WindowUpdateIncrement uint32 // valeur WINDOW_UPDATE sur stream 0
PseudoHeaderOrder []string // ordre des pseudo-headers [:method, :authority, ...]
HeaderKV map[string]string // en-têtes extraits du premier HEADERS frame
HeaderOrder []string // noms des en-têtes dans l'ordre d'arrivée
}
// ParseH2ClientPreface extrait les paramètres SETTINGS et le WINDOW_UPDATE
@ -106,6 +111,7 @@ func ParseH2ClientPreface(data []byte) (*HTTP2Settings, error) {
MaxFrameSize: -1,
MaxHeaderListSize: -1,
UnknownSettings: -1,
EnableConnectProtocol: -1,
}
offset := 0
@ -152,6 +158,8 @@ func ParseH2ClientPreface(data []byte) (*HTTP2Settings, error) {
settings.MaxHeaderListSize = int32(val)
case 7: // paramètre non standard (JA4H2)
settings.UnknownSettings = int32(val)
case h2SettingEnableConnectProtocol:
settings.EnableConnectProtocol = int32(val)
}
}
}
@ -163,9 +171,15 @@ func ParseH2ClientPreface(data []byte) (*HTTP2Settings, error) {
}
case h2FrameHeaders:
// Extraire l'ordre des pseudo-headers depuis le premier bloc HEADERS
// Extraire l'ordre des pseudo-headers et les en-têtes réguliers
if hdr.StreamID > 0 && len(settings.PseudoHeaderOrder) == 0 {
settings.PseudoHeaderOrder = ParseH2PseudoHeaders(payload)
// Extraire aussi les en-têtes réguliers (User-Agent, Accept, etc.)
kv, order := DecodeH2HeadersBlock(payload)
if len(kv) > 0 {
settings.HeaderKV = kv
settings.HeaderOrder = order
}
}
}
}
@ -263,3 +277,358 @@ func ParseH2PseudoHeaders(headersBlock []byte) []string {
return order
}
// ---------------------------------------------------------------------------
// HPACK static table (RFC 7541, Appendix A) — index → header name
// Seuls les noms sont listés (les valeurs par défaut sont ignorées car
// les en-têtes d'intérêt comme User-Agent sont toujours envoyés en littéral).
// ---------------------------------------------------------------------------
var hpackStaticTable = map[int]string{
1: ":authority",
2: ":method",
3: ":method",
4: ":path",
5: ":path",
6: ":scheme",
7: ":scheme",
8: ":status",
9: ":status",
10: ":status",
11: ":status",
12: ":status",
13: ":status",
14: ":status",
15: "accept-encoding",
16: "accept-encoding",
17: "accept-language",
18: "cache-control",
19: "cookie",
20: "date",
21: "etag",
22: "if-modified-since",
23: "if-none-match",
24: "last-modified",
25: "link",
26: "location",
27: "referer",
28: "set-cookie",
29: ":method",
30: ":method",
31: ":method",
32: ":path",
33: ":scheme",
34: ":status",
35: "accept",
36: "accept",
37: "accept",
38: "accept-encoding",
39: "accept-encoding",
40: "accept-language",
41: "accept-language",
42: "access-control-allow-headers",
43: "access-control-allow-headers",
44: "access-control-allow-methods",
45: "access-control-allow-origin",
46: "access-control-request-headers",
47: "access-control-request-method",
48: "age",
49: "authorization",
50: "cache-control",
51: "content-disposition",
52: "content-encoding",
53: "content-length",
54: "content-location",
55: "content-range",
56: "content-type",
57: "content-type",
58: "cookie",
59: "date",
60: "etag",
61: "expect",
62: "expires",
63: "from",
64: "host",
65: "if-match",
66: "if-modified-since",
67: "if-none-match",
68: "if-range",
69: "if-unmodified-since",
70: "last-modified",
71: "link",
72: "location",
73: "max-forwards",
74: "proxy-authenticate",
75: "proxy-authorization",
76: "range",
77: "referer",
78: "refresh",
79: "retry-after",
80: "server",
81: "set-cookie",
82: "strict-transport-security",
83: "transfer-encoding",
84: "user-agent",
85: "user-agent",
86: "vary",
87: "vary",
88: "via",
89: "www-authenticate",
90: "x-forwarded-for",
91: "x-forwarded-proto",
92: "x-requested-with",
93: "sec-websocket-key",
94: "sec-ch-ua",
95: "user-agent",
96: "sec-ch-ua-mobile",
97: "sec-ch-ua-platform",
98: "sec-fetch-dest",
99: "sec-fetch-mode",
100: "sec-fetch-site",
}
// hpackCapturedHeaders est la liste des en-têtes H2 dont on capture la valeur.
var hpackCapturedHeaders = map[string]bool{
"user-agent": true,
"accept": true,
"accept-encoding": true,
"accept-language": true,
"content-type": true,
"x-request-id": true,
"x-trace-id": true,
"x-forwarded-for": true,
"sec-ch-ua": true,
"sec-ch-ua-mobile": true,
"sec-ch-ua-platform": true,
"sec-fetch-dest": true,
"sec-fetch-mode": true,
"sec-fetch-site": true,
":method": true,
":path": true,
":authority": true,
":scheme": true,
"cookie": true,
"referer": true,
"host": true,
}
// hpackInteger décode un entier HPACK avec le préfixe spécifié (RFC 7541 §5.1).
// Retourne la valeur décodée et le nombre d'octets consommés.
func hpackInteger(data []byte, prefixBits int) (int, int) {
if len(data) == 0 {
return 0, 0
}
mask := (1 << prefixBits) - 1
value := int(data[0] & byte(mask))
offset := 1
if value < mask {
return value, offset
}
// Extension multi-octets
m := 0
for offset < len(data) && offset < 6 { // limite de sécurité
b := int(data[offset])
value += (b & 0x7F) << m
m += 7
offset++
if b&0x80 == 0 {
break
}
}
return value, offset
}
// hpackString décode une chaîne HPACK (RFC 7541 §5.2).
// Retourne la chaîne décodée et le nombre d'octets consommés.
// Le décodage Huffman n'est pas implémenté — les chaînes Huffman sont ignorées.
func hpackString(data []byte) (string, int) {
if len(data) == 0 {
return "", 0
}
isHuffman := data[0]&0x80 != 0
length, offset := hpackInteger(data, 7)
if isHuffman {
// Huffman non implémenté — on ne peut pas décoder la valeur
return "", offset + length
}
if offset+length > len(data) {
// Données tronquées — retourner ce qu'on peut
if offset < len(data) {
return string(data[offset:]), len(data)
}
return "", offset
}
return string(data[offset : offset+length]), offset + length
}
// DecodeH2HeadersBlock décode un bloc d'en-têtes HPACK depuis un HEADERS frame.
// Retourne un map nom→valeur et la liste ordonnée des noms.
// Gère les représentations les plus courantes :
// - Indexée (6.1) : index → nom+valeur de la table statique
// - Littérale avec index incrémental (6.2.1) : nom indexé + valeur littérale
// - Littérale sans indexation (6.2.2) : nom indexé + valeur littérale
// - Littérale jamais indexée (6.2.3) : nom indexé + valeur littérale
// - Nouveau nom littéral (6.2.x avec index=0) : nom littéral + valeur littérale
func DecodeH2HeadersBlock(block []byte) (map[string]string, []string) {
kv := make(map[string]string)
var order []string
offset := 0
for offset < len(block) && len(kv) < 50 { // limite de sécurité
b := block[offset]
// 1. Représentation indexée (bit 7 = 1) : RFC 7541 §6.1
if b&0x80 != 0 {
idx, n := hpackInteger(block[offset:], 7)
offset += n
if idx > 0 && idx <= len(hpackStaticTable) {
// Uniquement indexée — nom et valeur viennent de la table
// Pour les entrées "nom uniquement" (pas de valeur par défaut),
// on ne peut pas extraire la valeur sans table dynamique
_ = hpackStaticTable[idx]
}
continue
}
var name string
var nameLen int
// 2. Littérale avec index incrémental (bits 7-6 = 01) : RFC 7541 §6.2.1
if b&0xC0 == 0x40 {
idx, n := hpackInteger(block[offset:], 6)
offset += n
if idx == 0 {
// Nouveau nom : nom littéral suivi de valeur littérale
name, nameLen = hpackString(block[offset:])
offset += nameLen
} else if idx <= len(hpackStaticTable) {
name = hpackStaticTable[idx]
}
value, valueLen := hpackString(block[offset:])
offset += valueLen
nameLower := strings.ToLower(name)
if nameLower != "" && value != "" && hpackCapturedHeaders[nameLower] {
kv[nameLower] = value
order = append(order, nameLower)
}
continue
}
// 3. Littérale sans indexation (bits 7-5 = 000) : RFC 7541 §6.2.2
if b&0xF0 == 0x00 {
idx, n := hpackInteger(block[offset:], 4)
offset += n
if idx == 0 {
name, nameLen = hpackString(block[offset:])
offset += nameLen
} else if idx <= len(hpackStaticTable) {
name = hpackStaticTable[idx]
}
value, valueLen := hpackString(block[offset:])
offset += valueLen
nameLower := strings.ToLower(name)
if nameLower != "" && value != "" && hpackCapturedHeaders[nameLower] {
kv[nameLower] = value
order = append(order, nameLower)
}
continue
}
// 4. Littérale jamais indexée (bits 7-5 = 0001) : RFC 7541 §6.2.3
if b&0xF0 == 0x10 {
idx, n := hpackInteger(block[offset:], 4)
offset += n
if idx == 0 {
name, nameLen = hpackString(block[offset:])
offset += nameLen
} else if idx <= len(hpackStaticTable) {
name = hpackStaticTable[idx]
}
value, valueLen := hpackString(block[offset:])
offset += valueLen
nameLower := strings.ToLower(name)
if nameLower != "" && value != "" && hpackCapturedHeaders[nameLower] {
kv[nameLower] = value
order = append(order, nameLower)
}
continue
}
// Représentation inconnue — arrêter
break
}
return kv, order
}
// IsH2FrameHeader vérifie si les données commencent par un en-tête de frame HTTP/2 valide.
// Utilisé pour détecter les frames H2 seules (sans préface) dans les SSL_read ultérieurs.
func IsH2FrameHeader(data []byte) bool {
if len(data) < 9 {
return false
}
hdr, err := parseH2FrameHeader(data)
if err != nil {
return false
}
// Vérifications de plausibilité :
// - Longueur ≤ 16384 (16 KiB, limite conservatrice pour un seul read)
// - Type dans la plage 0-9 (types de frame définis)
// - Stream ID dans une plage raisonnable
if hdr.Length > 16384 {
return false
}
if hdr.Type > 9 {
return false
}
return true
}
// ExtractH2HeaderKV extrait les en-têtes des frames HEADERS HTTP/2.
// Parcourt toutes les frames dans les données et décode les blocs HEADERS.
func ExtractH2HeaderKV(data []byte) map[string]string {
kv := make(map[string]string)
offset := 0
for offset < len(data) && len(kv) < 50 {
if offset+9 > len(data) {
break
}
hdr, err := parseH2FrameHeader(data[offset:])
if err != nil {
break
}
offset += 9
payloadEnd := offset + int(hdr.Length)
if payloadEnd > len(data) {
break
}
payload := data[offset:payloadEnd]
offset = payloadEnd
if hdr.Type == h2FrameHeaders && hdr.StreamID > 0 {
frameKV, _ := DecodeH2HeadersBlock(payload)
for k, v := range frameKV {
if _, exists := kv[k]; !exists {
kv[k] = v
}
}
}
}
return kv
}