diff --git a/internal/httpclient/client.go b/internal/httpclient/client.go index 95071534c..84076a97e 100644 --- a/internal/httpclient/client.go +++ b/internal/httpclient/client.go @@ -36,6 +36,9 @@ import ( // ErrInvalidRequest is returned if a given HTTP request is invalid and cannot be performed. var ErrInvalidRequest = errors.New("invalid http request") +// ErrInvalidNetwork is returned if the request would not be performed over TCP +var ErrInvalidNetwork = errors.New("invalid network type") + // ErrReservedAddr is returned if a dialed address resolves to an IP within a blocked or reserved net. var ErrReservedAddr = errors.New("dial within blocked / reserved IP range") diff --git a/internal/httpclient/sanitizer.go b/internal/httpclient/sanitizer.go index 6eef6898a..75c0b34b9 100644 --- a/internal/httpclient/sanitizer.go +++ b/internal/httpclient/sanitizer.go @@ -38,6 +38,10 @@ func (s *sanitizer) Sanitize(ntwrk, addr string, _ syscall.RawConn) error { return err } + if !(ntwrk == "tcp4" || ntwrk == "tcp6") { + return ErrInvalidNetwork + } + // Seperate the IP ip := ipport.Addr() diff --git a/internal/netutil/validate.go b/internal/netutil/validate.go index 2cb5aad0a..d6b99e70d 100644 --- a/internal/netutil/validate.go +++ b/internal/netutil/validate.go @@ -24,16 +24,34 @@ import ( var ( // IPv6Reserved contains IPv6 reserved IP prefixes. + // https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml IPv6Reserved = [...]netip.Prefix{ - netip.MustParsePrefix("::1/128"), // Loopback - netip.MustParsePrefix("fe80::/10"), // Link-local - netip.MustParsePrefix("fc00::/7"), // Unique Local - netip.MustParsePrefix("2001:db8::/32"), // Test, doc, examples - netip.MustParsePrefix("ff00::/8"), // Multicast - netip.MustParsePrefix("fec0::/10"), // Site-local, deprecated + netip.MustParsePrefix("::1/128"), // Loopback + netip.MustParsePrefix("::/128"), // Unspecified address + netip.MustParsePrefix("::ffff:0:0/96"), // IPv4-mapped address + netip.MustParsePrefix("64:ff9b::/96"), // IPv4/IPv6 translation, RFC 6052 + netip.MustParsePrefix("64:ff9b:1::/48"), // IPv4/IPv6 translation, RFC 8215 + netip.MustParsePrefix("100::/64"), // Discard prefix, RFC 6666 + netip.MustParsePrefix("2001::/23"), // IETF Protocol Assignments, RFC 2928 + netip.MustParsePrefix("2001::/32"), // Teredo + netip.MustParsePrefix("2001:1::1/128"), // Port Control Protocol Anycast, RFC 7723 + netip.MustParsePrefix("2001:1::2/128"), // Traversal Using Relays around NAT Anycast, RFC 8155 + netip.MustParsePrefix("2001:2::/48"), // Benchmarking, RFC 5180 + netip.MustParsePrefix("2001:3::/32"), // AMT, RFC 7450 + netip.MustParsePrefix("2001:4:112::/48"), // AS112-v6, RFC 7535 + netip.MustParsePrefix("2001:10::/28"), // ORCHID, deprecated + netip.MustParsePrefix("2001:20::/28"), // ORCHIDv2 + netip.MustParsePrefix("2001:db8::/32"), // Test, doc, examples + netip.MustParsePrefix("2002::/16"), // 6to4 + netip.MustParsePrefix("2620:4f:8000::/48"), // Direct Delegation AS112 Service, RFC 7534 + netip.MustParsePrefix("fc00::/7"), // Unique Local + netip.MustParsePrefix("fe80::/10"), // Link-local + netip.MustParsePrefix("fec0::/10"), // Site-local, deprecated + netip.MustParsePrefix("ff00::/8"), // Multicast } // IPv4Reserved contains IPv4 reserved IP prefixes. + // https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml IPv4Reserved = [...]netip.Prefix{ netip.MustParsePrefix("0.0.0.0/8"), // Current network netip.MustParsePrefix("10.0.0.0/8"), // Private @@ -42,9 +60,13 @@ var ( netip.MustParsePrefix("169.254.0.0/16"), // Link-local netip.MustParsePrefix("172.16.0.0/12"), // Private netip.MustParsePrefix("192.0.0.0/24"), // RFC6890 + netip.MustParsePrefix("192.0.0.0/29"), // IPv4 Service Continuity Prefix, RFC 7335 netip.MustParsePrefix("192.0.2.0/24"), // Test, doc, examples + netip.MustParsePrefix("192.31.196.0/24"), // AS112-v4, RFC 7535 + netip.MustParsePrefix("192.52.193.0/24"), // AMT, RFC 7450 netip.MustParsePrefix("192.88.99.0/24"), // IPv6 to IPv4 relay netip.MustParsePrefix("192.168.0.0/16"), // Private + netip.MustParsePrefix("192.175.48.0/24"), // Direct Delegation AS112 Service, RFC 7534 netip.MustParsePrefix("198.18.0.0/15"), // Benchmarking tests netip.MustParsePrefix("198.51.100.0/24"), // Test, doc, examples netip.MustParsePrefix("203.0.113.0/24"), // Test, doc, examples diff --git a/internal/netutil/validate_test.go b/internal/netutil/validate_test.go new file mode 100644 index 000000000..37def4ce6 --- /dev/null +++ b/internal/netutil/validate_test.go @@ -0,0 +1,54 @@ +package netutil + +import ( + "net/netip" + "testing" +) + +func TestValidateIP(t *testing.T) { + tests := []struct { + name string + ip netip.Addr + }{ + // IPv4 tests + { + name: "IPv4 this host on this network", + ip: netip.MustParseAddr("0.0.0.0"), + }, + { + name: "IPv4 dummy address", + ip: netip.MustParseAddr("192.0.0.8"), + }, + { + name: "IPv4 Port Control Protocol Anycast", + ip: netip.MustParseAddr("192.0.0.9"), + }, + { + name: "IPv4 Traversal Using Relays around NAT Anycast", + ip: netip.MustParseAddr("192.0.0.10"), + }, + { + name: "IPv4 NAT64/DNS64 Discovery 1", + ip: netip.MustParseAddr("192.0.0.17"), + }, + { + name: "IPv4 NAT64/DNS64 Discovery 2", + ip: netip.MustParseAddr("192.0.0.171"), + }, + // IPv6 tests + { + name: "IPv4-mapped address", + ip: netip.MustParseAddr("::ffff:169.254.169.254"), + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + if valid := ValidateIP(tc.ip); valid != false { + t.Fatalf("Expected IP %s to be: %t, got: %t", tc.ip, false, valid) + } + }) + } +}