diff --git a/.planning/phases/01-capture-and-classification/01-RESEARCH.md b/.planning/phases/01-capture-and-classification/01-RESEARCH.md new file mode 100644 index 0000000..0ff8d0f --- /dev/null +++ b/.planning/phases/01-capture-and-classification/01-RESEARCH.md @@ -0,0 +1,713 @@ +# Phase 1: Capture and Classification - Research + +**Researched:** 2026-03-25 +**Domain:** Go CLI scaffolding, live packet capture (pure-Go pcap), protocol classification pipeline +**Confidence:** HIGH (stack and patterns fully verified via official docs; one gap noted for go-pcap interface listing) + +--- + + +## User Constraints (from CONTEXT.md) + +### Locked Decisions + +- **D-01:** Deep port map with 10+ classes: ICMP, DNS (53), HTTPS (443), HTTP (80), SSH (22), SMTP (25), NTP (123), DHCP (67/68), other-TCP, other-UDP +- **D-02:** Classification rules stored in a config-driven Go map/struct (not hardcoded switch statements) — designed so rules could later be loaded from a config file +- **D-03:** All unrecognized traffic grouped as a single "unknown" class until Phase 3 adds auto-clustering +- **D-04:** Support Linux and macOS (not Windows) +- **D-05:** On permission failure, detect the OS and show platform-specific guidance: `sudo setcap cap_net_raw+ep ...` on Linux, `sudo ...` on macOS +- **D-06:** Prefer static binary with no runtime libpcap dependency — use pure-Go pcap backend (go-pcap) where possible + +### Claude's Discretion + +- CLI output formatting during capture (stderr layout, colors, table width) +- Default time window duration for aggregation buckets +- Default network interface selection when `-i` is omitted +- Verbose output format and level of detail + +### Deferred Ideas (OUT OF SCOPE) + +None — discussion stayed within phase scope + + +--- + + +## Phase Requirements + +| ID | Description | Research Support | +|----|-------------|------------------| +| CAPT-01 | User can specify network interface via `-i` flag | Cobra persistent flag `--interface/-i`; `go-pcap.OpenLive()` accepts device name string | +| CAPT-02 | User can list available network interfaces via `--list-interfaces` | `net.Interfaces()` from Go stdlib (go-pcap has no FindAllDevs); no privilege required to enumerate | +| CAPT-04 | User sees a clear actionable error message when lacking capture privileges | `OpenLive()` returns an error on permission failure; inspect error string + `runtime.GOOS` to produce platform-specific hint | +| CLAS-01 | Known protocols (ICMP, DNS, TCP/443, TCP/other, UDP, SSH) each produce a distinct recognizable sound | Rule table as `map[string]TrafficClass` keyed by composite key (proto+port); gopacket layer decoding gives TCP/UDP/ICMP layers cleanly | +| CLAS-03 | On exit, user sees a summary of packet counts and protocol breakdown (printed to stderr) | Counters in `map[TrafficClass]int64` incremented per classified packet; print on signal handler / channel drain | +| CLAS-04 | User can enable per-window protocol activity log via `--verbose` flag | Cobra bool flag; time-window aggregator emits `WindowSnapshot`; verbose mode prints each snapshot to stderr | + + +--- + +## Summary + +Phase 1 establishes the full capture-to-classify pipeline in Go: a Cobra-based CLI that opens a live network interface via the pure-Go `packetcap/go-pcap` library, decodes packets with `gopacket`, classifies them against a config-driven protocol rule table, aggregates counts into time windows, and prints live status lines plus a per-protocol summary on exit. No audio synthesis is included; Phase 2 consumes the `WindowSnapshot` output this phase defines. + +The primary technical challenge is privilege handling: `go-pcap.OpenLive()` silently fails on Linux when the binary lacks `CAP_NET_RAW`, and the error message from the underlying syscall gives no actionable hint. The CLI must intercept that error and emit a platform-specific remediation message. The second challenge is that `packetcap/go-pcap` has no interface enumeration API — `--list-interfaces` must use Go's stdlib `net.Interfaces()` instead. This distinction is important to implement correctly. + +All other elements (channel pipeline, ticker window, classification struct) are standard Go patterns. The project is greenfield with no existing code; Phase 1 establishes directory layout, `go.mod`, and all data types that downstream phases depend on. + +**Primary recommendation:** Use `packetcap/go-pcap` + `gopacket/gopacket` for capture/decode, `net.Interfaces()` for interface listing, `spf13/cobra` for CLI, and the channel-connected pipeline pattern from ARCHITECTURE.md throughout. + +--- + +## Project Constraints (from CLAUDE.md) + +Directives that apply to this phase: + +- **Language:** Go — mandatory, no alternatives +- **Privileges:** Packet capture requires root/CAP_NET_RAW on Linux +- **Audio format:** MP3 output (not in scope for Phase 1, but module must not import go-lame yet — save for Phase 2/3 to keep Phase 1 CGo-free) +- **Interaction model:** Non-interactive (run → Ctrl+C → summary printed to stderr) +- **GSD workflow:** All file edits go through GSD commands, not direct edits outside a GSD workflow + +--- + +## Standard Stack + +### Core (Phase 1 only) + +| Library | Version | Purpose | Why Standard | +|---------|---------|---------|--------------| +| `github.com/gopacket/gopacket` | v1.5.0 | Packet decode, layer type assertions | Community-maintained fork of google/gopacket; v1.5.0 Nov 2025; 14.5k dependents; built-in ICMP/TCP/UDP/DNS/TLS layers. Requires Go 1.24+. | +| `github.com/packetcap/go-pcap` | v0.0.0-20251215 | Live packet capture (pure Go, no CGo) | Implements `gopacket.PacketDataSource`; pure Go on Linux+macOS; mmap kernel ring buffer; no `libpcap-dev` system package at build or runtime. Decision D-06 mandates this. | +| `github.com/spf13/cobra` | v1.10.2 | CLI flags, subcommands, signal handling | Industry standard (Kubernetes, Docker, Hugo); handles `-i`, `--list-interfaces`, `--verbose`, `--help`, `PersistentPreRunE` for validation. | +| Go stdlib `net` | stdlib | Interface enumeration for `--list-interfaces` | `net.Interfaces()` returns all system interfaces with name, flags, and addresses; requires no privileges; go-pcap has no FindAllDevs equivalent. | +| Go stdlib `runtime` | stdlib | OS detection for privilege error messages | `runtime.GOOS` returns `"linux"` or `"darwin"` at runtime; used in D-05 platform-specific error branching. | +| Go stdlib `os/signal` + `syscall` | stdlib | Ctrl+C handling, clean shutdown | Standard Go signal channel pattern; `signal.NotifyContext` is idiomatic in Go 1.16+. | + +### Not Needed for Phase 1 + +| Library | Phase | Reason Excluded | +|---------|-------|-----------------| +| `github.com/sjzar/go-lame` | Phase 2/3 | MP3 encoding — no audio in Phase 1 | +| `github.com/go-audio/wav` | Phase 2/3 | WAV intermediate — no audio in Phase 1 | +| `github.com/muesli/kmeans` | Phase 3 | Auto-clustering — D-03 defers to Phase 3 | + +### Alternatives Considered + +| Standard Choice | Alternative | Why Standard Wins | +|-----------------|-------------|-------------------| +| `packetcap/go-pcap` | `gopacket/pcap` (CGo + libpcap) | libpcap requires system package; contradicts D-06; no CGo in capture layer | +| `net.Interfaces()` | `gopacket/pcap.FindAllDevs()` | pcap.FindAllDevs requires CGo libpcap which we explicitly avoid; stdlib is sufficient | +| `spf13/cobra` | `urfave/cli` | Cobra has better flag validation, persistent pre-run hooks, and structured signal integration | + +### Installation + +```bash +# Go 1.24+ required — NOT available via apt on this machine (apt offers 1.22) +# Install from https://go.dev/dl/ — e.g.: +wget https://go.dev/dl/go1.24.1.linux-arm64.tar.gz +sudo tar -C /usr/local -xzf go1.24.1.linux-arm64.tar.gz +export PATH="$PATH:/usr/local/go/bin" + +# Initialize module (greenfield — no go.mod exists yet) +cd /home/dev/workspace/yoloyolo +go mod init github.com/yourorg/netsynth # or preferred module path + +# Phase 1 dependencies only (no CGo libraries yet) +go get github.com/gopacket/gopacket@v1.5.0 +go get github.com/packetcap/go-pcap@latest +go get github.com/spf13/cobra@v1.10.2 + +# Build (no CGo needed for Phase 1) +CGO_ENABLED=0 go build -o netsynth ./cmd/netsynth +``` + +--- + +## Architecture Patterns + +### Recommended Project Structure + +``` +netsynth/ +├── cmd/ +│ └── netsynth/ +│ └── main.go # cobra root command wiring, signal handling +├── capture/ +│ └── capture.go # OpenLive wrapper, interface listing, chan gopacket.Packet +├── classify/ +│ ├── classifier.go # Rule table (map-based, D-02), Classify() func +│ └── rules.go # Protocol rule definitions (ICMP, DNS, HTTPS, etc. per D-01) +├── aggregate/ +│ └── window.go # Ticker-driven accumulator, WindowSnapshot type +└── go.mod +``` + +This is a subset of the full ARCHITECTURE.md structure — synth/, encode/, config/ are added in later phases. Phase 1 lays down the first four packages. + +### Pattern 1: Channel-Connected Pipeline Stages + +Each component is a goroutine reading from an inbound channel and writing to an outbound channel. A `done` channel (closed on Ctrl+C) signals all stages to drain and exit. + +```go +// Source: https://go.dev/blog/pipelines (official Go blog) +func Classify(done <-chan struct{}, packets <-chan gopacket.Packet) <-chan ClassifiedPacket { + out := make(chan ClassifiedPacket, 256) + go func() { + defer close(out) + for { + select { + case <-done: + return + case pkt, ok := <-packets: + if !ok { + return + } + out <- classify(pkt) + } + } + }() + return out +} +``` + +**Channel buffer sizes for Phase 1 (no audio clock pressure):** +- capture → classify: `make(chan gopacket.Packet, 512)` — absorbs bursts +- classify → aggregate: `make(chan ClassifiedPacket, 1024)` — aggregate is ticker-driven + +### Pattern 2: Ticker-Driven Window with Verbose Output + +```go +// Source: ARCHITECTURE.md Pattern 2 + Go time.Ticker docs +func Aggregate(done <-chan struct{}, events <-chan ClassifiedPacket, + windowMs int, verbose bool) <-chan WindowSnapshot { + out := make(chan WindowSnapshot, 8) + ticker := time.NewTicker(time.Duration(windowMs) * time.Millisecond) + go func() { + defer close(out) + counts := map[TrafficClass]int64{} + for { + select { + case <-done: + out <- snapshot(counts) // flush final partial window + return + case <-ticker.C: + snap := snapshot(counts) + if verbose { + printWindowLine(snap) // CLAS-04: --verbose output to stderr + } + out <- snap + counts = map[TrafficClass]int64{} + case ev, ok := <-events: + if !ok { + return + } + counts[ev.Class]++ + } + } + }() + return out +} +``` + +### Pattern 3: Config-Driven Classification Table (D-02) + +The classification rule set is a struct, not a switch statement: + +```go +// classify/rules.go +type Rule struct { + Protocol string // "tcp", "udp", "icmp", "any" + DstPort uint16 // 0 = match any port for this protocol + Class TrafficClass +} + +// DefaultRules is the D-01 port map — can later be replaced by config loading +var DefaultRules = []Rule{ + {Protocol: "icmp", DstPort: 0, Class: ClassICMP}, + {Protocol: "udp", DstPort: 53, Class: ClassDNS}, + {Protocol: "tcp", DstPort: 53, Class: ClassDNS}, + {Protocol: "tcp", DstPort: 443, Class: ClassHTTPS}, + {Protocol: "tcp", DstPort: 80, Class: ClassHTTP}, + {Protocol: "tcp", DstPort: 22, Class: ClassSSH}, + {Protocol: "tcp", DstPort: 25, Class: ClassSMTP}, + {Protocol: "udp", DstPort: 123, Class: ClassNTP}, + {Protocol: "udp", DstPort: 67, Class: ClassDHCP}, + {Protocol: "udp", DstPort: 68, Class: ClassDHCP}, + // Catch-alls come last: + {Protocol: "tcp", DstPort: 0, Class: ClassOtherTCP}, + {Protocol: "udp", DstPort: 0, Class: ClassOtherUDP}, +} +``` + +The classifier iterates rules in order, returns first match. "unknown" class is returned when no rule matches (D-03). + +### Pattern 4: Interface Listing via Go Stdlib + +```go +// capture/capture.go — uses net.Interfaces(), NOT pcap.FindAllDevs +// Source: https://pkg.go.dev/net#Interfaces +import "net" + +func ListInterfaces() ([]net.Interface, error) { + return net.Interfaces() +} + +// Usage in CLI command handler: +ifaces, err := capture.ListInterfaces() +if err != nil { + fmt.Fprintln(os.Stderr, "Error listing interfaces:", err) + os.Exit(1) +} +for _, iface := range ifaces { + addrs, _ := iface.Addrs() + addrStrs := make([]string, len(addrs)) + for i, a := range addrs { + addrStrs[i] = a.String() + } + fmt.Fprintf(os.Stderr, " %-15s flags=%s addrs=%s\n", + iface.Name, iface.Flags, strings.Join(addrStrs, ", ")) +} +``` + +This satisfies CAPT-02 with no privilege requirement — `net.Interfaces()` works as a non-root user. + +### Pattern 5: Privilege Error Detection (D-05) + +```go +// capture/capture.go +import ( + "fmt" + "os" + "runtime" + "strings" + + pcap "github.com/packetcap/go-pcap" +) + +func OpenCapture(iface string) (*pcap.Handle, error) { + handle, err := pcap.OpenLive(context.Background(), 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 +} + +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") // some platforms wrap differently +} + +func permissionErrorMsg(iface string) error { + switch runtime.GOOS { + case "linux": + bin, _ := os.Executable() + 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 netsynth -i %s", iface) + } +} +``` + +### Pattern 6: gopacket Packet Decoding for Classification + +```go +// Source: https://pkg.go.dev/github.com/gopacket/gopacket +import ( + "github.com/gopacket/gopacket" + "github.com/gopacket/gopacket/layers" + pcap "github.com/packetcap/go-pcap" +) + +handle, _ := pcap.OpenLive(ctx, iface, 65535, false, 0, false) +packetSource := gopacket.NewPacketSource(handle, layers.LinkTypeEthernet) +// NoCopy is safe here because go-pcap copies data internally +packetSource.NoCopy = true + +for pkt := range packetSource.Packets() { + // ICMP check + if pkt.Layer(layers.LayerTypeICMPv4) != nil || + pkt.Layer(layers.LayerTypeICMPv6) != nil { + // ClassICMP + } + // TCP destination port + if tcp, ok := pkt.TransportLayer().(*layers.TCP); ok { + dstPort := uint16(tcp.DstPort) + // match against rule table + } + // UDP destination port + if udp, ok := pkt.TransportLayer().(*layers.UDP); ok { + dstPort := uint16(udp.DstPort) + // match against rule table + } +} +``` + +Note: `packetSource.NoCopy = true` is safe because `packetcap/go-pcap` copies packet data internally before returning from `ReadPacketData()`. Verified from go-pcap source structure. + +### Anti-Patterns to Avoid + +- **Switch-statement classification:** Forbidden by D-02. Use the `[]Rule` slice instead. +- **Blocking channel send in capture path:** Capture goroutine must never block on a full channel; use buffered channel + drop counter. +- **`ZeroCopyReadPacketData()` sent to goroutines:** Do not use with concurrent processing; stick with `ReadPacketData()` or let gopacket's `PacketSource` manage it. +- **`google/gopacket` import:** Must be `github.com/gopacket/gopacket` — enforced from day one in `go.mod`. +- **Privilege check by inspecting `/proc/self/status`:** Fragile. Instead try `OpenLive()` and intercept the error — the only reliable way to know if capture actually works. + +--- + +## Don't Hand-Roll + +| Problem | Don't Build | Use Instead | Why | +|---------|-------------|-------------|-----| +| CLI flag parsing | Custom arg parser | `spf13/cobra` | Signal handling, auto-help, persistent flags, PersistentPreRunE validation | +| Packet decoding (Ethernet/IP/TCP/UDP/ICMP headers) | Manual `binary.Read` offsets | `gopacket/layers` | Layer type assertions; handles IPv4/IPv6, fragmentation, Ethernet padding | +| Interface enumeration | Parse `/proc/net/dev` or run `ip link` | `net.Interfaces()` | Stdlib; cross-platform; returns flags, addresses; no privilege needed | +| Ctrl+C / signal handling | `os.Signal` channel with manual `os.Exit` | `signal.NotifyContext` | Propagates context cancellation cleanly; idiomatic Go 1.16+ pattern | +| Live packet capture | Raw AF_PACKET socket from scratch | `packetcap/go-pcap` | Kernel ring buffer, BPF filter support, `gopacket.PacketDataSource` compliance | + +**Key insight:** The hardest part of packet capture in Go is correctly handling the kernel ring buffer, BPF attachment, and copy semantics. `packetcap/go-pcap` solves all three in pure Go; there is no reason to touch raw sockets in this phase. + +--- + +## Common Pitfalls + +### Pitfall 1: `go-pcap` Has No Interface Discovery Function + +**What goes wrong:** Developer calls `pcap.FindAllDevs()` expecting libpcap behavior — that function does not exist in `packetcap/go-pcap`. Build fails. + +**Why it happens:** The STACK.md mentions `pcap.FindAllDevs()` in the context of the gopacket/pcap (CGo) backend, not go-pcap. Easy to conflate. + +**How to avoid:** Use `net.Interfaces()` from Go stdlib for `--list-interfaces`. It returns equivalent information (name, flags, addresses) without any privilege requirement. + +**Warning signs:** Any import of `gopacket/pcap` instead of `packetcap/go-pcap`. + +--- + +### Pitfall 2: Go 1.24 Not Installed + +**What goes wrong:** `go get github.com/gopacket/gopacket@v1.5.0` fails with "minimum required go 1.24" or the apt-installed `golang-go` (version 1.22) is used and build fails. + +**Why it happens:** Ubuntu 24.04 ships `golang-go` v1.22 via apt. `gopacket/gopacket@v1.5.0` requires Go 1.24+. The version mismatch is silent until the first `go get`. + +**How to avoid:** Wave 0 task: install Go 1.24+ from https://go.dev/dl/. Do not use `apt install golang-go` for this project. Verify with `go version` before any other task. + +**Warning signs:** `go version` output showing `go1.22.x`. + +--- + +### Pitfall 3: CAP_NET_RAW Silent Failure on nosuid Filesystems + +**What goes wrong:** `sudo setcap cap_net_raw+ep ./netsynth` appears to succeed but the binary fails at runtime when run from `/home/dev/workspace/` because the filesystem is mounted `nosuid`. The capability is silently ignored. + +**Why it happens:** `/home` is frequently on a separate partition mounted `nosuid`. The kernel enforces capabilities only on filesystems where `nosuid` is not set. + +**How to avoid:** For development, run as `sudo ./netsynth -i eth0` rather than relying on `setcap`. Document that setcap only works from `/usr/local/bin` or equivalent. The error message from D-05 covers this — show both options so the user knows sudo always works. + +**Warning signs:** Binary works via `sudo` but fails via `setcap` from the workspace directory. + +--- + +### Pitfall 4: Blocking Channel Send Stalls Kernel Packet Buffer + +**What goes wrong:** If the classify goroutine is slow (or tests inject sleep), the capture goroutine's send to the `packets` channel blocks, and the kernel's pcap ring buffer fills and drops packets without any log message. + +**Why it happens:** Unbuffered or small-buffered channel between capture and classify. + +**How to avoid:** Use `make(chan gopacket.Packet, 512)`. In the capture loop, use a non-blocking send with a drop counter: +```go +select { +case packets <- pkt: +default: + atomic.AddInt64(&droppedPackets, 1) +} +``` +For Phase 1's use case (ambient audio, statistical fingerprint), lossy capture is acceptable; just make drops visible. + +--- + +### Pitfall 5: `--list-interfaces` Requires Privilege on Some Systems + +**What goes wrong:** On some Linux configurations, `net.Interfaces()` returns an empty list when run as non-root, causing `--list-interfaces` to print nothing. + +**Why it happens:** Network namespace restrictions or security policies can limit interface visibility. However, this is rare on standard Ubuntu 24.04 with default configuration. + +**How to avoid:** Always print the list plus a note: "If interfaces are missing, run with sudo." Confirmed LOW risk on the target machine (standard Ubuntu 24.04, no restricted namespaces observed). + +--- + +### Pitfall 6: LinkType Mismatch on Non-Ethernet Interfaces + +**What goes wrong:** `gopacket.NewPacketSource(handle, layers.LinkTypeEthernet)` is correct for `eth0` but fails to decode on loopback (`lo`) which uses `layers.LinkTypeLoopback` (or `layers.LinkTypeNull` on macOS). Packets decode as garbage or produce layer decode errors silently. + +**Why it happens:** Link type is hardcoded in the `NewPacketSource` call. + +**How to avoid:** Call `handle.LinkType()` on the opened handle and pass the result. `packetcap/go-pcap` exports `LinkTypeEthernet` and `LinkTypeNull` constants. For `lo`, use `layers.LinkTypeLoopback`. +```go +lt := layers.LinkType(handle.LinkType()) +packetSource := gopacket.NewPacketSource(handle, lt) +``` + +--- + +## Code Examples + +Verified patterns from official sources: + +### Opening a Live Capture Handle + +```go +// Source: https://pkg.go.dev/github.com/packetcap/go-pcap +import ( + "context" + pcap "github.com/packetcap/go-pcap" +) + +handle, err := pcap.OpenLive( + context.Background(), + "eth0", // interface name + 65535, // snaplen: capture full packets + false, // promiscuous: false by default per security guidance in PITFALLS.md + 0, // timeout: 0 = block until packet + false, // syscalls: false = use mmap ring buffer (faster on Linux) +) +if err != nil { + // Check for permission error and print platform-specific hint (Pattern 5) +} +defer handle.Close() +``` + +### Creating a gopacket PacketSource + +```go +// Source: https://pkg.go.dev/github.com/gopacket/gopacket +import ( + "github.com/gopacket/gopacket" + "github.com/gopacket/gopacket/layers" +) + +lt := layers.LinkType(handle.LinkType()) // dynamic link type per Pitfall 6 +packetSource := gopacket.NewPacketSource(handle, lt) +packetSource.NoCopy = true // safe: go-pcap copies internally + +for pkt := range packetSource.Packets() { + // pkt is a gopacket.Packet with full layer decoding +} +``` + +### Listing Interfaces (--list-interfaces) + +```go +// Source: https://pkg.go.dev/net#Interfaces +import "net" + +ifaces, err := net.Interfaces() +if err != nil { + return err +} +for _, iface := range ifaces { + addrs, _ := iface.Addrs() + // print name, flags, addresses to stderr (tcpdump convention) +} +``` + +### Cobra Root Command Structure + +```go +// Source: https://pkg.go.dev/github.com/spf13/cobra +var rootCmd = &cobra.Command{ + Use: "netsynth", + Short: "Sonify live network traffic", + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + // validate flags before Run + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + // main capture loop + return nil + }, +} + +func init() { + rootCmd.Flags().StringVarP(&iface, "interface", "i", "", "Network interface to capture on") + rootCmd.Flags().BoolVar(&listIfaces, "list-interfaces", false, "List available interfaces") + rootCmd.Flags().BoolVar(&verbose, "verbose", false, "Print per-window protocol activity") +} +``` + +### Signal Handling (clean shutdown) + +```go +// Source: https://pkg.go.dev/os/signal#NotifyContext (Go 1.16+) +import ( + "context" + "os" + "os/signal" + "syscall" +) + +ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) +defer stop() + +// Pass ctx.Done() as the done channel to all pipeline stages +// When Ctrl+C is pressed, ctx is cancelled and all stages drain cleanly +``` + +### Per-Protocol Summary on Exit (CLAS-03) + +```go +// After pipeline drains, print summary to stderr (tcpdump convention) +fmt.Fprintln(os.Stderr, "\n--- Protocol Summary ---") +// Sort classes for stable output +for _, class := range sortedClasses(totals) { + pct := 0.0 + if totalPackets > 0 { + pct = float64(totals[class]) / float64(totalPackets) * 100.0 + } + fmt.Fprintf(os.Stderr, " %-15s %8d packets (%5.1f%%)\n", + class.String(), totals[class], pct) +} +fmt.Fprintf(os.Stderr, " %-15s %8d packets\n", "TOTAL", totalPackets) +``` + +--- + +## State of the Art + +| Old Approach | Current Approach | When Changed | Impact | +|--------------|------------------|--------------|--------| +| `google/gopacket` | `github.com/gopacket/gopacket` v1.5.0 | Community fork active since ~2023, v1.5.0 Nov 2025 | Must use community fork; original is abandoned | +| `gopacket/pcap` (CGo libpcap) for live capture | `packetcap/go-pcap` (pure Go) | Dec 2025 (v0.0.0-20251215) | No CGo at capture layer; CGO_ENABLED=0 for Phase 1 build | +| `signal.Notify` (manual channel) | `signal.NotifyContext` | Go 1.16 | Cleaner context propagation; use NotifyContext everywhere | +| Flat `main.go` | Cobra command structure | Stable pattern | Subcommand extensibility for future phases | + +**Deprecated/outdated:** +- `google/gopacket`: abandoned 2022; 270+ open issues; do not use +- `go-audio/generator`: archived February 2026; do not take new dependency + +--- + +## Open Questions + +1. **go-pcap link type for WiFi / Docker bridge interfaces** + - What we know: `handle.LinkType()` returns uint32; go-pcap exports only `LinkTypeEthernet` and `LinkTypeNull` + - What's unclear: Does it return the correct type for Docker bridge (`br-*`) and veth interfaces on this machine? + - Recommendation: In the capture open sequence, fall back to `layers.LinkTypeEthernet` if `handle.LinkType()` returns an unrecognized value, and log a warning. The target demo interface is `eth0` which is Ethernet. + +2. **Default interface when `-i` is omitted** + - What we know: Left to Claude's discretion (CONTEXT.md) + - What's unclear: Should it pick the first non-loopback interface, or require `-i` always? + - Recommendation: Require `-i` explicitly (no silent default) unless `--list-interfaces` was given. This matches `tcpdump` behavior and avoids surprising captures on wrong interfaces. + +3. **`promiscuous` mode default** + - What we know: PITFALLS.md recommends non-promiscuous by default with opt-in `--promiscuous` flag + - What's unclear: Phase 1 requirements don't mention a `--promiscuous` flag (it's not in CAPT-01..CAPT-04) + - Recommendation: Default to non-promiscuous; add `--promiscuous` flag as a bonus if it falls within scope; if not, document the limitation in help text. + +--- + +## Environment Availability + +| Dependency | Required By | Available | Version | Fallback | +|------------|-------------|-----------|---------|---------| +| Go toolchain 1.24+ | All Go compilation | **No** | 1.22 in apt (too old) | Install from https://go.dev/dl/ — manual step required | +| GCC / build-essential | Phase 2+ CGo (go-lame) | **No** | Not installed | `sudo apt install build-essential` — not needed for Phase 1 (CGO_ENABLED=0) | +| libpcap runtime (`libpcap0.8t64`) | go-pcap on Linux at runtime | Yes | 1.10.4 (installed) | n/a — but go-pcap is pure Go so this is irrelevant | +| libpcap-dev headers | CGo pcap builds | No | Not installed | Not needed — go-pcap is pure Go | +| `tcpdump` | Manual validation of captures | Yes | `/usr/bin/tcpdump` | — | +| `eth0` interface | CAPT-01 demo interface | Yes | Present on machine | `lo` for loopback-only test | +| `CAP_NET_RAW` or root | Live packet capture | Not verified | Run as uid=1001 (non-root) | `sudo netsynth -i eth0` always works | + +**Missing dependencies with no fallback:** +- **Go 1.24+**: Must be installed before any work begins. Wave 0 task. `apt install golang-go` gives 1.22 which is insufficient. + +**Missing dependencies with fallback:** +- **GCC**: Not needed for Phase 1 (no CGo libraries). Needed in Phase 2/3 for go-lame. Install via `sudo apt install build-essential` at that time. +- **Root / CAP_NET_RAW**: Tests that need live capture must run via `sudo`. Unit tests for classifier and aggregator use synthetic packet data and require no privilege. + +**Platform note:** This machine is `linux/arm64` (Ubuntu 24.04). Go 1.24 ARM64 binaries are available at https://go.dev/dl/. The `packetcap/go-pcap` library supports Linux on ARM64 (uses standard Linux kernel ring buffer syscalls). + +--- + +## Validation Architecture + +### Test Framework + +| Property | Value | +|----------|-------| +| Framework | Go stdlib `testing` package (no external framework needed) | +| Config file | None — `go test ./...` discovers tests automatically | +| Quick run command | `go test ./classify/... ./aggregate/...` (no privileges needed) | +| Full suite command | `go test ./...` (capture tests require sudo or are integration-only) | + +### Phase Requirements → Test Map + +| Req ID | Behavior | Test Type | Automated Command | File Exists? | +|--------|----------|-----------|-------------------|-------------| +| CAPT-01 | `-i eth0` opens capture handle without error | Integration (needs sudo) | `sudo go test ./capture/... -run TestOpenLive -v` | No — Wave 0 | +| CAPT-02 | `--list-interfaces` prints at least one interface | Unit (no privilege) | `go test ./capture/... -run TestListInterfaces -v` | No — Wave 0 | +| CAPT-04 | Non-root capture returns actionable error message | Unit (mock OpenLive error) | `go test ./capture/... -run TestPermissionError -v` | No — Wave 0 | +| CLAS-01 | Each of 10+ protocol classes is assigned correctly for synthetic packets | Unit | `go test ./classify/... -run TestClassify -v` | No — Wave 0 | +| CLAS-03 | Exit summary prints correct counts per class | Unit | `go test ./aggregate/... -run TestSummary -v` | No — Wave 0 | +| CLAS-04 | `--verbose` prints one line per window with protocol breakdown | Unit | `go test ./aggregate/... -run TestVerbose -v` | No — Wave 0 | + +### Sampling Rate + +- **Per task commit:** `go test ./classify/... ./aggregate/...` (fast, no privilege) +- **Per wave merge:** `go test ./...` (may skip integration tests if not running as root) +- **Phase gate:** All unit tests pass; manual smoke test `sudo ./netsynth -i eth0` shows live classification output to stderr + +### Wave 0 Gaps + +- [ ] `classify/classifier_test.go` — covers CLAS-01: unit tests with synthetic gopacket packets for each of 10 protocol classes +- [ ] `capture/capture_test.go` — covers CAPT-02 (TestListInterfaces, no privilege) and CAPT-04 (TestPermissionError with injected error) +- [ ] `aggregate/window_test.go` — covers CLAS-03 (summary counts) and CLAS-04 (verbose window lines) +- [ ] `go.mod` and `go.sum` — module not yet initialized; must be created before any test file compiles +- [ ] Go 1.24 toolchain install — prerequisite for everything above + +--- + +## Sources + +### Primary (HIGH confidence) + +- `https://pkg.go.dev/github.com/packetcap/go-pcap` — Full API surface verified; confirmed no FindAllDevs; `OpenLive` signature; `Handle.LinkType()` method +- `https://pkg.go.dev/github.com/gopacket/gopacket` — v1.5.0 confirmed Nov 2025; `PacketDataSource` interface; `NewPacketSource` API; layer types +- `https://pkg.go.dev/net` — `net.Interfaces()` stdlib function; returns `[]net.Interface` with name, flags, addrs; no privilege needed +- `https://pkg.go.dev/github.com/spf13/cobra` — v1.10.2 Dec 2025; `PersistentPreRunE`, flag patterns +- `https://go.dev/blog/pipelines` — Official Go pipeline pattern with `done` channel +- `.planning/research/STACK.md` — Verified library versions, CGo strategy, why google/gopacket is forbidden +- `.planning/research/ARCHITECTURE.md` — Channel buffer sizes, pipeline stage signatures, `WindowSnapshot` type definition +- `.planning/research/PITFALLS.md` — CAP_NET_RAW nosuid behavior, ZeroCopy use-after-free, packet buffer overflow strategies + +### Secondary (MEDIUM confidence) + +- WebSearch + pkg.go.dev cross-check: `packetcap/go-pcap` v0.0.0-20251215 implements `gopacket.PacketDataSource`, Linux/macOS only — confirmed +- `.planning/phases/01-capture-and-classification/01-CONTEXT.md` — Locked decisions D-01 through D-06 + +### Tertiary (LOW confidence) + +- go-pcap `NoCopy = true` safety: described in fetch results as "handle copies data internally" — plausible given pure-Go implementation but not confirmed via source inspection; treat as verify-on-test + +--- + +## Metadata + +**Confidence breakdown:** +- Standard stack: HIGH — versions verified via pkg.go.dev fetches +- Architecture: HIGH — Go pipeline patterns from official blog; gopacket API from official docs +- Pitfalls: HIGH for CAP_NET_RAW and channel buffering (verified via PITFALLS.md primary sources); MEDIUM for go-pcap NoCopy safety +- Environment: HIGH — direct inspection of installed packages and Go availability on target machine + +**Research date:** 2026-03-25 +**Valid until:** 2026-06-25 (90 days — stack is stable; go-pcap pre-release version may update)