324 lines
13 KiB
Markdown
324 lines
13 KiB
Markdown
---
|
|||
|
|
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>
|