From 207264b4907e406a82e7e690083c9bd003682824 Mon Sep 17 00:00:00 2001 From: Markus Graf Date: Fri, 27 Mar 2026 15:47:15 +0100 Subject: [PATCH] docs(10-classification-layer): create phase plan --- .planning/ROADMAP.md | 9 +- .../10-classification-layer/10-01-PLAN.md | 346 +++++++++++++++ .../10-classification-layer/10-02-PLAN.md | 417 ++++++++++++++++++ 3 files changed, 769 insertions(+), 3 deletions(-) create mode 100644 .planning/phases/10-classification-layer/10-01-PLAN.md create mode 100644 .planning/phases/10-classification-layer/10-02-PLAN.md 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 + + + +After completion, create `.planning/phases/10-classification-layer/10-01-SUMMARY.md` + 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) + + + +After completion, create `.planning/phases/10-classification-layer/10-02-SUMMARY.md` +