Files
yoloyolo/.planning/milestones/v1.2-phases/10-classification-layer/10-02-PLAN.md
T
gurixandClaude Opus 4.6 494385b528 chore: archive v1.2 milestone — Extended Protocol Coverage
35 traffic classes across 9 protocol families shipped. Archives
ROADMAP, REQUIREMENTS, and phase directories to milestones/v1.2-*.
Updates README with new protocol families, sound design table,
and [groups] TOML config documentation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:48:53 +01:00

15 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
10-classification-layer 02 execute 2
10-01
classify/classifier_test.go
true
PROTO-01
PROTO-02
PROTO-03
PROTO-04
PROTO-05
PROTO-06
PROTO-07
PROTO-08
PROTO-09
truths artifacts key_links
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
path provides contains
classify/classifier_test.go 26 new subtests covering all new port/protocol combinations ClassIMAP
from to via pattern
classify/classifier_test.go classify/types.go test assertions reference new TrafficClass constants classify.Class(IMAP|RDP|MySQL|QUIC|SIP)
from to via pattern
classify/classifier_test.go classify/rules.go NewClassifier(DefaultRules) uses updated rules 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.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_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

From classify/classifier_test.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):

// 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:

// --- 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 <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> 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:

if len(classes) != 14 {
    t.Errorf("AllClasses() returned %d classes, want 14", len(classes))
}

Becomes:

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 <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> 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.

<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>
After completion, create `.planning/phases/10-classification-layer/10-02-SUMMARY.md`