Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 40 additions & 11 deletions pkg/networking/utilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}

Expand All @@ -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 {
Expand All @@ -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
Expand Down
16 changes: 16 additions & 0 deletions pkg/networking/utilities_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading