docs(phase-01): research capture and classification phase

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-25 11:22:54 +01:00
co-authored by Claude Sonnet 4.6
parent dc45e97431
commit 55f74b66cb
@@ -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>
## 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
</user_constraints>
---
<phase_requirements>
## 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 |
</phase_requirements>
---
## 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)