- 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
115 lines
3.2 KiB
Go
115 lines
3.2 KiB
Go
package capture
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"runtime"
|
|
"strings"
|
|
"sync/atomic"
|
|
|
|
"github.com/gopacket/gopacket"
|
|
"github.com/gopacket/gopacket/layers"
|
|
pcap "github.com/packetcap/go-pcap"
|
|
)
|
|
|
|
// ListInterfaces returns all network interfaces using Go stdlib.
|
|
// Does NOT use pcap.FindAllDevs (go-pcap has no such function).
|
|
// Requires no privileges (CAPT-02).
|
|
func ListInterfaces() ([]net.Interface, error) {
|
|
return net.Interfaces()
|
|
}
|
|
|
|
// OpenCapture opens a live capture handle on the given interface.
|
|
// snaplen=65535, promiscuous=false, timeout=0 (block until packet).
|
|
// If filter is non-empty, applies a BPF filter to the handle (CAPT-05).
|
|
// Returns a platform-specific error message if permission is denied (CAPT-04, D-05).
|
|
func OpenCapture(ctx context.Context, iface string, filter string) (*pcap.Handle, error) {
|
|
handle, err := pcap.OpenLive(ctx, iface, 65535, false, 0, false)
|
|
if err != nil {
|
|
if isPermissionError(err) {
|
|
return nil, permissionErrorMsg(iface)
|
|
}
|
|
return nil, fmt.Errorf("failed to open interface %q: %w", iface, err)
|
|
}
|
|
if filter != "" {
|
|
if err := handle.SetBPFFilter(filter); err != nil {
|
|
handle.Close()
|
|
return nil, fmt.Errorf("invalid BPF filter %q: %w", filter, err)
|
|
}
|
|
}
|
|
return handle, nil
|
|
}
|
|
|
|
// StartCapture opens a capture handle and returns a channel of gopacket.Packet.
|
|
// The channel is closed when ctx is cancelled (Ctrl+C) or capture ends.
|
|
// If filter is non-empty, applies a BPF filter to the handle (CAPT-05).
|
|
// Uses non-blocking send with drop counter per Pitfall 4 from RESEARCH.md.
|
|
// Uses dynamic link type detection per Pitfall 6 from RESEARCH.md.
|
|
func StartCapture(ctx context.Context, iface string, filter string) (<-chan gopacket.Packet, *int64, error) {
|
|
handle, err := OpenCapture(ctx, iface, filter)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
lt := layers.LinkType(handle.LinkType())
|
|
packetSource := gopacket.NewPacketSource(handle, lt)
|
|
packetSource.NoCopy = true // safe: go-pcap copies internally
|
|
|
|
packets := make(chan gopacket.Packet, 512)
|
|
var droppedPackets int64
|
|
|
|
go func() {
|
|
defer close(packets)
|
|
defer handle.Close()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
default:
|
|
}
|
|
pkt, err := packetSource.NextPacket()
|
|
if err != nil {
|
|
// Context cancelled or handle closed
|
|
return
|
|
}
|
|
select {
|
|
case packets <- pkt:
|
|
default:
|
|
atomic.AddInt64(&droppedPackets, 1)
|
|
}
|
|
}
|
|
}()
|
|
|
|
return packets, &droppedPackets, nil
|
|
}
|
|
|
|
// isPermissionError checks if the error indicates insufficient privileges.
|
|
func isPermissionError(err error) bool {
|
|
s := strings.ToLower(err.Error())
|
|
return strings.Contains(s, "permission denied") ||
|
|
strings.Contains(s, "operation not permitted") ||
|
|
strings.Contains(s, "pcap_create")
|
|
}
|
|
|
|
// permissionErrorMsg returns a platform-specific privilege hint (D-05).
|
|
func permissionErrorMsg(iface string) error {
|
|
bin, _ := os.Executable()
|
|
if bin == "" {
|
|
bin = "netsynth"
|
|
}
|
|
switch runtime.GOOS {
|
|
case "linux":
|
|
return fmt.Errorf(
|
|
"packet capture requires root or CAP_NET_RAW.\n"+
|
|
"Run as root: sudo %s -i %s\n"+
|
|
"Or grant capability: sudo setcap cap_net_raw+ep %s",
|
|
bin, iface, bin)
|
|
default: // darwin and others
|
|
return fmt.Errorf(
|
|
"packet capture requires root privileges.\n"+
|
|
"Run as root: sudo %s -i %s", bin, iface)
|
|
}
|
|
}
|