Files

13 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
01-capture-and-classification 04 execute 3
01-01
01-02
01-03
cmd/netsynth/main.go
cmd/netsynth/main_test.go
false
CAPT-01
CAPT-02
CAPT-04
CLAS-01
CLAS-03
CLAS-04
truths artifacts key_links
User can run `netsynth -i eth0` and see packets being classified live to stderr
User can run `netsynth --list-interfaces` and see available interfaces
User without root sees platform-specific error with sudo hint
On exit (Ctrl+C), user sees per-protocol packet count summary on stderr
User can pass `--verbose` and see per-window protocol activity on stderr
path provides exports
cmd/netsynth/main.go Cobra CLI wiring, signal handling, pipeline assembly
main
path provides min_lines
cmd/netsynth/main_test.go Tests for flag parsing and list-interfaces output 30
from to via pattern
cmd/netsynth/main.go capture/capture.go StartCapture for packet channel, ListInterfaces for --list-interfaces capture.StartCapture|capture.ListInterfaces
from to via pattern
cmd/netsynth/main.go classify/classifier.go NewClassifier + Classify in pipeline goroutine classify.NewClassifier|classifier.Classify
from to via pattern
cmd/netsynth/main.go aggregate/window.go Aggregate consumes classified packets channel aggregate.Aggregate
from to via pattern
cmd/netsynth/main.go aggregate/summary.go PrintSummary on exit, PrintWindowLine for verbose callback aggregate.PrintSummary|aggregate.PrintWindowLine
Wire all packages into the Cobra CLI: flag parsing, signal handling, capture-classify-aggregate pipeline, verbose output, and exit summary.

Purpose: This is the final integration that makes netsynth a runnable tool. It connects capture -> classify -> aggregate stages via channels, handles Ctrl+C gracefully, and prints the exit summary. All Phase 1 success criteria become observable here.

Output: Working netsynth binary with -i, --list-interfaces, --verbose flags. Checkpoint for manual smoke test.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/01-capture-and-classification/01-CONTEXT.md @.planning/phases/01-capture-and-classification/01-RESEARCH.md @.planning/phases/01-capture-and-classification/01-01-SUMMARY.md @.planning/phases/01-capture-and-classification/01-02-SUMMARY.md @.planning/phases/01-capture-and-classification/01-03-SUMMARY.md ```go func ListInterfaces() ([]net.Interface, error) func OpenCapture(ctx context.Context, iface string) (*pcap.Handle, error) func StartCapture(ctx context.Context, iface string) (<-chan gopacket.Packet, *int64, error) ```
type TrafficClass string // 11 constants
type ClassifiedPacket struct { Class TrafficClass; SrcPort, DstPort uint16; Protocol string; Length int }
type WindowSnapshot struct { Counts map[TrafficClass]int64; TotalPackets int64; WindowIndex int }
type Classifier struct { ... }
func NewClassifier(rules []Rule) *Classifier
func (c *Classifier) Classify(pkt gopacket.Packet) ClassifiedPacket
var DefaultRules []Rule
const DefaultWindowMs = 500
func Aggregate(done <-chan struct{}, events <-chan classify.ClassifiedPacket, windowMs int, onSnapshot func(classify.WindowSnapshot)) <-chan classify.WindowSnapshot
func PrintSummary(w io.Writer, totals map[classify.TrafficClass]int64)
func PrintWindowLine(w io.Writer, snap classify.WindowSnapshot)
func AccumulateTotals(totals map[classify.TrafficClass]int64, snap classify.WindowSnapshot)
Task 1: Wire Cobra CLI with capture-classify-aggregate pipeline and signal handling cmd/netsynth/main.go, cmd/netsynth/main_test.go capture/capture.go classify/classifier.go classify/rules.go classify/types.go aggregate/window.go aggregate/summary.go go.mod .planning/phases/01-capture-and-classification/01-RESEARCH.md 1. Create cmd/netsynth/main.go with Cobra root command:
package main

import (
    "context"
    "fmt"
    "os"
    "os/signal"
    "strings"
    "sync/atomic"
    "syscall"

    "github.com/spf13/cobra"

    "github.com/netsynth/netsynth/aggregate"
    "github.com/netsynth/netsynth/capture"
    "github.com/netsynth/netsynth/classify"
)

