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
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:
@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user