Files

338 lines
12 KiB
Markdown
Raw Permalink Normal View History

---
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"
---
<objective>
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.
</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/STATE.md
@.planning/phases/01-capture-and-classification/01-CONTEXT.md
@.planning/phases/01-capture-and-classification/01-RESEARCH.md
</context>
<tasks>
<task type="auto">
<name>Task 1: Install Go 1.24, initialize module, and create shared types</name>
<files>go.mod, classify/types.go</files>
<read_first>
.planning/phases/01-capture-and-classification/01-RESEARCH.md
</read_first>
<action>
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
</action>
<verify>
<automated>cd /home/dev/workspace/yoloyolo && go version | grep -q "go1.24" && go build ./classify/...</automated>
</verify>
<acceptance_criteria>
- `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
</acceptance_criteria>
<done>Go 1.24 installed, module initialized with all Phase 1 deps, classify/types.go compiles with all 11 traffic classes and shared types.</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Implement config-driven classifier with tests for all protocol classes</name>
<files>classify/rules.go, classify/classifier.go, classify/classifier_test.go</files>
<read_first>
classify/types.go
.planning/phases/01-capture-and-classification/01-RESEARCH.md
</read_first>
<behavior>
- 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
</behavior>
<action>
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.
</action>
<verify>
<automated>cd /home/dev/workspace/yoloyolo && go test -v -count=1 ./classify/...</automated>
</verify>
<acceptance_criteria>
- 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
</acceptance_criteria>
<done>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.</done>
</task>
</tasks>
<verification>
- `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`)
</verification>
<success_criteria>
- 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
</success_criteria>
<output>
After completion, create `.planning/phases/01-capture-and-classification/01-01-SUMMARY.md`
</output>