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
45 changes: 25 additions & 20 deletions networking/utilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,12 @@ var ErrPrivateIPAddress = errors.New("the provided URL redirects to a private IP
// 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 identifies 6to4 addresses, whose embedded IPv4 address
// occupies bytes 2 through 5 of the IPv6 address (RFC 3056 §2).
var sixToFourPrefix *net.IPNet

// embeddedIPv4 returns the IPv4 address embedded in a NAT64 or 6to4 address,
// or nil otherwise.
func embeddedIPv4(ip net.IP) net.IP {
v6 := ip.To16()
if v6 == nil || ip.To4() != nil {
Expand All @@ -82,6 +86,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 @@ -93,6 +100,7 @@ func init() {
"172.16.0.0/12", // RFC1918
"192.168.0.0/16", // RFC1918
"169.254.0.0/16", // RFC3927 link-local
"2001::/32", // RFC4380 Teredo (embedded client IPv4 is obfuscated)
"::1/128", // IPv6 loopback
"fe80::/10", // IPv6 link-local
"fc00::/7", // IPv6 unique local addr
Expand Down Expand Up @@ -124,20 +132,23 @@ func init() {
}
nat64Prefixes = append(nat64Prefixes, block)
}
_, block, err := net.ParseCIDR("2002::/16")
if err != nil {
panic(fmt.Errorf("parse error on 6to4 prefix: %w", err))
}
sixToFourPrefix = block
}

// 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 translated address whose embedded IPv4 is private or link-local is
// treated as private, because it reaches that internal IPv4 through its
// translator. NAT64 addresses embed the IPv4 in their low 32 bits; 6to4
// addresses embed it in bytes 2 through 5. Teredo addresses are blocked
// wholesale because their embedded client IPv4 is obfuscated and cannot be
// safely derived from the IPv6 literal.
func IsPrivateIP(ip net.IP) bool {
if ip.IsLoopback() || ip.IsUnspecified() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
return true
Expand Down Expand Up @@ -282,16 +293,10 @@ const (
hostLoopbackV6 = "[::1]"
)

// IsLocalhost checks if a host is a loopback address (for development).
// Recognised forms: "localhost", "localhost:<port>", "127.0.0.1", "127.0.0.1:<port>",
// "[::1]", "[::1]:<port>".
// IsLocalhost checks whether a host is localhost or a literal loopback address.
// It accepts plain-host and host:port forms without resolving hostnames.
func IsLocalhost(host string) bool {
return strings.HasPrefix(host, hostLocalhost+":") ||
strings.HasPrefix(host, hostLoopbackV4+":") ||
strings.HasPrefix(host, hostLoopbackV6+":") ||
host == hostLocalhost ||
host == hostLoopbackV4 ||
host == hostLoopbackV6
return IsLoopbackHost(host)
}

// IsLoopbackHost reports whether the Host header value refers to a loopback
Expand Down
57 changes: 54 additions & 3 deletions networking/utilities_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,11 +183,26 @@ func TestIsLocalhost(t *testing.T) {
input: "127.0.0.1:65535",
expected: true,
},
{
name: "127.0.0.2 without port",
input: "127.0.0.2",
expected: true,
},
{
name: "127.0.0.2 with port",
input: "127.0.0.2:8080",
expected: true,
},
{
name: "IPv6 localhost without port",
input: hostLoopbackV6,
expected: true,
},
{
name: "bare IPv6 localhost",
input: "::1",
expected: true,
},
{
name: "IPv6 localhost with port",
input: testLoopbackV6WithPort,
Expand Down Expand Up @@ -293,17 +308,17 @@ func TestIsLocalhost(t *testing.T) {
{
name: "case insensitive localhost",
input: "LOCALHOST",
expected: false, // Current implementation is case sensitive
expected: true,
},
{
name: "case insensitive localhost with port",
input: "LOCALHOST:8080",
expected: false, // Current implementation is case sensitive
expected: true,
},
{
name: "mixed case localhost",
input: "LocalHost",
expected: false, // Current implementation is case sensitive
expected: true,
},
{
name: "localhost with spaces",
Expand Down Expand Up @@ -361,6 +376,17 @@ func TestIsPrivateIP(t *testing.T) {
{"documentation TEST-NET-3", "203.0.113.1", true},
{"public IPv4", testPublicIPv4, false},
{"public IPv6", "2001:db8::1", false},
{"global IPv6 outside blocked ranges", "2606:4700:4700::1111", false},

// Teredo (RFC 4380) obfuscates the client IPv4, so the whole prefix is blocked.
{"Teredo example", "2001:0000:4136:e378:8000:63bf:3fff:fdd2", true},
{"another Teredo address", "2001:0:1:2:3:4:5:6", true},

// 6to4 (RFC 3056): classify bytes 2 through 5 as the embedded IPv4.
{"6to4 -> IMDS link-local", "2002:a9fe:a9fe::", true},
{"6to4 -> RFC1918 10.x", "2002:0a00:0001::", true},
{"6to4 -> loopback", "2002:7f00:0001::", true},
{"6to4 -> public", "2002:0808:0808::", false},

// Unspecified / "this host" / reserved ranges (defense-in-depth).
{"unspecified IPv4", "0.0.0.0", true},
Expand Down Expand Up @@ -474,6 +500,31 @@ func TestAddressReferencesPrivateIp(t *testing.T) {
address: "[64:ff9b::8.8.8.8]:443",
expectError: false,
},
{
name: "6to4 to public IPv4 with port",
address: "[2002:0808:0808::]:443",
expectError: false,
},
{
name: "Teredo with port",
address: "[2001:0000:4136:e378:8000:63bf:3fff:fdd2]:443",
expectError: true,
},
{
name: "6to4 to IMDS link-local with port",
address: "[2002:a9fe:a9fe::]:443",
expectError: true,
},
{
name: "6to4 to RFC1918 with port",
address: "[2002:0a00:0001::]:443",
expectError: true,
},
{
name: "6to4 to loopback with port",
address: "[2002:7f00:0001::]:443",
expectError: true,
},
{
name: "unspecified IPv4 with port",
address: "0.0.0.0:80",
Expand Down