feat: implémentation complète du pipeline JA4 + Docker + tests
Nouveaux modules: - cmd/ja4sentinel/main.go : point d'entrée avec pipeline capture→parse→fingerprint→output - internal/config/loader.go : chargement YAML + env (JA4SENTINEL_*) + validation - internal/tlsparse/parser.go : extraction ClientHello avec suivi d'état de flux (NEW/WAIT_CLIENT_HELLO/JA4_DONE) - internal/fingerprint/engine.go : génération JA4/JA3 via psanford/tlsfingerprint - internal/output/writers.go : StdoutWriter, FileWriter, UnixSocketWriter, MultiWriter Infrastructure: - Dockerfile (multi-stage), Dockerfile.dev, Dockerfile.test-server - Makefile (build, test, lint, docker-build-*) - docker-compose.test.yml pour tests d'intégration - README.md (276 lignes) avec architecture, config, exemples API (api/types.go): - Ajout Close() aux interfaces Capture et Parser - Ajout FlowTimeoutSec dans Config (défaut: 30s, env: JA4SENTINEL_FLOW_TIMEOUT) - ServiceLog: +Timestamp, +TraceID, +ConnID - LogRecord: champs flatten (ip_meta_*, tcp_meta_*, ja4*) - Helper NewLogRecord() pour conversion TLSClientHello+Fingerprints→LogRecord Architecture (architecture.yml): - Documentation module logging + interfaces LoggerFactory/Logger - Section service.systemd complète (unit, security, capabilities) - Section logging.strategy (JSON lines, champs, règles) - api.Config: +FlowTimeoutSec documenté Fixes/cleanup: - Suppression internal/api/types.go (consolidé dans api/types.go) - Correction imports logging (ja4sentinel/api) - .dockerignore / .gitignore - config.yml.example Tests: - Tous les modules ont leurs tests (*_test.go) - Tests unitaires : capture, config, fingerprint, output, tlsparse - Tests d'intégration via docker-compose.test.yml Build: - Binaires dans dist/ (make build → dist/ja4sentinel) - Docker runtime avec COPY --from=builder /app/dist/ Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
389
internal/tlsparse/parser.go
Normal file
389
internal/tlsparse/parser.go
Normal file
@ -0,0 +1,389 @@
|
||||
// Package tlsparse provides TLS ClientHello extraction from captured packets
|
||||
package tlsparse
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"ja4sentinel/api"
|
||||
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/layers"
|
||||
)
|
||||
|
||||
// ConnectionState represents the state of a TCP connection for TLS parsing
|
||||
type ConnectionState int
|
||||
|
||||
const (
|
||||
// NEW: Observed SYN from client on a monitored port
|
||||
NEW ConnectionState = iota
|
||||
// WAIT_CLIENT_HELLO: Accumulating segments for complete ClientHello
|
||||
WAIT_CLIENT_HELLO
|
||||
// JA4_DONE: JA4 computed and logged, stop tracking this flow
|
||||
JA4_DONE
|
||||
)
|
||||
|
||||
// ConnectionFlow tracks a single TCP flow for TLS handshake extraction
|
||||
type ConnectionFlow struct {
|
||||
State ConnectionState
|
||||
CreatedAt time.Time
|
||||
LastSeen time.Time
|
||||
SrcIP string
|
||||
SrcPort uint16
|
||||
DstIP string
|
||||
DstPort uint16
|
||||
IPMeta api.IPMeta
|
||||
TCPMeta api.TCPMeta
|
||||
HelloBuffer []byte
|
||||
}
|
||||
|
||||
// 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{}
|
||||
}
|
||||
|
||||
// NewParser creates a new TLS parser with connection state tracking
|
||||
func NewParser() *ParserImpl {
|
||||
return NewParserWithTimeout(30 * time.Second)
|
||||
}
|
||||
|
||||
// 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{}),
|
||||
}
|
||||
go p.cleanupLoop()
|
||||
return p
|
||||
}
|
||||
|
||||
// flowKey generates a unique key for a TCP flow
|
||||
func flowKey(srcIP string, srcPort uint16, dstIP string, dstPort uint16) string {
|
||||
return fmt.Sprintf("%s:%d->%s:%d", srcIP, srcPort, dstIP, dstPort)
|
||||
}
|
||||
|
||||
// cleanupLoop periodically removes expired flows
|
||||
func (p *ParserImpl) cleanupLoop() {
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
p.cleanupExpiredFlows()
|
||||
case <-p.cleanupClose:
|
||||
close(p.cleanupDone)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cleanupExpiredFlows removes flows that have timed out or are done
|
||||
func (p *ParserImpl) cleanupExpiredFlows() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
for key, flow := range p.flows {
|
||||
if flow.State == JA4_DONE || now.Sub(flow.LastSeen) > p.flowTimeout {
|
||||
delete(p.flows, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process extracts TLS ClientHello from a raw packet
|
||||
func (p *ParserImpl) Process(pkt api.RawPacket) (*api.TLSClientHello, error) {
|
||||
if len(pkt.Data) == 0 {
|
||||
return nil, fmt.Errorf("empty packet data")
|
||||
}
|
||||
|
||||
// Parse packet layers
|
||||
packet := gopacket.NewPacket(pkt.Data, layers.LinkTypeEthernet, gopacket.Default)
|
||||
|
||||
// Get IP layer
|
||||
ipLayer := packet.Layer(layers.LayerTypeIPv4)
|
||||
if ipLayer == nil {
|
||||
ipLayer = packet.Layer(layers.LayerTypeIPv6)
|
||||
}
|
||||
if ipLayer == nil {
|
||||
return nil, nil // Not an IP packet
|
||||
}
|
||||
|
||||
// Get TCP layer
|
||||
tcpLayer := packet.Layer(layers.LayerTypeTCP)
|
||||
if tcpLayer == nil {
|
||||
return nil, nil // Not a TCP packet
|
||||
}
|
||||
|
||||
ip, ok := ipLayer.(gopacket.Layer)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to cast IP layer")
|
||||
}
|
||||
|
||||
tcp, ok := tcpLayer.(*layers.TCP)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to cast TCP layer")
|
||||
}
|
||||
|
||||
// Extract IP metadata
|
||||
ipMeta := extractIPMeta(ip)
|
||||
|
||||
// Extract TCP metadata
|
||||
tcpMeta := extractTCPMeta(tcp)
|
||||
|
||||
// Get source/destination info
|
||||
var srcIP, dstIP string
|
||||
var srcPort, dstPort uint16
|
||||
|
||||
switch v := ip.(type) {
|
||||
case *layers.IPv4:
|
||||
srcIP = v.SrcIP.String()
|
||||
dstIP = v.DstIP.String()
|
||||
case *layers.IPv6:
|
||||
srcIP = v.SrcIP.String()
|
||||
dstIP = v.DstIP.String()
|
||||
}
|
||||
|
||||
srcPort = uint16(tcp.SrcPort)
|
||||
dstPort = uint16(tcp.DstPort)
|
||||
|
||||
// Get TCP payload (TLS data)
|
||||
payload := tcp.Payload
|
||||
if len(payload) == 0 {
|
||||
return nil, nil // No payload
|
||||
}
|
||||
|
||||
// Get or create connection flow
|
||||
key := flowKey(srcIP, srcPort, dstIP, dstPort)
|
||||
flow := p.getOrCreateFlow(key, srcIP, srcPort, dstIP, dstPort, ipMeta, tcpMeta)
|
||||
|
||||
// Check if flow is already done
|
||||
p.mu.RLock()
|
||||
isDone := flow.State == JA4_DONE
|
||||
p.mu.RUnlock()
|
||||
if isDone {
|
||||
return nil, nil // Already processed this flow
|
||||
}
|
||||
|
||||
// Check if this is a TLS ClientHello
|
||||
clientHello, err := parseClientHello(payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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,
|
||||
SrcPort: srcPort,
|
||||
DstIP: dstIP,
|
||||
DstPort: dstPort,
|
||||
Payload: clientHello,
|
||||
IPMeta: ipMeta,
|
||||
TCPMeta: tcpMeta,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Check for fragmented ClientHello (accumulate segments)
|
||||
if flow.State == WAIT_CLIENT_HELLO || flow.State == NEW {
|
||||
p.mu.Lock()
|
||||
flow.State = WAIT_CLIENT_HELLO
|
||||
flow.HelloBuffer = append(flow.HelloBuffer, payload...)
|
||||
bufferCopy := make([]byte, len(flow.HelloBuffer))
|
||||
copy(bufferCopy, flow.HelloBuffer)
|
||||
p.mu.Unlock()
|
||||
|
||||
// Try to parse accumulated buffer
|
||||
clientHello, err := parseClientHello(bufferCopy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if clientHello != nil {
|
||||
// Complete ClientHello found
|
||||
p.mu.Lock()
|
||||
flow.State = JA4_DONE
|
||||
p.mu.Unlock()
|
||||
|
||||
return &api.TLSClientHello{
|
||||
SrcIP: srcIP,
|
||||
SrcPort: srcPort,
|
||||
DstIP: dstIP,
|
||||
DstPort: dstPort,
|
||||
Payload: clientHello,
|
||||
IPMeta: ipMeta,
|
||||
TCPMeta: tcpMeta,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil // No ClientHello found yet
|
||||
}
|
||||
|
||||
// getOrCreateFlow gets existing flow or creates a new one
|
||||
func (p *ParserImpl) getOrCreateFlow(key string, srcIP string, srcPort uint16, dstIP string, dstPort uint16, ipMeta api.IPMeta, tcpMeta api.TCPMeta) *ConnectionFlow {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if flow, exists := p.flows[key]; exists {
|
||||
flow.LastSeen = time.Now()
|
||||
return flow
|
||||
}
|
||||
|
||||
flow := &ConnectionFlow{
|
||||
State: NEW,
|
||||
CreatedAt: time.Now(),
|
||||
LastSeen: time.Now(),
|
||||
SrcIP: srcIP,
|
||||
SrcPort: srcPort,
|
||||
DstIP: dstIP,
|
||||
DstPort: dstPort,
|
||||
IPMeta: ipMeta,
|
||||
TCPMeta: tcpMeta,
|
||||
HelloBuffer: make([]byte, 0),
|
||||
}
|
||||
p.flows[key] = flow
|
||||
return flow
|
||||
}
|
||||
|
||||
// Close cleans up the parser and stops background goroutines
|
||||
func (p *ParserImpl) Close() error {
|
||||
close(p.cleanupClose)
|
||||
<-p.cleanupDone
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractIPMeta extracts IP metadata from the IP layer
|
||||
func extractIPMeta(ipLayer gopacket.Layer) api.IPMeta {
|
||||
meta := api.IPMeta{}
|
||||
|
||||
switch v := ipLayer.(type) {
|
||||
case *layers.IPv4:
|
||||
meta.TTL = v.TTL
|
||||
meta.TotalLength = v.Length
|
||||
meta.IPID = v.Id
|
||||
meta.DF = v.Flags&layers.IPv4DontFragment != 0
|
||||
case *layers.IPv6:
|
||||
meta.TTL = v.HopLimit
|
||||
meta.TotalLength = uint16(v.Length)
|
||||
meta.IPID = 0 // IPv6 doesn't have IP ID
|
||||
meta.DF = true // IPv6 doesn't fragment at source
|
||||
}
|
||||
|
||||
return meta
|
||||
}
|
||||
|
||||
// extractTCPMeta extracts TCP metadata from the TCP layer
|
||||
func extractTCPMeta(tcp *layers.TCP) api.TCPMeta {
|
||||
meta := api.TCPMeta{
|
||||
WindowSize: tcp.Window,
|
||||
Options: make([]string, 0),
|
||||
}
|
||||
|
||||
// Parse TCP options
|
||||
for _, opt := range tcp.Options {
|
||||
switch opt.OptionType {
|
||||
case layers.TCPOptionKindMSS:
|
||||
meta.MSS = binary.BigEndian.Uint16(opt.OptionData)
|
||||
meta.Options = append(meta.Options, "MSS")
|
||||
case layers.TCPOptionKindWindowScale:
|
||||
if len(opt.OptionData) > 0 {
|
||||
meta.WindowScale = opt.OptionData[0]
|
||||
}
|
||||
meta.Options = append(meta.Options, "WS")
|
||||
case layers.TCPOptionKindSACKPermitted:
|
||||
meta.Options = append(meta.Options, "SACK")
|
||||
case layers.TCPOptionKindTimestamps:
|
||||
meta.Options = append(meta.Options, "TS")
|
||||
default:
|
||||
meta.Options = append(meta.Options, fmt.Sprintf("OPT%d", opt.OptionType))
|
||||
}
|
||||
}
|
||||
|
||||
return meta
|
||||
}
|
||||
|
||||
// parseClientHello checks if the payload contains a TLS ClientHello and returns it
|
||||
func parseClientHello(payload []byte) ([]byte, error) {
|
||||
if len(payload) < 5 {
|
||||
return nil, nil // Too short for TLS record
|
||||
}
|
||||
|
||||
// TLS record layer: Content Type (1 byte), Version (2 bytes), Length (2 bytes)
|
||||
contentType := payload[0]
|
||||
|
||||
// Check for TLS handshake (content type 22)
|
||||
if contentType != 22 {
|
||||
return nil, nil // Not a TLS handshake
|
||||
}
|
||||
|
||||
// Check TLS version (TLS 1.0 = 0x0301, TLS 1.1 = 0x0302, TLS 1.2 = 0x0303, TLS 1.3 = 0x0304)
|
||||
version := binary.BigEndian.Uint16(payload[1:3])
|
||||
if version < 0x0301 || version > 0x0304 {
|
||||
return nil, nil // Unknown TLS version
|
||||
}
|
||||
|
||||
recordLength := int(binary.BigEndian.Uint16(payload[3:5]))
|
||||
if len(payload) < 5+recordLength {
|
||||
return nil, nil // Incomplete TLS record
|
||||
}
|
||||
|
||||
// Parse handshake protocol
|
||||
handshakePayload := payload[5 : 5+recordLength]
|
||||
if len(handshakePayload) < 1 {
|
||||
return nil, nil // Too short for handshake type
|
||||
}
|
||||
|
||||
handshakeType := handshakePayload[0]
|
||||
|
||||
// Check for ClientHello (handshake type 1)
|
||||
if handshakeType != 1 {
|
||||
return nil, nil // Not a ClientHello
|
||||
}
|
||||
|
||||
// Return the full TLS record (header + payload) for fingerprinting
|
||||
return payload[:5+recordLength], nil
|
||||
}
|
||||
|
||||
// IsClientHello checks if a payload contains a TLS ClientHello
|
||||
func IsClientHello(payload []byte) bool {
|
||||
if len(payload) < 6 {
|
||||
return false
|
||||
}
|
||||
|
||||
// TLS handshake record
|
||||
if payload[0] != 22 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check version
|
||||
version := binary.BigEndian.Uint16(payload[1:3])
|
||||
if version < 0x0301 || version > 0x0304 {
|
||||
return false
|
||||
}
|
||||
|
||||
recordLength := int(binary.BigEndian.Uint16(payload[3:5]))
|
||||
if len(payload) < 5+recordLength {
|
||||
return false
|
||||
}
|
||||
|
||||
handshakePayload := payload[5 : 5+recordLength]
|
||||
if len(handshakePayload) < 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
// ClientHello type
|
||||
return handshakePayload[0] == 1
|
||||
}
|
||||
253
internal/tlsparse/parser_test.go
Normal file
253
internal/tlsparse/parser_test.go
Normal file
@ -0,0 +1,253 @@
|
||||
package tlsparse
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/gopacket/layers"
|
||||
)
|
||||
|
||||
func TestIsClientHello(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
payload []byte
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "empty payload",
|
||||
payload: []byte{},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "too short",
|
||||
payload: []byte{0x16, 0x03, 0x03},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "valid TLS 1.2 ClientHello",
|
||||
payload: createTLSClientHello(0x0303),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "valid TLS 1.3 ClientHello",
|
||||
payload: createTLSClientHello(0x0304),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "not a handshake",
|
||||
payload: []byte{0x17, 0x03, 0x03, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "ServerHello (type 2)",
|
||||
payload: createTLSServerHello(0x0303),
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := IsClientHello(tt.payload)
|
||||
if got != tt.want {
|
||||
t.Errorf("IsClientHello() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseClientHello(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
payload []byte
|
||||
wantErr bool
|
||||
wantNil bool
|
||||
}{
|
||||
{
|
||||
name: "empty payload",
|
||||
payload: []byte{},
|
||||
wantErr: false,
|
||||
wantNil: true,
|
||||
},
|
||||
{
|
||||
name: "valid ClientHello",
|
||||
payload: createTLSClientHello(0x0303),
|
||||
wantErr: false,
|
||||
wantNil: false,
|
||||
},
|
||||
{
|
||||
name: "incomplete record",
|
||||
payload: []byte{0x16, 0x03, 0x03, 0x01, 0x00, 0x01},
|
||||
wantErr: false,
|
||||
wantNil: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := parseClientHello(tt.payload)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("parseClientHello() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if (got == nil) != tt.wantNil {
|
||||
t.Errorf("parseClientHello() = %v, wantNil %v", got == nil, tt.wantNil)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractIPMeta(t *testing.T) {
|
||||
ipLayer := &layers.IPv4{
|
||||
TTL: 64,
|
||||
Length: 1500,
|
||||
Id: 12345,
|
||||
Flags: layers.IPv4DontFragment,
|
||||
SrcIP: []byte{192, 168, 1, 1},
|
||||
DstIP: []byte{10, 0, 0, 1},
|
||||
}
|
||||
|
||||
meta := extractIPMeta(ipLayer)
|
||||
|
||||
if meta.TTL != 64 {
|
||||
t.Errorf("TTL = %v, want 64", meta.TTL)
|
||||
}
|
||||
if meta.TotalLength != 1500 {
|
||||
t.Errorf("TotalLength = %v, want 1500", meta.TotalLength)
|
||||
}
|
||||
if meta.IPID != 12345 {
|
||||
t.Errorf("IPID = %v, want 12345", meta.IPID)
|
||||
}
|
||||
if !meta.DF {
|
||||
t.Error("DF = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractTCPMeta(t *testing.T) {
|
||||
tcp := &layers.TCP{
|
||||
SrcPort: 12345,
|
||||
DstPort: 443,
|
||||
Window: 65535,
|
||||
Options: []layers.TCPOption{
|
||||
{
|
||||
OptionType: layers.TCPOptionKindMSS,
|
||||
OptionData: []byte{0x05, 0xb4}, // 1460
|
||||
},
|
||||
{
|
||||
OptionType: layers.TCPOptionKindWindowScale,
|
||||
OptionData: []byte{0x07}, // scale 7
|
||||
},
|
||||
{
|
||||
OptionType: layers.TCPOptionKindSACKPermitted,
|
||||
OptionData: []byte{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
meta := extractTCPMeta(tcp)
|
||||
|
||||
if meta.WindowSize != 65535 {
|
||||
t.Errorf("WindowSize = %v, want 65535", meta.WindowSize)
|
||||
}
|
||||
if meta.MSS != 1460 {
|
||||
t.Errorf("MSS = %v, want 1460", meta.MSS)
|
||||
}
|
||||
if meta.WindowScale != 7 {
|
||||
t.Errorf("WindowScale = %v, want 7", meta.WindowScale)
|
||||
}
|
||||
if len(meta.Options) != 3 {
|
||||
t.Errorf("Options length = %v, want 3", len(meta.Options))
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions to create test TLS records
|
||||
|
||||
func createTLSClientHello(version uint16) []byte {
|
||||
// Minimal TLS ClientHello record
|
||||
handshake := []byte{
|
||||
0x01, // Handshake type: ClientHello
|
||||
0x00, 0x00, 0x00, 0x10, // Handshake length (16 bytes)
|
||||
// ClientHello body (simplified)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
}
|
||||
|
||||
record := make([]byte, 5+len(handshake))
|
||||
record[0] = 0x16 // Handshake
|
||||
record[1] = byte(version >> 8)
|
||||
record[2] = byte(version)
|
||||
record[3] = byte(len(handshake) >> 8)
|
||||
record[4] = byte(len(handshake))
|
||||
copy(record[5:], handshake)
|
||||
|
||||
return record
|
||||
}
|
||||
|
||||
func createTLSServerHello(version uint16) []byte {
|
||||
// Minimal TLS ServerHello record
|
||||
handshake := []byte{
|
||||
0x02, // Handshake type: ServerHello
|
||||
0x00, 0x00, 0x00, 0x10, // Handshake length
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
}
|
||||
|
||||
record := make([]byte, 5+len(handshake))
|
||||
record[0] = 0x16 // Handshake
|
||||
record[1] = byte(version >> 8)
|
||||
record[2] = byte(version)
|
||||
record[3] = byte(len(handshake) >> 8)
|
||||
record[4] = byte(len(handshake))
|
||||
copy(record[5:], handshake)
|
||||
|
||||
return record
|
||||
}
|
||||
|
||||
func TestNewParser(t *testing.T) {
|
||||
parser := NewParser()
|
||||
if parser == nil {
|
||||
t.Error("NewParser() returned nil")
|
||||
}
|
||||
if parser.flows == nil {
|
||||
t.Error("NewParser() flows map not initialized")
|
||||
}
|
||||
if parser.flowTimeout == 0 {
|
||||
t.Error("NewParser() flowTimeout not set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParserClose(t *testing.T) {
|
||||
parser := NewParser()
|
||||
err := parser.Close()
|
||||
if err != nil {
|
||||
t.Errorf("Close() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlowKey(t *testing.T) {
|
||||
key := flowKey("192.168.1.1", 12345, "10.0.0.1", 443)
|
||||
expected := "192.168.1.1:12345->10.0.0.1:443"
|
||||
if key != expected {
|
||||
t.Errorf("flowKey() = %v, want %v", key, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParserConnectionStateTracking(t *testing.T) {
|
||||
parser := NewParser()
|
||||
defer parser.Close()
|
||||
|
||||
// Create a valid ClientHello payload
|
||||
clientHello := createTLSClientHello(0x0303)
|
||||
|
||||
// Test parseClientHello directly (lower-level test)
|
||||
result, err := parseClientHello(clientHello)
|
||||
if err != nil {
|
||||
t.Errorf("parseClientHello() error = %v", err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Error("parseClientHello() should return ClientHello")
|
||||
}
|
||||
|
||||
// Test IsClientHello helper
|
||||
if !IsClientHello(clientHello) {
|
||||
t.Error("IsClientHello() should return true for valid ClientHello")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user