Files

324 lines
13 KiB
Markdown
Raw Permalink Normal View History

---
phase: 01-capture-and-classification
plan: 04
type: execute
wave: 3
depends_on: ["01-01", "01-02", "01-03"]
files_modified:
- cmd/netsynth/main.go
- cmd/netsynth/main_test.go
autonomous: false
requirements: [CAPT-01, CAPT-02, CAPT-04, CLAS-01, CLAS-03, CLAS-04]
must_haves:
truths:
- "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"
artifacts:
- path: "cmd/netsynth/main.go"
provides: "Cobra CLI wiring, signal handling, pipeline assembly"
exports: ["main"]
- path: "cmd/netsynth/main_test.go"
provides: "Tests for flag parsing and list-interfaces output"
min_lines: 30
key_links:
- from: "cmd/netsynth/main.go"
to: "capture/capture.go"
via: "StartCapture for packet channel, ListInterfaces for --list-interfaces"
pattern: "capture\\.StartCapture|capture\\.ListInterfaces"
- from: "cmd/netsynth/main.go"
to: "classify/classifier.go"
via: "NewClassifier + Classify in pipeline goroutine"
pattern: "classify\\.NewClassifier|classifier\\.Classify"
- from: "cmd/netsynth/main.go"
to: "aggregate/window.go"
via: "Aggregate consumes classified packets channel"
pattern: "aggregate\\.Aggregate"
- from: "cmd/netsynth/main.go"
to: "aggregate/summary.go"
via: "PrintSummary on exit, PrintWindowLine for verbose callback"
pattern: "aggregate\\.PrintSummary|aggregate\\.PrintWindowLine"
---
<objective>
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.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<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
<interfaces>
<!-- From capture/capture.go (Plan 02): -->
```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)
```
<!-- From classify/ (Plan 01): -->
```go
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
```
<!-- From aggregate/ (Plan 03): -->
```go
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)
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Wire Cobra CLI with capture-classify-aggregate pipeline and signal handling</name>
<files>cmd/netsynth/main.go, cmd/netsynth/main_test.go</files>
<read_first>
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
</read_first>
<action>
1. Create cmd/netsynth/main.go with Cobra root command:
```go
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
}
```
2. 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"
3. 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
4. Run full test suite: `go test -v -race ./...`
</action>
<verify>
<automated>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 ./...</automated>
</verify>
<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>
<done>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.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 2: Smoke test live capture on real interface</name>
<files>cmd/netsynth/main.go</files>
<action>
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.
</action>
<what-built>
Complete Phase 1 pipeline: netsynth CLI with live packet capture, protocol classification, and stderr output.
</what-built>
<how-to-verify>
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
</how-to-verify>
<verify>
<automated>cd /home/dev/workspace/yoloyolo && ./netsynth --list-interfaces 2>&1 | grep -q "flags="</automated>
</verify>
<done>User confirmed live capture works: packets classified, verbose output shows per-window activity, Ctrl+C produces protocol summary.</done>
<resume-signal>Type "approved" if live capture works correctly, or describe issues</resume-signal>
</task>
</tasks>
<verification>
- `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
</verification>
<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>
<output>
After completion, create `.planning/phases/01-capture-and-classification/01-04-SUMMARY.md`
</output>