Files
yoloyolo/.planning/phases/01-capture-and-classification/01-RESEARCH.md
T
2026-03-25 11:22:54 +01:00

32 KiB

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

# 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

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.

// 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

// 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:

// 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

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

// 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

// 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:

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.

lt := layers.LinkType(handle.LinkType())
packetSource := gopacket.NewPacketSource(handle, lt)

Code Examples

Verified patterns from official sources:

Opening a Live Capture Handle

// 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

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

// 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

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

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

// 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/netnet.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)