Some checks failed
Build RPM Package / Build RPM Packages (CentOS 7, Rocky 8/9/10) (push) Has been cancelled
Features: - Add ErrorCallback type for UNIX socket connection error reporting - Add WithErrorCallback option for UnixSocketWriter configuration - Add BuilderImpl.WithErrorCallback() for propagating callbacks - Add consecutive failure tracking in processQueue Testing (50+ new tests): - Add integration tests for full pipeline (capture → tlsparse → fingerprint → output) - Add tests for FileWriter.rotate() and Reopen() log rotation - Add tests for cleanupExpiredFlows() and cleanupLoop() in TLS parser - Add tests for extractSNIFromPayload() and extractJA4Hash() helpers - Add tests for config load error paths (invalid YAML, permission denied) - Add tests for capture.Run() error conditions - Add tests for signal handling documentation Documentation: - Update architecture.yml with new fields (LogLevel, TLSClientHello extensions) - Update architecture.yml with Close() methods for Capture and Parser interfaces - Update RPM spec changelog Cleanup: - Remove empty internal/api/ directory Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
255 lines
6.2 KiB
Go
255 lines
6.2 KiB
Go
package fingerprint
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"ja4sentinel/api"
|
|
)
|
|
|
|
func TestFromClientHello(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ch api.TLSClientHello
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "empty payload",
|
|
ch: api.TLSClientHello{
|
|
Payload: []byte{},
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "invalid payload",
|
|
ch: api.TLSClientHello{
|
|
Payload: []byte{0x00, 0x01, 0x02},
|
|
},
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
engine := NewEngine()
|
|
_, err := engine.FromClientHello(tt.ch)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("FromClientHello() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewEngine(t *testing.T) {
|
|
engine := NewEngine()
|
|
if engine == nil {
|
|
t.Error("NewEngine() returned nil")
|
|
}
|
|
}
|
|
|
|
func TestFromClientHello_ValidPayload(t *testing.T) {
|
|
// Use a minimal valid TLS 1.2 ClientHello with extensions
|
|
// Build a proper ClientHello using the same structure as parser tests
|
|
clientHello := buildMinimalClientHelloForTest()
|
|
|
|
ch := api.TLSClientHello{
|
|
SrcIP: "192.168.1.100",
|
|
SrcPort: 54321,
|
|
DstIP: "10.0.0.1",
|
|
DstPort: 443,
|
|
Payload: clientHello,
|
|
}
|
|
|
|
engine := NewEngine()
|
|
fp, err := engine.FromClientHello(ch)
|
|
|
|
if err != nil {
|
|
t.Fatalf("FromClientHello() error = %v", err)
|
|
}
|
|
if fp == nil {
|
|
t.Fatal("FromClientHello() returned nil")
|
|
}
|
|
|
|
// Verify JA4 is populated (format: t13d... or t12d...)
|
|
if fp.JA4 == "" {
|
|
t.Error("JA4 should not be empty")
|
|
}
|
|
|
|
// JA4Hash is populated for internal use (but not serialized to LogRecord)
|
|
// It contains the hash portions of the JA4 string
|
|
if fp.JA4Hash == "" {
|
|
t.Error("JA4Hash should be populated for internal use")
|
|
}
|
|
}
|
|
|
|
// buildMinimalClientHelloForTest creates a minimal valid TLS 1.2 ClientHello
|
|
func buildMinimalClientHelloForTest() []byte {
|
|
// Cipher suites (minimal set)
|
|
cipherSuites := []byte{0x00, 0x04, 0x13, 0x01, 0x13, 0x02, 0xc0, 0x2f}
|
|
// Compression methods (null only)
|
|
compressionMethods := []byte{0x01, 0x00}
|
|
// No extensions
|
|
extensions := []byte{}
|
|
extLen := len(extensions)
|
|
|
|
// Build ClientHello handshake body
|
|
handshakeBody := []byte{
|
|
0x03, 0x03, // Version: TLS 1.2
|
|
// Random (32 bytes)
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, // Session ID length: 0
|
|
}
|
|
|
|
// Add cipher suites (with length prefix)
|
|
cipherSuiteLen := len(cipherSuites)
|
|
handshakeBody = append(handshakeBody, byte(cipherSuiteLen>>8), byte(cipherSuiteLen))
|
|
handshakeBody = append(handshakeBody, cipherSuites...)
|
|
|
|
// Add compression methods (with length prefix)
|
|
handshakeBody = append(handshakeBody, compressionMethods...)
|
|
|
|
// Add extensions (with length prefix)
|
|
handshakeBody = append(handshakeBody, byte(extLen>>8), byte(extLen))
|
|
handshakeBody = append(handshakeBody, extensions...)
|
|
|
|
// Now build full handshake with type and length
|
|
handshakeLen := len(handshakeBody)
|
|
handshake := append([]byte{
|
|
0x01, // Handshake type: ClientHello
|
|
byte(handshakeLen >> 16), byte(handshakeLen >> 8), byte(handshakeLen), // Handshake length
|
|
}, handshakeBody...)
|
|
|
|
// Build TLS record
|
|
recordLen := len(handshake)
|
|
record := make([]byte, 5+recordLen)
|
|
record[0] = 0x16 // Handshake
|
|
record[1] = 0x03 // Version: TLS 1.2
|
|
record[2] = 0x03
|
|
record[3] = byte(recordLen >> 8)
|
|
record[4] = byte(recordLen)
|
|
copy(record[5:], handshake)
|
|
|
|
return record
|
|
}
|
|
|
|
// TestExtractJA4Hash tests the extractJA4Hash helper function
|
|
func TestExtractJA4Hash(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ja4 string
|
|
want string
|
|
}{
|
|
{
|
|
name: "standard_ja4_format",
|
|
ja4: "t13d1516h2_8daaf6152771_02cb136f2775",
|
|
want: "8daaf6152771_02cb136f2775",
|
|
},
|
|
{
|
|
name: "ja4_with_single_underscore",
|
|
ja4: "t12d1234h1_abcdef123456",
|
|
want: "abcdef123456",
|
|
},
|
|
{
|
|
name: "ja4_no_underscore_returns_empty",
|
|
ja4: "t13d1516h2",
|
|
want: "",
|
|
},
|
|
{
|
|
name: "empty_ja4_returns_empty",
|
|
ja4: "",
|
|
want: "",
|
|
},
|
|
{
|
|
name: "underscore_at_start",
|
|
ja4: "_hash1_hash2",
|
|
want: "hash1_hash2",
|
|
},
|
|
{
|
|
name: "multiple_underscores_returns_after_first",
|
|
ja4: "base_part1_part2_part3",
|
|
want: "part1_part2_part3",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := extractJA4Hash(tt.ja4)
|
|
if got != tt.want {
|
|
t.Errorf("extractJA4Hash(%q) = %q, want %q", tt.ja4, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestFromClientHello_NilPayload tests error handling for nil payload
|
|
func TestFromClientHello_NilPayload(t *testing.T) {
|
|
engine := NewEngine()
|
|
ch := api.TLSClientHello{
|
|
Payload: nil,
|
|
}
|
|
|
|
_, err := engine.FromClientHello(ch)
|
|
|
|
if err == nil {
|
|
t.Error("FromClientHello() with nil payload should return error")
|
|
}
|
|
if err.Error() != "empty ClientHello payload" {
|
|
t.Errorf("FromClientHello() error = %v, want 'empty ClientHello payload'", err)
|
|
}
|
|
}
|
|
|
|
// TestFromClientHello_JA3Hash tests that JA3Hash is correctly populated
|
|
func TestFromClientHello_JA3Hash(t *testing.T) {
|
|
clientHello := buildMinimalClientHelloForTest()
|
|
|
|
ch := api.TLSClientHello{
|
|
Payload: clientHello,
|
|
}
|
|
|
|
engine := NewEngine()
|
|
fp, err := engine.FromClientHello(ch)
|
|
|
|
if err != nil {
|
|
t.Fatalf("FromClientHello() error = %v", err)
|
|
}
|
|
|
|
// JA3Hash should be populated (MD5 hash of JA3 string)
|
|
if fp.JA3Hash == "" {
|
|
t.Error("JA3Hash should be populated")
|
|
}
|
|
|
|
// JA3 should also be populated
|
|
if fp.JA3 == "" {
|
|
t.Error("JA3 should be populated")
|
|
}
|
|
}
|
|
|
|
// TestFromClientHello_EmptyJA4Hash tests behavior when JA4 has no underscore
|
|
func TestFromClientHello_EmptyJA4Hash(t *testing.T) {
|
|
// This test verifies that even if JA4 format changes, the code handles it gracefully
|
|
engine := NewEngine()
|
|
|
|
// Use a valid ClientHello - the library should produce a proper JA4
|
|
clientHello := buildMinimalClientHelloForTest()
|
|
|
|
ch := api.TLSClientHello{
|
|
Payload: clientHello,
|
|
}
|
|
|
|
fp, err := engine.FromClientHello(ch)
|
|
|
|
if err != nil {
|
|
t.Fatalf("FromClientHello() error = %v", err)
|
|
}
|
|
|
|
// JA4 should always be populated
|
|
if fp.JA4 == "" {
|
|
t.Error("JA4 should be populated")
|
|
}
|
|
|
|
// JA4Hash may be empty if the JA4 format doesn't include underscores
|
|
// This is acceptable behavior
|
|
}
|