--- phase: 01-capture-and-classification plan: 02 type: execute wave: 2 depends_on: ["01-01"] files_modified: - capture/capture.go - capture/capture_test.go autonomous: true requirements: [CAPT-01, CAPT-02, CAPT-04] must_haves: truths: - "User can specify a network interface via -i flag and packets are captured from it" - "User can list available network interfaces via --list-interfaces" - "User without root/CAP_NET_RAW sees platform-specific error with sudo hint" artifacts: - path: "capture/capture.go" provides: "OpenCapture, ListInterfaces, permission error handling" exports: ["OpenCapture", "ListInterfaces", "StartCapture"] - path: "capture/capture_test.go" provides: "Tests for ListInterfaces and permission error messages" min_lines: 60 key_links: - from: "capture/capture.go" to: "github.com/packetcap/go-pcap" via: "pcap.OpenLive for live capture" pattern: "pcap\\.OpenLive" - from: "capture/capture.go" to: "net.Interfaces" via: "stdlib for interface enumeration (NOT pcap.FindAllDevs)" pattern: "net\\.Interfaces" - from: "capture/capture.go" to: "runtime.GOOS" via: "OS detection for platform-specific error messages" pattern: "runtime\\.GOOS" --- Implement the capture package: live packet capture via go-pcap, interface listing via net.Interfaces(), and platform-specific privilege error handling. Purpose: This package is the data source for the entire pipeline. It wraps go-pcap with proper error handling and provides the channel-based packet source that the classifier consumes. Output: capture/ package with OpenCapture, ListInterfaces, StartCapture functions and unit tests. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/01-capture-and-classification/01-CONTEXT.md @.planning/phases/01-capture-and-classification/01-RESEARCH.md @.planning/phases/01-capture-and-classification/01-01-SUMMARY.md ```go package classify type TrafficClass string // 11 constants: ClassICMP, ClassDNS, ClassHTTPS, ClassHTTP, ClassSSH, ClassSMTP, ClassNTP, ClassDHCP, ClassOtherTCP, ClassOtherUDP, ClassUnknown type ClassifiedPacket struct { Class TrafficClass SrcPort uint16 DstPort uint16 Protocol string Length int } type WindowSnapshot struct { Counts map[TrafficClass]int64 TotalPackets int64 WindowIndex int } ``` Task 1: Implement capture package with OpenCapture, ListInterfaces, and StartCapture capture/capture.go, capture/capture_test.go classify/types.go go.mod .planning/phases/01-capture-and-classification/01-RESEARCH.md - TestListInterfaces: net.Interfaces() returns at least one interface (lo always exists) - TestListInterfacesFormat: Each interface has Name and Flags fields populated - TestPermissionErrorLinux: When runtime.GOOS=="linux" and OpenLive fails with "operation not permitted", error message contains "sudo setcap cap_net_raw+ep" - TestPermissionErrorDarwin: When runtime.GOOS=="darwin" and OpenLive fails with "permission denied", error message contains "sudo netsynth" - TestIsPermissionError: Recognizes "permission denied", "operation not permitted", and "pcap_create" error strings - TestIsPermissionErrorNegative: Does not flag "interface not found" as permission error 1. Create capture/capture.go with these functions: ```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). // 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) } } ``` 2. Create capture/capture_test.go: - TestListInterfaces: Call ListInterfaces(), assert len > 0 (lo always exists), assert first interface has non-empty Name - TestIsPermissionError: Table-driven test with positive cases ("permission denied", "operation not permitted", "pcap_create: foo") and negative cases ("interface not found", "no such device") - TestPermissionErrorMsg: Call permissionErrorMsg("eth0") on current OS, verify the error string contains "sudo" and the interface name "eth0". On linux verify it contains "setcap cap_net_raw+ep" - NOTE: Do NOT test OpenCapture or StartCapture directly in unit tests -- they require root/CAP_NET_RAW. Those are integration-tested manually via `sudo ./netsynth -i eth0`. Write tests first (RED), then implement (GREEN). cd /home/dev/workspace/yoloyolo && go test -v -count=1 ./capture/... - capture/capture.go contains `func ListInterfaces() ([]net.Interface, error)` - capture/capture.go contains `func OpenCapture(ctx context.Context, iface string) (*pcap.Handle, error)` - capture/capture.go contains `func StartCapture(ctx context.Context, iface string) (<-chan gopacket.Packet, *int64, error)` - capture/capture.go contains `net.Interfaces()` (NOT pcap.FindAllDevs) - capture/capture.go contains `runtime.GOOS` for platform detection - capture/capture.go contains `"setcap cap_net_raw+ep"` in linux error branch - capture/capture.go contains `atomic.AddInt64(&droppedPackets, 1)` for non-blocking send - capture/capture.go contains `layers.LinkType(handle.LinkType())` for dynamic link type - capture/capture_test.go contains `TestListInterfaces` - capture/capture_test.go contains `TestIsPermissionError` - capture/capture_test.go contains `TestPermissionErrorMsg` - `go test -v -count=1 ./capture/...` exits 0 capture package compiles and tests pass. ListInterfaces uses net.Interfaces (no pcap dependency for listing). Permission errors show platform-specific sudo/setcap hints. StartCapture uses non-blocking send with drop counter and dynamic link type. - `go test -v -count=1 ./capture/...` all tests pass - `go vet ./capture/...` no issues - No import of `gopacket/pcap` (must use `packetcap/go-pcap`) - No import of `google/gopacket` (must use `gopacket/gopacket`) - ListInterfaces returns system interfaces without privileges - Permission error detection covers "permission denied", "operation not permitted", "pcap_create" - Error messages are platform-specific (Linux: setcap hint, macOS: sudo hint) - StartCapture uses buffered channel (512) with atomic drop counter - Dynamic link type detection (not hardcoded LinkTypeEthernet) - All unit tests passing After completion, create `.planning/phases/01-capture-and-classification/01-02-SUMMARY.md`