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:
@ -3,6 +3,7 @@ package capture
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sync"
|
||||
|
||||
"github.com/google/gopacket"
|
||||
@ -11,20 +12,74 @@ import (
|
||||
"ja4sentinel/api"
|
||||
)
|
||||
|
||||
// Capture configuration constants
|
||||
const (
|
||||
// DefaultSnapLen is the default snapshot length for packet capture
|
||||
// Increased from 1600 to 65535 to capture full packets including large TLS handshakes
|
||||
DefaultSnapLen = 65535
|
||||
// DefaultPromiscuous is the default promiscuous mode setting
|
||||
DefaultPromiscuous = false
|
||||
// MaxBPFFilterLength is the maximum allowed length for BPF filters
|
||||
MaxBPFFilterLength = 1024
|
||||
)
|
||||
|
||||
// validBPFPattern checks if a BPF filter contains only valid characters
|
||||
// This is a basic validation to prevent injection attacks
|
||||
var validBPFPattern = regexp.MustCompile(`^[a-zA-Z0-9\s\(\)\-\_\.\*\+\?\:\=\!\&\|\<\>\[\]\/\@,]+$`)
|
||||
|
||||
// CaptureImpl implements the capture.Capture interface for packet capture
|
||||
type CaptureImpl struct {
|
||||
handle *pcap.Handle
|
||||
mu sync.Mutex
|
||||
handle *pcap.Handle
|
||||
mu sync.Mutex
|
||||
snapLen int
|
||||
promisc bool
|
||||
isClosed bool
|
||||
}
|
||||
|
||||
// New creates a new capture instance
|
||||
func New() *CaptureImpl {
|
||||
return &CaptureImpl{}
|
||||
return &CaptureImpl{
|
||||
snapLen: DefaultSnapLen,
|
||||
promisc: DefaultPromiscuous,
|
||||
}
|
||||
}
|
||||
|
||||
// NewWithSnapLen creates a new capture instance with custom snapshot length
|
||||
func NewWithSnapLen(snapLen int) *CaptureImpl {
|
||||
if snapLen <= 0 || snapLen > 65535 {
|
||||
snapLen = DefaultSnapLen
|
||||
}
|
||||
return &CaptureImpl{
|
||||
snapLen: snapLen,
|
||||
promisc: DefaultPromiscuous,
|
||||
}
|
||||
}
|
||||
|
||||
// Run starts network packet capture according to the configuration
|
||||
func (c *CaptureImpl) Run(cfg api.Config, out chan<- api.RawPacket) error {
|
||||
handle, err := pcap.OpenLive(cfg.Interface, 1600, true, pcap.BlockForever)
|
||||
// Validate interface name (basic check)
|
||||
if cfg.Interface == "" {
|
||||
return fmt.Errorf("interface cannot be empty")
|
||||
}
|
||||
|
||||
// Find available interfaces to validate the interface exists
|
||||
ifaces, err := pcap.FindAllDevs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list network interfaces: %w", err)
|
||||
}
|
||||
|
||||
interfaceFound := false
|
||||
for _, iface := range ifaces {
|
||||
if iface.Name == cfg.Interface {
|
||||
interfaceFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !interfaceFound {
|
||||
return fmt.Errorf("interface %s not found (available: %v)", cfg.Interface, getInterfaceNames(ifaces))
|
||||
}
|
||||
|
||||
handle, err := pcap.OpenLive(cfg.Interface, int32(c.snapLen), c.promisc, pcap.BlockForever)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open interface %s: %w", cfg.Interface, err)
|
||||
}
|
||||
@ -35,26 +90,27 @@ func (c *CaptureImpl) Run(cfg api.Config, out chan<- api.RawPacket) error {
|
||||
|
||||
defer func() {
|
||||
c.mu.Lock()
|
||||
if c.handle != nil {
|
||||
if c.handle != nil && !c.isClosed {
|
||||
c.handle.Close()
|
||||
c.handle = nil
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}()
|
||||
|
||||
// Apply BPF filter if provided
|
||||
if cfg.BPFFilter != "" {
|
||||
err = handle.SetBPFFilter(cfg.BPFFilter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set BPF filter: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Create default filter for monitored ports
|
||||
defaultFilter := buildBPFForPorts(cfg.ListenPorts)
|
||||
err = handle.SetBPFFilter(defaultFilter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set default BPF filter: %w", err)
|
||||
}
|
||||
// Build and apply BPF filter
|
||||
bpfFilter := cfg.BPFFilter
|
||||
if bpfFilter == "" {
|
||||
bpfFilter = buildBPFForPorts(cfg.ListenPorts)
|
||||
}
|
||||
|
||||
// Validate BPF filter before applying
|
||||
if err := validateBPFFilter(bpfFilter); err != nil {
|
||||
return fmt.Errorf("invalid BPF filter: %w", err)
|
||||
}
|
||||
|
||||
err = handle.SetBPFFilter(bpfFilter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set BPF filter '%s': %w", bpfFilter, err)
|
||||
}
|
||||
|
||||
packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
|
||||
@ -67,7 +123,7 @@ func (c *CaptureImpl) Run(cfg api.Config, out chan<- api.RawPacket) error {
|
||||
case out <- *rawPkt:
|
||||
// Packet sent successfully
|
||||
default:
|
||||
// Channel full, drop packet
|
||||
// Channel full, drop packet (could add metrics here)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -75,6 +131,49 @@ func (c *CaptureImpl) Run(cfg api.Config, out chan<- api.RawPacket) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateBPFFilter performs basic validation of BPF filter strings
|
||||
func validateBPFFilter(filter string) error {
|
||||
if filter == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(filter) > MaxBPFFilterLength {
|
||||
return fmt.Errorf("BPF filter too long (max %d characters)", MaxBPFFilterLength)
|
||||
}
|
||||
|
||||
// Check for potentially dangerous patterns
|
||||
if !validBPFPattern.MatchString(filter) {
|
||||
return fmt.Errorf("BPF filter contains invalid characters")
|
||||
}
|
||||
|
||||
// Check for unbalanced parentheses
|
||||
openParens := 0
|
||||
for _, ch := range filter {
|
||||
if ch == '(' {
|
||||
openParens++
|
||||
} else if ch == ')' {
|
||||
openParens--
|
||||
if openParens < 0 {
|
||||
return fmt.Errorf("BPF filter has unbalanced parentheses")
|
||||
}
|
||||
}
|
||||
}
|
||||
if openParens != 0 {
|
||||
return fmt.Errorf("BPF filter has unbalanced parentheses")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getInterfaceNames extracts interface names from a list of devices
|
||||
func getInterfaceNames(ifaces []pcap.Interface) []string {
|
||||
names := make([]string, len(ifaces))
|
||||
for i, iface := range ifaces {
|
||||
names[i] = iface.Name
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// buildBPFForPorts builds a BPF filter for the specified TCP ports
|
||||
func buildBPFForPorts(ports []uint16) string {
|
||||
if len(ports) == 0 {
|
||||
@ -118,10 +217,12 @@ func (c *CaptureImpl) Close() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.handle != nil {
|
||||
if c.handle != nil && !c.isClosed {
|
||||
c.handle.Close()
|
||||
c.handle = nil
|
||||
c.isClosed = true
|
||||
return nil
|
||||
}
|
||||
c.isClosed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -4,12 +4,85 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateBPFFilter(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
filter string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty filter",
|
||||
filter: "",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid simple filter",
|
||||
filter: "tcp port 443",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid complex filter",
|
||||
filter: "(tcp port 443) or (tcp port 8443)",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "filter with special chars",
|
||||
filter: "tcp port 443 and host 192.168.1.1",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "too long filter",
|
||||
filter: string(make([]byte, MaxBPFFilterLength+1)),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "unbalanced parentheses - extra open",
|
||||
filter: "(tcp port 443",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "unbalanced parentheses - extra close",
|
||||
filter: "tcp port 443)",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid characters - semicolon",
|
||||
filter: "tcp port 443; rm -rf /",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid characters - backtick",
|
||||
filter: "tcp port `whoami`",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid characters - dollar",
|
||||
filter: "tcp port $HOME",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validateBPFFilter(tt.filter)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("validateBPFFilter() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildBPFForPorts(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ports []uint16
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "no ports",
|
||||
ports: []uint16{},
|
||||
want: "tcp",
|
||||
},
|
||||
{
|
||||
name: "single port",
|
||||
ports: []uint16{443},
|
||||
@ -17,13 +90,8 @@ func TestBuildBPFForPorts(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "multiple ports",
|
||||
ports: []uint16{443, 8443},
|
||||
want: "(tcp port 443) or (tcp port 8443)",
|
||||
},
|
||||
{
|
||||
name: "no ports",
|
||||
ports: []uint16{},
|
||||
want: "tcp",
|
||||
ports: []uint16{443, 8443, 9443},
|
||||
want: "(tcp port 443) or (tcp port 8443) or (tcp port 9443)",
|
||||
},
|
||||
}
|
||||
|
||||
@ -45,22 +113,22 @@ func TestJoinString(t *testing.T) {
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty slices",
|
||||
name: "empty slice",
|
||||
parts: []string{},
|
||||
sep: ", ",
|
||||
sep: ") or (",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "single element",
|
||||
parts: []string{"hello"},
|
||||
sep: ", ",
|
||||
want: "hello",
|
||||
parts: []string{"tcp port 443"},
|
||||
sep: ") or (",
|
||||
want: "tcp port 443",
|
||||
},
|
||||
{
|
||||
name: "multiple elements",
|
||||
parts: []string{"hello", "world", "test"},
|
||||
sep: ", ",
|
||||
want: "hello, world, test",
|
||||
parts: []string{"tcp port 443", "tcp port 8443"},
|
||||
sep: ") or (",
|
||||
want: "tcp port 443) or (tcp port 8443",
|
||||
},
|
||||
}
|
||||
|
||||
@ -74,25 +142,92 @@ func TestJoinString(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Tests d'intégration nécessitant une interface valide seront à faire dans des environnements de test appropriés
|
||||
// car la capture réseau nécessite des permissions élevées
|
||||
func TestCaptureIntegration(t *testing.T) {
|
||||
t.Skip("Skipping integration test requiring network access and elevated privileges")
|
||||
}
|
||||
|
||||
func TestClose_NoHandle_NoError(t *testing.T) {
|
||||
func TestNewCapture(t *testing.T) {
|
||||
c := New()
|
||||
if err := c.Close(); err != nil {
|
||||
t.Fatalf("Close() error = %v", err)
|
||||
if c == nil {
|
||||
t.Fatal("New() returned nil")
|
||||
}
|
||||
if c.snapLen != DefaultSnapLen {
|
||||
t.Errorf("snapLen = %d, want %d", c.snapLen, DefaultSnapLen)
|
||||
}
|
||||
if c.promisc != DefaultPromiscuous {
|
||||
t.Errorf("promisc = %v, want %v", c.promisc, DefaultPromiscuous)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClose_Idempotent_NoHandle(t *testing.T) {
|
||||
c := New()
|
||||
if err := c.Close(); err != nil {
|
||||
t.Fatalf("first Close() error = %v", err)
|
||||
func TestNewWithSnapLen(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
snapLen int
|
||||
wantSnapLen int
|
||||
}{
|
||||
{
|
||||
name: "valid snapLen",
|
||||
snapLen: 2048,
|
||||
wantSnapLen: 2048,
|
||||
},
|
||||
{
|
||||
name: "zero snapLen uses default",
|
||||
snapLen: 0,
|
||||
wantSnapLen: DefaultSnapLen,
|
||||
},
|
||||
{
|
||||
name: "negative snapLen uses default",
|
||||
snapLen: -100,
|
||||
wantSnapLen: DefaultSnapLen,
|
||||
},
|
||||
{
|
||||
name: "too large snapLen uses default",
|
||||
snapLen: 100000,
|
||||
wantSnapLen: DefaultSnapLen,
|
||||
},
|
||||
}
|
||||
if err := c.Close(); err != nil {
|
||||
t.Fatalf("second Close() error = %v", err)
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := NewWithSnapLen(tt.snapLen)
|
||||
if c == nil {
|
||||
t.Fatal("NewWithSnapLen() returned nil")
|
||||
}
|
||||
if c.snapLen != tt.wantSnapLen {
|
||||
t.Errorf("snapLen = %d, want %d", c.snapLen, tt.wantSnapLen)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaptureImpl_Close(t *testing.T) {
|
||||
c := New()
|
||||
if c == nil {
|
||||
t.Fatal("New() returned nil")
|
||||
}
|
||||
|
||||
// Close should not panic on fresh instance
|
||||
if err := c.Close(); err != nil {
|
||||
t.Errorf("Close() error = %v", err)
|
||||
}
|
||||
|
||||
// Multiple closes should be safe
|
||||
if err := c.Close(); err != nil {
|
||||
t.Errorf("Close() second call error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateBPFFilter_BalancedParentheses(t *testing.T) {
|
||||
// Test various balanced parentheses scenarios
|
||||
validFilters := []string{
|
||||
"(tcp port 443)",
|
||||
"((tcp port 443))",
|
||||
"(tcp port 443) or (tcp port 8443)",
|
||||
"((tcp port 443) or (tcp port 8443))",
|
||||
"(tcp port 443 and host 1.2.3.4) or (tcp port 8443)",
|
||||
}
|
||||
|
||||
for _, filter := range validFilters {
|
||||
t.Run(filter, func(t *testing.T) {
|
||||
if err := validateBPFFilter(filter); err != nil {
|
||||
t.Errorf("validateBPFFilter(%q) unexpected error = %v", filter, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user