Files
yoloyolo/.planning/phases/01-capture-and-classification/01-02-PLAN.md
T

10 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
01-capture-and-classification 02 execute 2
01-01
capture/capture.go
capture/capture_test.go
true
CAPT-01
CAPT-02
CAPT-04
truths artifacts key_links
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
path provides exports
capture/capture.go OpenCapture, ListInterfaces, permission error handling
OpenCapture
ListInterfaces
StartCapture
path provides min_lines
capture/capture_test.go Tests for ListInterfaces and permission error messages 60
from to via pattern
capture/capture.go github.com/packetcap/go-pcap pcap.OpenLive for live capture pcap.OpenLive
from to via pattern
capture/capture.go net.Interfaces stdlib for interface enumeration (NOT pcap.FindAllDevs) net.Interfaces
from to via pattern
capture/capture.go runtime.GOOS OS detection for platform-specific error messages 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.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_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 ```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)
    }
}
  1. 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/... <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> 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`)

<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>
After completion, create `.planning/phases/01-capture-and-classification/01-02-SUMMARY.md`