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