feat(ja4ebpf): add multi-interface TC, LPM_TRIE ignore_src, unit tests, and fix bugs

- Add multi-interface TC attachment (default "any" = all UP interfaces)
- Add BPF LPM_TRIE map ignored_src for kernel-side CIDR filtering
- Add userspace ignore_src filtering for SSL/accept4 path via net.IPNet.Contains()
- Add AcceptCache for fd→SessionKey correlation with TTL and Close()
- Add 5 test files covering writer, procutil, dispatcher, accept_cache, and cmd
- Fix formatTCPOptions infinite loop on EOL (case 0 break→return)
- Fix pseudoOrderToShort panic on empty slice (negative cap)
- Fix AcceptCache goroutine leak (add done channel + Close())
- Update config.yml.example with interfaces, listen_ports, ignore_src
- Rewrite docs/services/ja4ebpf.md (was massively stale: XDP, RingBuffer, etc.)
- Fix stale XDP/RingBuffer references in docs/architecture.md, thesis, tls.go

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jacquin Antoine
2026-04-16 01:49:26 +02:00
parent fd84aebc44
commit f0c8fe81c6
20 changed files with 3053 additions and 1261 deletions

View File

@ -90,6 +90,7 @@ type sessionRecord struct {
H2WindowUpdate uint32 `json:"h2_window_update,omitempty"`
H2PseudoOrder string `json:"h2_pseudo_order,omitempty"`
H2HasPriority uint8 `json:"h2_has_priority,omitempty"`
H2SettingsAck uint8 `json:"h2_settings_ack,omitempty"`
H2HeaderTableSize *int32 `json:"h2_header_table_size,omitempty"`
H2EnablePush *int32 `json:"h2_enable_push,omitempty"`
H2MaxConcurrentStreams *int32 `json:"h2_max_concurrent_streams,omitempty"`
@ -110,6 +111,11 @@ func NewClickHouseWriter(dsn string, batchSize int, flushInterval time.Duration)
return nil, fmt.Errorf("analyse DSN ClickHouse: %w", err)
}
// Désactiver l'insertion asynchrone pour les writes transactionnels
opts.Settings = map[string]interface{}{
"async_insert": 0,
}
conn, err := clickhouse.Open(opts)
if err != nil {
return nil, fmt.Errorf("connexion ClickHouse: %w", err)
@ -201,6 +207,10 @@ func (w *ClickHouseWriter) flushBatch(ctx context.Context, batch []*correlation.
}
for _, s := range batch {
// Ignorer les sessions sans aucune donnée applicative (SYN-only)
if len(s.Requests) == 0 && s.TLS == nil {
continue
}
record := sessionToRecord(s)
jsonBytes, err := json.Marshal(record)
if err != nil {
@ -371,12 +381,29 @@ func sessionToRecord(s *correlation.SessionState) sessionRecord {
}
}
// Champs HTTP/2 au niveau connexion (H2ConnState)
if s.H2Conn != nil {
if s.H2Conn.SettingsAck {
rec.H2SettingsAck = 1
}
// Vérifier si un stream a reçu une frame PRIORITY
for _, stream := range s.H2Conn.Streams {
if stream.Priority != nil {
rec.H2HasPriority = 1
break
}
}
}
return rec
}
// pseudoOrderToShort convertit la liste de pseudo-headers en notation abrégée.
// Ex: [":method", ":authority", ":scheme", ":path"] → "m,a,s,p"
func pseudoOrderToShort(headers []string) string {
if len(headers) == 0 {
return ""
}
short := make([]byte, 0, len(headers)*2-1)
for i, h := range headers {
if i > 0 {
@ -391,6 +418,8 @@ func pseudoOrderToShort(headers []string) string {
short = append(short, 's')
case h == ":path":
short = append(short, 'p')
case h == ":status":
short = append(short, 't')
default:
short = append(short, '?')
}
@ -506,7 +535,7 @@ func formatTCPOptions(opts []byte) string {
kind := opts[i]
switch kind {
case 0: // End of Options List
break
return strings.Join(names, ",")
case 1: // NOP
names = append(names, "NOP")
i++