var (
    ifaceName  string
    listIfaces bool
    verbose    bool
)

func main() {
    rootCmd := &cobra.Command{
        Use:   "netsynth",
        Short: "Sonify live network traffic into ambient audio",
        Long:  "NetSynth captures network traffic, classifies it by protocol, and (in future phases) synthesizes an ambient MP3 soundscape.",
        RunE:  run,
    }

    rootCmd.Flags().StringVarP(&ifaceName, "interface", "i", "", "Network interface to capture on (required unless --list-interfaces)")
    rootCmd.Flags().BoolVar(&listIfaces, "list-interfaces", false, "List available network interfaces and exit")
    rootCmd.Flags().BoolVar(&verbose, "verbose", false, "Print per-window protocol activity to stderr")

    if err := rootCmd.Execute(); err != nil {
        os.Exit(1)
    }
}

func run(cmd *cobra.Command, args []string) error {
    // --list-interfaces mode (CAPT-02)
    if listIfaces {
        return runListInterfaces()
    }

    // Require -i flag
    if ifaceName == "" {
        return fmt.Errorf("interface required: use -i <interface> or --list-interfaces to see available interfaces")
    }

    // Set up signal handling (Ctrl+C)
    ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
    defer stop()

    // Stage 1: Capture (CAPT-01)
    fmt.Fprintf(os.Stderr, "Starting capture on %s... (press Ctrl+C to stop)\n", ifaceName)
    packets, droppedPtr, err := capture.StartCapture(ctx, ifaceName)
    if err != nil {
        return err // CAPT-04: permission error already has platform-specific message
    }

    // Stage 2: Classify (CLAS-01)
    classifier := classify.NewClassifier(classify.DefaultRules)
    classified := make(chan classify.ClassifiedPacket, 1024)
    go func() {
        defer close(classified)
        for pkt := range packets {
            classified <- classifier.Classify(pkt)
        }
    }()

    // Stage 3: Aggregate with optional verbose callback (CLAS-04)
    var onSnapshot func(classify.WindowSnapshot)
    if verbose {
        onSnapshot = func(snap classify.WindowSnapshot) {
            aggregate.PrintWindowLine(os.Stderr, snap) // CLAS-04: --verbose output
        }
    }
    snapshots := aggregate.Aggregate(ctx.Done(), classified, aggregate.DefaultWindowMs, onSnapshot)

    // Consume snapshots and accumulate totals
    totals := make(map[classify.TrafficClass]int64)
    for snap := range snapshots {
        aggregate.AccumulateTotals(totals, snap)
    }

    // Print exit summary (CLAS-03)
    dropped := atomic.LoadInt64(droppedPtr)
    if dropped > 0 {
        fmt.Fprintf(os.Stderr, "\nWarning: %d packets dropped (channel buffer full)\n", dropped)
    }
    aggregate.PrintSummary(os.Stderr, totals)

    return nil
}

