35 traffic classes across 9 protocol families shipped. Archives ROADMAP, REQUIREMENTS, and phase directories to milestones/v1.2-*. Updates README with new protocol families, sound design table, and [groups] TOML config documentation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
418 lines
15 KiB
Markdown
418 lines
15 KiB
Markdown
---
|
|
phase: 10-classification-layer
|
|
plan: 02
|
|
type: execute
|
|
wave: 2
|
|
depends_on:
|
|
- 10-01
|
|
files_modified:
|
|
- classify/classifier_test.go
|
|
autonomous: true
|
|
requirements:
|
|
- PROTO-01
|
|
- PROTO-02
|
|
- PROTO-03
|
|
- PROTO-04
|
|
- PROTO-05
|
|
- PROTO-06
|
|
- PROTO-07
|
|
- PROTO-08
|
|
- PROTO-09
|
|
|
|
must_haves:
|
|
truths:
|
|
- "Every new protocol port is tested and classifies to the correct TrafficClass"
|
|
- "Plain/TLS port variants of the same protocol classify to the same class"
|
|
- "SIP is tested on both TCP and UDP transports"
|
|
- "QUIC (UDP 443) classifies as ClassQUIC, not ClassHTTPS"
|
|
- "All 10 existing protocol tests still pass unchanged (PROTO-09)"
|
|
- "TestAllClassesCount expects 32"
|
|
artifacts:
|
|
- path: "classify/classifier_test.go"
|
|
provides: "26 new subtests covering all new port/protocol combinations"
|
|
contains: "ClassIMAP"
|
|
key_links:
|
|
- from: "classify/classifier_test.go"
|
|
to: "classify/types.go"
|
|
via: "test assertions reference new TrafficClass constants"
|
|
pattern: "classify\\.Class(IMAP|RDP|MySQL|QUIC|SIP)"
|
|
- from: "classify/classifier_test.go"
|
|
to: "classify/rules.go"
|
|
via: "NewClassifier(DefaultRules) uses updated rules"
|
|
pattern: "classify\\.DefaultRules"
|
|
---
|
|
|
|
<objective>
|
|
Add comprehensive test coverage for all 21 new protocol classifications and update TestAllClassesCount.
|
|
|
|
Purpose: Verify every new port-matching rule in DefaultRules produces the correct TrafficClass. This is the verification gate for Phase 10 — `go test ./classify/...` must pass.
|
|
|
|
Output: Updated classify/classifier_test.go with 26 new subtests and updated count assertion.
|
|
</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/10-classification-layer/10-01-SUMMARY.md
|
|
@classify/classifier_test.go
|
|
@classify/types.go
|
|
@classify/rules.go
|
|
|
|
<interfaces>
|
|
<!-- Existing test helpers available for reuse -->
|
|
|
|
From classify/classifier_test.go:
|
|
```go
|
|
func buildTCPPacket(t *testing.T, dstPort uint16) gopacket.Packet
|
|
func buildUDPPacket(t *testing.T, dstPort uint16) gopacket.Packet
|
|
func buildICMPPacket(t *testing.T) gopacket.Packet
|
|
|
|
// Existing TestClassify subtests follow pattern:
|
|
t.Run("TestClassifyHTTPS", func(t *testing.T) {
|
|
pkt := buildTCPPacket(t, 443)
|
|
got := c.Classify(pkt)
|
|
if got.Class != classify.ClassHTTPS {
|
|
t.Errorf("HTTPS packet: got class %q, want %q", got.Class, classify.ClassHTTPS)
|
|
}
|
|
})
|
|
```
|
|
|
|
From classify/types.go (after Plan 01):
|
|
```go
|
|
// New constants available: ClassIMAP, ClassPOP3, ClassSMTPSub, ClassFTP, ClassSMB,
|
|
// ClassTFTP, ClassRDP, ClassTelnet, ClassVNC, ClassMySQL, ClassPostgreSQL, ClassRedis,
|
|
// ClassMongoDB, ClassMDNS, ClassSSDP, ClassSNMP, ClassSIP, ClassQUIC,
|
|
// ClassLDAP, ClassKerberos, ClassSyslog
|
|
```
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Add 26 new classification subtests to TestClassify</name>
|
|
<files>classify/classifier_test.go</files>
|
|
<read_first>
|
|
- classify/classifier_test.go (existing test structure and helpers)
|
|
- classify/types.go (verify new constants from Plan 01)
|
|
- classify/rules.go (verify new rules from Plan 01)
|
|
</read_first>
|
|
<action>
|
|
Add the following subtests inside the existing `TestClassify` function, after the existing subtests and before the `TestRulesAreOrderDependent` subtest. Use the same pattern as existing subtests: `buildTCPPacket` or `buildUDPPacket` with the port, then assert `got.Class` matches the expected constant.
|
|
|
|
Complete list of 26 new subtests to add:
|
|
|
|
```go
|
|
// --- Mail (PROTO-01) ---
|
|
t.Run("TestClassifyIMAP_port143", func(t *testing.T) {
|
|
pkt := buildTCPPacket(t, 143)
|
|
got := c.Classify(pkt)
|
|
if got.Class != classify.ClassIMAP {
|
|
t.Errorf("IMAP port 143: got class %q, want %q", got.Class, classify.ClassIMAP)
|
|
}
|
|
})
|
|
t.Run("TestClassifyIMAP_port993", func(t *testing.T) {
|
|
pkt := buildTCPPacket(t, 993)
|
|
got := c.Classify(pkt)
|
|
if got.Class != classify.ClassIMAP {
|
|
t.Errorf("IMAPS port 993: got class %q, want %q", got.Class, classify.ClassIMAP)
|
|
}
|
|
})
|
|
t.Run("TestClassifyPOP3_port110", func(t *testing.T) {
|
|
pkt := buildTCPPacket(t, 110)
|
|
got := c.Classify(pkt)
|
|
if got.Class != classify.ClassPOP3 {
|
|
t.Errorf("POP3 port 110: got class %q, want %q", got.Class, classify.ClassPOP3)
|
|
}
|
|
})
|
|
t.Run("TestClassifyPOP3_port995", func(t *testing.T) {
|
|
pkt := buildTCPPacket(t, 995)
|
|
got := c.Classify(pkt)
|
|
if got.Class != classify.ClassPOP3 {
|
|
t.Errorf("POP3S port 995: got class %q, want %q", got.Class, classify.ClassPOP3)
|
|
}
|
|
})
|
|
t.Run("TestClassifySMTPSub_port587", func(t *testing.T) {
|
|
pkt := buildTCPPacket(t, 587)
|
|
got := c.Classify(pkt)
|
|
if got.Class != classify.ClassSMTPSub {
|
|
t.Errorf("SMTP-sub port 587: got class %q, want %q", got.Class, classify.ClassSMTPSub)
|
|
}
|
|
})
|
|
|
|
// --- File Transfer (PROTO-02) ---
|
|
t.Run("TestClassifyFTP_port20", func(t *testing.T) {
|
|
pkt := buildTCPPacket(t, 20)
|
|
got := c.Classify(pkt)
|
|
if got.Class != classify.ClassFTP {
|
|
t.Errorf("FTP data port 20: got class %q, want %q", got.Class, classify.ClassFTP)
|
|
}
|
|
})
|
|
t.Run("TestClassifyFTP_port21", func(t *testing.T) {
|
|
pkt := buildTCPPacket(t, 21)
|
|
got := c.Classify(pkt)
|
|
if got.Class != classify.ClassFTP {
|
|
t.Errorf("FTP control port 21: got class %q, want %q", got.Class, classify.ClassFTP)
|
|
}
|
|
})
|
|
t.Run("TestClassifySMB_port445", func(t *testing.T) {
|
|
pkt := buildTCPPacket(t, 445)
|
|
got := c.Classify(pkt)
|
|
if got.Class != classify.ClassSMB {
|
|
t.Errorf("SMB port 445: got class %q, want %q", got.Class, classify.ClassSMB)
|
|
}
|
|
})
|
|
t.Run("TestClassifyTFTP_port69", func(t *testing.T) {
|
|
pkt := buildUDPPacket(t, 69)
|
|
got := c.Classify(pkt)
|
|
if got.Class != classify.ClassTFTP {
|
|
t.Errorf("TFTP port 69: got class %q, want %q", got.Class, classify.ClassTFTP)
|
|
}
|
|
})
|
|
|
|
// --- Remote Access (PROTO-03) ---
|
|
t.Run("TestClassifyRDP_port3389", func(t *testing.T) {
|
|
pkt := buildTCPPacket(t, 3389)
|
|
got := c.Classify(pkt)
|
|
if got.Class != classify.ClassRDP {
|
|
t.Errorf("RDP port 3389: got class %q, want %q", got.Class, classify.ClassRDP)
|
|
}
|
|
})
|
|
t.Run("TestClassifyTelnet_port23", func(t *testing.T) {
|
|
pkt := buildTCPPacket(t, 23)
|
|
got := c.Classify(pkt)
|
|
if got.Class != classify.ClassTelnet {
|
|
t.Errorf("Telnet port 23: got class %q, want %q", got.Class, classify.ClassTelnet)
|
|
}
|
|
})
|
|
t.Run("TestClassifyVNC_port5900", func(t *testing.T) {
|
|
pkt := buildTCPPacket(t, 5900)
|
|
got := c.Classify(pkt)
|
|
if got.Class != classify.ClassVNC {
|
|
t.Errorf("VNC port 5900: got class %q, want %q", got.Class, classify.ClassVNC)
|
|
}
|
|
})
|
|
|
|
// --- Database (PROTO-04) ---
|
|
t.Run("TestClassifyMySQL_port3306", func(t *testing.T) {
|
|
pkt := buildTCPPacket(t, 3306)
|
|
got := c.Classify(pkt)
|
|
if got.Class != classify.ClassMySQL {
|
|
t.Errorf("MySQL port 3306: got class %q, want %q", got.Class, classify.ClassMySQL)
|
|
}
|
|
})
|
|
t.Run("TestClassifyPostgreSQL_port5432", func(t *testing.T) {
|
|
pkt := buildTCPPacket(t, 5432)
|
|
got := c.Classify(pkt)
|
|
if got.Class != classify.ClassPostgreSQL {
|
|
t.Errorf("PostgreSQL port 5432: got class %q, want %q", got.Class, classify.ClassPostgreSQL)
|
|
}
|
|
})
|
|
t.Run("TestClassifyRedis_port6379", func(t *testing.T) {
|
|
pkt := buildTCPPacket(t, 6379)
|
|
got := c.Classify(pkt)
|
|
if got.Class != classify.ClassRedis {
|
|
t.Errorf("Redis port 6379: got class %q, want %q", got.Class, classify.ClassRedis)
|
|
}
|
|
})
|
|
t.Run("TestClassifyMongoDB_port27017", func(t *testing.T) {
|
|
pkt := buildTCPPacket(t, 27017)
|
|
got := c.Classify(pkt)
|
|
if got.Class != classify.ClassMongoDB {
|
|
t.Errorf("MongoDB port 27017: got class %q, want %q", got.Class, classify.ClassMongoDB)
|
|
}
|
|
})
|
|
|
|
// --- Discovery (PROTO-05) ---
|
|
t.Run("TestClassifyMDNS_port5353", func(t *testing.T) {
|
|
pkt := buildUDPPacket(t, 5353)
|
|
got := c.Classify(pkt)
|
|
if got.Class != classify.ClassMDNS {
|
|
t.Errorf("mDNS port 5353: got class %q, want %q", got.Class, classify.ClassMDNS)
|
|
}
|
|
})
|
|
t.Run("TestClassifySDP_port1900", func(t *testing.T) {
|
|
pkt := buildUDPPacket(t, 1900)
|
|
got := c.Classify(pkt)
|
|
if got.Class != classify.ClassSSDP {
|
|
t.Errorf("SSDP port 1900: got class %q, want %q", got.Class, classify.ClassSSDP)
|
|
}
|
|
})
|
|
t.Run("TestClassifySNMP_port161", func(t *testing.T) {
|
|
pkt := buildUDPPacket(t, 161)
|
|
got := c.Classify(pkt)
|
|
if got.Class != classify.ClassSNMP {
|
|
t.Errorf("SNMP port 161: got class %q, want %q", got.Class, classify.ClassSNMP)
|
|
}
|
|
})
|
|
t.Run("TestClassifySNMP_port162", func(t *testing.T) {
|
|
pkt := buildUDPPacket(t, 162)
|
|
got := c.Classify(pkt)
|
|
if got.Class != classify.ClassSNMP {
|
|
t.Errorf("SNMP-trap port 162: got class %q, want %q", got.Class, classify.ClassSNMP)
|
|
}
|
|
})
|
|
|
|
// --- VoIP (PROTO-06) --- SIP runs on both TCP and UDP
|
|
t.Run("TestClassifySIP_TCP5060", func(t *testing.T) {
|
|
pkt := buildTCPPacket(t, 5060)
|
|
got := c.Classify(pkt)
|
|
if got.Class != classify.ClassSIP {
|
|
t.Errorf("SIP TCP 5060: got class %q, want %q", got.Class, classify.ClassSIP)
|
|
}
|
|
})
|
|
t.Run("TestClassifySIP_UDP5060", func(t *testing.T) {
|
|
pkt := buildUDPPacket(t, 5060)
|
|
got := c.Classify(pkt)
|
|
if got.Class != classify.ClassSIP {
|
|
t.Errorf("SIP UDP 5060: got class %q, want %q", got.Class, classify.ClassSIP)
|
|
}
|
|
})
|
|
|
|
// --- Web extension (PROTO-07) --- QUIC is UDP 443, must NOT match HTTPS (TCP 443)
|
|
t.Run("TestClassifyQUIC_UDP443", func(t *testing.T) {
|
|
pkt := buildUDPPacket(t, 443)
|
|
got := c.Classify(pkt)
|
|
if got.Class != classify.ClassQUIC {
|
|
t.Errorf("QUIC UDP 443: got class %q, want %q", got.Class, classify.ClassQUIC)
|
|
}
|
|
})
|
|
|
|
// --- Infrastructure extension (PROTO-08) ---
|
|
t.Run("TestClassifyLDAP_port389", func(t *testing.T) {
|
|
pkt := buildTCPPacket(t, 389)
|
|
got := c.Classify(pkt)
|
|
if got.Class != classify.ClassLDAP {
|
|
t.Errorf("LDAP port 389: got class %q, want %q", got.Class, classify.ClassLDAP)
|
|
}
|
|
})
|
|
t.Run("TestClassifyLDAP_port636", func(t *testing.T) {
|
|
pkt := buildTCPPacket(t, 636)
|
|
got := c.Classify(pkt)
|
|
if got.Class != classify.ClassLDAP {
|
|
t.Errorf("LDAPS port 636: got class %q, want %q", got.Class, classify.ClassLDAP)
|
|
}
|
|
})
|
|
t.Run("TestClassifyKerberos_TCP88", func(t *testing.T) {
|
|
pkt := buildTCPPacket(t, 88)
|
|
got := c.Classify(pkt)
|
|
if got.Class != classify.ClassKerberos {
|
|
t.Errorf("Kerberos TCP 88: got class %q, want %q", got.Class, classify.ClassKerberos)
|
|
}
|
|
})
|
|
t.Run("TestClassifyKerberos_UDP88", func(t *testing.T) {
|
|
pkt := buildUDPPacket(t, 88)
|
|
got := c.Classify(pkt)
|
|
if got.Class != classify.ClassKerberos {
|
|
t.Errorf("Kerberos UDP 88: got class %q, want %q", got.Class, classify.ClassKerberos)
|
|
}
|
|
})
|
|
t.Run("TestClassifySyslog_port514", func(t *testing.T) {
|
|
pkt := buildUDPPacket(t, 514)
|
|
got := c.Classify(pkt)
|
|
if got.Class != classify.ClassSyslog {
|
|
t.Errorf("Syslog port 514: got class %q, want %q", got.Class, classify.ClassSyslog)
|
|
}
|
|
})
|
|
```
|
|
|
|
That is 28 subtests (not 26 — the PROTO-06 SIP tests cover 2 extra transport variants). Add them inside `TestClassify`, after the existing `TestClassifyUnknown` subtest and before `TestRulesAreOrderDependent`.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /home/dev/workspace/yoloyolo && go test ./classify/... -run TestClassify -v 2>&1 | tail -40</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- classifier_test.go contains `TestClassifyIMAP_port143`
|
|
- classifier_test.go contains `TestClassifyIMAP_port993`
|
|
- classifier_test.go contains `TestClassifyPOP3_port110`
|
|
- classifier_test.go contains `TestClassifyPOP3_port995`
|
|
- classifier_test.go contains `TestClassifySMTPSub_port587`
|
|
- classifier_test.go contains `TestClassifyFTP_port20`
|
|
- classifier_test.go contains `TestClassifyFTP_port21`
|
|
- classifier_test.go contains `TestClassifySMB_port445`
|
|
- classifier_test.go contains `TestClassifyTFTP_port69`
|
|
- classifier_test.go contains `TestClassifyRDP_port3389`
|
|
- classifier_test.go contains `TestClassifyTelnet_port23`
|
|
- classifier_test.go contains `TestClassifyVNC_port5900`
|
|
- classifier_test.go contains `TestClassifyMySQL_port3306`
|
|
- classifier_test.go contains `TestClassifyPostgreSQL_port5432`
|
|
- classifier_test.go contains `TestClassifyRedis_port6379`
|
|
- classifier_test.go contains `TestClassifyMongoDB_port27017`
|
|
- classifier_test.go contains `TestClassifyMDNS_port5353`
|
|
- classifier_test.go contains `TestClassifySNMP_port161`
|
|
- classifier_test.go contains `TestClassifySNMP_port162`
|
|
- classifier_test.go contains `TestClassifySIP_TCP5060`
|
|
- classifier_test.go contains `TestClassifySIP_UDP5060`
|
|
- classifier_test.go contains `TestClassifyQUIC_UDP443`
|
|
- classifier_test.go contains `TestClassifyLDAP_port389`
|
|
- classifier_test.go contains `TestClassifyLDAP_port636`
|
|
- classifier_test.go contains `TestClassifyKerberos_TCP88`
|
|
- classifier_test.go contains `TestClassifyKerberos_UDP88`
|
|
- classifier_test.go contains `TestClassifySyslog_port514`
|
|
- `go test ./classify/... -run TestClassify` passes (exit 0)
|
|
</acceptance_criteria>
|
|
<done>28 new subtests added covering all new port/protocol/transport combinations, all pass</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Update TestAllClassesCount from 14 to 32</name>
|
|
<files>classify/classifier_test.go</files>
|
|
<read_first>
|
|
- classify/classifier_test.go (current TestAllClassesCount with hardcoded 14)
|
|
</read_first>
|
|
<action>
|
|
In `TestAllClassesCount`, change the hardcoded assertion from `!= 14` to `!= 32`.
|
|
|
|
The line:
|
|
```go
|
|
if len(classes) != 14 {
|
|
t.Errorf("AllClasses() returned %d classes, want 14", len(classes))
|
|
}
|
|
```
|
|
|
|
Becomes:
|
|
```go
|
|
if len(classes) != 32 {
|
|
t.Errorf("AllClasses() returned %d classes, want 32", len(classes))
|
|
}
|
|
```
|
|
|
|
The count is 32 = 14 existing + 18 new (LDAP, Kerberos, Syslog excluded from AllClasses per D-01/D-02). This will become 35 in Phase 11 when those three are added.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /home/dev/workspace/yoloyolo && go test ./classify/... -run TestAllClassesCount -v</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- classifier_test.go contains `!= 32` (not `!= 14`)
|
|
- classifier_test.go contains `want 32` (not `want 14`)
|
|
- `go test ./classify/... -run TestAllClassesCount` passes (exit 0)
|
|
</acceptance_criteria>
|
|
<done>TestAllClassesCount asserts 32 classes, test passes</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `go test ./classify/... -v` — all tests pass (existing + 28 new subtests + updated count)
|
|
- `go test ./classify/... -count=1` — no cached results, clean pass
|
|
- Existing tests (ICMP, DNS, HTTPS, HTTP, SSH, SMTP, NTP, DHCP, OtherTCP, OtherUDP, Unknown) still pass (PROTO-09)
|
|
- Note: `go test ./synth/...` is EXPECTED TO FAIL after Phase 10 because AllClasses() now has 32 entries but ClassFreqConfigs only has 14 entries. This is intentional — Phase 11 will add the missing 18 ClassFreqConfigs entries. The phase gate is `go test ./classify/...` only.
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- `go test ./classify/...` passes with 0 failures
|
|
- 28 new subtests verify every new port/protocol combination
|
|
- TestAllClassesCount expects 32
|
|
- All existing 10 protocol tests pass unchanged (PROTO-09 regression check)
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/10-classification-layer/10-02-SUMMARY.md`
|
|
</output>
|