From 5d657342d38a3d2cc4e7bda38badfd24550cc823 Mon Sep 17 00:00:00 2001 From: Markus Graf Date: Wed, 25 Mar 2026 11:30:21 +0100 Subject: [PATCH] docs(01-capture-and-classification): create phase plan --- .planning/ROADMAP.md | 10 +- .planning/config.json | 3 +- .../01-01-PLAN.md | 337 +++++++++++++++++ .../01-02-PLAN.md | 263 +++++++++++++ .../01-03-PLAN.md | 344 ++++++++++++++++++ .../01-04-PLAN.md | 323 ++++++++++++++++ 6 files changed, 1277 insertions(+), 3 deletions(-) create mode 100644 .planning/phases/01-capture-and-classification/01-01-PLAN.md create mode 100644 .planning/phases/01-capture-and-classification/01-02-PLAN.md create mode 100644 .planning/phases/01-capture-and-classification/01-03-PLAN.md create mode 100644 .planning/phases/01-capture-and-classification/01-04-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 55dfd9f..3e8fa09 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -29,7 +29,13 @@ Decimal phases appear between their surrounding integers in numeric order. 3. User running without root/CAP_NET_RAW sees a clear error message with a `sudo` hint — not a panic or silent failure 4. On exit, user sees a per-protocol packet count summary printed to stderr 5. User can pass `--verbose` and see per-window protocol activity lines on stderr -**Plans**: TBD +**Plans:** 4 plans + +Plans: +- [ ] 01-01-PLAN.md — Go 1.24 setup, module init, shared types, config-driven classifier with tests +- [ ] 01-02-PLAN.md — Capture package: OpenCapture, ListInterfaces, privilege error handling +- [ ] 01-03-PLAN.md — Aggregation: time-windowed accumulator, exit summary, verbose output +- [ ] 01-04-PLAN.md — CLI wiring: Cobra commands, signal handling, pipeline assembly, smoke test ### Phase 2: Audio Synthesis Engine **Goal**: The synthesis and encoding stack produces a valid MP3 from synthetic WindowSnapshot inputs — audio pipeline fully validated before any real traffic flows through it @@ -71,7 +77,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 | Phase | Plans Complete | Status | Completed | |-------|----------------|--------|-----------| -| 1. Capture and Classification | 0/? | Not started | - | +| 1. Capture and Classification | 0/4 | Planning complete | - | | 2. Audio Synthesis Engine | 0/? | Not started | - | | 3. Pipeline Integration and MVP | 0/? | Not started | - | | 4. Power User Features | 0/? | Not started | - | diff --git a/.planning/config.json b/.planning/config.json index 67cbcac..b4645a1 100644 --- a/.planning/config.json +++ b/.planning/config.json @@ -25,7 +25,8 @@ "text_mode": false, "research_before_questions": false, "discuss_mode": "discuss", - "skip_discuss": false + "skip_discuss": false, + "_auto_chain_active": false }, "hooks": { "context_warnings": true diff --git a/.planning/phases/01-capture-and-classification/01-01-PLAN.md b/.planning/phases/01-capture-and-classification/01-01-PLAN.md new file mode 100644 index 0000000..728e96c --- /dev/null +++ b/.planning/phases/01-capture-and-classification/01-01-PLAN.md @@ -0,0 +1,337 @@ +--- +phase: 01-capture-and-classification +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - go.mod + - go.sum + - classify/rules.go + - classify/classifier.go + - classify/classifier_test.go + - classify/types.go +autonomous: true +requirements: [CLAS-01] + +must_haves: + truths: + - "Each of 10+ protocol classes (ICMP, DNS, HTTPS, HTTP, SSH, SMTP, NTP, DHCP, other-TCP, other-UDP, unknown) is correctly identified from packet headers" + - "Classification rules are stored in a config-driven slice/struct, not hardcoded switch statements" + - "Unrecognized traffic is grouped as 'unknown' class" + artifacts: + - path: "classify/types.go" + provides: "TrafficClass type, ClassifiedPacket struct, WindowSnapshot struct" + exports: ["TrafficClass", "ClassifiedPacket", "WindowSnapshot"] + - path: "classify/rules.go" + provides: "Rule struct and DefaultRules slice with 10+ protocol rules" + contains: "DefaultRules" + - path: "classify/classifier.go" + provides: "Classify function that matches packets against rule table" + exports: ["Classify", "NewClassifier"] + - path: "classify/classifier_test.go" + provides: "Unit tests for all 10+ protocol classes" + min_lines: 80 + key_links: + - from: "classify/classifier.go" + to: "classify/rules.go" + via: "Classifier uses DefaultRules slice" + pattern: "DefaultRules" + - from: "classify/classifier.go" + to: "classify/types.go" + via: "Returns TrafficClass from Classify function" + pattern: "TrafficClass" +--- + + +Bootstrap Go project and implement the protocol classification engine with full test coverage. + +Purpose: Establish the Go module, core types shared across all packages, and the config-driven classifier that maps packets to one of 10+ traffic classes. This is the foundation every other plan depends on. + +Output: Compiling Go module with classify package, passing unit tests for all protocol classes. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/01-capture-and-classification/01-CONTEXT.md +@.planning/phases/01-capture-and-classification/01-RESEARCH.md + + + + + + Task 1: Install Go 1.24, initialize module, and create shared types + go.mod, classify/types.go + + .planning/phases/01-capture-and-classification/01-RESEARCH.md + + +1. Install Go 1.24+ from https://go.dev/dl/ (NOT apt which gives 1.22). Download the linux-arm64 tarball: + - wget https://go.dev/dl/go1.24.1.linux-arm64.tar.gz + - sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.24.1.linux-arm64.tar.gz + - Ensure /usr/local/go/bin is in PATH (export PATH=$PATH:/usr/local/go/bin) + - Verify: `go version` must show go1.24.x + +2. Initialize the Go module: + - cd /home/dev/workspace/yoloyolo + - go mod init github.com/netsynth/netsynth + - mkdir -p classify capture aggregate cmd/netsynth + +3. Create classify/types.go with the following types (these are the contracts for all packages): + +```go +package classify + +// TrafficClass represents a classified network traffic category. +type TrafficClass string + +const ( + ClassICMP TrafficClass = "ICMP" + ClassDNS TrafficClass = "DNS" + ClassHTTPS TrafficClass = "HTTPS" + ClassHTTP TrafficClass = "HTTP" + ClassSSH TrafficClass = "SSH" + ClassSMTP TrafficClass = "SMTP" + ClassNTP TrafficClass = "NTP" + ClassDHCP TrafficClass = "DHCP" + ClassOtherTCP TrafficClass = "other-TCP" + ClassOtherUDP TrafficClass = "other-UDP" + ClassUnknown TrafficClass = "unknown" +) + +// AllClasses returns all known traffic classes in display order. +func AllClasses() []TrafficClass { + return []TrafficClass{ + ClassICMP, ClassDNS, ClassHTTPS, ClassHTTP, ClassSSH, + ClassSMTP, ClassNTP, ClassDHCP, ClassOtherTCP, ClassOtherUDP, ClassUnknown, + } +} + +// ClassifiedPacket pairs a raw packet's classification result with metadata. +type ClassifiedPacket struct { + Class TrafficClass + SrcPort uint16 + DstPort uint16 + Protocol string // "tcp", "udp", "icmp" + Length int +} + +// WindowSnapshot holds aggregated packet counts for a time window. +// This is the contract between the aggregation stage and future audio synthesis (Phase 2). +type WindowSnapshot struct { + Counts map[TrafficClass]int64 + TotalPackets int64 + WindowIndex int +} +``` + +4. Fetch Phase 1 dependencies: + - 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 + - go mod tidy + + + cd /home/dev/workspace/yoloyolo && go version | grep -q "go1.24" && go build ./classify/... + + + - `go version` output contains "go1.24" + - go.mod contains `module github.com/netsynth/netsynth` + - go.mod contains `github.com/gopacket/gopacket` + - go.mod contains `github.com/packetcap/go-pcap` + - go.mod contains `github.com/spf13/cobra` + - classify/types.go contains `type TrafficClass string` + - classify/types.go contains `ClassICMP`, `ClassDNS`, `ClassHTTPS`, `ClassHTTP`, `ClassSSH`, `ClassSMTP`, `ClassNTP`, `ClassDHCP`, `ClassOtherTCP`, `ClassOtherUDP`, `ClassUnknown` + - classify/types.go contains `type ClassifiedPacket struct` + - classify/types.go contains `type WindowSnapshot struct` + - `go build ./classify/...` exits 0 + + Go 1.24 installed, module initialized with all Phase 1 deps, classify/types.go compiles with all 11 traffic classes and shared types. + + + + Task 2: Implement config-driven classifier with tests for all protocol classes + classify/rules.go, classify/classifier.go, classify/classifier_test.go + + classify/types.go + .planning/phases/01-capture-and-classification/01-RESEARCH.md + + + - TestClassifyICMP: ICMP packet (no port) -> ClassICMP + - TestClassifyDNS_UDP: UDP dst port 53 -> ClassDNS + - TestClassifyDNS_TCP: TCP dst port 53 -> ClassDNS + - TestClassifyHTTPS: TCP dst port 443 -> ClassHTTPS + - TestClassifyHTTP: TCP dst port 80 -> ClassHTTP + - TestClassifySSH: TCP dst port 22 -> ClassSSH + - TestClassifySMTP: TCP dst port 25 -> ClassSMTP + - TestClassifyNTP: UDP dst port 123 -> ClassNTP + - TestClassifyDHCP: UDP dst port 67 or 68 -> ClassDHCP + - TestClassifyOtherTCP: TCP dst port 8080 (unmatched) -> ClassOtherTCP + - TestClassifyOtherUDP: UDP dst port 9999 (unmatched) -> ClassOtherUDP + - TestClassifyUnknown: Packet with no TCP/UDP/ICMP layer -> ClassUnknown + - TestRulesAreOrderDependent: First matching rule wins + + +1. Create classify/rules.go with Rule struct and DefaultRules per D-01 and D-02: + +```go +package classify + +// Rule defines a classification rule. Protocol is "tcp", "udp", or "icmp". +// DstPort 0 means match any port for this protocol. +// Rules are evaluated in order; first match wins. +type Rule struct { + Protocol string + DstPort uint16 + Class TrafficClass +} + +// DefaultRules is the D-01 protocol map — ordered, first-match-wins. +// Per D-02: config-driven slice, not a switch statement. +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 (must be last): + {Protocol: "tcp", DstPort: 0, Class: ClassOtherTCP}, + {Protocol: "udp", DstPort: 0, Class: ClassOtherUDP}, +} +``` + +2. Create classify/classifier.go: + +```go +package classify + +import ( + "github.com/gopacket/gopacket" + "github.com/gopacket/gopacket/layers" +) + +// Classifier classifies packets against a rule table. +type Classifier struct { + rules []Rule +} + +// NewClassifier creates a classifier with the given rules. +func NewClassifier(rules []Rule) *Classifier { + return &Classifier{rules: rules} +} + +// Classify inspects a gopacket.Packet and returns a ClassifiedPacket. +// It checks ICMP first, then extracts TCP/UDP port info and matches against rules. +// Per D-03: returns ClassUnknown if no rule matches. +func (c *Classifier) Classify(pkt gopacket.Packet) ClassifiedPacket { + result := ClassifiedPacket{ + Class: ClassUnknown, + Length: len(pkt.Data()), + } + + // Check ICMP + if pkt.Layer(layers.LayerTypeICMPv4) != nil || pkt.Layer(layers.LayerTypeICMPv6) != nil { + result.Protocol = "icmp" + // Match against rules + for _, rule := range c.rules { + if rule.Protocol == "icmp" { + result.Class = rule.Class + return result + } + } + return result + } + + // Check TCP + if tcpLayer := pkt.Layer(layers.LayerTypeTCP); tcpLayer != nil { + tcp := tcpLayer.(*layers.TCP) + result.Protocol = "tcp" + result.SrcPort = uint16(tcp.SrcPort) + result.DstPort = uint16(tcp.DstPort) + for _, rule := range c.rules { + if rule.Protocol == "tcp" && (rule.DstPort == 0 || rule.DstPort == result.DstPort) { + result.Class = rule.Class + return result + } + } + return result + } + + // Check UDP + if udpLayer := pkt.Layer(layers.LayerTypeUDP); udpLayer != nil { + udp := udpLayer.(*layers.UDP) + result.Protocol = "udp" + result.SrcPort = uint16(udp.SrcPort) + result.DstPort = uint16(udp.DstPort) + for _, rule := range c.rules { + if rule.Protocol == "udp" && (rule.DstPort == 0 || rule.DstPort == result.DstPort) { + result.Class = rule.Class + return result + } + } + return result + } + + // No recognized transport layer -> ClassUnknown per D-03 + return result +} +``` + +3. Write tests in classify/classifier_test.go using gopacket test packet construction: + - Build synthetic packets using gopacket.SerializeBuffer with layers.Ethernet + layers.IPv4 + layers.TCP/UDP/ICMP headers + - Test each of the 10+ protocol classes plus ClassUnknown + - Test that rule order matters (first match wins) + +Run RED phase first: write all tests, verify they fail (classifier not yet imported or logic wrong). +Then GREEN: implement until all pass. + + + cd /home/dev/workspace/yoloyolo && go test -v -count=1 ./classify/... + + + - classify/rules.go contains `type Rule struct` + - classify/rules.go contains `var DefaultRules = []Rule{` + - classify/rules.go contains at least 12 Rule entries (10 specific + 2 catch-all) + - classify/classifier.go contains `func (c *Classifier) Classify(pkt gopacket.Packet) ClassifiedPacket` + - classify/classifier.go contains `func NewClassifier(rules []Rule) *Classifier` + - classify/classifier.go does NOT contain `switch` keyword (per D-02) + - classify/classifier_test.go contains `TestClassify` with subtests for ICMP, DNS, HTTPS, HTTP, SSH, SMTP, NTP, DHCP, OtherTCP, OtherUDP, Unknown + - `go test -v -count=1 ./classify/...` exits 0 with all subtests passing + + Classifier passes tests for all 11 traffic classes. Rules stored as config-driven slice (D-02). First-match-wins ordering verified. No switch statements in classification logic. + + + + + +- `go version` shows 1.24+ +- `go build ./classify/...` compiles without errors +- `go test -v -count=1 ./classify/...` all tests pass +- `go vet ./classify/...` no issues +- No import of `google/gopacket` anywhere (must be `gopacket/gopacket`) + + + +- Go 1.24 installed and working +- Module initialized with gopacket, go-pcap, cobra dependencies +- 11 TrafficClass constants defined +- ClassifiedPacket and WindowSnapshot types defined (shared contracts) +- Config-driven classifier with 12+ rules in a slice (not switch) +- All protocol class unit tests passing + + + +After completion, create `.planning/phases/01-capture-and-classification/01-01-SUMMARY.md` + diff --git a/.planning/phases/01-capture-and-classification/01-02-PLAN.md b/.planning/phases/01-capture-and-classification/01-02-PLAN.md new file mode 100644 index 0000000..83d98c4 --- /dev/null +++ b/.planning/phases/01-capture-and-classification/01-02-PLAN.md @@ -0,0 +1,263 @@ +--- +phase: 01-capture-and-classification +plan: 02 +type: execute +wave: 2 +depends_on: ["01-01"] +files_modified: + - capture/capture.go + - capture/capture_test.go +autonomous: true +requirements: [CAPT-01, CAPT-02, CAPT-04] + +must_haves: + truths: + - "User can specify a network interface via -i flag and packets are captured from it" + - "User can list available network interfaces via --list-interfaces" + - "User without root/CAP_NET_RAW sees platform-specific error with sudo hint" + artifacts: + - path: "capture/capture.go" + provides: "OpenCapture, ListInterfaces, permission error handling" + exports: ["OpenCapture", "ListInterfaces", "StartCapture"] + - path: "capture/capture_test.go" + provides: "Tests for ListInterfaces and permission error messages" + min_lines: 60 + key_links: + - from: "capture/capture.go" + to: "github.com/packetcap/go-pcap" + via: "pcap.OpenLive for live capture" + pattern: "pcap\\.OpenLive" + - from: "capture/capture.go" + to: "net.Interfaces" + via: "stdlib for interface enumeration (NOT pcap.FindAllDevs)" + pattern: "net\\.Interfaces" + - from: "capture/capture.go" + to: "runtime.GOOS" + via: "OS detection for platform-specific error messages" + pattern: "runtime\\.GOOS" +--- + + +Implement the capture package: live packet capture via go-pcap, interface listing via net.Interfaces(), and platform-specific privilege error handling. + +Purpose: This package is the data source for the entire pipeline. It wraps go-pcap with proper error handling and provides the channel-based packet source that the classifier consumes. + +Output: capture/ package with OpenCapture, ListInterfaces, StartCapture functions and unit tests. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.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 + + + +```go +package classify + +type TrafficClass string +// 11 constants: ClassICMP, ClassDNS, ClassHTTPS, ClassHTTP, ClassSSH, ClassSMTP, ClassNTP, ClassDHCP, ClassOtherTCP, ClassOtherUDP, ClassUnknown + +type ClassifiedPacket struct { + Class TrafficClass + SrcPort uint16 + DstPort uint16 + Protocol string + Length int +} + +type WindowSnapshot struct { + Counts map[TrafficClass]int64 + TotalPackets int64 + WindowIndex int +} +``` + + + + + + + Task 1: Implement capture package with OpenCapture, ListInterfaces, and StartCapture + capture/capture.go, capture/capture_test.go + + classify/types.go + go.mod + .planning/phases/01-capture-and-classification/01-RESEARCH.md + + + - TestListInterfaces: net.Interfaces() returns at least one interface (lo always exists) + - TestListInterfacesFormat: Each interface has Name and Flags fields populated + - TestPermissionErrorLinux: When runtime.GOOS=="linux" and OpenLive fails with "operation not permitted", error message contains "sudo setcap cap_net_raw+ep" + - TestPermissionErrorDarwin: When runtime.GOOS=="darwin" and OpenLive fails with "permission denied", error message contains "sudo netsynth" + - TestIsPermissionError: Recognizes "permission denied", "operation not permitted", and "pcap_create" error strings + - TestIsPermissionErrorNegative: Does not flag "interface not found" as permission error + + +1. Create capture/capture.go with these functions: + +```go +package capture + +import ( + "context" + "fmt" + "net" + "os" + "runtime" + "strings" + "sync/atomic" + + "github.com/gopacket/gopacket" + "github.com/gopacket/gopacket/layers" + pcap "github.com/packetcap/go-pcap" +) + +// ListInterfaces returns all network interfaces using Go stdlib. +// Does NOT use pcap.FindAllDevs (go-pcap has no such function). +// Requires no privileges (CAPT-02). +func ListInterfaces() ([]net.Interface, error) { + return net.Interfaces() +} + +// OpenCapture opens a live capture handle on the given interface. +// snaplen=65535, promiscuous=false, timeout=0 (block until packet). +// Returns a platform-specific error message if permission is denied (CAPT-04, D-05). +func OpenCapture(ctx context.Context, iface string) (*pcap.Handle, error) { + handle, err := pcap.OpenLive(ctx, 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 +} + +// StartCapture opens a capture handle and returns a channel of gopacket.Packet. +// The channel is closed when ctx is cancelled (Ctrl+C) or capture ends. +// Uses non-blocking send with drop counter per Pitfall 4 from RESEARCH.md. +// Uses dynamic link type detection per Pitfall 6 from RESEARCH.md. +func StartCapture(ctx context.Context, iface string) (<-chan gopacket.Packet, *int64, error) { + handle, err := OpenCapture(ctx, iface) + if err != nil { + return nil, nil, err + } + + lt := layers.LinkType(handle.LinkType()) + packetSource := gopacket.NewPacketSource(handle, lt) + packetSource.NoCopy = true // safe: go-pcap copies internally + + packets := make(chan gopacket.Packet, 512) + var droppedPackets int64 + + go func() { + defer close(packets) + defer handle.Close() + for { + select { + case <-ctx.Done(): + return + default: + } + pkt, err := packetSource.NextPacket() + if err != nil { + // Context cancelled or handle closed + return + } + select { + case packets <- pkt: + default: + atomic.AddInt64(&droppedPackets, 1) + } + } + }() + + return packets, &droppedPackets, nil +} + +// isPermissionError checks if the error indicates insufficient privileges. +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") +} + +// permissionErrorMsg returns a platform-specific privilege hint (D-05). +func permissionErrorMsg(iface string) error { + bin, _ := os.Executable() + if bin == "" { + bin = "netsynth" + } + switch runtime.GOOS { + case "linux": + 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 %s -i %s", bin, iface) + } +} +``` + +2. Create capture/capture_test.go: + - TestListInterfaces: Call ListInterfaces(), assert len > 0 (lo always exists), assert first interface has non-empty Name + - TestIsPermissionError: Table-driven test with positive cases ("permission denied", "operation not permitted", "pcap_create: foo") and negative cases ("interface not found", "no such device") + - TestPermissionErrorMsg: Call permissionErrorMsg("eth0") on current OS, verify the error string contains "sudo" and the interface name "eth0". On linux verify it contains "setcap cap_net_raw+ep" + - NOTE: Do NOT test OpenCapture or StartCapture directly in unit tests -- they require root/CAP_NET_RAW. Those are integration-tested manually via `sudo ./netsynth -i eth0`. + +Write tests first (RED), then implement (GREEN). + + + cd /home/dev/workspace/yoloyolo && go test -v -count=1 ./capture/... + + + - capture/capture.go contains `func ListInterfaces() ([]net.Interface, error)` + - capture/capture.go contains `func OpenCapture(ctx context.Context, iface string) (*pcap.Handle, error)` + - capture/capture.go contains `func StartCapture(ctx context.Context, iface string) (<-chan gopacket.Packet, *int64, error)` + - capture/capture.go contains `net.Interfaces()` (NOT pcap.FindAllDevs) + - capture/capture.go contains `runtime.GOOS` for platform detection + - capture/capture.go contains `"setcap cap_net_raw+ep"` in linux error branch + - capture/capture.go contains `atomic.AddInt64(&droppedPackets, 1)` for non-blocking send + - capture/capture.go contains `layers.LinkType(handle.LinkType())` for dynamic link type + - capture/capture_test.go contains `TestListInterfaces` + - capture/capture_test.go contains `TestIsPermissionError` + - capture/capture_test.go contains `TestPermissionErrorMsg` + - `go test -v -count=1 ./capture/...` exits 0 + + capture package compiles and tests pass. ListInterfaces uses net.Interfaces (no pcap dependency for listing). Permission errors show platform-specific sudo/setcap hints. StartCapture uses non-blocking send with drop counter and dynamic link type. + + + + + +- `go test -v -count=1 ./capture/...` all tests pass +- `go vet ./capture/...` no issues +- No import of `gopacket/pcap` (must use `packetcap/go-pcap`) +- No import of `google/gopacket` (must use `gopacket/gopacket`) + + + +- ListInterfaces returns system interfaces without privileges +- Permission error detection covers "permission denied", "operation not permitted", "pcap_create" +- Error messages are platform-specific (Linux: setcap hint, macOS: sudo hint) +- StartCapture uses buffered channel (512) with atomic drop counter +- Dynamic link type detection (not hardcoded LinkTypeEthernet) +- All unit tests passing + + + +After completion, create `.planning/phases/01-capture-and-classification/01-02-SUMMARY.md` + diff --git a/.planning/phases/01-capture-and-classification/01-03-PLAN.md b/.planning/phases/01-capture-and-classification/01-03-PLAN.md new file mode 100644 index 0000000..28daf37 --- /dev/null +++ b/.planning/phases/01-capture-and-classification/01-03-PLAN.md @@ -0,0 +1,344 @@ +--- +phase: 01-capture-and-classification +plan: 03 +type: execute +wave: 2 +depends_on: ["01-01"] +files_modified: + - aggregate/window.go + - aggregate/summary.go + - aggregate/window_test.go + - aggregate/summary_test.go +autonomous: true +requirements: [CLAS-03, CLAS-04] + +must_haves: + truths: + - "On exit, user sees a per-protocol packet count summary printed to stderr" + - "User can pass --verbose and see per-window protocol activity lines on stderr" + - "Aggregator accumulates packet counts into time windows and emits WindowSnapshots" + artifacts: + - path: "aggregate/window.go" + provides: "Ticker-driven window aggregator that emits WindowSnapshots" + exports: ["Aggregate"] + - path: "aggregate/summary.go" + provides: "PrintSummary and PrintWindowLine functions for stderr output" + exports: ["PrintSummary", "PrintWindowLine"] + - path: "aggregate/window_test.go" + provides: "Tests for window aggregation logic" + min_lines: 50 + - path: "aggregate/summary_test.go" + provides: "Tests for summary formatting and verbose output" + min_lines: 60 + key_links: + - from: "aggregate/window.go" + to: "classify/types.go" + via: "Consumes ClassifiedPacket, produces WindowSnapshot" + pattern: "classify\\.ClassifiedPacket" + - from: "aggregate/summary.go" + to: "classify/types.go" + via: "Uses TrafficClass and WindowSnapshot for formatting" + pattern: "classify\\.TrafficClass" +--- + + +Implement the aggregation and stats reporting package: time-windowed accumulation, exit summary, and verbose per-window output. + +Purpose: This package bridges classified packets and the eventual audio engine (Phase 2). It accumulates per-class counts into time windows, prints verbose activity lines during capture, and prints a final protocol summary on exit. + +Output: aggregate/ package with Aggregate, PrintSummary, PrintWindowLine functions and full unit tests. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.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 + + + +```go +package classify + +type TrafficClass string + +const ( + ClassICMP TrafficClass = "ICMP" + ClassDNS TrafficClass = "DNS" + ClassHTTPS TrafficClass = "HTTPS" + ClassHTTP TrafficClass = "HTTP" + ClassSSH TrafficClass = "SSH" + ClassSMTP TrafficClass = "SMTP" + ClassNTP TrafficClass = "NTP" + ClassDHCP TrafficClass = "DHCP" + ClassOtherTCP TrafficClass = "other-TCP" + ClassOtherUDP TrafficClass = "other-UDP" + ClassUnknown TrafficClass = "unknown" +) + +func AllClasses() []TrafficClass { ... } + +type ClassifiedPacket struct { + Class TrafficClass + SrcPort uint16 + DstPort uint16 + Protocol string + Length int +} + +type WindowSnapshot struct { + Counts map[TrafficClass]int64 + TotalPackets int64 + WindowIndex int +} +``` + + + + + + + Task 1: Implement ticker-driven window aggregator + aggregate/window.go, aggregate/window_test.go + + classify/types.go + .planning/phases/01-capture-and-classification/01-RESEARCH.md + + + - TestAggregateEmitsSnapshot: Send 5 ClassifiedPackets, trigger window tick, receive WindowSnapshot with correct counts + - TestAggregateMultipleClasses: Send packets of 3 different classes, verify snapshot has all 3 with correct counts + - TestAggregateDoneFlushesPartial: Close done channel mid-window, verify final partial snapshot is emitted + - TestAggregateEmptyWindow: Trigger tick with no packets, verify snapshot has zero counts and TotalPackets==0 + - TestAggregateWindowIndex: Multiple ticks increment WindowIndex (0, 1, 2...) + + +1. Create aggregate/window.go implementing the ticker-driven aggregation pattern from RESEARCH.md Pattern 2: + +```go +package aggregate + +import ( + "time" + + "github.com/netsynth/netsynth/classify" +) + +// DefaultWindowMs is the default time window duration in milliseconds. +// Claude's discretion per CONTEXT.md. +const DefaultWindowMs = 500 + +// Aggregate reads ClassifiedPackets from events, accumulates counts per TrafficClass +// in time windows of windowMs duration, and emits a WindowSnapshot per window. +// When done is closed, flushes the current partial window and closes the output channel. +// If verbose is true, calls onSnapshot for each window (used for --verbose stderr output). +func Aggregate(done <-chan struct{}, events <-chan classify.ClassifiedPacket, + windowMs int, onSnapshot func(classify.WindowSnapshot)) <-chan classify.WindowSnapshot { + + out := make(chan classify.WindowSnapshot, 8) + go func() { + defer close(out) + ticker := time.NewTicker(time.Duration(windowMs) * time.Millisecond) + defer ticker.Stop() + + counts := make(map[classify.TrafficClass]int64) + var totalPackets int64 + windowIndex := 0 + + flush := func() { + snap := classify.WindowSnapshot{ + Counts: counts, + TotalPackets: totalPackets, + WindowIndex: windowIndex, + } + if onSnapshot != nil { + onSnapshot(snap) + } + out <- snap + counts = make(map[classify.TrafficClass]int64) + totalPackets = 0 + windowIndex++ + } + + for { + select { + case <-done: + flush() // flush final partial window + return + case <-ticker.C: + flush() + case ev, ok := <-events: + if !ok { + flush() + return + } + counts[ev.Class]++ + totalPackets++ + } + } + }() + return out +} +``` + +2. Create aggregate/window_test.go: + - Use a done channel and a buffered events channel to control timing without real tickers + - For ticker tests, use a short windowMs (10ms) and sleep briefly to trigger ticks + - For done-flush tests, close done channel immediately and verify output + - Verify WindowSnapshot.Counts map contains expected class keys and values + - Verify WindowSnapshot.TotalPackets matches sum of counts + - Verify WindowIndex increments across windows + +Write tests first (RED), then implement (GREEN). + + + cd /home/dev/workspace/yoloyolo && go test -v -count=1 -race ./aggregate/... + + + - aggregate/window.go contains `func Aggregate(done <-chan struct{}, events <-chan classify.ClassifiedPacket` + - aggregate/window.go contains `const DefaultWindowMs = 500` + - aggregate/window.go contains `time.NewTicker` + - aggregate/window.go imports `github.com/netsynth/netsynth/classify` + - aggregate/window_test.go contains `TestAggregate` with subtests for snapshot emission, multiple classes, done flush, empty window, and window index + - `go test -v -count=1 -race ./aggregate/...` exits 0 + + Aggregate function emits WindowSnapshots on ticker ticks and flushes partial window on done. All timing-based tests pass including race detector. + + + + Task 2: Implement exit summary and verbose window output formatting + aggregate/summary.go, aggregate/summary_test.go + + classify/types.go + aggregate/window.go + .planning/phases/01-capture-and-classification/01-RESEARCH.md + + + - TestPrintSummary: Given totals map {ICMP: 10, DNS: 50, HTTPS: 100}, output contains "ICMP", "DNS", "HTTPS" with correct counts and percentages, plus "TOTAL" line + - TestPrintSummaryEmpty: Given empty totals, output contains "TOTAL" with 0 packets + - TestPrintSummarySorted: Output classes appear in stable alphabetical or canonical order (not random map order) + - TestPrintWindowLine: Given WindowSnapshot with {DNS: 5, HTTPS: 10}, output contains window index, class names, and counts on a single line + - TestPrintWindowLineEmpty: Given empty snapshot, output contains window index with no class entries + - TestAccumulateTotals: Given multiple WindowSnapshots, AccumulateTotals produces correct cumulative counts + + +1. Create aggregate/summary.go: + +```go +package aggregate + +import ( + "fmt" + "io" + "sort" + + "github.com/netsynth/netsynth/classify" +) + +// PrintSummary writes a per-protocol packet count summary to w (typically os.Stderr). +// Output format matches tcpdump conventions (CLAS-03). +func PrintSummary(w io.Writer, totals map[classify.TrafficClass]int64) { + var totalPackets int64 + for _, count := range totals { + totalPackets += count + } + + fmt.Fprintln(w, "\n--- Protocol Summary ---") + + // Sort classes for stable output + classes := make([]string, 0, len(totals)) + for c := range totals { + classes = append(classes, string(c)) + } + sort.Strings(classes) + + for _, className := range classes { + c := classify.TrafficClass(className) + count := totals[c] + pct := 0.0 + if totalPackets > 0 { + pct = float64(count) / float64(totalPackets) * 100.0 + } + fmt.Fprintf(w, " %-15s %8d packets (%5.1f%%)\n", c, count, pct) + } + fmt.Fprintf(w, " %-15s %8d packets\n", "TOTAL", totalPackets) +} + +// PrintWindowLine writes a single verbose per-window activity line to w (CLAS-04). +// Format: [window N] CLASS:count CLASS:count ... (total: N) +func PrintWindowLine(w io.Writer, snap classify.WindowSnapshot) { + fmt.Fprintf(w, "[window %d]", snap.WindowIndex) + // Sort for stable output + classes := make([]string, 0, len(snap.Counts)) + for c := range snap.Counts { + classes = append(classes, string(c)) + } + sort.Strings(classes) + for _, className := range classes { + c := classify.TrafficClass(className) + if snap.Counts[c] > 0 { + fmt.Fprintf(w, " %s:%d", c, snap.Counts[c]) + } + } + fmt.Fprintf(w, " (total: %d)\n", snap.TotalPackets) +} + +// AccumulateTotals merges a WindowSnapshot's counts into cumulative totals. +func AccumulateTotals(totals map[classify.TrafficClass]int64, snap classify.WindowSnapshot) { + for class, count := range snap.Counts { + totals[class] += count + } +} +``` + +2. Create aggregate/summary_test.go: + - Use bytes.Buffer as io.Writer to capture output + - TestPrintSummary: Verify output contains expected class names, counts, percentages, and TOTAL line + - TestPrintSummaryEmpty: Verify output contains "TOTAL" with 0 + - TestPrintSummarySorted: Verify output lines are in alphabetical order (capture output, split lines, check ordering) + - TestPrintWindowLine: Verify format "[window 0] DNS:5 HTTPS:10 (total: 15)" + - TestAccumulateTotals: Two snapshots accumulated, verify merged counts + +Write tests first (RED), then implement (GREEN). + + + cd /home/dev/workspace/yoloyolo && go test -v -count=1 ./aggregate/... + + + - aggregate/summary.go contains `func PrintSummary(w io.Writer, totals map[classify.TrafficClass]int64)` + - aggregate/summary.go contains `func PrintWindowLine(w io.Writer, snap classify.WindowSnapshot)` + - aggregate/summary.go contains `func AccumulateTotals(totals map[classify.TrafficClass]int64, snap classify.WindowSnapshot)` + - aggregate/summary.go contains `"--- Protocol Summary ---"` + - aggregate/summary.go contains `sort.Strings` for stable output ordering + - aggregate/summary_test.go contains `TestPrintSummary`, `TestPrintSummaryEmpty`, `TestPrintSummarySorted` + - aggregate/summary_test.go contains `TestPrintWindowLine`, `TestAccumulateTotals` + - `go test -v -count=1 ./aggregate/...` exits 0 + + PrintSummary outputs per-protocol counts with percentages and TOTAL to stderr (CLAS-03). PrintWindowLine outputs per-window activity for --verbose mode (CLAS-04). AccumulateTotals correctly merges snapshots. All formatting tests pass with stable sorted output. + + + + + +- `go test -v -count=1 -race ./aggregate/...` all tests pass +- `go vet ./aggregate/...` no issues +- Output format matches tcpdump convention (stderr, per-protocol counts with percentages) + + + +- Aggregate emits WindowSnapshots on time ticks and flushes on done +- PrintSummary prints per-protocol packet counts with percentages and TOTAL to an io.Writer +- PrintWindowLine prints per-window activity for verbose mode +- AccumulateTotals correctly accumulates across multiple snapshots +- All output is sorted for deterministic display +- All unit tests passing (including race detector) + + + +After completion, create `.planning/phases/01-capture-and-classification/01-03-SUMMARY.md` + diff --git a/.planning/phases/01-capture-and-classification/01-04-PLAN.md b/.planning/phases/01-capture-and-classification/01-04-PLAN.md new file mode 100644 index 0000000..8fe57c1 --- /dev/null +++ b/.planning/phases/01-capture-and-classification/01-04-PLAN.md @@ -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" +--- + + +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. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.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) +``` + + +```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 +``` + + +```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) +``` + + + + + + + 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: + +```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 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 ./...` + + + 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 ./... + + + - 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) + + 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 + + + +- 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/ + + + +After completion, create `.planning/phases/01-capture-and-classification/01-04-SUMMARY.md` +