func runListInterfaces() error {
    ifaces, err := capture.ListInterfaces()
    if err != nil {
        return fmt.Errorf("error listing interfaces: %w", err)
    }
    if len(ifaces) == 0 {
        fmt.Fprintln(os.Stderr, "No interfaces found. If interfaces are missing, run with sudo.")
        return nil
    }
    fmt.Fprintln(os.Stderr, "Available interfaces:")
    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, ", "))
    }
    return nil
}
  1. Create cmd/netsynth/main_test.go with:

    • TestListInterfacesFlag: Execute rootCmd with --list-interfaces, verify it exits 0 (integration-lite test)
    • TestMissingInterfaceFlag: Execute rootCmd with no flags, verify error message contains "interface required"
    • TestHelpOutput: Execute rootCmd with --help, verify output contains "-i", "--list-interfaces", "--verbose"
  2. Build the binary:

    • CGO_ENABLED=0 go build -o netsynth ./cmd/netsynth
    • Verify: ./netsynth --help shows usage
    • Verify: ./netsynth --list-interfaces shows interfaces
    • Verify: ./netsynth -i eth0 without root shows permission error with sudo hint
  3. Run full test suite: go test -v -race ./... cd /home/dev/workspace/yoloyolo && CGO_ENABLED=0 go build -o netsynth ./cmd/netsynth && ./netsynth --list-interfaces 2>&1 | grep -q "flags=" && go test -v -count=1 ./... <acceptance_criteria>

    • cmd/netsynth/main.go contains rootCmd.Flags().StringVarP(&ifaceName, "interface", "i"
    • cmd/netsynth/main.go contains rootCmd.Flags().BoolVar(&listIfaces, "list-interfaces"
    • cmd/netsynth/main.go contains rootCmd.Flags().BoolVar(&verbose, "verbose"
    • cmd/netsynth/main.go contains signal.NotifyContext
    • cmd/netsynth/main.go contains capture.StartCapture
    • cmd/netsynth/main.go contains classify.NewClassifier(classify.DefaultRules)
    • cmd/netsynth/main.go contains aggregate.Aggregate(
    • cmd/netsynth/main.go contains aggregate.PrintSummary(os.Stderr
    • cmd/netsynth/main.go contains aggregate.PrintWindowLine(os.Stderr
    • CGO_ENABLED=0 go build -o netsynth ./cmd/netsynth exits 0
    • ./netsynth --list-interfaces output contains interface names and "flags="
    • ./netsynth 2>&1 (no flags) output contains "interface required"
    • go test -v -count=1 ./... exits 0 (all unit tests pass) </acceptance_criteria> netsynth binary builds and runs. --list-interfaces shows interfaces. Missing -i shows clear error. Signal handling wires Ctrl+C to clean pipeline shutdown. Exit summary prints per-protocol counts. All tests pass.
Task 2: Smoke test live capture on real interface cmd/netsynth/main.go This is a human verification checkpoint. The executor should present the smoke test instructions below to the user and wait for approval. No code changes are needed -- this verifies the work done in Task 1. Complete Phase 1 pipeline: netsynth CLI with live packet capture, protocol classification, and stderr output. 1. Build: `cd /home/dev/workspace/yoloyolo && CGO_ENABLED=0 go build -o netsynth ./cmd/netsynth` 2. List interfaces: `./netsynth --list-interfaces` -- should show eth0 and lo at minimum 3. Test permission error (non-root): `./netsynth -i eth0` -- should show sudo/setcap hint 4. Test live capture (needs root): `sudo ./netsynth -i eth0 --verbose` - Generate some traffic in another terminal: `curl https://example.com`, `ping -c 3 8.8.8.8` - You should see per-window verbose lines on stderr showing HTTPS, ICMP, DNS counts - Press Ctrl+C - You should see "--- Protocol Summary ---" with per-protocol packet counts and percentages 5. Verify no-flag error: `./netsynth` -- should say "interface required" 6. Verify help: `./netsynth --help` -- should show -i, --list-interfaces, --verbose flags cd /home/dev/workspace/yoloyolo && ./netsynth --list-interfaces 2>&1 | grep -q "flags=" User confirmed live capture works: packets classified, verbose output shows per-window activity, Ctrl+C produces protocol summary. Type "approved" if live capture works correctly, or describe issues - `CGO_ENABLED=0 go build -o netsynth ./cmd/netsynth` compiles successfully - `./netsynth --list-interfaces` shows system interfaces - `./netsynth` (no flags) shows clear error about missing -i flag - `./netsynth --help` shows all three flags - `go test -v -race ./...` all tests pass across all packages - No import of `google/gopacket` anywhere - No switch statements in classify/classifier.go

<success_criteria>

  • netsynth binary builds with CGO_ENABLED=0 (no CGo needed for Phase 1)
  • -i flag accepted, --list-interfaces works, --verbose works
  • Permission error shows platform-specific sudo hint
  • Live capture classifies packets and prints verbose lines (when --verbose)
  • Ctrl+C produces clean exit with per-protocol summary
  • All unit tests pass across classify/, capture/, aggregate/, and cmd/netsynth/ </success_criteria>
After completion, create `.planning/phases/01-capture-and-classification/01-04-SUMMARY.md`