diff --git a/pkg/networking/utilities.go b/pkg/networking/utilities.go index bf2c7aeebc..74a76f218c 100644 --- a/pkg/networking/utilities.go +++ b/pkg/networking/utilities.go @@ -72,8 +72,18 @@ const ( // is blocked wholesale via privateIPBlocks to avoid a false-negative bypass. var nat64Prefixes []*net.IPNet -// embeddedIPv4 returns the IPv4 address embedded in the low 32 bits of a NAT64 -// address if ip falls inside a NAT64 translation prefix, or nil otherwise. +// sixToFourPrefix is the 6to4 prefix 2002::/16 (RFC 3056, deprecated by +// RFC 7526). A 6to4 address embeds a full IPv4 address in bits 16-47 +// (2002:aabb:ccdd::/48 encodes aa.bb.cc.dd), so — like NAT64 — its true +// reachability is determined by the embedded IPv4, not by the (global-unicast) +// IPv6 form. Unlike the NAT64 remainder above, this embedding is exact and +// reversible for the whole prefix, so it is decoded rather than blocked +// wholesale (blocking 2002::/16 outright would also reject legitimate public +// 6to4 addresses). +var sixToFourPrefix *net.IPNet + +// embeddedIPv4 returns the IPv4 address embedded in a NAT64 or 6to4 address +// if ip falls inside one of those translation prefixes, or nil otherwise. func embeddedIPv4(ip net.IP) net.IP { v6 := ip.To16() if v6 == nil || ip.To4() != nil { @@ -84,6 +94,9 @@ func embeddedIPv4(ip net.IP) net.IP { return net.IPv4(v6[12], v6[13], v6[14], v6[15]) } } + if sixToFourPrefix.Contains(ip) { + return net.IPv4(v6[2], v6[3], v6[4], v6[5]) + } return nil } @@ -109,6 +122,11 @@ func init() { // to its embedded IPv4 below; this catch-all blocks the remaining // non-/96 embeddings, which cannot be decoded from the address alone. "64:ff9b:1::/48", + // Teredo (RFC 4380). The embedded client IPv4 lives in the low 32 bits + // but is obfuscated via bitwise NOT (RFC 4380 §4), so — unlike NAT64 and + // 6to4 — it cannot be reliably decoded from the address alone. Blocked + // wholesale to avoid a false-negative bypass. + "2001::/32", } { _, block, err := net.ParseCIDR(cidr) if err != nil { @@ -126,20 +144,31 @@ func init() { } nat64Prefixes = append(nat64Prefixes, block) } + const sixToFourCIDR = "2002::/16" + _, sixToFourBlock, err := net.ParseCIDR(sixToFourCIDR) + if err != nil { + panic(fmt.Errorf("parse error on %q: %w", sixToFourCIDR, err)) + } + sixToFourPrefix = sixToFourBlock } // IsPrivateIP reports whether ip is a private, loopback, link-local, // unspecified, or otherwise reserved/non-public address. // -// NAT64-translated addresses are evaluated by the IPv4 address they embed: a -// NAT64 address whose low 32 bits map to a private/link-local IPv4 (e.g. -// 64:ff9b:1::a9fe:a9fe -> 169.254.169.254, the cloud metadata endpoint) is -// treated as private, because behind a NAT64 gateway it reaches exactly that -// internal IPv4, while NAT64 addresses embedding a genuinely public IPv4 remain -// allowed. This /96 decoding covers the well-known 64:ff9b::/96 (RFC 6052) and -// the 64:ff9b:1::/96 sub-prefix of the RFC 8215 local-use range; the rest of -// 64:ff9b:1::/48 uses a non-/96 embedding that cannot be decoded from the -// address alone and is blocked wholesale (see privateIPBlocks). +// NAT64- and 6to4-translated addresses are evaluated by the IPv4 address they +// embed: a NAT64 address whose low 32 bits map to a private/link-local IPv4 +// (e.g. 64:ff9b:1::a9fe:a9fe -> 169.254.169.254, the cloud metadata endpoint) +// is treated as private, because behind a NAT64 gateway it reaches exactly +// that internal IPv4, while NAT64 addresses embedding a genuinely public IPv4 +// remain allowed. This /96 decoding covers the well-known 64:ff9b::/96 +// (RFC 6052) and the 64:ff9b:1::/96 sub-prefix of the RFC 8215 local-use +// range; the rest of 64:ff9b:1::/48 uses a non-/96 embedding that cannot be +// decoded from the address alone and is blocked wholesale (see +// privateIPBlocks). 6to4 addresses (2002::/16, RFC 3056) are decoded the same +// way, extracting the IPv4 from bits 16-47 (e.g. 2002:a9fe:a9fe:: -> +// 169.254.169.254). Teredo addresses (2001::/32, RFC 4380) obfuscate their +// embedded IPv4 via bitwise NOT and so cannot be decoded from the address +// alone; they are blocked wholesale (see privateIPBlocks). func IsPrivateIP(ip net.IP) bool { if ip.IsLoopback() || ip.IsUnspecified() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() { return true diff --git a/pkg/networking/utilities_test.go b/pkg/networking/utilities_test.go index 0402764514..68b4456cef 100644 --- a/pkg/networking/utilities_test.go +++ b/pkg/networking/utilities_test.go @@ -363,6 +363,22 @@ func TestIsPrivateIP(t *testing.T) { // decoded, so it is blocked wholesale even when the low 32 bits look // public (would otherwise be a false-negative SSRF bypass). {"NAT64 local-use non-/96 blocked", "64:ff9b:1:1::8.8.8.8", true}, + + // 6to4 (RFC 3056): classified by the embedded IPv4, decoded from bits 16-47. + {"6to4 -> IMDS link-local", "2002:a9fe:a9fe::", true}, + {"6to4 -> RFC1918 192.168.x", "2002:c0a8:0101::", true}, + {"6to4 -> loopback", "2002:7f00:0001::", true}, + // 6to4 embedding a genuinely public IPv4 must stay allowed, proving the + // prefix is decoded rather than blocked wholesale. + {"6to4 -> public", "2002:0808:0808::", false}, + // Just outside the 6to4 prefix: no embedded-IPv4 decoding applies. + {"just outside 6to4 prefix", "2003::", false}, + + // Teredo (RFC 4380): the embedded client IPv4 is obfuscated via bitwise + // NOT and cannot be decoded from the address alone, so the whole prefix + // is blocked regardless of what the low 32 bits would decode to. + {"Teredo simple address", "2001::1", true}, + {"Teredo address decoding to a public-looking IPv4", "2001:0:4136:e378:8000:63bf:3fff:fdf6", true}, } for _, tt := range tests {