chore(canary-phase8): mysql fake TCP server complete — generator set CLOSED

Phase 8 ships the SEVENTH AND FINAL generator. All 7 token types
(webbug, slowredirect, docx, pdf, kubeconfig, envfile, mysql) are now
registered; the canary token-type set is closed. Phases 9-18 are
service/handler/wire-up/notification/manage-page/admin/geoip/frontend
work — no more generators.

mysql is the only non-HTTP generator: the trigger fires when an
attacker tries to authenticate against our fake MySQL server using
the canary-issued connection string. We accept the TCP connection,
write a real-looking HandshakeV10 packet (5.7.40-canary banner +
mysql_native_password auth plugin + valid capability mask), read the
client's HandshakeResponse41, extract the username (which is
"canary_"+token.ID per spec §9.7 line 1341), look up the token by
stripping the prefix, record an event with the kubectl-equivalent
forensic fields (mysql_username + mysql_client_capabilities formatted
as 0x%08x + mysql_client_charset), and respond with the canonical
ERR_Packet 1045 "Access denied" message before closing. To the
attacker the response is byte-indistinguishable from a real MySQL
server rejecting bad credentials.

Implementation commits in this phase:
  - 712202ab feat(canary): mysql wire protocol (handshake + auth +
    ERR packets)
  - 4d4d4951 feat(canary): mysql server + connection handler
  - fe80c94a feat(canary): mysql generator + register in registry

Deliverables:
  - protocol.go: byte-exact HandshakeV10 builder per spec §9.7 lines
    1366-1379 (3-byte LE length + seq id, protocol version 0x0a,
    "5.7.40-canary\x00", random conn id, 8+12 split auth-plugin-data,
    capability flags 0xf7ff/0x81ff, charset 0x21, status 0x0002,
    auth-plugin-data length 0x15, mysql_native_password); ClientAuth
    parser for HandshakeResponse41; BuildAccessDeniedErr ERR_Packet
    with code 1045 + SQL state 28000; crypto/rand-backed
    NewRandomAuthData (20 bytes) + NewRandomConnectionID (uint32);
    error sentinels (ErrShortPacket, ErrPacketTooLarge,
    ErrInvalidPayload, ErrUsernameMissing, ErrHTTPTriggerNotSupported)
  - handler.go: HandleConnection per spec §9.7 lines 1410-1442 —
    10-second deadline, handshake-first, defense-in-depth silent
    drops for non-canary username / unknown token / lookup error /
    nil recorder; TokenLookup + EventRecorder interfaces decouple
    from Phase 9/10 services
  - server.go: ctx-aware net.ListenConfig listener, per-connection
    goroutines drained via WaitGroup on shutdown, net.ErrClosed
    silenced during graceful close
  - generator.go: New() + NewWithAddress(host, port); Generate writes
    mysql:// connection string and persists mysql_username into
    t.Metadata via setMySQLUsername (map[string]json.RawMessage
    merge — sanctioned deviation from spec §9.7 line 1343's
    map[string]any indexing because real Token.Metadata is
    json.RawMessage); Trigger returns ErrHTTPTriggerNotSupported
    (TCP-only, no HTTP route)
  - Registry expanded 6 → 7; TestBuild_AllSevenGeneratorsRegistered
    closes the cardinality-test sequence (no more "pending types"
    list — the set is complete)

~115 test cases added across 4 test files:
  - protocol_test.go (~24): byte-exact packet header re-decode, LE
    conn-id round-trip, 5-username Build/Read round-trip, error
    paths (short, missing terminator, empty reader, oversize),
    randomness distinctness for both crypto/rand helpers
  - handler_test.go (~10): real socket pairs (net.Listen + Dial),
    handshake-first assertion, non-canary username silent drop,
    full known-token flow with ERR packet + event Extra capture,
    unknown token silent, lookup error silent, nil EventRecorder
    tolerated, bad auth packet silent
  - server_test.go (~5): nil handler rejection, real-dial dispatch,
    10 concurrent connections, ctx cancellation graceful shutdown,
    invalid address rejection
  - generator_test.go (~11): Type, KindConnectionString, default +
    custom address, canary_ prefix invariant, metadata persistence
    on empty token, existing-fields preservation (non-string field
    round-trips), malformed-metadata recovery, stale-username
    overwrite, baseURL irrelevance, Trigger sentinel error path
    (happy + nil token)

Audit outcome (2 agents in parallel per standing pattern):
  - superpowers:code-reviewer: PASS-WITH-NITS (0 BLOCKER, 0
    SHOULD-FIX, 8 NITs all explicitly marked "not worth fixing" /
    "leave as-is" / "theoretical, never happens")
  - general-purpose spec-adherence vs §9.7 + sanctioned deviations:
    PASS (0 BLOCKER, 0 SHOULD-FIX, 0 NITs — confirmed both
    sanctioned deviations matched the slowredirect/envfile metadata
    pattern and the TCP-only Trigger shape)

No audit-fix commit this phase. Matching Phase 5 / Phase 7 pattern
(Phase 6 was the only phase to require an audit-fix commit before
rollup).

DEFERRED TO PHASE 9 (sanctioned per handoff anti-relitigation):
  - Task 8.5: wire `go mysql.Run(ctx, addr, handler)` into
    cmd/canary/main.go via cfg.MySQL.Enabled + cfg.MySQL.Addr.
    Requires config.Config.MySQL field which lands in Phase 9
    alongside config.Config.Canary.BaseURL. Wiring it in Phase 8
    would require partial config additions Phase 9 then re-touches.

  - Task 8.6: manual `mysql -h ... -u canary_<id> -ptest internal_db`
    smoke test. Operator-deferred per standing rule.

Pre-rollup acceptance gate clean at HEAD fe80c94a:
  - go build ./... + go vet ./... clean
  - go test -race -timeout=60s ./... all packages pass
  - go test -tags=integration -race -timeout=300s ./internal/token/...
    ./internal/event/... pass
  - golangci-lint run ./... → 0 issues.
  - grep -rn "//nolint" --include="*.go" returns nothing
  - BACKLOG.md open section still _(none)_
  - go.mod surface unchanged (no new deps for the entire phase)

# GENERATOR SET CLOSED

  webbug         (Phase 2) — /c/{id} GIF, simplest case
  slowredirect   (Phase 3) — HTML page + fingerprint endpoint
  docx           (Phase 4) — zip-patched OOXML INCLUDEPICTURE
  pdf            (Phase 5) — byte-replace fixed-width URI action
  kubeconfig     (Phase 6) — YAML + fake K8s /k/{id}/* handler
  envfile        (Phase 7) — shuffled bait + INTERNAL_METRICS_ENDPOINT
  mysql          (Phase 8) — TCP wire protocol + ERR_Packet 1045

Forward state for Phase 9 (token service + handlers + Turnstile +
fingerprint mw): wires the registry into cmd/canary/main.go, brings
config.Config.Canary{BaseURL} + config.Config.MySQL{Enabled,Addr,
PublicHost,PublicPort}, defines token.Service.Create (id+manage_id
generation, registry dispatch, repository persistence) and
token.Handler (POST /api/tokens, GET /tokens/types, route mounting
for /c/{id} + /c/{id}/fingerprint + /k/{id}/*), promotes the realIP
helper from webbug into middleware, adds turnstile verification +
fingerprint rate-limit middleware. Closes Phase 8's deferred task 8.5
by spawning `go mysql.Run(...)` from main.go.
This commit is contained in:
CarterPerez-dev 2026-05-13 14:59:56 -04:00
parent fe80c94a73
commit 47907a3eb4

Diff Content Not Available