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) } }