fix(ja4ebpf): correct BPF struct byte offsets and regenerate SSL_write programs

Fix two critical offset bugs introduced when ip_total_length was added to
tcp_syn_event: tcp_options_raw offset 21→23 and tcp_options_len offset 61→63,
plus minimum size check 70→72. Fix ssl_data_event direction field offset from
4118 (inside timestamp_ns) to 4126. Simplify attachSSLWrite to use generated
objects directly instead of dynamic spec loading. Regenerate BPF objects with
SSL_write uprobe programs included.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jacquin Antoine
2026-04-15 14:06:28 +02:00
parent 24306ef390
commit 65d833bb18
3 changed files with 32 additions and 52 deletions

View File

@ -309,9 +309,9 @@ func consumeSynEvents(ctx context.Context, rd *perf.Reader, mgr *correlation.Man
// src_ip(4)+dst_ip(4)+src_port(2)+dst_port(2)+ttl(1)+df_bit(1)+ip_id(2)+
// ip_total_length(2)+window_size(2)+window_scale(1)+mss(2)+tcp_options_raw[40]+
// 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 {
// offsets: 0 4 8 10 12 13 14 16 18 20 21 23 63
// total = 64 + 8 = 72
if len(record.RawSample) < 72 {
continue
}
data := record.RawSample
@ -342,12 +342,12 @@ func consumeSynEvents(ctx context.Context, rd *perf.Reader, mgr *correlation.Man
ipTotalLength := binary.LittleEndian.Uint16(data[16:18])
windowSize := binary.LittleEndian.Uint16(data[18:20])
optLen := int(data[61])
optLen := int(data[63])
if optLen > 40 {
optLen = 40
}
tcpOpts := make([]byte, optLen)
copy(tcpOpts, data[21:21+optLen])
copy(tcpOpts, data[23:23+optLen])
// Analyser les options TCP brutes pour extraire MSS et Window Scale
mss, windowScale := parseTCPOptions(tcpOpts)
@ -499,12 +499,13 @@ func consumeSSLEvents(ctx context.Context, rd *perf.Reader, mgr *correlation.Man
srcIPRaw := binary.LittleEndian.Uint32(data[12:16])
srcPort := binary.LittleEndian.Uint16(data[16:18])
// data[4096] commence à offset 18, data_len à offset 4114, direction à offset 4118
if len(data) < 4119 {
// data[4096] commence à offset 18, data_len à offset 4114,
// timestamp_ns à offset 4118, direction à offset 4126
if len(data) < 4127 {
continue
}
dataLen := binary.LittleEndian.Uint32(data[4114:4118])
direction := data[4118] // 0 = SSL_read (client→serveur), 1 = SSL_write (serveur→client)
direction := data[4126] // 0 = SSL_read (client→serveur), 1 = SSL_write (serveur→client)
if dataLen > 4096 {
dataLen = 4096
}

View File

@ -111,7 +111,9 @@ type Ja4SslProgramSpecs struct {
KretprobeAccept4Exit *ebpf.ProgramSpec `ebpf:"kretprobe_accept4_exit"`
UprobeSslReadEntry *ebpf.ProgramSpec `ebpf:"uprobe_ssl_read_entry"`
UprobeSslSetFd *ebpf.ProgramSpec `ebpf:"uprobe_ssl_set_fd"`
UprobeSslWriteEntry *ebpf.ProgramSpec `ebpf:"uprobe_ssl_write_entry"`
UretprobeSslReadExit *ebpf.ProgramSpec `ebpf:"uretprobe_ssl_read_exit"`
UretprobeSslWriteExit *ebpf.ProgramSpec `ebpf:"uretprobe_ssl_write_exit"`
}
// Ja4SslMapSpecs contains maps before they are loaded into the kernel.
@ -193,7 +195,9 @@ type Ja4SslPrograms struct {
KretprobeAccept4Exit *ebpf.Program `ebpf:"kretprobe_accept4_exit"`
UprobeSslReadEntry *ebpf.Program `ebpf:"uprobe_ssl_read_entry"`
UprobeSslSetFd *ebpf.Program `ebpf:"uprobe_ssl_set_fd"`
UprobeSslWriteEntry *ebpf.Program `ebpf:"uprobe_ssl_write_entry"`
UretprobeSslReadExit *ebpf.Program `ebpf:"uretprobe_ssl_read_exit"`
UretprobeSslWriteExit *ebpf.Program `ebpf:"uretprobe_ssl_write_exit"`
}
func (p *Ja4SslPrograms) Close() error {
@ -202,7 +206,9 @@ func (p *Ja4SslPrograms) Close() error {
p.KretprobeAccept4Exit,
p.UprobeSslReadEntry,
p.UprobeSslSetFd,
p.UprobeSslWriteEntry,
p.UretprobeSslReadExit,
p.UretprobeSslWriteExit,
)
}

View File

@ -266,10 +266,9 @@ func (l *Loader) AttachUprobes(sslLibPath string) error {
l.uprobeLinks = append(l.uprobeLinks, readExitLink)
// SSL_write — capture les réponses HTTP du serveur (direction=1)
// Les programmes BPF uprobe/SSL_write et uretprobe/SSL_write sont
// chargés depuis les objets Ja4Ssl. Si les objets BPF n'ont pas été
// régénérés (pas de clang sur le host), ces programmes sont absents.
_ = l.attachSSLWrite(ex)
if err := l.attachSSLWrite(ex); err != nil {
return fmt.Errorf("attachement SSL_write: %w", err)
}
return nil
}
@ -293,43 +292,17 @@ func (l *Loader) AttachAcceptProbe() error {
return nil
}
// attachSSLWrite tente d'attacher les uprobes SSL_write pour capturer
// les réponses HTTP du serveur. Si les programmes BPF SSL_write ne sont
// pas disponibles (objets non régénérés), retourne nil sans bloquer.
// attachSSLWrite attache les uprobes SSL_write pour capturer
// les réponses HTTP du serveur (direction=1).
func (l *Loader) attachSSLWrite(ex *link.Executable) error {
// Charger la collection spec embarquée pour vérifier si SSL_write existe
spec, err := LoadJa4Ssl()
entryLink, err := ex.Uprobe("SSL_write", l.sslObjs.UprobeSslWriteEntry, nil)
if err != nil {
return nil
}
entrySpec, hasEntry := spec.Programs["uprobe_ssl_write_entry"]
exitSpec, hasExit := spec.Programs["uretprobe_ssl_write_exit"]
if !hasEntry || !hasExit {
return nil // programmes SSL_write absents — BPF non régénéré
}
writeEntry, err := ebpf.NewProgram(entrySpec)
if err != nil {
return nil
}
writeExit, err := ebpf.NewProgram(exitSpec)
if err != nil {
writeEntry.Close()
return nil
}
entryLink, err := ex.Uprobe("SSL_write", writeEntry, nil)
if err != nil {
writeEntry.Close()
writeExit.Close()
return fmt.Errorf("attachement uprobe SSL_write (entry): %w", err)
}
l.uprobeLinks = append(l.uprobeLinks, entryLink)
exitLink, err := ex.Uretprobe("SSL_write", writeExit, nil)
exitLink, err := ex.Uretprobe("SSL_write", l.sslObjs.UretprobeSslWriteExit, nil)
if err != nil {
writeExit.Close()
return fmt.Errorf("attachement uretprobe SSL_write (exit): %w", err)
}
l.uprobeLinks = append(l.uprobeLinks, exitLink)