docs(10-classification-layer): create phase plan

This commit is contained in:
2026-03-27 15:47:15 +01:00
parent 974a2728c1
commit 207264b490
3 changed files with 769 additions and 3 deletions
+6 -3
View File
@@ -37,7 +37,7 @@ Full details: `.planning/milestones/v1.1-ROADMAP.md`
- [x] **Phase 8: Test and Constant Cleanup** - 1 plan — Remove stale constants and update test bounds that would block all subsequent v1.2 work (completed 2026-03-27) - [x] **Phase 8: Test and Constant Cleanup** - 1 plan — Remove stale constants and update test bounds that would block all subsequent v1.2 work (completed 2026-03-27)
- [ ] **Phase 9: Frequency Design and Group Architecture** - 2 plans — Design complete Hz allocation for all ~35 classes in family bands and add Group field to FreqConfig - [ ] **Phase 9: Frequency Design and Group Architecture** - 2 plans — Design complete Hz allocation for all ~35 classes in family bands and add Group field to FreqConfig
- [ ] **Phase 10: Classification Layer** - Add ~21 new TrafficClass constants and port rules covering all new protocol families - [ ] **Phase 10: Classification Layer** - 2 plans — Add 21 new TrafficClass constants, 30 port rules, and test coverage for all new protocol families
- [ ] **Phase 11: Synthesis and Config Layer** - Add ClassFreqConfigs entries for all new classes, update auto-assign range, and add group-header output to --print-config - [ ] **Phase 11: Synthesis and Config Layer** - Add ClassFreqConfigs entries for all new classes, update auto-assign range, and add group-header output to --print-config
## Phase Details ## Phase Details
@@ -77,7 +77,10 @@ Plans:
2. All existing 10 protocol classifications continue to match as before — no regression in rule order or port assignments 2. All existing 10 protocol classifications continue to match as before — no regression in rule order or port assignments
3. Multiple ports mapping to the same family class (e.g., IMAP port 143 and IMAPS port 993 both classify as the same Mail-IMAP class) behave identically in the classifier output 3. Multiple ports mapping to the same family class (e.g., IMAP port 143 and IMAPS port 993 both classify as the same Mail-IMAP class) behave identically in the classifier output
4. `go test ./classify/...` passes with no new test failures 4. `go test ./classify/...` passes with no new test failures
**Plans**: TBD **Plans:** 2 plans
Plans:
- [ ] 10-01-PLAN.md — Add 21 TrafficClass constants and 30 port-matching rules
- [ ] 10-02-PLAN.md — Add 28 classification subtests and update TestAllClassesCount
### Phase 11: Synthesis and Config Layer ### Phase 11: Synthesis and Config Layer
**Goal**: Every new traffic class produces a distinct, family-coherent sound and --print-config shows all classes organized by group with section headers **Goal**: Every new traffic class produces a distinct, family-coherent sound and --print-config shows all classes organized by group with section headers
@@ -103,5 +106,5 @@ Plans:
| 7. Custom Rules and Print-Config | v1.1 | 2/2 | Complete | 2026-03-26 | | 7. Custom Rules and Print-Config | v1.1 | 2/2 | Complete | 2026-03-26 |
| 8. Test and Constant Cleanup | v1.2 | 1/1 | Complete | 2026-03-27 | | 8. Test and Constant Cleanup | v1.2 | 1/1 | Complete | 2026-03-27 |
| 9. Frequency Design and Group Architecture | v1.2 | 1/2 | In Progress| | | 9. Frequency Design and Group Architecture | v1.2 | 1/2 | In Progress| |
| 10. Classification Layer | v1.2 | 0/? | Not started | - | | 10. Classification Layer | v1.2 | 0/2 | Not started | - |
| 11. Synthesis and Config Layer | v1.2 | 0/? | Not started | - | | 11. Synthesis and Config Layer | v1.2 | 0/? | Not started | - |
@@ -0,0 +1,346 @@
---
phase: 10-classification-layer
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- classify/types.go
- classify/rules.go
autonomous: true
requirements:
- PROTO-01
- PROTO-02
- PROTO-03
- PROTO-04
- PROTO-05
- PROTO-06
- PROTO-07
- PROTO-08
must_haves:
truths:
- "21 new TrafficClass constants exist with correct string values per D-05"
- "AllClasses() returns 32 classes (18 new + 14 existing, excluding LDAP/Kerberos/Syslog per D-01/D-02)"
- "DefaultRules contains 30 new port-matching rules before the catch-alls per D-06"
- "Plain/TLS variants share a single class constant per D-03"
- "SMTP (port 25) and SMTP-sub (port 587) remain separate classes per D-04"
artifacts:
- path: "classify/types.go"
provides: "21 new TrafficClass constants and updated AllClasses()"
contains: "ClassIMAP"
- path: "classify/rules.go"
provides: "30 new port-matching rules"
contains: "ClassMongoDB"
key_links:
- from: "classify/rules.go"
to: "classify/types.go"
via: "Rule.Class references TrafficClass constants"
pattern: "Class:\\s+Class(IMAP|POP3|FTP|RDP|MySQL)"
---
<objective>
Add all 21 new TrafficClass constants and 30 new port-matching rules to the classify package.
Purpose: Phase 10 expands protocol coverage from 14 to 35 traffic classes. This plan adds the production code — constants in types.go and rules in rules.go. Tests are added in Plan 02.
Output: Updated classify/types.go with 21 new constants and updated AllClasses(), updated classify/rules.go with 30 new rules organized by family.
</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
@classify/types.go
@classify/rules.go
<interfaces>
<!-- Key types and contracts the executor needs. -->
From classify/types.go:
```go
type TrafficClass string
const (
ClassICMP TrafficClass = "ICMP"
ClassDNS TrafficClass = "DNS"
// ... 12 more existing constants
)
func AllClasses() []TrafficClass {
return []TrafficClass{
ClassICMP, ClassDNS, ClassHTTPS, ClassHTTP, ClassSSH,
ClassSMTP, ClassNTP, ClassDHCP, ClassOtherTCP, ClassOtherUDP,
ClassUnknown1, ClassUnknown2, ClassUnknown3, ClassUnknown4,
}
}
```
From classify/rules.go:
```go
type Rule struct {
Protocol string
DstPort uint16
Class TrafficClass
}
var DefaultRules = []Rule{
// 10 specific rules + 2 catch-alls (tcp/0 and udp/0 must be last)
}
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Add 21 new TrafficClass constants and update AllClasses()</name>
<files>classify/types.go</files>
<read_first>
- classify/types.go (current constants and AllClasses implementation)
- synth/config.go lines 74-110 (frequency table comment showing expected class names)
</read_first>
<action>
Add 21 new TrafficClass constants to the existing const block in classify/types.go. Group them by family with comments. The exact constants and string values (per D-05 naming convention):
```go
// --- Mail (PROTO-01) ---
ClassIMAP TrafficClass = "IMAP"
ClassPOP3 TrafficClass = "POP3"
ClassSMTPSub TrafficClass = "SMTP-sub" // D-04: separate from ClassSMTP (port 25)
// --- File Transfer (PROTO-02) ---
ClassFTP TrafficClass = "FTP"
ClassSMB TrafficClass = "SMB"
ClassTFTP TrafficClass = "TFTP"
// --- Remote Access (PROTO-03) ---
ClassRDP TrafficClass = "RDP"
ClassTelnet TrafficClass = "Telnet"
ClassVNC TrafficClass = "VNC"
// --- Database (PROTO-04) ---
ClassMySQL TrafficClass = "MySQL"
ClassPostgreSQL TrafficClass = "PostgreSQL"
ClassRedis TrafficClass = "Redis"
ClassMongoDB TrafficClass = "MongoDB"
// --- Discovery (PROTO-05) ---
ClassMDNS TrafficClass = "mDNS"
ClassSSDP TrafficClass = "SSDP"
ClassSNMP TrafficClass = "SNMP"
// --- VoIP (PROTO-06) ---
ClassSIP TrafficClass = "SIP"
// --- Web extension (PROTO-07) ---
ClassQUIC TrafficClass = "QUIC"
// --- Infrastructure extension (PROTO-08, D-01: no ClassFreqConfigs until Phase 11) ---
ClassLDAP TrafficClass = "LDAP"
ClassKerberos TrafficClass = "Kerberos"
ClassSyslog TrafficClass = "Syslog"
```
Update AllClasses() to return 32 classes. Add 18 new classes (all except LDAP, Kerberos, Syslog — those are excluded per D-01/D-02 to keep synth/config_test.go green). Organize by group with comments:
```go
func AllClasses() []TrafficClass {
return []TrafficClass{
// Infrastructure
ClassICMP, ClassDNS, ClassNTP, ClassDHCP,
ClassMDNS, ClassSSDP, ClassSNMP,
// Web
ClassHTTPS, ClassHTTP, ClassQUIC,
// Mail
ClassSMTP, ClassIMAP, ClassPOP3, ClassSMTPSub,
// Remote Access
ClassSSH, ClassRDP, ClassTelnet, ClassVNC,
// File Transfer
ClassFTP, ClassSMB, ClassTFTP,
// Database
ClassMySQL, ClassPostgreSQL, ClassRedis, ClassMongoDB,
// VoIP
ClassSIP,
// Unknown / catch-all
ClassUnknown1, ClassUnknown2, ClassUnknown3, ClassUnknown4,
ClassOtherTCP, ClassOtherUDP,
}
}
```
IMPORTANT: Do NOT add ClassLDAP, ClassKerberos, or ClassSyslog to AllClasses(). They get constants and rules but are excluded from AllClasses() to avoid breaking TestAllClassesHaveConfig in synth/config_test.go. They will be added in Phase 11 when their ClassFreqConfigs entries are created.
</action>
<verify>
<automated>cd /home/dev/workspace/yoloyolo && go build ./classify/...</automated>
</verify>
<acceptance_criteria>
- classify/types.go contains `ClassIMAP TrafficClass = "IMAP"`
- classify/types.go contains `ClassPOP3 TrafficClass = "POP3"`
- classify/types.go contains `ClassSMTPSub TrafficClass = "SMTP-sub"`
- classify/types.go contains `ClassFTP TrafficClass = "FTP"`
- classify/types.go contains `ClassSMB TrafficClass = "SMB"`
- classify/types.go contains `ClassTFTP TrafficClass = "TFTP"`
- classify/types.go contains `ClassRDP TrafficClass = "RDP"`
- classify/types.go contains `ClassTelnet TrafficClass = "Telnet"`
- classify/types.go contains `ClassVNC TrafficClass = "VNC"`
- classify/types.go contains `ClassMySQL TrafficClass = "MySQL"`
- classify/types.go contains `ClassPostgreSQL TrafficClass = "PostgreSQL"`
- classify/types.go contains `ClassRedis TrafficClass = "Redis"`
- classify/types.go contains `ClassMongoDB TrafficClass = "MongoDB"`
- classify/types.go contains `ClassMDNS TrafficClass = "mDNS"`
- classify/types.go contains `ClassSSDP TrafficClass = "SSDP"`
- classify/types.go contains `ClassSNMP TrafficClass = "SNMP"`
- classify/types.go contains `ClassSIP TrafficClass = "SIP"`
- classify/types.go contains `ClassQUIC TrafficClass = "QUIC"`
- classify/types.go contains `ClassLDAP TrafficClass = "LDAP"`
- classify/types.go contains `ClassKerberos TrafficClass = "Kerberos"`
- classify/types.go contains `ClassSyslog TrafficClass = "Syslog"`
- AllClasses() body contains ClassSIP but does NOT contain ClassLDAP, ClassKerberos, or ClassSyslog
- `go build ./classify/...` succeeds
</acceptance_criteria>
<done>21 new TrafficClass constants defined, AllClasses() returns 32 classes (excluding LDAP/Kerberos/Syslog), package compiles</done>
</task>
<task type="auto">
<name>Task 2: Add 30 new port-matching rules to DefaultRules</name>
<files>classify/rules.go</files>
<read_first>
- classify/rules.go (current DefaultRules with 12 entries)
- classify/types.go (after Task 1 — verify new constants exist)
</read_first>
<action>
Replace the DefaultRules slice in classify/rules.go with the expanded version containing all 42 rules (12 existing + 30 new). All new specific-port rules MUST appear BEFORE the two catch-all entries (per D-06). Group rules by family with comments for readability.
The complete DefaultRules slice:
```go
var DefaultRules = []Rule{
// --- Infrastructure ---
{Protocol: "icmp", DstPort: 0, Class: ClassICMP},
{Protocol: "udp", DstPort: 53, Class: ClassDNS},
{Protocol: "tcp", DstPort: 53, Class: ClassDNS},
{Protocol: "udp", DstPort: 123, Class: ClassNTP},
{Protocol: "udp", DstPort: 67, Class: ClassDHCP},
{Protocol: "udp", DstPort: 68, Class: ClassDHCP},
// --- Discovery (PROTO-05) ---
{Protocol: "udp", DstPort: 5353, Class: ClassMDNS},
{Protocol: "udp", DstPort: 1900, Class: ClassSSDP},
{Protocol: "udp", DstPort: 161, Class: ClassSNMP},
{Protocol: "udp", DstPort: 162, Class: ClassSNMP},
// --- Web (existing + PROTO-07) ---
{Protocol: "tcp", DstPort: 443, Class: ClassHTTPS},
{Protocol: "udp", DstPort: 443, Class: ClassQUIC}, // PROTO-07: must use "udp", NOT "tcp"
{Protocol: "tcp", DstPort: 80, Class: ClassHTTP},
// --- Mail (existing SMTP + PROTO-01) ---
{Protocol: "tcp", DstPort: 25, Class: ClassSMTP},
{Protocol: "tcp", DstPort: 143, Class: ClassIMAP}, // D-03: IMAP plain
{Protocol: "tcp", DstPort: 993, Class: ClassIMAP}, // D-03: IMAPS
{Protocol: "tcp", DstPort: 110, Class: ClassPOP3}, // D-03: POP3 plain
{Protocol: "tcp", DstPort: 995, Class: ClassPOP3}, // D-03: POP3S
{Protocol: "tcp", DstPort: 587, Class: ClassSMTPSub}, // D-04: separate from SMTP
// --- Remote Access (existing SSH + PROTO-03) ---
{Protocol: "tcp", DstPort: 22, Class: ClassSSH},
{Protocol: "tcp", DstPort: 3389, Class: ClassRDP},
{Protocol: "tcp", DstPort: 23, Class: ClassTelnet},
{Protocol: "tcp", DstPort: 5900, Class: ClassVNC},
// --- File Transfer (PROTO-02) ---
{Protocol: "tcp", DstPort: 20, Class: ClassFTP}, // D-03: FTP data
{Protocol: "tcp", DstPort: 21, Class: ClassFTP}, // D-03: FTP control
{Protocol: "tcp", DstPort: 445, Class: ClassSMB},
{Protocol: "udp", DstPort: 69, Class: ClassTFTP},
// --- Database (PROTO-04) ---
{Protocol: "tcp", DstPort: 3306, Class: ClassMySQL},
{Protocol: "tcp", DstPort: 5432, Class: ClassPostgreSQL},
{Protocol: "tcp", DstPort: 6379, Class: ClassRedis},
{Protocol: "tcp", DstPort: 27017, Class: ClassMongoDB},
// --- VoIP (PROTO-06) ---
{Protocol: "tcp", DstPort: 5060, Class: ClassSIP}, // D-03: SIP plain TCP
{Protocol: "tcp", DstPort: 5061, Class: ClassSIP}, // D-03: SIPS TCP
{Protocol: "udp", DstPort: 5060, Class: ClassSIP}, // D-03: SIP plain UDP
{Protocol: "udp", DstPort: 5061, Class: ClassSIP}, // D-03: SIPS UDP
// --- Infrastructure extension (PROTO-08, D-01) ---
{Protocol: "tcp", DstPort: 389, Class: ClassLDAP}, // D-03: LDAP plain
{Protocol: "tcp", DstPort: 636, Class: ClassLDAP}, // D-03: LDAPS
{Protocol: "tcp", DstPort: 88, Class: ClassKerberos},
{Protocol: "udp", DstPort: 88, Class: ClassKerberos},
{Protocol: "udp", DstPort: 514, Class: ClassSyslog},
// Catch-alls (must be last — D-06):
{Protocol: "tcp", DstPort: 0, Class: ClassOtherTCP},
{Protocol: "udp", DstPort: 0, Class: ClassOtherUDP},
}
```
CRITICAL: The two catch-all rules `{tcp, 0, ClassOtherTCP}` and `{udp, 0, ClassOtherUDP}` MUST remain as the last two entries. All 30 new specific-port rules go before them. Do NOT add port 465 (SMTPS) — PROTO-01 only specifies port 587.
</action>
<verify>
<automated>cd /home/dev/workspace/yoloyolo && go build ./classify/...</automated>
</verify>
<acceptance_criteria>
- classify/rules.go contains `DstPort: 143, Class: ClassIMAP`
- classify/rules.go contains `DstPort: 993, Class: ClassIMAP`
- classify/rules.go contains `DstPort: 110, Class: ClassPOP3`
- classify/rules.go contains `DstPort: 995, Class: ClassPOP3`
- classify/rules.go contains `DstPort: 587, Class: ClassSMTPSub`
- classify/rules.go contains `DstPort: 20, Class: ClassFTP`
- classify/rules.go contains `DstPort: 21, Class: ClassFTP`
- classify/rules.go contains `DstPort: 445, Class: ClassSMB`
- classify/rules.go contains `DstPort: 69, Class: ClassTFTP`
- classify/rules.go contains `DstPort: 3389, Class: ClassRDP`
- classify/rules.go contains `DstPort: 23, Class: ClassTelnet`
- classify/rules.go contains `DstPort: 5900, Class: ClassVNC`
- classify/rules.go contains `DstPort: 3306, Class: ClassMySQL`
- classify/rules.go contains `DstPort: 5432, Class: ClassPostgreSQL`
- classify/rules.go contains `DstPort: 6379, Class: ClassRedis`
- classify/rules.go contains `DstPort: 27017, Class: ClassMongoDB`
- classify/rules.go contains `DstPort: 5353, Class: ClassMDNS`
- classify/rules.go contains `DstPort: 1900, Class: ClassSSDP`
- classify/rules.go contains `DstPort: 161, Class: ClassSNMP`
- classify/rules.go contains `DstPort: 162, Class: ClassSNMP`
- classify/rules.go contains `DstPort: 5060, Class: ClassSIP` (both tcp and udp)
- classify/rules.go contains `DstPort: 5061, Class: ClassSIP` (both tcp and udp)
- classify/rules.go contains `Protocol: "udp", DstPort: 443, Class: ClassQUIC`
- classify/rules.go contains `DstPort: 389, Class: ClassLDAP`
- classify/rules.go contains `DstPort: 636, Class: ClassLDAP`
- classify/rules.go contains `DstPort: 88, Class: ClassKerberos` (both tcp and udp)
- classify/rules.go contains `DstPort: 514, Class: ClassSyslog`
- classify/rules.go does NOT contain `DstPort: 465`
- The last two entries in DefaultRules are the catch-all rules (DstPort: 0)
- `go build ./classify/...` succeeds
</acceptance_criteria>
<done>DefaultRules contains 42 rules (12 existing + 30 new), all specific-port rules before catch-alls, package compiles</done>
</task>
</tasks>
<verification>
- `go build ./classify/...` compiles without errors
- 21 new constants exist in types.go
- AllClasses() has 32 entries (14 existing + 18 new, excluding LDAP/Kerberos/Syslog)
- DefaultRules has 42 entries (12 existing + 30 new) with catch-alls last
</verification>
<success_criteria>
- All 21 new TrafficClass constants compile
- AllClasses() returns exactly 32 classes
- DefaultRules has exactly 42 rules with catch-alls as last 2 entries
- `go build ./classify/...` passes
</success_criteria>
<output>
After completion, create `.planning/phases/10-classification-layer/10-01-SUMMARY.md`
</output>
@@ -0,0 +1,417 @@
---
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>