docs(01-capture-and-classification): create phase plan
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
---
|
||||
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>
|
||||
Reference in New Issue
Block a user