feat(ja4ebpf): add dst_ip/dst_port to TLS and HTTP plain events for complete L3/L4

Add dst_ip and dst_port fields to tls_hello_event BPF struct and populate
them in tc_capture.c. Update Go TLS event handler with new byte offsets
(payload[2048]+src_ip(4)+dst_ip(4)+src_port(2)+dst_port(2)+payload_len(2)+
timestamp_ns(8) = 2070 bytes). Read dst_ip/dst_port from HTTP plain events
and use them to populate L3L4 when SYN was not captured, ensuring dst_ip
and dst_port are always available in ClickHouse for both TLS and HTTP sessions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jacquin Antoine
2026-04-15 14:31:46 +02:00
parent 65d833bb18
commit 0975d40609
5 changed files with 44 additions and 10 deletions

View File

@ -54,7 +54,9 @@ struct tcp_syn_event {
struct tls_hello_event {
__u8 payload[2048]; /* payload ClientHello brut (offset 0) */
__u32 src_ip; /* adresse source (host byte order) */
__u32 dst_ip; /* adresse destination (host byte order) */
__u16 src_port; /* port source (host byte order) */
__u16 dst_port; /* port destination (host byte order) */
__u16 payload_len; /* longueur effective du payload */
__u64 timestamp_ns; /* horodatage kernel */
} __attribute__((packed));

View File

@ -209,12 +209,16 @@ int capture_tc(struct __sk_buff *ctx)
return TC_ACT_OK;
tls_evt->src_ip = 0;
tls_evt->dst_ip = 0;
tls_evt->src_port = 0;
tls_evt->dst_port = 0;
tls_evt->payload_len = 0;
tls_evt->timestamp_ns = 0;
tls_evt->src_ip = bpf_ntohl(src_ip);
tls_evt->dst_ip = bpf_ntohl(dst_ip);
tls_evt->src_port = src_port;
tls_evt->dst_port = dst_port;
tls_evt->timestamp_ns = bpf_ktime_get_ns();
/* Copie via bpf_skb_load_bytes avec tailles constantes en cascade.

View File

@ -394,16 +394,18 @@ func consumeTLSEvents(ctx context.Context, rd *perf.Reader, mgr *correlation.Man
}
// struct tls_hello_event (packed):
// src_ip(4) + src_port(2) + payload[2048] + payload_len(2) + timestamp_ns(8)
// offsets: 0 4 6 2054 2056
if len(record.RawSample) < 2064 {
// payload[2048] + src_ip(4) + dst_ip(4) + src_port(2) + dst_port(2) + payload_len(2) + timestamp_ns(8)
// offsets: 0 2048 2052 2056 2058 2060 2062
if len(record.RawSample) < 2070 {
continue
}
data := record.RawSample
srcIPRaw := binary.LittleEndian.Uint32(data[2048:2052])
srcPort := binary.LittleEndian.Uint16(data[2052:2054])
payloadLen := binary.LittleEndian.Uint16(data[2054:2056])
dstIPRaw := binary.LittleEndian.Uint32(data[2052:2056])
srcPort := binary.LittleEndian.Uint16(data[2056:2058])
dstPort := binary.LittleEndian.Uint16(data[2058:2060])
payloadLen := binary.LittleEndian.Uint16(data[2060:2062])
if int(payloadLen) > 2048 {
payloadLen = 2048
@ -418,6 +420,12 @@ func consumeTLSEvents(ctx context.Context, rd *perf.Reader, mgr *correlation.Man
key.SrcIP[3] = byte(srcIPRaw)
key.SrcPort = srcPort
var tlsDstIP [4]byte
tlsDstIP[0] = byte(dstIPRaw >> 24)
tlsDstIP[1] = byte(dstIPRaw >> 16)
tlsDstIP[2] = byte(dstIPRaw >> 8)
tlsDstIP[3] = byte(dstIPRaw)
// Parser le ClientHello et calculer JA4
ch, err := parser.ParseClientHello(payload)
if err != nil {
@ -460,9 +468,12 @@ func consumeTLSEvents(ctx context.Context, rd *perf.Reader, mgr *correlation.Man
TLSVersion: tlsVer,
Timestamp: time.Now(),
}
// Corréler si L3/L4 est déjà présent
if s.L3L4 != nil {
_ = s.L3L4 // corrélation implicite par présence des deux champs
// Peupler L3/L4 si absent (SYN non capturé, TLS arrivé en premier)
if s.L3L4 == nil && dstIPRaw != 0 {
s.L3L4 = &correlation.L3L4{
DstIP: tlsDstIP,
DstPort: dstPort,
}
}
})
counter.Add(1)
@ -825,7 +836,9 @@ func consumeHTTPPlainEvents(ctx context.Context, rd *perf.Reader, mgr *correlati
// src_ip et src_port en host byte order (bpf_ntohl appliqué dans tc_capture.c)
srcIPRaw := binary.LittleEndian.Uint32(data[4096:4100])
dstIPRaw := binary.LittleEndian.Uint32(data[4100:4104])
srcPort := binary.LittleEndian.Uint16(data[4104:4106])
dstPort := binary.LittleEndian.Uint16(data[4106:4108])
if srcIPRaw == 0 && srcPort == 0 {
continue
@ -838,6 +851,12 @@ func consumeHTTPPlainEvents(ctx context.Context, rd *perf.Reader, mgr *correlati
key.SrcIP[3] = byte(srcIPRaw)
key.SrcPort = srcPort
var httpDstIP [4]byte
httpDstIP[0] = byte(dstIPRaw >> 24)
httpDstIP[1] = byte(dstIPRaw >> 16)
httpDstIP[2] = byte(dstIPRaw >> 8)
httpDstIP[3] = byte(dstIPRaw)
// Extraire le payload HTTP
if len(data) < 4110 {
continue
@ -872,8 +891,13 @@ func consumeHTTPPlainEvents(ctx context.Context, rd *perf.Reader, mgr *correlati
HeaderKV: req.HeaderKV,
HTTPVersion: req.Protocol,
})
// Corréler si L3/L4 est déjà présent (TCP SYN capturé)
_ = s.L3L4 // corrélation implicite
// Peupler L3/L4 si absent (SYN non capturé)
if s.L3L4 == nil && dstIPRaw != 0 {
s.L3L4 = &correlation.L3L4{
DstIP: httpDstIP,
DstPort: dstPort,
}
}
})
counter.Add(1)
}

View File

@ -61,7 +61,9 @@ type Ja4SslSslReadArgs struct {
type Ja4SslTlsHelloEvent struct {
Payload [2048]uint8
SrcIp uint32
DstIp uint32
SrcPort uint16
DstPort uint16
PayloadLen uint16
TimestampNs uint64
}

View File

@ -61,7 +61,9 @@ type Ja4TcSslReadArgs struct {
type Ja4TcTlsHelloEvent struct {
Payload [2048]uint8
SrcIp uint32
DstIp uint32
SrcPort uint16
DstPort uint16
PayloadLen uint16
TimestampNs uint64
}