diff --git a/net/dns/publicdns/publicdns.go b/net/dns/publicdns/publicdns.go index 571e21348..fe4d1caa6 100644 --- a/net/dns/publicdns/publicdns.go +++ b/net/dns/publicdns/publicdns.go @@ -9,7 +9,6 @@ "bytes" "encoding/hex" "fmt" - "math/big" "net/netip" "slices" "sort" @@ -57,17 +56,15 @@ func DoHEndpointFromIP(ip netip.Addr) (dohBase string, dohOnly bool, ok bool) { return sb.String(), true, true } - // Control D DoH URLs are of the form "https://dns.controld.com/8yezwenugs" - // where the path component is represented by 8 bytes (7-14) of the IPv6 address in base36 + // Control D's free anycast resolvers (freedns.controld.com/pN) are the only + // Control D addresses that serve DoH; they're registered as exact entries in + // dohOfIP (see populate) and are handled by the lookup above. // - // TODO(#20433): the ID-encoded addresses in this /48 are legacy port-53-only - // endpoints and refuse DoH on :443, so upgrading them to DoH is wrong. Only the - // shared anycast addresses (e.g. freedns.controld.com/pN) actually serve DoH. - // Distinguish the two rather than mapping the whole range. - if controlDv6RangeA.Contains(ip) || controlDv6RangeB.Contains(ip) { - path := big.NewInt(0).SetBytes(ip.AsSlice()[6:14]).Text(36) - return controlDBase + path, true, true - } + // The ID-encoded addresses in the 2606:1a40::/48 ranges (customer resolver ID + // base36-encoded into the address) are legacy plaintext-DNS (port 53) endpoints + // that refuse DoH on :443, so we deliberately don't upgrade them to DoH here: + // they fall through and are used as ordinary port-53 resolvers. Premium DoH is + // only reachable via the dns.controld.com/ URL path (see DoHIPsOfBase). return "", false, false } diff --git a/net/dns/publicdns/publicdns_test.go b/net/dns/publicdns/publicdns_test.go index 7a4cc06a3..5c3031fab 100644 --- a/net/dns/publicdns/publicdns_test.go +++ b/net/dns/publicdns/publicdns_test.go @@ -28,6 +28,46 @@ func TestInit(t *testing.T) { } } +func TestDoHEndpointFromIP(t *testing.T) { + tests := []struct { + name string + ip string + wantBase string + wantOnly bool + wantOK bool + }{ + // Control D free anycast resolvers (freedns.controld.com/pN) serve DoH + // and are dual-stack (also speak port 53), so dohOnly is false. + {"controld_free_v6_p0", "2606:1a40::", "https://freedns.controld.com/p0", false, true}, + {"controld_free_v6_p1", "2606:1a40:1::1", "https://freedns.controld.com/p1", false, true}, + {"controld_free_v4_p1", "76.76.2.1", "https://freedns.controld.com/p1", false, true}, + {"controld_free_v4_family", "76.76.10.4", "https://freedns.controld.com/family", false, true}, + + // ID-encoded addresses in the Control D /48 ranges are legacy + // plaintext-DNS (port 53) endpoints that refuse DoH, so they must not be + // upgraded to DoH; DoHEndpointFromIP reports them as unknown. + {"controld_id_encoded_a", "2606:1a40:0:0:0:1234:5678:9abc", "", false, false}, + {"controld_id_encoded_b", "2606:1a40:1:0:0:1234:5678:9abc", "", false, false}, + // The premium anycast address has no resolver ID in the bare IP, so it + // can't be mapped to a DoH URL here either. + {"controld_premium_anycast", "2606:1a40::22", "", false, false}, + + // A well-known dual-stack DoH server upgrades but isn't dohOnly. + {"google_v4", "8.8.8.8", "https://dns.google/dns-query", false, true}, + // A NextDNS ID-encoded address is DoH-only. + {"nextdns_v6", "2a07:a8c0::c3:a884", "https://dns.nextdns.io/c3a884", true, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + base, only, ok := DoHEndpointFromIP(netip.MustParseAddr(tt.ip)) + if base != tt.wantBase || only != tt.wantOnly || ok != tt.wantOK { + t.Errorf("DoHEndpointFromIP(%s) = (%q, %v, %v); want (%q, %v, %v)", + tt.ip, base, only, ok, tt.wantBase, tt.wantOnly, tt.wantOK) + } + }) + } +} + func TestDoHV6(t *testing.T) { tests := []struct { in string diff --git a/net/dns/resolver/forwarder_test.go b/net/dns/resolver/forwarder_test.go index 17f2732a3..7aca3d061 100644 --- a/net/dns/resolver/forwarder_test.go +++ b/net/dns/resolver/forwarder_test.go @@ -146,9 +146,19 @@ func TestResolversWithDelays(t *testing.T) { want: o("https://dns.nextdns.io/c3a884"), }, { - name: "controld-ipv6-expand", + // ID-encoded Control D addresses are legacy port-53-only + // endpoints (see #20433); they must not be upgraded to DoH and + // instead pass through as ordinary port-53 resolvers. + name: "controld-ipv6-id-encoded-not-doh", in: q("2606:1a40:0:6:7b5b:5949:35ad:0"), - want: o("https://dns.controld.com/hyq3ipr2ct"), + want: o("2606:1a40:0:6:7b5b:5949:35ad:0"), + }, + { + // The free anycast resolvers (freedns.controld.com/pN) do serve + // DoH and are upgraded. + name: "controld-free-anycast-doh", + in: q("2606:1a40::1"), + want: o("https://freedns.controld.com/p1", "2606:1a40::1+0.5s"), }, { name: "controld-doh-input",