diff --git a/internal/netxlite/bogon.go b/internal/netxlite/bogon.go index d805648..47a1c27 100644 --- a/internal/netxlite/bogon.go +++ b/internal/netxlite/bogon.go @@ -14,40 +14,113 @@ import ( "github.com/ooni/probe-cli/v3/internal/runtimex" ) -// IsBogon returns whether if an IP address is bogon. Passing to this +// IsBogon returns whether an IP address is bogon. Passing to this // function a non-IP address causes it to return true. func IsBogon(address string) bool { ip := net.ParseIP(address) - return ip == nil || isPrivate(ip) + return ip == nil || isPrivate(address, ip) } -var privateIPBlocks []*net.IPNet +// IsLoopback returns whether an IP address is loopback. Passing to this +// function a non-IP address causes it to return true. +func IsLoopback(address string) bool { + ip := net.ParseIP(address) + return ip == nil || ip.IsLoopback() +} -func init() { - for _, cidr := range []string{ - "0.0.0.0/8", // "This" network (however, Linux...) - "10.0.0.0/8", // RFC1918 - "100.64.0.0/10", // Carrier grade NAT - "127.0.0.0/8", // IPv4 loopback - "169.254.0.0/16", // RFC3927 link-local - "172.16.0.0/12", // RFC1918 - "192.168.0.0/16", // RFC1918 - "224.0.0.0/4", // Multicast - "::1/128", // IPv6 loopback - "fe80::/10", // IPv6 link-local - "fc00::/7", // IPv6 unique local addr - } { +var ( + bogons4 []*net.IPNet + bogons6 []*net.IPNet +) + +func expandbogons(cidrs []string) (out []*net.IPNet) { + for _, cidr := range cidrs { _, block, err := net.ParseCIDR(cidr) runtimex.PanicOnError(err, "net.ParseCIDR failed") - privateIPBlocks = append(privateIPBlocks, block) + out = append(out, block) } + return } -func isPrivate(ip net.IP) bool { +func init() { + bogons4 = append(bogons4, expandbogons([]string{ + // + // List extracted from https://ipinfo.io/bogon + // + "0.0.0.0/8", // "This" network + "10.0.0.0/8", // Private-use networks + "100.64.0.0/10", // Carrier-grade NAT + "127.0.0.0/8", // Loopback + "127.0.53.53/32", // Name collision occurrence + "169.254.0.0/16", // Link local + "172.16.0.0/12", // Private-use networks + "192.0.0.0/24", // IETF protocol assignments + "192.0.2.0/24", // TEST-NET-1 + "192.168.0.0/16", // Private-use networks + "198.18.0.0/15", // Network interconnect device benchmark testing + "198.51.100.0/24", // TEST-NET-2 + "203.0.113.0/24", // TEST-NET-3 + "224.0.0.0/4", // Multicast + "240.0.0.0/4", // Reserved for future use + "255.255.255.255/32", // Limited broadcast + })...) + bogons6 = append(bogons6, expandbogons([]string{ + // + // List extracted from https://ipinfo.io/bogon + // + "::/128", // Node-scope unicast unspecified address + "::1/128", // Node-scope unicast loopback address + "::ffff:0:0/96", // IPv4-mapped addresses + "::/96", // IPv4-compatible addresses + "100::/64", // Remotely triggered black hole addresses + "2001:10::/28", // Overlay routable cryptographic hash identifiers (ORCHID) + "2001:db8::/32", // Documentation prefix + "fc00::/7", // Unique local addresses (ULA) + "fe80::/10", // Link-local unicast + "fec0::/10", // Site-local unicast (deprecated) + "ff00::/8", // Multicast (Note: ff0e:/16 is global scope and may appear on the global internet.) + "2002::/24", // 6to4 bogon (0.0.0.0/8) + "2002:a00::/24", // 6to4 bogon (10.0.0.0/8) + "2002:7f00::/24", // 6to4 bogon (127.0.0.0/8) + "2002:a9fe::/32", // 6to4 bogon (169.254.0.0/16) + "2002:ac10::/28", // 6to4 bogon (172.16.0.0/12) + "2002:c000::/40", // 6to4 bogon (192.0.0.0/24) + "2002:c000:200::/40", // 6to4 bogon (192.0.2.0/24) + "2002:c0a8::/32", // 6to4 bogon (192.168.0.0/16) + "2002:c612::/31", // 6to4 bogon (198.18.0.0/15) + "2002:c633:6400::/40", // 6to4 bogon (198.51.100.0/24) + "2002:cb00:7100::/40", // 6to4 bogon (203.0.113.0/24) + "2002:e000::/20", // 6to4 bogon (224.0.0.0/4) + "2002:f000::/20", // 6to4 bogon (240.0.0.0/4) + "2002:ffff:ffff::/48", // 6to4 bogon (255.255.255.255/32) + "2001::/40", // Teredo bogon (0.0.0.0/8) + "2001:0:a00::/40", // Teredo bogon (10.0.0.0/8) + "2001:0:7f00::/40", // Teredo bogon (127.0.0.0/8) + "2001:0:a9fe::/48", // Teredo bogon (169.254.0.0/16) + "2001:0:ac10::/44", // Teredo bogon (172.16.0.0/12) + "2001:0:c000::/56", // Teredo bogon (192.0.0.0/24) + "2001:0:c000:200::/56", // Teredo bogon (192.0.2.0/24) + "2001:0:c0a8::/48", // Teredo bogon (192.168.0.0/16) + "2001:0:c612::/47", // Teredo bogon (198.18.0.0/15) + "2001:0:c633:6400::/56", // Teredo bogon (198.51.100.0/24) + "2001:0:cb00:7100::/56", // Teredo bogon (203.0.113.0/24) + "2001:0:e000::/36", // Teredo bogon (224.0.0.0/4) + "2001:0:f000::/36", // Teredo bogon (240.0.0.0/4) + "2001:0:ffff:ffff::/64", // Teredo bogon (255.255.255.255/32) + })...) +} + +func isPrivate(address string, ip net.IP) bool { if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() { return true } - for _, block := range privateIPBlocks { + var bogons []*net.IPNet + if isIPv6(address) { + bogons = bogons6 + } else { + bogons = bogons4 + } + for _, block := range bogons { if block.Contains(ip) { return true } diff --git a/internal/netxlite/bogon_test.go b/internal/netxlite/bogon_test.go index 9ff284d..86666d1 100644 --- a/internal/netxlite/bogon_test.go +++ b/internal/netxlite/bogon_test.go @@ -12,7 +12,40 @@ func TestIsBogon(t *testing.T) { if IsBogon("1.1.1.1") != false { t.Fatal("unexpected result") } + if IsBogon("8.8.4.4") != false { + t.Fatal("unexpected result") + } + if IsBogon("2001:4860:4860::8844") != false { + t.Fatal("unexpected result") + } if IsBogon("10.0.1.1") != true { t.Fatal("unexpected result") } + if IsBogon("::1") != true { + t.Fatal("unexpected result") + } +} + +func TestIsLoopback(t *testing.T) { + if IsLoopback("antani") != true { + t.Fatal("unexpected result") + } + if IsLoopback("127.0.0.1") != true { + t.Fatal("unexpected result") + } + if IsLoopback("1.1.1.1") != false { + t.Fatal("unexpected result") + } + if IsLoopback("8.8.4.4") != false { + t.Fatal("unexpected result") + } + if IsLoopback("2001:4860:4860::8844") != false { + t.Fatal("unexpected result") + } + if IsLoopback("10.0.1.1") != false { + t.Fatal("unexpected result") + } + if IsLoopback("::1") != true { + t.Fatal("unexpected result") + } } diff --git a/internal/netxlite/quic.go b/internal/netxlite/quic.go index 2913a69..a8ed918 100644 --- a/internal/netxlite/quic.go +++ b/internal/netxlite/quic.go @@ -88,8 +88,8 @@ type quicDialerQUICGo struct { var _ model.QUICDialer = &quicDialerQUICGo{} -// errInvalidIP indicates that a string is not a valid IP. -var errInvalidIP = errors.New("netxlite: invalid IP") +// ErrInvalidIP indicates that a string is not a valid IP. +var ErrInvalidIP = errors.New("netxlite: invalid IP") // DialContext implements QUICDialer.DialContext. This function will // apply the following TLS defaults: @@ -112,7 +112,7 @@ func (d *quicDialerQUICGo) DialContext(ctx context.Context, network string, } ip := net.ParseIP(onlyhost) if ip == nil { - return nil, errInvalidIP + return nil, ErrInvalidIP } pconn, err := d.QUICListener.Listen(&net.UDPAddr{IP: net.IPv4zero, Port: 0}) if err != nil { diff --git a/internal/netxlite/quic_test.go b/internal/netxlite/quic_test.go index d0d02ac..cbd0495 100644 --- a/internal/netxlite/quic_test.go +++ b/internal/netxlite/quic_test.go @@ -93,7 +93,7 @@ func TestQUICDialerQUICGo(t *testing.T) { ctx := context.Background() qconn, err := systemdialer.DialContext( ctx, "udp", "a.b.c.d:0", tlsConfig, &quic.Config{}) - if !errors.Is(err, errInvalidIP) { + if !errors.Is(err, ErrInvalidIP) { t.Fatal("not the error we expected", err) } if qconn != nil { diff --git a/internal/netxlite/resolver.go b/internal/netxlite/resolver.go index ba1fc15..401cb62 100644 --- a/internal/netxlite/resolver.go +++ b/internal/netxlite/resolver.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net" + "strings" "time" "github.com/ooni/probe-cli/v3/internal/model" @@ -208,6 +209,23 @@ func (r *resolverShortCircuitIPAddr) LookupHost(ctx context.Context, hostname st return r.Resolver.LookupHost(ctx, hostname) } +// IsIPv6 returns true if the given candidate is a valid IP address +// representation and such representation is IPv6. +func IsIPv6(candidate string) (bool, error) { + if net.ParseIP(candidate) == nil { + return false, ErrInvalidIP + } + return isIPv6(candidate), nil +} + +// isIPv6 returns true if the given IP address is IPv6. +func isIPv6(candidate string) bool { + // This check for identifying IPv6 is discussed + // at https://stackoverflow.com/questions/22751035 + // and seems good-enough for our purposes. + return strings.Contains(candidate, ":") +} + // ErrNoResolver is the type of error returned by "without resolver" // dialer when asked to dial for and endpoint containing a domain name, // since they can only dial for endpoints containing IP addresses.