fix: renforcer limites TLS, timeouts socket et validation config
Some checks failed
Build RPM Package / Build RPM Packages (CentOS 7, Rocky 8/9/10) (push) Has been cancelled

Co-authored-by: aider (openrouter/openai/gpt-5.3-codex) <aider@aider.chat>
This commit is contained in:
Jacquin Antoine
2026-02-28 20:01:39 +01:00
parent b15c20b4cc
commit c7e8fe874f
7 changed files with 618 additions and 64 deletions

View File

@ -42,12 +42,14 @@ type ConnectionFlow struct {
// ParserImpl implements the api.Parser interface for TLS parsing
type ParserImpl struct {
mu sync.RWMutex
flows map[string]*ConnectionFlow
flowTimeout time.Duration
cleanupDone chan struct{}
cleanupClose chan struct{}
closeOnce sync.Once
mu sync.RWMutex
flows map[string]*ConnectionFlow
flowTimeout time.Duration
cleanupDone chan struct{}
cleanupClose chan struct{}
closeOnce sync.Once
maxTrackedFlows int
maxHelloBufferBytes int
}
// NewParser creates a new TLS parser with connection state tracking
@ -58,10 +60,12 @@ func NewParser() *ParserImpl {
// NewParserWithTimeout creates a new TLS parser with a custom flow timeout
func NewParserWithTimeout(timeout time.Duration) *ParserImpl {
p := &ParserImpl{
flows: make(map[string]*ConnectionFlow),
flowTimeout: timeout,
cleanupDone: make(chan struct{}),
cleanupClose: make(chan struct{}),
flows: make(map[string]*ConnectionFlow),
flowTimeout: timeout,
cleanupDone: make(chan struct{}),
cleanupClose: make(chan struct{}),
maxTrackedFlows: 50000,
maxHelloBufferBytes: 256 * 1024, // 256 KiB
}
go p.cleanupLoop()
return p
@ -164,15 +168,26 @@ func (p *ParserImpl) Process(pkt api.RawPacket) (*api.TLSClientHello, error) {
return nil, nil // No payload
}
// Get or create connection flow
key := flowKey(srcIP, srcPort, dstIP, dstPort)
p.mu.RLock()
_, flowExists := p.flows[key]
p.mu.RUnlock()
if !flowExists && payload[0] != 22 {
return nil, nil
}
flow := p.getOrCreateFlow(key, srcIP, srcPort, dstIP, dstPort, ipMeta, tcpMeta)
if flow == nil {
return nil, nil
}
// Check if flow is already done
p.mu.RLock()
isDone := flow.State == JA4_DONE
state := flow.State
p.mu.RUnlock()
if isDone {
if state == JA4_DONE {
return nil, nil // Already processed this flow
}
@ -201,8 +216,13 @@ func (p *ParserImpl) Process(pkt api.RawPacket) (*api.TLSClientHello, error) {
}
// Check for fragmented ClientHello (accumulate segments)
if flow.State == WAIT_CLIENT_HELLO || flow.State == NEW {
if state == WAIT_CLIENT_HELLO || state == NEW {
p.mu.Lock()
if len(flow.HelloBuffer)+len(payload) > p.maxHelloBufferBytes {
delete(p.flows, key)
p.mu.Unlock()
return nil, nil
}
flow.State = WAIT_CLIENT_HELLO
flow.HelloBuffer = append(flow.HelloBuffer, payload...)
bufferCopy := make([]byte, len(flow.HelloBuffer))
@ -246,13 +266,17 @@ func (p *ParserImpl) getOrCreateFlow(key string, srcIP string, srcPort uint16, d
return flow
}
if len(p.flows) >= p.maxTrackedFlows {
return nil
}
flow := &ConnectionFlow{
State: NEW,
CreatedAt: time.Now(),
LastSeen: time.Now(),
SrcIP: srcIP, // Client IP
SrcIP: srcIP, // Client IP
SrcPort: srcPort, // Client port
DstIP: dstIP, // Server IP (local machine)
DstIP: dstIP, // Server IP (local machine)
DstPort: dstPort, // Server port (local machine)
IPMeta: ipMeta,
TCPMeta: tcpMeta,

View File

@ -1,8 +1,13 @@
package tlsparse
import (
"net"
"testing"
"time"
"ja4sentinel/api"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
)
@ -203,6 +208,8 @@ func createTLSServerHello(version uint16) []byte {
func TestNewParser(t *testing.T) {
parser := NewParser()
defer parser.Close()
if parser == nil {
t.Error("NewParser() returned nil")
}
@ -288,3 +295,152 @@ func TestExtractTCPMeta_MSSInvalid_NoPanic(t *testing.T) {
t.Fatalf("expected MSS_INVALID in options, got %v", meta.Options)
}
}
func TestGetOrCreateFlow_RespectsMaxTrackedFlows(t *testing.T) {
parser := NewParser()
defer parser.Close()
parser.maxTrackedFlows = 1
flow1 := parser.getOrCreateFlow(
flowKey("192.168.1.1", 12345, "10.0.0.1", 443),
"192.168.1.1", 12345, "10.0.0.1", 443,
api.IPMeta{}, api.TCPMeta{},
)
if flow1 == nil {
t.Fatal("first flow should be created")
}
flow2 := parser.getOrCreateFlow(
flowKey("192.168.1.2", 12346, "10.0.0.1", 443),
"192.168.1.2", 12346, "10.0.0.1", 443,
api.IPMeta{}, api.TCPMeta{},
)
if flow2 != nil {
t.Fatal("second flow should be nil when maxTrackedFlows is reached")
}
}
func TestProcess_DropsWhenHelloBufferExceedsLimit(t *testing.T) {
parser := NewParserWithTimeout(30 * time.Second)
defer parser.Close()
parser.maxHelloBufferBytes = 10
srcIP := "192.168.1.10"
dstIP := "10.0.0.1"
srcPort := uint16(12345)
dstPort := uint16(443)
// TLS-like payload, but intentionally incomplete to trigger accumulation.
payloadChunk := []byte{0x16, 0x03, 0x03, 0x00, 0x20, 0x01} // len = 6
pkt1 := buildRawPacket(t, srcIP, dstIP, srcPort, dstPort, payloadChunk)
ch, err := parser.Process(pkt1)
if err != nil {
t.Fatalf("first Process() error = %v", err)
}
if ch != nil {
t.Fatal("first Process() should not return complete ClientHello")
}
key := flowKey(srcIP, srcPort, dstIP, dstPort)
parser.mu.RLock()
_, existsAfterFirst := parser.flows[key]
parser.mu.RUnlock()
if !existsAfterFirst {
t.Fatal("flow should exist after first chunk")
}
pkt2 := buildRawPacket(t, srcIP, dstIP, srcPort, dstPort, payloadChunk)
ch, err = parser.Process(pkt2)
if err != nil {
t.Fatalf("second Process() error = %v", err)
}
if ch != nil {
t.Fatal("second Process() should not return ClientHello")
}
parser.mu.RLock()
_, existsAfterSecond := parser.flows[key]
parser.mu.RUnlock()
if existsAfterSecond {
t.Fatal("flow should be removed when hello buffer exceeds maxHelloBufferBytes")
}
}
func TestProcess_NonTLSNewFlowNotTracked(t *testing.T) {
parser := NewParser()
defer parser.Close()
srcIP := "192.168.1.20"
dstIP := "10.0.0.2"
srcPort := uint16(23456)
dstPort := uint16(443)
// Non-TLS content type (not 22)
payload := []byte{0x17, 0x03, 0x03, 0x00, 0x05, 0x00}
pkt := buildRawPacket(t, srcIP, dstIP, srcPort, dstPort, payload)
ch, err := parser.Process(pkt)
if err != nil {
t.Fatalf("Process() error = %v", err)
}
if ch != nil {
t.Fatal("Process() should return nil for non-TLS new flow")
}
key := flowKey(srcIP, srcPort, dstIP, dstPort)
parser.mu.RLock()
_, exists := parser.flows[key]
parser.mu.RUnlock()
if exists {
t.Fatal("non-TLS new flow should not be tracked")
}
}
func buildRawPacket(t *testing.T, srcIP, dstIP string, srcPort, dstPort uint16, payload []byte) api.RawPacket {
t.Helper()
ip := &layers.IPv4{
Version: 4,
TTL: 64,
SrcIP: net.ParseIP(srcIP).To4(),
DstIP: net.ParseIP(dstIP).To4(),
Protocol: layers.IPProtocolTCP,
}
tcp := &layers.TCP{
SrcPort: layers.TCPPort(srcPort),
DstPort: layers.TCPPort(dstPort),
Seq: 1,
ACK: true,
Window: 65535,
}
if err := tcp.SetNetworkLayerForChecksum(ip); err != nil {
t.Fatalf("SetNetworkLayerForChecksum() error = %v", err)
}
eth := &layers.Ethernet{
SrcMAC: net.HardwareAddr{0x00, 0x11, 0x22, 0x33, 0x44, 0x55},
DstMAC: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff},
EthernetType: layers.EthernetTypeIPv4,
}
buf := gopacket.NewSerializeBuffer()
opts := gopacket.SerializeOptions{
FixLengths: true,
ComputeChecksums: true,
}
if err := gopacket.SerializeLayers(buf, opts, eth, ip, tcp, gopacket.Payload(payload)); err != nil {
t.Fatalf("SerializeLayers() error = %v", err)
}
return api.RawPacket{
Data: buf.Bytes(),
Timestamp: time.Now().UnixNano(),
}
}