feat: maximize data completeness across L3/L4/TLS/HTTP layers and add E2E test infra
Add SSL_write uprobe for HTTP response capture, HPACK decoder for HTTP/2 header extraction, and AcceptCache for reliable SSL/TC session correlation. Populate all ClickHouse fields including tcp_meta_options, ip_meta_total_length, syn_to_clienthello_ms, client_headers, TLS cipher suites/extensions, and h2_enable_connect_protocol. Increase BPF capture buffers (HTTP 512B, TLS 1024B). Add distributed E2E testing infrastructure with multi-VM Vagrant setup. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@ -153,6 +153,7 @@ int capture_tc(struct __sk_buff *ctx)
|
|||||||
evt.ttl = ttl;
|
evt.ttl = ttl;
|
||||||
evt.df_bit = df_bit;
|
evt.df_bit = df_bit;
|
||||||
evt.ip_id = ip_id;
|
evt.ip_id = ip_id;
|
||||||
|
evt.ip_total_length = bpf_ntohs(iph.tot_len);
|
||||||
evt.window_size = window;
|
evt.window_size = window;
|
||||||
evt.window_scale = 0xFF;
|
evt.window_scale = 0xFF;
|
||||||
evt.mss = 0;
|
evt.mss = 0;
|
||||||
@ -218,17 +219,17 @@ int capture_tc(struct __sk_buff *ctx)
|
|||||||
|
|
||||||
/* Copie via bpf_skb_load_bytes avec tailles constantes en cascade.
|
/* Copie via bpf_skb_load_bytes avec tailles constantes en cascade.
|
||||||
* Kernel 4.18 ne supporte pas les tailles variables vers map values.
|
* Kernel 4.18 ne supporte pas les tailles variables vers map values.
|
||||||
* On essaie 512 puis 256 puis 128 pour capturer SNI et extensions.
|
* On essaie 1024 puis 512 puis 256 pour capturer SNI et extensions.
|
||||||
* La taille réellement copiée est stockée dans payload_len. */
|
* La taille réellement copiée est stockée dans payload_len. */
|
||||||
if (payload_off + 512 <= pkt_len) {
|
if (payload_off + 1024 <= pkt_len) {
|
||||||
|
bpf_skb_load_bytes(ctx, payload_off, tls_evt, 1024);
|
||||||
|
tls_evt->payload_len = 1024;
|
||||||
|
} else if (payload_off + 512 <= pkt_len) {
|
||||||
bpf_skb_load_bytes(ctx, payload_off, tls_evt, 512);
|
bpf_skb_load_bytes(ctx, payload_off, tls_evt, 512);
|
||||||
tls_evt->payload_len = 512;
|
tls_evt->payload_len = 512;
|
||||||
} else if (payload_off + 256 <= pkt_len) {
|
} else if (payload_off + 256 <= pkt_len) {
|
||||||
bpf_skb_load_bytes(ctx, payload_off, tls_evt, 256);
|
bpf_skb_load_bytes(ctx, payload_off, tls_evt, 256);
|
||||||
tls_evt->payload_len = 256;
|
tls_evt->payload_len = 256;
|
||||||
} else if (payload_off + 128 <= pkt_len) {
|
|
||||||
bpf_skb_load_bytes(ctx, payload_off, tls_evt, 128);
|
|
||||||
tls_evt->payload_len = 128;
|
|
||||||
} else {
|
} else {
|
||||||
return TC_ACT_OK;
|
return TC_ACT_OK;
|
||||||
}
|
}
|
||||||
@ -281,16 +282,16 @@ int capture_tc(struct __sk_buff *ctx)
|
|||||||
h_evt->timestamp_ns = bpf_ktime_get_ns();
|
h_evt->timestamp_ns = bpf_ktime_get_ns();
|
||||||
|
|
||||||
/* Copie via bpf_skb_load_bytes avec tailles constantes en cascade.
|
/* Copie via bpf_skb_load_bytes avec tailles constantes en cascade.
|
||||||
* Les requêtes HTTP sont souvent < 256 octets, on descend à 128 puis 64. */
|
* Les requêtes HTTP sont souvent < 512 octets, on descend à 256 puis 128. */
|
||||||
if (payload_off + 256 <= pkt_len) {
|
if (payload_off + 512 <= pkt_len) {
|
||||||
|
bpf_skb_load_bytes(ctx, payload_off, h_evt, 512);
|
||||||
|
h_evt->payload_len = 512;
|
||||||
|
} else if (payload_off + 256 <= pkt_len) {
|
||||||
bpf_skb_load_bytes(ctx, payload_off, h_evt, 256);
|
bpf_skb_load_bytes(ctx, payload_off, h_evt, 256);
|
||||||
h_evt->payload_len = 256;
|
h_evt->payload_len = 256;
|
||||||
} else if (payload_off + 128 <= pkt_len) {
|
} else if (payload_off + 128 <= pkt_len) {
|
||||||
bpf_skb_load_bytes(ctx, payload_off, h_evt, 128);
|
bpf_skb_load_bytes(ctx, payload_off, h_evt, 128);
|
||||||
h_evt->payload_len = 128;
|
h_evt->payload_len = 128;
|
||||||
} else if (payload_off + 64 <= pkt_len) {
|
|
||||||
bpf_skb_load_bytes(ctx, payload_off, h_evt, 64);
|
|
||||||
h_evt->payload_len = 64;
|
|
||||||
} else {
|
} else {
|
||||||
return TC_ACT_OK;
|
return TC_ACT_OK;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,10 @@ import (
|
|||||||
// Durée de vie d'une entrée : 5 secondes (suffisant pour une requête HTTP).
|
// Durée de vie d'une entrée : 5 secondes (suffisant pour une requête HTTP).
|
||||||
var fdCache = procutil.NewFDCache(5 * time.Second)
|
var fdCache = procutil.NewFDCache(5 * time.Second)
|
||||||
|
|
||||||
|
// acceptCache maps {tgid, fd} → SessionKey depuis les événements accept4.
|
||||||
|
// Prioritaire sur fdCache car source de vérité (tracepoint kernel).
|
||||||
|
var acceptCache = correlation.NewAcceptCache(10 * time.Second)
|
||||||
|
|
||||||
// Config décrit la configuration complète du démon ja4ebpf.
|
// Config décrit la configuration complète du démon ja4ebpf.
|
||||||
// Chargée depuis un fichier YAML et enrichie par les variables d'environnement
|
// Chargée depuis un fichier YAML et enrichie par les variables d'environnement
|
||||||
// avec le préfixe JA4EBPF_.
|
// avec le préfixe JA4EBPF_.
|
||||||
@ -64,7 +68,7 @@ func loadConfig(path string) (*Config, error) {
|
|||||||
cfg.ClickHouse.DSN = "clickhouse://default:@localhost:9000/ja4_logs"
|
cfg.ClickHouse.DSN = "clickhouse://default:@localhost:9000/ja4_logs"
|
||||||
cfg.ClickHouse.BatchSize = 500
|
cfg.ClickHouse.BatchSize = 500
|
||||||
cfg.ClickHouse.FlushSecs = 1
|
cfg.ClickHouse.FlushSecs = 1
|
||||||
cfg.Correlation.TimeoutMS = 500
|
cfg.Correlation.TimeoutMS = 5000
|
||||||
cfg.Correlation.SlowlorisMS = 10000
|
cfg.Correlation.SlowlorisMS = 10000
|
||||||
cfg.Log.Level = "info"
|
cfg.Log.Level = "info"
|
||||||
cfg.Log.Format = "json"
|
cfg.Log.Format = "json"
|
||||||
@ -303,8 +307,10 @@ func consumeSynEvents(ctx context.Context, rd *perf.Reader, mgr *correlation.Man
|
|||||||
|
|
||||||
// struct tcp_syn_event (packed):
|
// struct tcp_syn_event (packed):
|
||||||
// src_ip(4)+dst_ip(4)+src_port(2)+dst_port(2)+ttl(1)+df_bit(1)+ip_id(2)+
|
// src_ip(4)+dst_ip(4)+src_port(2)+dst_port(2)+ttl(1)+df_bit(1)+ip_id(2)+
|
||||||
// window_size(2)+window_scale(1)+mss(2)+tcp_options_raw[40]+tcp_options_len(1)+timestamp_ns(8)
|
// ip_total_length(2)+window_size(2)+window_scale(1)+mss(2)+tcp_options_raw[40]+
|
||||||
// offsets: 0 4 8 10 12 13 14 16 18 19 21 61 62
|
// tcp_options_len(1)+timestamp_ns(8)
|
||||||
|
// offsets: 0 4 8 10 12 13 14 16 18 19 21 61
|
||||||
|
// total = 62 + 8 = 70
|
||||||
if len(record.RawSample) < 70 {
|
if len(record.RawSample) < 70 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -329,11 +335,12 @@ func consumeSynEvents(ctx context.Context, rd *perf.Reader, mgr *correlation.Man
|
|||||||
dstIP[2] = byte(dstIPRaw >> 8)
|
dstIP[2] = byte(dstIPRaw >> 8)
|
||||||
dstIP[3] = byte(dstIPRaw)
|
dstIP[3] = byte(dstIPRaw)
|
||||||
|
|
||||||
// Champs IP/TCP aux offsets corrects (dst_ip occupe les octets 4-7)
|
// Champs IP/TCP aux offsets corrects (ip_total_length inséré entre ip_id et window_size)
|
||||||
ttl := data[12]
|
ttl := data[12]
|
||||||
dfBit := data[13] != 0
|
dfBit := data[13] != 0
|
||||||
ipID := binary.LittleEndian.Uint16(data[14:16])
|
ipID := binary.LittleEndian.Uint16(data[14:16])
|
||||||
windowSize := binary.LittleEndian.Uint16(data[16:18])
|
ipTotalLength := binary.LittleEndian.Uint16(data[16:18])
|
||||||
|
windowSize := binary.LittleEndian.Uint16(data[18:20])
|
||||||
|
|
||||||
optLen := int(data[61])
|
optLen := int(data[61])
|
||||||
if optLen > 40 {
|
if optLen > 40 {
|
||||||
@ -347,16 +354,17 @@ func consumeSynEvents(ctx context.Context, rd *perf.Reader, mgr *correlation.Man
|
|||||||
|
|
||||||
mgr.Update(key, func(s *correlation.SessionState) {
|
mgr.Update(key, func(s *correlation.SessionState) {
|
||||||
s.L3L4 = &correlation.L3L4{
|
s.L3L4 = &correlation.L3L4{
|
||||||
DstIP: dstIP,
|
DstIP: dstIP,
|
||||||
DstPort: dstPort,
|
DstPort: dstPort,
|
||||||
TTL: ttl,
|
TTL: ttl,
|
||||||
DFBit: dfBit,
|
DFBit: dfBit,
|
||||||
IPID: ipID,
|
IPID: ipID,
|
||||||
WindowSize: windowSize,
|
IPTotalLength: ipTotalLength,
|
||||||
WindowScale: windowScale,
|
WindowSize: windowSize,
|
||||||
MSS: mss,
|
WindowScale: windowScale,
|
||||||
TCPOptionsRaw: tcpOpts,
|
MSS: mss,
|
||||||
SYNTimestamp: time.Now(),
|
TCPOptionsRaw: tcpOpts,
|
||||||
|
SYNTimestamp: time.Now(),
|
||||||
}
|
}
|
||||||
// Si TLS est déjà présent (arrivé avant SYN), les deux couches sont disponibles.
|
// Si TLS est déjà présent (arrivé avant SYN), les deux couches sont disponibles.
|
||||||
if s.TLS != nil {
|
if s.TLS != nil {
|
||||||
@ -491,11 +499,12 @@ func consumeSSLEvents(ctx context.Context, rd *perf.Reader, mgr *correlation.Man
|
|||||||
srcIPRaw := binary.LittleEndian.Uint32(data[12:16])
|
srcIPRaw := binary.LittleEndian.Uint32(data[12:16])
|
||||||
srcPort := binary.LittleEndian.Uint16(data[16:18])
|
srcPort := binary.LittleEndian.Uint16(data[16:18])
|
||||||
|
|
||||||
// data[4096] commence à offset 18, data_len à offset 4114
|
// data[4096] commence à offset 18, data_len à offset 4114, direction à offset 4118
|
||||||
if len(data) < 4118 {
|
if len(data) < 4119 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
dataLen := binary.LittleEndian.Uint32(data[4114:4118])
|
dataLen := binary.LittleEndian.Uint32(data[4114:4118])
|
||||||
|
direction := data[4118] // 0 = SSL_read (client→serveur), 1 = SSL_write (serveur→client)
|
||||||
if dataLen > 4096 {
|
if dataLen > 4096 {
|
||||||
dataLen = 4096
|
dataLen = 4096
|
||||||
}
|
}
|
||||||
@ -504,38 +513,103 @@ func consumeSSLEvents(ctx context.Context, rd *perf.Reader, mgr *correlation.Man
|
|||||||
}
|
}
|
||||||
sslData := data[18 : 18+dataLen]
|
sslData := data[18 : 18+dataLen]
|
||||||
|
|
||||||
// --- Fallback /proc quand accept4 n'a pas fourni l'IP ---
|
var key correlation.SessionKey
|
||||||
|
var dstIPFromAccept [4]byte
|
||||||
|
var dstPortFromAccept uint16
|
||||||
|
|
||||||
if srcIPRaw == 0 && fd != 0 {
|
if srcIPRaw == 0 && fd != 0 {
|
||||||
|
// ssl_conn_map non peuplé : chercher la clé de session via le cache accept4
|
||||||
tgid := uint32(pidTgid >> 32)
|
tgid := uint32(pidTgid >> 32)
|
||||||
if tgid == 0 {
|
if tgid == 0 {
|
||||||
tgid = uint32(pidTgid) // fallback: utiliser le TID si TGID=0
|
tgid = uint32(pidTgid)
|
||||||
}
|
}
|
||||||
if ip, port, lookupErr := fdCache.Lookup(tgid, fd); lookupErr == nil {
|
|
||||||
ipv4 := ip.To4()
|
// Priorité 1 : cache accept4 (source de vérité — tracepoint kernel)
|
||||||
if ipv4 != nil {
|
if skey, dstIP, dstPort, ok := acceptCache.Lookup(tgid, fd); ok {
|
||||||
srcIPRaw = uint32(ipv4[0])<<24 | uint32(ipv4[1])<<16 | uint32(ipv4[2])<<8 | uint32(ipv4[3])
|
key = skey
|
||||||
srcPort = port
|
dstIPFromAccept = dstIP
|
||||||
|
dstPortFromAccept = dstPort
|
||||||
|
} else {
|
||||||
|
// Priorité 2 : fallback /proc (moins fiable — port parfois erroné)
|
||||||
|
if ip, port, lookupErr := fdCache.Lookup(tgid, fd); lookupErr == nil {
|
||||||
|
ipv4 := ip.To4()
|
||||||
|
if ipv4 != nil {
|
||||||
|
key.SrcIP[0] = ipv4[0]
|
||||||
|
key.SrcIP[1] = ipv4[1]
|
||||||
|
key.SrcIP[2] = ipv4[2]
|
||||||
|
key.SrcIP[3] = ipv4[3]
|
||||||
|
key.SrcPort = port
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
key.SrcIP[0] = byte(srcIPRaw >> 24)
|
||||||
|
key.SrcIP[1] = byte(srcIPRaw >> 16)
|
||||||
|
key.SrcIP[2] = byte(srcIPRaw >> 8)
|
||||||
|
key.SrcIP[3] = byte(srcIPRaw)
|
||||||
|
key.SrcPort = srcPort
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignorer les événements sans IP identifiable (ex: connexions locales non HTTP)
|
// Ignorer les événements sans IP identifiable (ex: connexions locales non HTTP)
|
||||||
if srcIPRaw == 0 && srcPort == 0 {
|
if key.SrcIP == [4]byte{} && key.SrcPort == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var key correlation.SessionKey
|
|
||||||
key.SrcIP[0] = byte(srcIPRaw >> 24)
|
|
||||||
key.SrcIP[1] = byte(srcIPRaw >> 16)
|
|
||||||
key.SrcIP[2] = byte(srcIPRaw >> 8)
|
|
||||||
key.SrcIP[3] = byte(srcIPRaw)
|
|
||||||
key.SrcPort = srcPort
|
|
||||||
|
|
||||||
counter.Add(1)
|
counter.Add(1)
|
||||||
// === Routeur Magic Bytes ===
|
|
||||||
|
// === Routeur par direction ===
|
||||||
|
// direction=0 (SSL_read) : requêtes du client
|
||||||
|
// direction=1 (SSL_write) : réponses du serveur
|
||||||
|
|
||||||
|
if direction == 1 {
|
||||||
|
// === Serveur → Client : réponses HTTP ===
|
||||||
|
|
||||||
|
// HTTP/1.x response
|
||||||
|
if parser.IsHTTP1Response(sslData) {
|
||||||
|
resp := parser.ParseHTTP1Response(sslData)
|
||||||
|
if resp == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mgr.Update(key, func(s *correlation.SessionState) {
|
||||||
|
if len(s.Requests) > 0 {
|
||||||
|
last := &s.Requests[len(s.Requests)-1]
|
||||||
|
if last.StatusCode == 0 {
|
||||||
|
last.StatusCode = resp.StatusCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP/2 server HEADERS frame (contient :status)
|
||||||
|
if parser.IsH2FrameHeader(sslData) {
|
||||||
|
h2kv := parser.ExtractH2HeaderKV(sslData)
|
||||||
|
if statusCode, ok := h2kv[":status"]; ok {
|
||||||
|
mgr.Update(key, func(s *correlation.SessionState) {
|
||||||
|
if len(s.Requests) > 0 {
|
||||||
|
last := &s.Requests[len(s.Requests)-1]
|
||||||
|
if last.StatusCode == 0 {
|
||||||
|
// Conversion du code de statut H2 (ex: "200" → 200)
|
||||||
|
code := 0
|
||||||
|
for _, c := range statusCode {
|
||||||
|
if c >= '0' && c <= '9' {
|
||||||
|
code = code*10 + int(c-'0')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if code >= 100 && code <= 599 {
|
||||||
|
last.StatusCode = code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Client → Serveur : requêtes HTTP (direction=0) ===
|
||||||
|
|
||||||
if parser.DetectH2Preface(sslData) {
|
if parser.DetectH2Preface(sslData) {
|
||||||
// HTTP/2 : extraire les paramètres SETTINGS depuis la préface
|
// HTTP/2 : extraire les paramètres SETTINGS et en-têtes depuis la préface
|
||||||
afterPreface := sslData
|
afterPreface := sslData
|
||||||
if len(afterPreface) > parser.H2MagicPrefaceLen() {
|
if len(afterPreface) > parser.H2MagicPrefaceLen() {
|
||||||
afterPreface = sslData[parser.H2MagicPrefaceLen():]
|
afterPreface = sslData[parser.H2MagicPrefaceLen():]
|
||||||
@ -557,19 +631,86 @@ func consumeSSLEvents(ctx context.Context, rd *perf.Reader, mgr *correlation.Man
|
|||||||
MaxFrameSize: h2settings.MaxFrameSize,
|
MaxFrameSize: h2settings.MaxFrameSize,
|
||||||
MaxHeaderListSize: h2settings.MaxHeaderListSize,
|
MaxHeaderListSize: h2settings.MaxHeaderListSize,
|
||||||
UnknownSettings: h2settings.UnknownSettings,
|
UnknownSettings: h2settings.UnknownSettings,
|
||||||
|
EnableConnectProtocol: h2settings.EnableConnectProtocol,
|
||||||
WindowUpdateIncrement: h2settings.WindowUpdateIncrement,
|
WindowUpdateIncrement: h2settings.WindowUpdateIncrement,
|
||||||
PseudoHeaderOrder: h2settings.PseudoHeaderOrder,
|
PseudoHeaderOrder: h2settings.PseudoHeaderOrder,
|
||||||
}
|
}
|
||||||
|
// Extraire les en-têtes H2 (User-Agent, Accept, etc.)
|
||||||
|
if len(h2settings.HeaderKV) > 0 {
|
||||||
|
req.HeaderKV = h2settings.HeaderKV
|
||||||
|
req.HeaderOrder = h2settings.HeaderOrder
|
||||||
|
req.HeaderOrderSig = strings.Join(h2settings.HeaderOrder, ";")
|
||||||
|
if h2settings.HeaderKV[":method"] != "" {
|
||||||
|
req.Method = h2settings.HeaderKV[":method"]
|
||||||
|
}
|
||||||
|
if h2settings.HeaderKV[":path"] != "" {
|
||||||
|
p := h2settings.HeaderKV[":path"]
|
||||||
|
if idx := strings.Index(p, "?"); idx >= 0 {
|
||||||
|
req.Path = p[:idx]
|
||||||
|
req.QueryString = p[idx+1:]
|
||||||
|
} else {
|
||||||
|
req.Path = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if h2settings.HeaderKV[":authority"] != "" {
|
||||||
|
req.Host = h2settings.HeaderKV[":authority"]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if len(s.Requests) == 0 {
|
if len(s.Requests) == 0 {
|
||||||
req.HTTPVersion = "HTTP/2"
|
req.HTTPVersion = "HTTP/2"
|
||||||
s.Requests = append(s.Requests, req)
|
s.Requests = append(s.Requests, req)
|
||||||
}
|
}
|
||||||
|
// Si la session n'a pas de L3L4 (pas de SYN capturé),
|
||||||
|
// peupler dst_ip/dst_port depuis le cache accept4
|
||||||
|
if s.L3L4 == nil && (dstIPFromAccept != [4]byte{} || dstPortFromAccept != 0) {
|
||||||
|
s.L3L4 = &correlation.L3L4{
|
||||||
|
DstIP: dstIPFromAccept,
|
||||||
|
DstPort: dstPortFromAccept,
|
||||||
|
}
|
||||||
|
}
|
||||||
_ = s.TLS // corrélation implicite
|
_ = s.TLS // corrélation implicite
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HTTP/2 frames seules (sans préface — SSL_read ultérieurs)
|
||||||
|
if parser.IsH2FrameHeader(sslData) {
|
||||||
|
h2kv := parser.ExtractH2HeaderKV(sslData)
|
||||||
|
if len(h2kv) > 0 {
|
||||||
|
mgr.Update(key, func(s *correlation.SessionState) {
|
||||||
|
if len(s.Requests) > 0 {
|
||||||
|
last := &s.Requests[len(s.Requests)-1]
|
||||||
|
if last.HeaderKV == nil {
|
||||||
|
last.HeaderKV = make(map[string]string)
|
||||||
|
}
|
||||||
|
for k, v := range h2kv {
|
||||||
|
if _, exists := last.HeaderKV[k]; !exists {
|
||||||
|
last.HeaderKV[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Mettre à jour method/path/host si pas encore remplis
|
||||||
|
if last.Method == "" && h2kv[":method"] != "" {
|
||||||
|
last.Method = h2kv[":method"]
|
||||||
|
}
|
||||||
|
if last.Path == "" && h2kv[":path"] != "" {
|
||||||
|
p := h2kv[":path"]
|
||||||
|
if idx := strings.Index(p, "?"); idx >= 0 {
|
||||||
|
last.Path = p[:idx]
|
||||||
|
last.QueryString = p[idx+1:]
|
||||||
|
} else {
|
||||||
|
last.Path = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if last.Host == "" && h2kv[":authority"] != "" {
|
||||||
|
last.Host = h2kv[":authority"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if parser.IsHTTP1Request(sslData) {
|
if parser.IsHTTP1Request(sslData) {
|
||||||
// HTTP/1.x : parser la requête
|
// HTTP/1.x : parser la requête
|
||||||
req := parser.ParseHTTP1Request(sslData)
|
req := parser.ParseHTTP1Request(sslData)
|
||||||
@ -588,27 +729,20 @@ func consumeSSLEvents(ctx context.Context, rd *perf.Reader, mgr *correlation.Man
|
|||||||
HeaderKV: req.HeaderKV,
|
HeaderKV: req.HeaderKV,
|
||||||
HTTPVersion: req.Protocol,
|
HTTPVersion: req.Protocol,
|
||||||
})
|
})
|
||||||
|
// Si la session n'a pas de L3L4 (pas de SYN capturé),
|
||||||
|
// peupler dst_ip/dst_port depuis le cache accept4
|
||||||
|
if s.L3L4 == nil && (dstIPFromAccept != [4]byte{} || dstPortFromAccept != 0) {
|
||||||
|
s.L3L4 = &correlation.L3L4{
|
||||||
|
DstIP: dstIPFromAccept,
|
||||||
|
DstPort: dstPortFromAccept,
|
||||||
|
}
|
||||||
|
}
|
||||||
_ = s.TLS // corrélation implicite
|
_ = s.TLS // corrélation implicite
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if parser.IsHTTP1Response(sslData) {
|
// Les réponses HTTP sont maintenant traitées dans le bloc direction=1 ci-dessus
|
||||||
// Réponse HTTP/1.x : extraire le code de statut
|
|
||||||
resp := parser.ParseHTTP1Response(sslData)
|
|
||||||
if resp == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
mgr.Update(key, func(s *correlation.SessionState) {
|
|
||||||
// Mettre à jour le code de statut de la dernière requête
|
|
||||||
if len(s.Requests) > 0 {
|
|
||||||
last := &s.Requests[len(s.Requests)-1]
|
|
||||||
if last.StatusCode == 0 {
|
|
||||||
last.StatusCode = resp.StatusCode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -636,6 +770,8 @@ func consumeAcceptEvents(ctx context.Context, rd *perf.Reader, mgr *correlation.
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pidTgid := binary.LittleEndian.Uint64(data[0:8])
|
||||||
|
fd := binary.LittleEndian.Uint32(data[8:12])
|
||||||
srcIPRaw := binary.LittleEndian.Uint32(data[12:16])
|
srcIPRaw := binary.LittleEndian.Uint32(data[12:16])
|
||||||
srcPort := binary.LittleEndian.Uint16(data[16:18])
|
srcPort := binary.LittleEndian.Uint16(data[16:18])
|
||||||
|
|
||||||
@ -651,6 +787,10 @@ func consumeAcceptEvents(ctx context.Context, rd *perf.Reader, mgr *correlation.
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Peupler le cache accept4 pour corrélation SSL
|
||||||
|
tgid := uint32(pidTgid >> 32)
|
||||||
|
acceptCache.Store(tgid, fd, key, [4]byte{}, 0)
|
||||||
|
|
||||||
// S'assurer que la session existe
|
// S'assurer que la session existe
|
||||||
mgr.GetOrCreate(key)
|
mgr.GetOrCreate(key)
|
||||||
counter.Add(1)
|
counter.Add(1)
|
||||||
|
|||||||
@ -15,16 +15,17 @@ type SessionKey struct {
|
|||||||
|
|
||||||
// L3L4 contient les caractéristiques réseau et transport de la connexion.
|
// L3L4 contient les caractéristiques réseau et transport de la connexion.
|
||||||
type L3L4 struct {
|
type L3L4 struct {
|
||||||
DstIP [4]byte // adresse IP destination
|
DstIP [4]byte // adresse IP destination
|
||||||
DstPort uint16 // port destination
|
DstPort uint16 // port destination
|
||||||
TTL uint8 // TTL IP observé dans le SYN
|
TTL uint8 // TTL IP observé dans le SYN
|
||||||
DFBit bool // bit Don't Fragment actif
|
DFBit bool // bit Don't Fragment actif
|
||||||
IPID uint16 // champ identification IP
|
IPID uint16 // champ identification IP
|
||||||
WindowSize uint16 // taille de fenêtre TCP initiale
|
IPTotalLength uint16 // longueur totale IP (octets)
|
||||||
WindowScale uint8 // facteur d'échelle de fenêtre (0xFF = absent)
|
WindowSize uint16 // taille de fenêtre TCP initiale
|
||||||
MSS uint16 // Maximum Segment Size (0 = absent)
|
WindowScale uint8 // facteur d'échelle de fenêtre (0xFF = absent)
|
||||||
TCPOptionsRaw []byte // options TCP brutes (max 40 octets)
|
MSS uint16 // Maximum Segment Size (0 = absent)
|
||||||
SYNTimestamp time.Time // horodatage du paquet SYN
|
TCPOptionsRaw []byte // options TCP brutes (max 40 octets)
|
||||||
|
SYNTimestamp time.Time // horodatage du paquet SYN
|
||||||
}
|
}
|
||||||
|
|
||||||
// TLSInfo contient les données extraites du ClientHello TLS.
|
// TLSInfo contient les données extraites du ClientHello TLS.
|
||||||
@ -43,15 +44,16 @@ type TLSInfo struct {
|
|||||||
|
|
||||||
// HTTP2Settings contient les paramètres SETTINGS et WINDOW_UPDATE du client HTTP/2.
|
// HTTP2Settings contient les paramètres SETTINGS et WINDOW_UPDATE du client HTTP/2.
|
||||||
type HTTP2Settings struct {
|
type HTTP2Settings struct {
|
||||||
HeaderTableSize int32 // SETTINGS_HEADER_TABLE_SIZE (-1 si absent)
|
HeaderTableSize int32 // SETTINGS_HEADER_TABLE_SIZE (-1 si absent)
|
||||||
EnablePush int32 // SETTINGS_ENABLE_PUSH
|
EnablePush int32 // SETTINGS_ENABLE_PUSH
|
||||||
MaxConcurrentStreams int32 // SETTINGS_MAX_CONCURRENT_STREAMS
|
MaxConcurrentStreams int32 // SETTINGS_MAX_CONCURRENT_STREAMS
|
||||||
InitialWindowSize int32 // SETTINGS_INITIAL_WINDOW_SIZE
|
InitialWindowSize int32 // SETTINGS_INITIAL_WINDOW_SIZE
|
||||||
MaxFrameSize int32 // SETTINGS_MAX_FRAME_SIZE
|
MaxFrameSize int32 // SETTINGS_MAX_FRAME_SIZE
|
||||||
MaxHeaderListSize int32 // SETTINGS_MAX_HEADER_LIST_SIZE
|
MaxHeaderListSize int32 // SETTINGS_MAX_HEADER_LIST_SIZE
|
||||||
UnknownSettings int32 // paramètre 0x7 (JA4H2)
|
UnknownSettings int32 // paramètre 0x7 (JA4H2)
|
||||||
WindowUpdateIncrement uint32 // valeur WINDOW_UPDATE sur stream 0
|
EnableConnectProtocol int32 // SETTINGS_ENABLE_CONNECT_PROTOCOL (0x8, RFC 8441)
|
||||||
PseudoHeaderOrder []string // ordre des pseudo-headers [:method, :authority, ...]
|
WindowUpdateIncrement uint32 // valeur WINDOW_UPDATE sur stream 0
|
||||||
|
PseudoHeaderOrder []string // ordre des pseudo-headers [:method, :authority, ...]
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPRequest représente une requête HTTP observée dans la session.
|
// HTTPRequest représente une requête HTTP observée dans la session.
|
||||||
|
|||||||
@ -46,12 +46,15 @@ type sessionRecord struct {
|
|||||||
TCPMetaOptions string `json:"tcp_meta_options,omitempty"`
|
TCPMetaOptions string `json:"tcp_meta_options,omitempty"`
|
||||||
|
|
||||||
// TLS (noms attendus par le MV)
|
// TLS (noms attendus par le MV)
|
||||||
JA4Hash string `json:"ja4,omitempty"`
|
JA4Hash string `json:"ja4,omitempty"`
|
||||||
JA3Raw string `json:"ja3,omitempty"`
|
JA3Raw string `json:"ja3,omitempty"`
|
||||||
JA3Hash string `json:"ja3_hash,omitempty"`
|
JA3Hash string `json:"ja3_hash,omitempty"`
|
||||||
TLSSNI string `json:"tls_sni,omitempty"`
|
TLSSNI string `json:"tls_sni,omitempty"`
|
||||||
TLSALPN string `json:"tls_alpn,omitempty"`
|
TLSALPN string `json:"tls_alpn,omitempty"`
|
||||||
TLSVersion string `json:"tls_version,omitempty"`
|
TLSVersion string `json:"tls_version,omitempty"`
|
||||||
|
TLSCipherSuites string `json:"tls_cipher_suites,omitempty"`
|
||||||
|
TLSExtensions string `json:"tls_extensions,omitempty"`
|
||||||
|
TLSSupportedGroups string `json:"tls_supported_groups,omitempty"`
|
||||||
|
|
||||||
// HTTP
|
// HTTP
|
||||||
Method string `json:"method,omitempty"`
|
Method string `json:"method,omitempty"`
|
||||||
@ -82,18 +85,22 @@ type sessionRecord struct {
|
|||||||
HeaderSecFetchSite string `json:"header_Sec-Fetch-Site,omitempty"`
|
HeaderSecFetchSite string `json:"header_Sec-Fetch-Site,omitempty"`
|
||||||
|
|
||||||
// HTTP/2 fingerprinting passif
|
// HTTP/2 fingerprinting passif
|
||||||
H2Fingerprint string `json:"h2_fingerprint,omitempty"`
|
H2Fingerprint string `json:"h2_fingerprint,omitempty"`
|
||||||
H2SettingsFP string `json:"h2_settings_fp,omitempty"`
|
H2SettingsFP string `json:"h2_settings_fp,omitempty"`
|
||||||
H2WindowUpdate uint32 `json:"h2_window_update,omitempty"`
|
H2WindowUpdate uint32 `json:"h2_window_update,omitempty"`
|
||||||
H2PseudoOrder string `json:"h2_pseudo_order,omitempty"`
|
H2PseudoOrder string `json:"h2_pseudo_order,omitempty"`
|
||||||
H2HasPriority uint8 `json:"h2_has_priority,omitempty"`
|
H2HasPriority uint8 `json:"h2_has_priority,omitempty"`
|
||||||
H2HeaderTableSize *int32 `json:"h2_header_table_size,omitempty"`
|
H2HeaderTableSize *int32 `json:"h2_header_table_size,omitempty"`
|
||||||
H2EnablePush *int32 `json:"h2_enable_push,omitempty"`
|
H2EnablePush *int32 `json:"h2_enable_push,omitempty"`
|
||||||
H2MaxConcurrentStreams *int32 `json:"h2_max_concurrent_streams,omitempty"`
|
H2MaxConcurrentStreams *int32 `json:"h2_max_concurrent_streams,omitempty"`
|
||||||
H2InitialWindowSize *int64 `json:"h2_initial_window_size,omitempty"`
|
H2InitialWindowSize *int64 `json:"h2_initial_window_size,omitempty"`
|
||||||
H2MaxFrameSize *int32 `json:"h2_max_frame_size,omitempty"`
|
H2MaxFrameSize *int32 `json:"h2_max_frame_size,omitempty"`
|
||||||
H2MaxHeaderListSize *int32 `json:"h2_max_header_list_size,omitempty"`
|
H2MaxHeaderListSize *int32 `json:"h2_max_header_list_size,omitempty"`
|
||||||
H2EnableConnectProtocol *int32 `json:"h2_enable_connect_protocol,omitempty"`
|
H2EnableConnectProtocol *int32 `json:"h2_enable_connect_protocol,omitempty"`
|
||||||
|
|
||||||
|
// Champs de corrélation
|
||||||
|
ClientHeaders string `json:"client_headers,omitempty"`
|
||||||
|
SynToClientHelloMs *int32 `json:"syn_to_clienthello_ms,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClickHouseWriter crée un writer et établit la connexion ClickHouse.
|
// NewClickHouseWriter crée un writer et établit la connexion ClickHouse.
|
||||||
@ -222,6 +229,11 @@ func sessionToRecord(s *correlation.SessionState) sessionRecord {
|
|||||||
KeepAlives: len(s.Requests),
|
KeepAlives: len(s.Requests),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback dst_port pour sessions TLS sans L3L4 : 443 pour HTTPS
|
||||||
|
if s.L3L4 == nil && s.TLS != nil {
|
||||||
|
rec.DstPort = 443
|
||||||
|
}
|
||||||
|
|
||||||
// Champs métadonnées IP/TCP
|
// Champs métadonnées IP/TCP
|
||||||
if s.L3L4 != nil {
|
if s.L3L4 != nil {
|
||||||
rec.DstIP = fmt.Sprintf("%d.%d.%d.%d",
|
rec.DstIP = fmt.Sprintf("%d.%d.%d.%d",
|
||||||
@ -230,6 +242,9 @@ func sessionToRecord(s *correlation.SessionState) sessionRecord {
|
|||||||
rec.IPMetaDF = &s.L3L4.DFBit
|
rec.IPMetaDF = &s.L3L4.DFBit
|
||||||
rec.IPMetaID = &s.L3L4.IPID
|
rec.IPMetaID = &s.L3L4.IPID
|
||||||
rec.IPMetaTTL = &s.L3L4.TTL
|
rec.IPMetaTTL = &s.L3L4.TTL
|
||||||
|
if s.L3L4.IPTotalLength > 0 {
|
||||||
|
rec.IPMetaTotalLength = &s.L3L4.IPTotalLength
|
||||||
|
}
|
||||||
rec.TCPMetaWindowSize = &s.L3L4.WindowSize
|
rec.TCPMetaWindowSize = &s.L3L4.WindowSize
|
||||||
// WindowScale 0xFF = absent (convention C), ne pas inclure
|
// WindowScale 0xFF = absent (convention C), ne pas inclure
|
||||||
if s.L3L4.WindowScale != 0xFF {
|
if s.L3L4.WindowScale != 0xFF {
|
||||||
@ -239,6 +254,10 @@ func sessionToRecord(s *correlation.SessionState) sessionRecord {
|
|||||||
if s.L3L4.MSS > 0 {
|
if s.L3L4.MSS > 0 {
|
||||||
rec.TCPMetaMSS = &s.L3L4.MSS
|
rec.TCPMetaMSS = &s.L3L4.MSS
|
||||||
}
|
}
|
||||||
|
// Options TCP : noms abrégés (MSS, WS, SACK, TS, etc.)
|
||||||
|
if len(s.L3L4.TCPOptionsRaw) > 0 {
|
||||||
|
rec.TCPMetaOptions = formatTCPOptions(s.L3L4.TCPOptionsRaw)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Champs TLS
|
// Champs TLS
|
||||||
@ -249,6 +268,25 @@ func sessionToRecord(s *correlation.SessionState) sessionRecord {
|
|||||||
rec.TLSSNI = s.TLS.SNI
|
rec.TLSSNI = s.TLS.SNI
|
||||||
rec.TLSALPN = strings.Join(s.TLS.ALPN, ",")
|
rec.TLSALPN = strings.Join(s.TLS.ALPN, ",")
|
||||||
rec.TLSVersion = formatTLSVersion(s.TLS.TLSVersion)
|
rec.TLSVersion = formatTLSVersion(s.TLS.TLSVersion)
|
||||||
|
// Cipher suites : liste hex séparée par des tirets
|
||||||
|
if len(s.TLS.CipherSuites) > 0 {
|
||||||
|
parts := make([]string, len(s.TLS.CipherSuites))
|
||||||
|
for i, cs := range s.TLS.CipherSuites {
|
||||||
|
parts[i] = fmt.Sprintf("%04x", cs)
|
||||||
|
}
|
||||||
|
rec.TLSCipherSuites = strings.Join(parts, "-")
|
||||||
|
}
|
||||||
|
// Extensions : liste d'IDs hex séparés par des tirets
|
||||||
|
if len(s.TLS.Extensions) > 0 {
|
||||||
|
parts := make([]string, len(s.TLS.Extensions))
|
||||||
|
for i, ext := range s.TLS.Extensions {
|
||||||
|
parts[i] = fmt.Sprintf("%04x", ext)
|
||||||
|
}
|
||||||
|
rec.TLSExtensions = strings.Join(parts, "-")
|
||||||
|
}
|
||||||
|
// Supported groups : liste hex séparée par des tirets
|
||||||
|
// (disponible via TLSInfo.Extensions — extraction depuis ClientHelloRaw si nécessaire)
|
||||||
|
_ = s.TLS.ClientHelloRaw // réservé pour extraction future
|
||||||
// Fallback : si pas de Host HTTP, utiliser TLS SNI
|
// Fallback : si pas de Host HTTP, utiliser TLS SNI
|
||||||
if rec.Host == "" && s.TLS.SNI != "" {
|
if rec.Host == "" && s.TLS.SNI != "" {
|
||||||
rec.Host = s.TLS.SNI
|
rec.Host = s.TLS.SNI
|
||||||
@ -259,6 +297,12 @@ func sessionToRecord(s *correlation.SessionState) sessionRecord {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// syn_to_clienthello_ms : délai entre SYN et ClientHello
|
||||||
|
if s.L3L4 != nil && s.TLS != nil && !s.L3L4.SYNTimestamp.IsZero() && !s.TLS.Timestamp.IsZero() {
|
||||||
|
delta := int32(s.TLS.Timestamp.Sub(s.L3L4.SYNTimestamp).Milliseconds())
|
||||||
|
rec.SynToClientHelloMs = &delta
|
||||||
|
}
|
||||||
|
|
||||||
// Champs HTTP (dernière requête)
|
// Champs HTTP (dernière requête)
|
||||||
if len(s.Requests) > 0 {
|
if len(s.Requests) > 0 {
|
||||||
last := &s.Requests[len(s.Requests)-1]
|
last := &s.Requests[len(s.Requests)-1]
|
||||||
@ -275,22 +319,25 @@ func sessionToRecord(s *correlation.SessionState) sessionRecord {
|
|||||||
rec.DurationMS = &last.DurationMS
|
rec.DurationMS = &last.DurationMS
|
||||||
rec.HeaderOrderSig = last.HeaderOrderSig
|
rec.HeaderOrderSig = last.HeaderOrderSig
|
||||||
|
|
||||||
// En-têtes HTTP individuels
|
// En-têtes HTTP individuels (HTTP/1.1: clés Title-Case, HTTP/2: clés lowercase)
|
||||||
if last.HeaderKV != nil {
|
if last.HeaderKV != nil {
|
||||||
rec.HeaderUserAgent = last.HeaderKV["User-Agent"]
|
rec.HeaderUserAgent = headerVal(last.HeaderKV, "User-Agent", "user-agent")
|
||||||
rec.HeaderAccept = last.HeaderKV["Accept"]
|
rec.HeaderAccept = headerVal(last.HeaderKV, "Accept", "accept")
|
||||||
rec.HeaderAcceptEnc = last.HeaderKV["Accept-Encoding"]
|
rec.HeaderAcceptEnc = headerVal(last.HeaderKV, "Accept-Encoding", "accept-encoding")
|
||||||
rec.HeaderAcceptLang = last.HeaderKV["Accept-Language"]
|
rec.HeaderAcceptLang = headerVal(last.HeaderKV, "Accept-Language", "accept-language")
|
||||||
rec.HeaderContentType = last.HeaderKV["Content-Type"]
|
rec.HeaderContentType = headerVal(last.HeaderKV, "Content-Type", "content-type")
|
||||||
rec.HeaderXReqID = last.HeaderKV["X-Request-Id"]
|
rec.HeaderXReqID = headerVal(last.HeaderKV, "X-Request-Id", "x-request-id")
|
||||||
rec.HeaderXTraceID = last.HeaderKV["X-Trace-Id"]
|
rec.HeaderXTraceID = headerVal(last.HeaderKV, "X-Trace-Id", "x-trace-id")
|
||||||
rec.HeaderXForwarded = last.HeaderKV["X-Forwarded-For"]
|
rec.HeaderXForwarded = headerVal(last.HeaderKV, "X-Forwarded-For", "x-forwarded-for")
|
||||||
rec.HeaderSecCHUA = last.HeaderKV["Sec-CH-UA"]
|
rec.HeaderSecCHUA = headerVal(last.HeaderKV, "Sec-CH-UA", "sec-ch-ua")
|
||||||
rec.HeaderSecCHUAMobile = last.HeaderKV["Sec-CH-UA-Mobile"]
|
rec.HeaderSecCHUAMobile = headerVal(last.HeaderKV, "Sec-CH-UA-Mobile", "sec-ch-ua-mobile")
|
||||||
rec.HeaderSecCHUAPlat = last.HeaderKV["Sec-CH-UA-Platform"]
|
rec.HeaderSecCHUAPlat = headerVal(last.HeaderKV, "Sec-CH-UA-Platform", "sec-ch-ua-platform")
|
||||||
rec.HeaderSecFetchDest = last.HeaderKV["Sec-Fetch-Dest"]
|
rec.HeaderSecFetchDest = headerVal(last.HeaderKV, "Sec-Fetch-Dest", "sec-fetch-dest")
|
||||||
rec.HeaderSecFetchMode = last.HeaderKV["Sec-Fetch-Mode"]
|
rec.HeaderSecFetchMode = headerVal(last.HeaderKV, "Sec-Fetch-Mode", "sec-fetch-mode")
|
||||||
rec.HeaderSecFetchSite = last.HeaderKV["Sec-Fetch-Site"]
|
rec.HeaderSecFetchSite = headerVal(last.HeaderKV, "Sec-Fetch-Site", "sec-fetch-site")
|
||||||
|
|
||||||
|
// client_headers : JSON des en-têtes capturés
|
||||||
|
rec.ClientHeaders = buildClientHeaders(last.HeaderKV)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construire headers_raw : ordre des noms joints par ";"
|
// Construire headers_raw : ordre des noms joints par ";"
|
||||||
@ -316,6 +363,7 @@ func sessionToRecord(s *correlation.SessionState) sessionRecord {
|
|||||||
rec.H2InitialWindowSize = &h2InitWin
|
rec.H2InitialWindowSize = &h2InitWin
|
||||||
rec.H2MaxFrameSize = &h2.MaxFrameSize
|
rec.H2MaxFrameSize = &h2.MaxFrameSize
|
||||||
rec.H2MaxHeaderListSize = &h2.MaxHeaderListSize
|
rec.H2MaxHeaderListSize = &h2.MaxHeaderListSize
|
||||||
|
rec.H2EnableConnectProtocol = &h2.EnableConnectProtocol
|
||||||
|
|
||||||
// Fingerprints composites Akamai
|
// Fingerprints composites Akamai
|
||||||
rec.H2Fingerprint = buildH2Fingerprint(h2)
|
rec.H2Fingerprint = buildH2Fingerprint(h2)
|
||||||
@ -423,3 +471,83 @@ func formatTLSVersion(v uint16) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// headerVal cherche un en-tête dans le map avec deux clés possibles :
|
||||||
|
// HTTP/1.1 utilise Title-Case (ex: "User-Agent"), HTTP/2 utilise lowercase (ex: "user-agent").
|
||||||
|
func headerVal(kv map[string]string, titleKey, lowerKey string) string {
|
||||||
|
if v := kv[titleKey]; v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return kv[lowerKey]
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildClientHeaders sérialise les en-têtes capturés en JSON pour la colonne client_headers.
|
||||||
|
func buildClientHeaders(kv map[string]string) string {
|
||||||
|
if len(kv) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// Sérialiser en JSON trié par clé pour un résultat déterministe
|
||||||
|
b, err := json.Marshal(kv)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatTCPOptions convertit les options TCP brutes en chaîne lisible.
|
||||||
|
// Noms abrégés : MSS=2, WS=3, SACK=4, TS=8, etc.
|
||||||
|
func formatTCPOptions(opts []byte) string {
|
||||||
|
if len(opts) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var names []string
|
||||||
|
i := 0
|
||||||
|
for i < len(opts) {
|
||||||
|
kind := opts[i]
|
||||||
|
switch kind {
|
||||||
|
case 0: // End of Options List
|
||||||
|
break
|
||||||
|
case 1: // NOP
|
||||||
|
names = append(names, "NOP")
|
||||||
|
i++
|
||||||
|
case 2: // MSS
|
||||||
|
names = append(names, "MSS")
|
||||||
|
if i+3 < len(opts) {
|
||||||
|
i += 4
|
||||||
|
} else {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
case 3: // Window Scale
|
||||||
|
names = append(names, "WS")
|
||||||
|
if i+2 < len(opts) {
|
||||||
|
i += 3
|
||||||
|
} else {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
case 4: // SACK Permitted
|
||||||
|
names = append(names, "SACK")
|
||||||
|
i += 2
|
||||||
|
case 5: // SACK
|
||||||
|
names = append(names, "SACKb")
|
||||||
|
if i+1 < len(opts) {
|
||||||
|
i += int(opts[i+1])
|
||||||
|
} else {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
case 8: // Timestamp
|
||||||
|
names = append(names, "TS")
|
||||||
|
if i+9 < len(opts) {
|
||||||
|
i += 10
|
||||||
|
} else {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if i+1 < len(opts) && int(opts[i+1]) > 0 {
|
||||||
|
i += int(opts[i+1])
|
||||||
|
} else {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(names, ",")
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user