fix: correction race conditions et amélioration robustesse
Some checks failed
Build RPM Package / Build RPM Packages (CentOS 7, Rocky 8/9/10) (push) Has been cancelled

- Correction race condition dans tlsparse avec mutex par ConnectionFlow
- Fix fuite mémoire buffer HelloBuffer
- Ajout rotation de fichiers logs (100MB, 3 backups)
- Implémentation queue asynchrone avec reconnexion exponentielle (socket UNIX)
- Validation BPF (caractères, longueur, parenthèses)
- Augmentation snapLen pcap de 1600 à 65535 bytes
- Permissions fichiers sécurisées (0600)
- Ajout 46 tests unitaires (capture, output, logging)
- Passage go test -race sans erreur

Tests: go test -race ./... ✓
Build: go build ./... ✓
Lint: go vet ./... ✓

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
Jacquin Antoine
2026-02-28 21:15:45 +01:00
parent d14d6d6bf0
commit fec500ba46
9 changed files with 1127 additions and 510 deletions

View File

@ -4,6 +4,7 @@ package tlsparse
import (
"encoding/binary"
"fmt"
"strings"
"sync"
"time"
@ -25,9 +26,20 @@ const (
JA4_DONE
)
// Parser configuration constants
const (
// DefaultMaxTrackedFlows is the maximum number of concurrent flows to track
DefaultMaxTrackedFlows = 50000
// DefaultMaxHelloBufferBytes is the maximum buffer size for fragmented ClientHello
DefaultMaxHelloBufferBytes = 256 * 1024 // 256 KiB
// DefaultCleanupInterval is the interval between cleanup runs
DefaultCleanupInterval = 10 * time.Second
)
// ConnectionFlow tracks a single TCP flow for TLS handshake extraction
// Only tracks incoming traffic from client to the local machine
type ConnectionFlow struct {
mu sync.Mutex // Protects all fields below
State ConnectionState
CreatedAt time.Time
LastSeen time.Time
@ -64,8 +76,8 @@ func NewParserWithTimeout(timeout time.Duration) *ParserImpl {
flowTimeout: timeout,
cleanupDone: make(chan struct{}),
cleanupClose: make(chan struct{}),
maxTrackedFlows: 50000,
maxHelloBufferBytes: 256 * 1024, // 256 KiB
maxTrackedFlows: DefaultMaxTrackedFlows,
maxHelloBufferBytes: DefaultMaxHelloBufferBytes,
}
go p.cleanupLoop()
return p
@ -79,7 +91,7 @@ func flowKey(srcIP string, srcPort uint16, dstIP string, dstPort uint16) string
// cleanupLoop periodically removes expired flows
func (p *ParserImpl) cleanupLoop() {
ticker := time.NewTicker(10 * time.Second)
ticker := time.NewTicker(DefaultCleanupInterval)
defer ticker.Stop()
for {
@ -100,7 +112,10 @@ func (p *ParserImpl) cleanupExpiredFlows() {
now := time.Now()
for key, flow := range p.flows {
if flow.State == JA4_DONE || now.Sub(flow.LastSeen) > p.flowTimeout {
flow.mu.Lock()
shouldDelete := flow.State == JA4_DONE || now.Sub(flow.LastSeen) > p.flowTimeout
flow.mu.Unlock()
if shouldDelete {
delete(p.flows, key)
}
}
@ -170,24 +185,27 @@ func (p *ParserImpl) Process(pkt api.RawPacket) (*api.TLSClientHello, error) {
key := flowKey(srcIP, srcPort, dstIP, dstPort)
// Check if flow exists before acquiring write lock
p.mu.RLock()
_, flowExists := p.flows[key]
flow, flowExists := p.flows[key]
p.mu.RUnlock()
// Early exit for non-ClientHello first packet
if !flowExists && payload[0] != 22 {
return nil, nil
}
flow := p.getOrCreateFlow(key, srcIP, srcPort, dstIP, dstPort, ipMeta, tcpMeta)
flow = p.getOrCreateFlow(key, srcIP, srcPort, dstIP, dstPort, ipMeta, tcpMeta)
if flow == nil {
return nil, nil
}
// Lock the flow for the entire processing to avoid race conditions
flow.mu.Lock()
defer flow.mu.Unlock()
// Check if flow is already done
p.mu.RLock()
state := flow.State
p.mu.RUnlock()
if state == JA4_DONE {
if flow.State == JA4_DONE {
return nil, nil // Already processed this flow
}
@ -199,10 +217,8 @@ func (p *ParserImpl) Process(pkt api.RawPacket) (*api.TLSClientHello, error) {
if clientHello != nil {
// Found ClientHello, mark flow as done
p.mu.Lock()
flow.State = JA4_DONE
flow.HelloBuffer = clientHello
p.mu.Unlock()
return &api.TLSClientHello{
SrcIP: srcIP,
@ -216,18 +232,21 @@ func (p *ParserImpl) Process(pkt api.RawPacket) (*api.TLSClientHello, error) {
}
// Check for fragmented ClientHello (accumulate segments)
if state == WAIT_CLIENT_HELLO || state == NEW {
p.mu.Lock()
if flow.State == WAIT_CLIENT_HELLO || flow.State == NEW {
if len(flow.HelloBuffer)+len(payload) > p.maxHelloBufferBytes {
// Buffer would exceed limit, drop this flow
p.mu.Lock()
delete(p.flows, key)
p.mu.Unlock()
return nil, nil
}
flow.State = WAIT_CLIENT_HELLO
flow.HelloBuffer = append(flow.HelloBuffer, payload...)
flow.LastSeen = time.Now()
// Make a copy of the buffer for parsing (outside the lock)
bufferCopy := make([]byte, len(flow.HelloBuffer))
copy(bufferCopy, flow.HelloBuffer)
p.mu.Unlock()
// Try to parse accumulated buffer
clientHello, err := parseClientHello(bufferCopy)
@ -236,9 +255,7 @@ func (p *ParserImpl) Process(pkt api.RawPacket) (*api.TLSClientHello, error) {
}
if clientHello != nil {
// Complete ClientHello found
p.mu.Lock()
flow.State = JA4_DONE
p.mu.Unlock()
return &api.TLSClientHello{
SrcIP: srcIP,
@ -262,7 +279,9 @@ func (p *ParserImpl) getOrCreateFlow(key string, srcIP string, srcPort uint16, d
defer p.mu.Unlock()
if flow, exists := p.flows[key]; exists {
flow.mu.Lock()
flow.LastSeen = time.Now()
flow.mu.Unlock()
return flow
}
@ -319,7 +338,7 @@ func extractIPMeta(ipLayer gopacket.Layer) api.IPMeta {
func extractTCPMeta(tcp *layers.TCP) api.TCPMeta {
meta := api.TCPMeta{
WindowSize: tcp.Window,
Options: make([]string, 0),
Options: make([]string, 0, len(tcp.Options)),
}
// Parse TCP options
@ -421,3 +440,12 @@ func IsClientHello(payload []byte) bool {
// ClientHello type
return handshakePayload[0] == 1
}
// Helper function to join string slice with separator (kept for backward compatibility)
// Deprecated: Use strings.Join instead
func joinStringSlice(slice []string, sep string) string {
if len(slice) == 0 {
return ""
}
return strings.Join(slice, sep)
}