feat(04-01): add BPF validation, pcap reading, Timestamp field, and filter support

- Add Timestamp time.Time field to ClassifiedPacket (classify/types.go)
- Create capture/bpf.go: ValidateBPFFilter and CompileSoftwareBPF
- Create capture/pcap_reader.go: ReadPcapFile with optional software BPF filter
- Update OpenCapture and StartCapture to accept filter string param
- Update cmd/netsynth/main.go to pass empty filter to StartCapture
- All new tests pass; existing tests unaffected
This commit is contained in:
2026-03-26 14:33:29 +01:00
parent 38c74415ed
commit 52c601019b
7 changed files with 353 additions and 9 deletions
+58
View File
@@ -0,0 +1,58 @@
package capture
import (
"fmt"
"os"
"github.com/gopacket/gopacket"
"github.com/gopacket/gopacket/layers"
"github.com/gopacket/gopacket/pcapgo"
"golang.org/x/net/bpf"
)
// ReadPcapFile opens a pcap file and returns a channel of packets.
// If filter is non-empty, applies software BPF filtering (kernel BPF unavailable for files).
// Per D-03: replaces -i as packet source. Per D-04: --filter works with --read.
// The returned channel is closed when all packets have been emitted.
func ReadPcapFile(path string, filter string) (<-chan gopacket.Packet, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("cannot open pcap file %q: %w", path, err)
}
r, err := pcapgo.NewReader(f)
if err != nil {
f.Close()
return nil, fmt.Errorf("not a valid pcap file %q: %w", path, err)
}
// Compile software BPF filter if provided.
var vm *bpf.VM
if filter != "" {
vm, err = CompileSoftwareBPF(filter)
if err != nil {
f.Close()
return nil, err
}
}
lt := layers.LinkType(r.LinkType())
packetSource := gopacket.NewPacketSource(r, lt)
packetSource.NoCopy = false // CRITICAL: do NOT use NoCopy=true with pcapgo (Pitfall 3)
packets := make(chan gopacket.Packet, 512)
go func() {
defer close(packets)
defer f.Close()
for pkt := range packetSource.Packets() {
// Apply software BPF filter if active.
if vm != nil {
result, err := vm.Run(pkt.Data())
if err != nil || result == 0 {
continue // packet does not match filter
}
}
packets <- pkt
}
}()
return packets, nil
}