Files
summercms/.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-08-PLAN.md
2026-09-20 16:14:21 +02:00

9.1 KiB

phase: 06-http-routing-auth-groups-and-rate-limiting plan: 08 type: execute wave: 6 depends_on: ["06-06"] files_modified: - summercms.go/fetchguard/ip.go - summercms.go/fetchguard/ip_test.go - summercms.go/fetchguard/fetch_test.go autonomous: true gap_closure: true requirements: [HTTP-07] must_haves: truths: - "NAT64 64:ff9b::/96, local-use NAT64 64:ff9b:1::/48, and 6to4 2002::/16 addresses embedding loopback, RFC1918, or 169.254.169.254 are rejected as private_ip" - "The same three transition formats embedding a public IPv4 address remain classifiable as public rather than being blanket-rejected" - "The dial-time control path, not only a standalone helper test, rejects unsafe transition addresses before connection" - "Existing IPv4, IPv4-mapped IPv6, native private IPv6, CGNAT, multicast, and unspecified-address behavior remains intact" artifacts: - path: summercms.go/fetchguard/ip.go provides: "IPv4 extraction/classification for the three supported IPv6 transition prefixes" - path: summercms.go/fetchguard/ip_test.go provides: "Transition-address tables covering embedded loopback, RFC1918, metadata, and public IPv4" - path: summercms.go/fetchguard/fetch_test.go provides: "dialControl regression proving transition rejection occurs at the actual connect boundary" key_links: - from: summercms.go/fetchguard/fetch.go to: summercms.go/fetchguard/ip.go via: "dialControl parses the actual dial address and invokes isReservedOrPrivate after Unmap" pattern: "isReservedOrPrivate(addr)" Close the transition-address SSRF bypass by decoding embedded IPv4 from both NAT64 prefixes and 6to4 before the dial-time allow decision.

Purpose: HTTP-07 treats fetchguard as a security boundary; an environment-dependent path through an IPv6 translator to loopback, RFC1918, or cloud metadata must be rejected exactly like the plain IPv4 target. Output: transition-aware classification plus table-driven helper and dial-control security regressions.

<execution_context> @/home/jin/.codex/get-shit-done/workflows/execute-plan.md @/home/jin/.codex/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-CONTEXT.md @.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-RESEARCH.md @.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-VERIFICATION.md @.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-REVIEW.md @.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-04-SUMMARY.md Task 1: Decode and reclassify embedded IPv4 at the dial-time SSRF boundary summercms.go/fetchguard/ip.go, summercms.go/fetchguard/ip_test.go, summercms.go/fetchguard/fetch_test.go - `64:ff9b::7f00:1`, `64:ff9b::a00:1`, and `64:ff9b::a9fe:a9fe` classify private; `64:ff9b::808:808` classifies public. - RFC 6052 /48 encodings under `64:ff9b:1::/48` of 127.0.0.1, 10.0.0.1, and 169.254.169.254 classify private; the encoding of 8.8.8.8 classifies public. - `2002:7f00:1::`, `2002:a00:1::`, and `2002:a9fe:a9fe::` classify private; `2002:808:808::` classifies public. - Calling the production dialControl callback for every unsafe transition literal returns an error mapped to ReasonPrivateIP before using the RawConn. summercms.go/fetchguard/ip.go (existing privateV4/privateV6 tables and classifier) summercms.go/fetchguard/ip_test.go (existing boundary cases, including IPv4-mapped Unmap behavior) summercms.go/fetchguard/fetch.go (dialControl and mapTransportError; the production connection-time link) summercms.go/fetchguard/fetch_test.go (existing real-network private-IP and reason assertions) .planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-CONTEXT.md (D-11 through D-14) .planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-VERIFICATION.md (second authoritative gap) .planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-REVIEW.md (CR-03 transition examples) RFC 6052 section 2.2 (for /96 and /48 extraction: /48 uses bits 48-63 plus 72-87 and skips the zero u octet at bits 64-71) RFC 3056 section 2 (6to4 embeds IPv4 in bits 16-47) In `ip.go`, normalize with `addr.Unmap()` inside the classifier so direct and dial-time callers cannot forget mapped-IPv4 normalization. Add package-level prefixes for `64:ff9b::/96`, `64:ff9b:1::/48`, and `2002::/16`, then an unexported extraction helper returning `(netip.Addr, bool)` for only these formats. Use `addr.As16()` and exact byte positions: the /96 NAT64 IPv4 is bytes 12-15; the RFC 6052 /48 local-use NAT64 IPv4 is bytes 6-7 followed by bytes 9-10 (byte 8 is the required zero `u` octet); 6to4 IPv4 is bytes 2-5. If a `64:ff9b:1::/48` address has a non-zero u octet, fail closed as reserved rather than treating it as native public IPv6.
Before returning public for a native IPv6 address, if the helper recognizes one of the three transition prefixes, pass the extracted IPv4 back through the ordinary IPv4 private/reserved/CGNAT/metadata classification. Reject only when the embedded address is unsafe (or the form is malformed); keep a correctly encoded public IPv4 result public. Preserve the existing native IPv6 prefix table and multicast/unspecified handling.

