docs(01-capture-and-classification): create phase plan
This commit is contained in:
@@ -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"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</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
|
||||
|
||||
<interfaces>
|
||||
<!-- From classify/types.go (created in Plan 01): -->
|
||||
```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
|
||||
}
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Implement capture package with OpenCapture, ListInterfaces, and StartCapture</name>
|
||||
<files>capture/capture.go, capture/capture_test.go</files>
|
||||
<read_first>
|
||||
classify/types.go
|
||||
go.mod
|
||||
.planning/phases/01-capture-and-classification/01-RESEARCH.md
|
||||
</read_first>
|
||||
<behavior>
|
||||
- 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
|
||||
</behavior>
|
||||
<action>
|
||||
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).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/dev/workspace/yoloyolo && go test -v -count=1 ./capture/...</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 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
|
||||
</acceptance_criteria>
|
||||
<done>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.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `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`)
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 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
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-capture-and-classification/01-02-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user