From 87d47324fb776ffb66eb494d091a80f0c8f49c76 Mon Sep 17 00:00:00 2001 From: Jacquin Antoine Date: Wed, 25 Feb 2026 04:14:51 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20Ajouter=20le=20module=20de=20capture=20?= =?UTF-8?q?r=C3=A9seau=20avec=20filtres=20BPF=20et=20tests=20associ=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: aider (openrouter/qwen/qwen3-coder-plus) --- internal/capture/capture.go | 112 +++++++++++++++++++++++++++++++ internal/capture/capture_test.go | 84 +++++++++++++++++++++++ 2 files changed, 196 insertions(+) create mode 100644 internal/capture/capture.go create mode 100644 internal/capture/capture_test.go diff --git a/internal/capture/capture.go b/internal/capture/capture.go new file mode 100644 index 0000000..28b72ee --- /dev/null +++ b/internal/capture/capture.go @@ -0,0 +1,112 @@ +package capture + +import ( + "fmt" + "net" + "time" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/google/gopacket/pcap" + + "ja4sentinel/internal/api" +) + +// CaptureImpl implémente l'interface capture.Capture +type CaptureImpl struct { + handle *pcap.Handle +} + +// New crée une nouvelle instance de capture +func New() *CaptureImpl { + return &CaptureImpl{} +} + +// Run démarre la capture des paquets réseau selon la configuration +func (c *CaptureImpl) Run(cfg api.Config, out chan<- api.RawPacket) error { + var err error + c.handle, err = pcap.OpenLive(cfg.Interface, 1600, true, pcap.BlockForever) + if err != nil { + return fmt.Errorf("failed to open interface %s: %w", cfg.Interface, err) + } + defer c.handle.Close() + + // Appliquer le filtre BPF s'il est fourni + if cfg.BPFFilter != "" { + err = c.handle.SetBPFFilter(cfg.BPFFilter) + if err != nil { + return fmt.Errorf("failed to set BPF filter: %w", err) + } + } else { + // Créer un filtre par défaut pour les ports surveillés + defaultFilter := buildBPFForPorts(cfg.ListenPorts) + err = c.handle.SetBPFFilter(defaultFilter) + if err != nil { + return fmt.Errorf("failed to set default BPF filter: %w", err) + } + } + + packetSource := gopacket.NewPacketSource(c.handle, c.handle.LinkType()) + + for packet := range packetSource.Packets() { + // Convertir le paquet en RawPacket + rawPkt := packetToRawPacket(packet) + if rawPkt != nil { + select { + case out <- *rawPkt: + // Paquet envoyé avec succès + default: + // Canal plein, ignorer le paquet + } + } + } + + return nil +} + +// buildBPFForPorts construit un filtre BPF pour les ports TCP spécifiés +func buildBPFForPorts(ports []uint16) string { + if len(ports) == 0 { + return "tcp" + } + + filterParts := make([]string, len(ports)) + for i, port := range ports { + filterParts[i] = fmt.Sprintf("tcp port %d", port) + } + return "(" + joinString(filterParts, ") or (") + ")" +} + +// joinString joint des chaînes avec un séparateur +func joinString(parts []string, sep string) string { + if len(parts) == 0 { + return "" + } + result := parts[0] + for _, part := range parts[1:] { + result += sep + part + } + return result +} + +// packetToRawPacket convertit un paquet gopacket en RawPacket +func packetToRawPacket(packet gopacket.Packet) *api.RawPacket { + data := packet.Data() + if len(data) == 0 { + return nil + } + + return &api.RawPacket{ + Data: data, + Timestamp: packet.Metadata().Timestamp.UnixNano(), + } +} + +// Close ferme correctement la capture +func (c *CaptureImpl) Close() error { + if c.handle != nil { + c.handle.Close() + return nil + } + return nil +} diff --git a/internal/capture/capture_test.go b/internal/capture/capture_test.go new file mode 100644 index 0000000..ddaf857 --- /dev/null +++ b/internal/capture/capture_test.go @@ -0,0 +1,84 @@ +package capture + +import ( + "testing" + "time" + + "ja4sentinel/internal/api" +) + +func TestBuildBPFForPorts(t *testing.T) { + tests := []struct { + name string + ports []uint16 + want string + }{ + { + name: "single port", + ports: []uint16{443}, + want: "(tcp port 443)", + }, + { + name: "multiple ports", + ports: []uint16{443, 8443}, + want: "(tcp port 443) or (tcp port 8443)", + }, + { + name: "no ports", + ports: []uint16{}, + want: "tcp", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := buildBPFForPorts(tt.ports) + if got != tt.want { + t.Errorf("buildBPFForPorts() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestJoinString(t *testing.T) { + tests := []struct { + name string + parts []string + sep string + want string + }{ + { + name: "empty slices", + parts: []string{}, + sep: ", ", + want: "", + }, + { + name: "single element", + parts: []string{"hello"}, + sep: ", ", + want: "hello", + }, + { + name: "multiple elements", + parts: []string{"hello", "world", "test"}, + sep: ", ", + want: "hello, world, test", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := joinString(tt.parts, tt.sep) + if got != tt.want { + t.Errorf("joinString() = %v, want %v", got, tt.want) + } + }) + } +} + +// 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") +}