Add table-driven tests for all four categories (loopback, RFC1918, metadata link-local, public) under each of the three formats. Use exact /48 literals following RFC 6052, including `64:ff9b:1:7f00:0:100::` for 127.0.0.1, `64:ff9b:1:a00:0:100::` for 10.0.0.1, `64:ff9b:1:a9fe:a9:fe00::` for 169.254.169.254, and `64:ff9b:1:808:8:800::` for 8.8.8.8. Add a malformed non-zero-u /48 case and assert fail-closed.

In `fetch_test.go`, call the real `dialControl(Policy{Mode: PublicOnlyMode})` callback with bracketed IPv6 host:443 addresses and nil RawConn (the callback classifies before touching RawConn). Cover at least one loopback, one RFC1918, and the metadata address in every transition prefix; assert `errors.Is(err, errPrivateIP)` and `mapTransportError(err).Reason == ReasonPrivateIP`. This test must exercise production dialControl, not only `isReservedOrPrivate`.
cd /media/nvme/dev/golem15/summercms.io/summercms/summercms.go && go test ./fetchguard -run 'Test(IsReservedOrPrivate|.*Transition|.*NAT64|.*6to4)' -count=1 -race -short && go vet ./fetchguard && go test ./fetchguard -count=1 -race -short - Tests cover embedded 127.0.0.1, 10.0.0.1, and 169.254.169.254 for `64:ff9b::/96`, `64:ff9b:1::/48`, and `2002::/16`. - Tests cover an embedded public 8.8.8.8 in all three formats and assert it is not classified private. - The /48 decoder skips exactly the RFC 6052 u octet and a non-zero u octet fails closed. - A dialControl-level table proves every unsafe transition address returns the private-IP sentinel and maps to `ReasonPrivateIP` before connection. - Existing plain IPv4, mapped IPv4, native IPv6, CGNAT, metadata, multicast, and unspecified tests remain present and green under `-race`. All three supported IPv6 transition formats receive the same private/reserved IPv4 policy at dial time, while public embedded IPv4 remains allowed.

<threat_model>

Trust Boundaries

Boundary Description
DNS result -> TCP dial address An attacker-controlled hostname can resolve to an IPv6 transition address whose embedded IPv4 targets private infrastructure
IPv6 syntax -> IPv4 policy NAT64/6to4 representation must not bypass the ordinary loopback, RFC1918, link-local metadata, or CGNAT table

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-06-25 Elevation of Privilege / Information Disclosure fetchguard.isReservedOrPrivate and dialControl mitigate Decode RFC 6052 /96 and /48 plus 6to4 embedded IPv4, reapply the private table, fail closed on malformed local-use NAT64, and prove the production dial hook rejects unsafe cases
T-06-SC Tampering package supply chain accept No dependencies or manifests change; implementation uses net/netip and existing fetchguard code
</threat_model>
Run transition-specific tables and the full fetchguard package under the race detector. Confirm the tests distinguish unsafe embedded IPv4 from public 8.8.8.8 for each supported transition prefix and include dialControl-level evidence.

<success_criteria>

  • NAT64 and 6to4 cannot encode loopback, RFC1918, or metadata IPv4 past fetchguard.
  • The rejection is enforced at actual dial-address classification.
  • Correct public embeddings are not blanket-blocked.
  • Existing SSRF, redirect, timeout, and byte-cap behavior remains green. </success_criteria>
Create `.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-08-SUMMARY.md` when done.