diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index 890757f..fc497b9 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -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)
- [ ] **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 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
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
-**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
**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 |
| 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| |
-| 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 | - |
diff --git a/.planning/phases/10-classification-layer/10-01-PLAN.md b/.planning/phases/10-classification-layer/10-01-PLAN.md
new file mode 100644
index 0000000..b54e9a1
--- /dev/null
+++ b/.planning/phases/10-classification-layer/10-01-PLAN.md
@@ -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)"
+---
+
+
+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.
+
+
+
+@$HOME/.claude/get-shit-done/workflows/execute-plan.md
+@$HOME/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@classify/types.go
+@classify/rules.go
+
+
+
+
+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)
+}
+```
+
+
+
+
+
+
+ Task 1: Add 21 new TrafficClass constants and update AllClasses()
+ classify/types.go
+
+ - classify/types.go (current constants and AllClasses implementation)
+ - synth/config.go lines 74-110 (frequency table comment showing expected class names)
+
+
+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.
+
+
+ cd /home/dev/workspace/yoloyolo && go build ./classify/...
+
+
+ - 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
+
+ 21 new TrafficClass constants defined, AllClasses() returns 32 classes (excluding LDAP/Kerberos/Syslog), package compiles
+
+
+
+ Task 2: Add 30 new port-matching rules to DefaultRules
+ classify/rules.go
+
+ - classify/rules.go (current DefaultRules with 12 entries)
+ - classify/types.go (after Task 1 — verify new constants exist)
+
+
+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.
+
+
+ cd /home/dev/workspace/yoloyolo && go build ./classify/...
+
+
+ - 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
+
+ DefaultRules contains 42 rules (12 existing + 30 new), all specific-port rules before catch-alls, package compiles
+
+
+
+
+
+- `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
+
+
+
+- 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
+
+
+
diff --git a/.planning/phases/10-classification-layer/10-02-PLAN.md b/.planning/phases/10-classification-layer/10-02-PLAN.md
new file mode 100644
index 0000000..785ee36
--- /dev/null
+++ b/.planning/phases/10-classification-layer/10-02-PLAN.md
@@ -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"
+---
+
+
+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.
+
+
+
+@$HOME/.claude/get-shit-done/workflows/execute-plan.md
+@$HOME/.claude/get-shit-done/templates/summary.md
+
+
+
+@.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
+
+
+
+
+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
+```
+
+
+
+
+
+
+ Task 1: Add 26 new classification subtests to TestClassify
+ classify/classifier_test.go
+
+ - 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)
+
+
+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`.
+
+
+ cd /home/dev/workspace/yoloyolo && go test ./classify/... -run TestClassify -v 2>&1 | tail -40
+
+
+ - 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)
+
+ 28 new subtests added covering all new port/protocol/transport combinations, all pass
+
+
+
+ Task 2: Update TestAllClassesCount from 14 to 32
+ classify/classifier_test.go
+
+ - classify/classifier_test.go (current TestAllClassesCount with hardcoded 14)
+
+
+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.
+
+
+ cd /home/dev/workspace/yoloyolo && go test ./classify/... -run TestAllClassesCount -v
+
+
+ - classifier_test.go contains `!= 32` (not `!= 14`)
+ - classifier_test.go contains `want 32` (not `want 14`)
+ - `go test ./classify/... -run TestAllClassesCount` passes (exit 0)
+
+ TestAllClassesCount asserts 32 classes, test passes
+
+
+
+
+
+- `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.
+
+
+
+- `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)
+
+
+