- 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>
266 lines
7.6 KiB
Go
266 lines
7.6 KiB
Go
package parser
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
)
|
|
|
|
// H2Magic est la préface HTTP/2 client (RFC 7540 §3.5), exportée pour usage
|
|
// par le routeur Magic Bytes (package dispatcher) et les consommateurs RingBuffer.
|
|
const H2Magic = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
|
|
|
|
// h2MagicPrefaceLen est la longueur du préambule HTTP/2 client.
|
|
const h2MagicPrefaceLen = 24
|
|
|
|
// h2MagicPreface est le préambule ("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n") envoyé
|
|
// par tout client HTTP/2 avant la première frame SETTINGS.
|
|
var h2MagicPreface = []byte(H2Magic)
|
|
|
|
// Identifiants de types de frames HTTP/2 (RFC 7540, §11.2).
|
|
const (
|
|
h2FrameData = 0
|
|
h2FrameHeaders = 1
|
|
h2FramePriority = 2
|
|
h2FrameRSTStream = 3
|
|
h2FrameSettings = 4
|
|
h2FramePushPromise = 5
|
|
h2FramePing = 6
|
|
h2FrameGoAway = 7
|
|
h2FrameWindowUpdate = 8
|
|
h2FrameContinuation = 9
|
|
)
|
|
|
|
// Identifiants des paramètres SETTINGS (RFC 7540, §11.3).
|
|
const (
|
|
h2SettingHeaderTableSize = 1
|
|
h2SettingEnablePush = 2
|
|
h2SettingMaxConcurrentStreams = 3
|
|
h2SettingInitialWindowSize = 4
|
|
h2SettingMaxFrameSize = 5
|
|
h2SettingMaxHeaderListSize = 6
|
|
)
|
|
|
|
// h2FrameHeader représente l'en-tête fixe de 9 octets d'une frame HTTP/2.
|
|
type h2FrameHeader struct {
|
|
Length uint32 // longueur du payload (3 octets)
|
|
Type uint8 // type de frame
|
|
Flags uint8 // flags
|
|
StreamID uint32 // identifiant de stream (masque 0x7FFFFFFF)
|
|
}
|
|
|
|
// parseH2FrameHeader décode l'en-tête de 9 octets d'une frame HTTP/2.
|
|
func parseH2FrameHeader(data []byte) (h2FrameHeader, error) {
|
|
if len(data) < 9 {
|
|
return h2FrameHeader{}, fmt.Errorf("données insuffisantes pour l'en-tête frame HTTP/2: %d octets", len(data))
|
|
}
|
|
// Longueur sur 3 octets big-endian
|
|
length := uint32(data[0])<<16 | uint32(data[1])<<8 | uint32(data[2])
|
|
return h2FrameHeader{
|
|
Length: length,
|
|
Type: data[3],
|
|
Flags: data[4],
|
|
StreamID: binary.BigEndian.Uint32(data[5:9]) & 0x7FFFFFFF,
|
|
}, nil
|
|
}
|
|
|
|
// DetectH2Preface vérifie si le buffer commence par le préambule HTTP/2.
|
|
func DetectH2Preface(data []byte) bool {
|
|
if len(data) < h2MagicPrefaceLen {
|
|
return false
|
|
}
|
|
for i := 0; i < h2MagicPrefaceLen; i++ {
|
|
if data[i] != h2MagicPreface[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// H2MagicPrefaceLen retourne la longueur du préambule HTTP/2.
|
|
func H2MagicPrefaceLen() int {
|
|
return h2MagicPrefaceLen
|
|
}
|
|
|
|
// 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, ...]
|
|
}
|
|
|
|
// ParseH2ClientPreface extrait les paramètres SETTINGS et le WINDOW_UPDATE
|
|
// depuis le flux HTTP/2 déchiffré du client.
|
|
// data doit commencer APRÈS le magic preface (offset 24).
|
|
func ParseH2ClientPreface(data []byte) (*HTTP2Settings, error) {
|
|
settings := &HTTP2Settings{
|
|
HeaderTableSize: -1,
|
|
EnablePush: -1,
|
|
MaxConcurrentStreams: -1,
|
|
InitialWindowSize: -1,
|
|
MaxFrameSize: -1,
|
|
MaxHeaderListSize: -1,
|
|
UnknownSettings: -1,
|
|
}
|
|
|
|
offset := 0
|
|
// Parser au maximum 10 frames pour éviter une boucle infinie
|
|
for frameIdx := 0; frameIdx < 10 && offset < len(data); frameIdx++ {
|
|
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
|
|
|
|
switch hdr.Type {
|
|
case h2FrameSettings:
|
|
// Parser uniquement les SETTINGS du client (stream 0)
|
|
if hdr.StreamID == 0 {
|
|
pairs, err := parseH2SettingsFrame(payload)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for id, val := range pairs {
|
|
switch id {
|
|
case h2SettingHeaderTableSize:
|
|
settings.HeaderTableSize = int32(val)
|
|
case h2SettingEnablePush:
|
|
settings.EnablePush = int32(val)
|
|
case h2SettingMaxConcurrentStreams:
|
|
settings.MaxConcurrentStreams = int32(val)
|
|
case h2SettingInitialWindowSize:
|
|
settings.InitialWindowSize = int32(val)
|
|
case h2SettingMaxFrameSize:
|
|
settings.MaxFrameSize = int32(val)
|
|
case h2SettingMaxHeaderListSize:
|
|
settings.MaxHeaderListSize = int32(val)
|
|
case 7: // paramètre non standard (JA4H2)
|
|
settings.UnknownSettings = int32(val)
|
|
}
|
|
}
|
|
}
|
|
|
|
case h2FrameWindowUpdate:
|
|
// WINDOW_UPDATE sur stream 0 = flux de connexion
|
|
if hdr.StreamID == 0 && len(payload) >= 4 {
|
|
settings.WindowUpdateIncrement = binary.BigEndian.Uint32(payload[0:4]) & 0x7FFFFFFF
|
|
}
|
|
|
|
case h2FrameHeaders:
|
|
// Extraire l'ordre des pseudo-headers depuis le premier bloc HEADERS
|
|
if hdr.StreamID > 0 && len(settings.PseudoHeaderOrder) == 0 {
|
|
settings.PseudoHeaderOrder = ParseH2PseudoHeaders(payload)
|
|
}
|
|
}
|
|
}
|
|
|
|
return settings, nil
|
|
}
|
|
|
|
// parseH2SettingsFrame extrait les paires (identifiant, valeur) d'une frame SETTINGS.
|
|
// Chaque paire fait 6 octets : identifiant(2) + valeur(4).
|
|
func parseH2SettingsFrame(payload []byte) (map[uint16]uint32, error) {
|
|
if len(payload)%6 != 0 {
|
|
return nil, fmt.Errorf("longueur de frame SETTINGS invalide: %d (doit être multiple de 6)", len(payload))
|
|
}
|
|
result := make(map[uint16]uint32)
|
|
for i := 0; i+6 <= len(payload); i += 6 {
|
|
id := binary.BigEndian.Uint16(payload[i : i+2])
|
|
val := binary.BigEndian.Uint32(payload[i+2 : i+6])
|
|
result[id] = val
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// ParseH2PseudoHeaders extrait l'ordre des pseudo-headers depuis un bloc HPACK.
|
|
//
|
|
// Implémentation simplifiée : détecte les pseudo-headers via les index HPACK statiques.
|
|
// Table statique HPACK (RFC 7541, Annexe A) — index pertinents :
|
|
// 1 :authority
|
|
// 2 :method = GET
|
|
// 3 :method = POST
|
|
// 4 :path = /
|
|
// 5 :path = /index.html
|
|
// 6 :scheme = http
|
|
// 7 :scheme = https
|
|
func ParseH2PseudoHeaders(headersBlock []byte) []string {
|
|
// Index HPACK statique → pseudo-header
|
|
hpackStaticPseudo := map[int]string{
|
|
1: ":authority",
|
|
2: ":method",
|
|
3: ":method",
|
|
4: ":path",
|
|
5: ":path",
|
|
6: ":scheme",
|
|
7: ":scheme",
|
|
}
|
|
|
|
seen := make(map[string]bool)
|
|
var order []string
|
|
offset := 0
|
|
|
|
for offset < len(headersBlock) {
|
|
b := headersBlock[offset]
|
|
|
|
// Représentation indexée (bit 7 = 1) : RFC 7541 §6.1
|
|
if b&0x80 != 0 {
|
|
idx := int(b & 0x7F)
|
|
if name, ok := hpackStaticPseudo[idx]; ok {
|
|
if !seen[name] {
|
|
seen[name] = true
|
|
order = append(order, name)
|
|
}
|
|
} else if idx == 0 {
|
|
// Fin de la liste d'index ou encodage multi-octets
|
|
offset++
|
|
continue
|
|
} else {
|
|
// Index dynamique ou non-pseudo-header : arrêter le scan
|
|
break
|
|
}
|
|
offset++
|
|
continue
|
|
}
|
|
|
|
// Représentation littérale avec index incrémental (bits 7-6 = 01) : RFC 7541 §6.2.1
|
|
if b&0xC0 == 0x40 {
|
|
idx := int(b & 0x3F)
|
|
if name, ok := hpackStaticPseudo[idx]; ok {
|
|
if !seen[name] {
|
|
seen[name] = true
|
|
order = append(order, name)
|
|
}
|
|
}
|
|
offset++
|
|
// Sauter la valeur (longueur + contenu)
|
|
if offset >= len(headersBlock) {
|
|
break
|
|
}
|
|
valueLen := int(headersBlock[offset] & 0x7F) // ignorer le bit Huffman
|
|
offset += 1 + valueLen
|
|
continue
|
|
}
|
|
|
|
// Tout autre encodage : arrêter (ce n'est probablement plus un pseudo-header)
|
|
break
|
|
}
|
|
|
|
return order
|
|
}
|