diff --git a/capture/capture.go b/capture/capture.go new file mode 100644 index 0000000..d01bfc1 --- /dev/null +++ b/capture/capture.go @@ -0,0 +1,106 @@ +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). +// Returns a platform-specific error message if permission is denied (CAPT-04, D-05). +func OpenCapture(ctx context.Context, iface 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) + } + 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. +// 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) (<-chan gopacket.Packet, *int64, error) { + handle, err := OpenCapture(ctx, iface) + 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) + } +} diff --git a/go.mod b/go.mod index b3a0b78..285487d 100644 --- a/go.mod +++ b/go.mod @@ -3,3 +3,10 @@ module github.com/netsynth/netsynth go 1.24.1 require github.com/gopacket/gopacket v1.5.0 + +require ( + github.com/packetcap/go-pcap v0.0.0-20251215121130-f2cf9f991e7c // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/sys v0.32.0 // indirect +) diff --git a/go.sum b/go.sum index a0e66be..c411b12 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,18 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gopacket/gopacket v1.5.0 h1:9s9fcSUVKFlRV97B77Bq9XNV3ly2gvvsneFMQUGjc+M= github.com/gopacket/gopacket v1.5.0/go.mod h1:i3NaGaqfoWKAr1+g7qxEdWsmfT+MXuWkAe9+THv8LME= +github.com/packetcap/go-pcap v0.0.0-20251215121130-f2cf9f991e7c h1:B5gWB1LB6OxpoXz+FsLUhQEcCAtMpajhBZ2K0X9KjfE= +github.com/packetcap/go-pcap v0.0.0-20251215121130-f2cf9f991e7c/go.mod h1:1jryUz9E2ndKwZBNHzVhLMzS3WHO0fOKydYi9XWWu9w= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=