feat(01-02): implement capture package with OpenCapture, ListInterfaces, StartCapture
- ListInterfaces uses net.Interfaces() stdlib (no pcap dependency for listing) - OpenCapture wraps packetcap/go-pcap with platform-specific permission error handling - StartCapture uses buffered channel (512) with atomic drop counter - Dynamic link type detection via layers.LinkType(handle.LinkType()) - Linux error shows setcap cap_net_raw+ep hint; macOS shows sudo hint - Add github.com/packetcap/go-pcap v0.0.0-20251215 to go.mod
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
Reference in New Issue
Block a user