net/dns/publicdns: use Control D anycast IPs for premium DoH (#20434)

For dns.controld.com premium resolvers we synthesized per-resolver IPv6
addresses by encoding the resolver ID into the 2606:1a40::/48 range, but
those are legacy plaintext-DNS (port 53) endpoints that refuse TCP :443.
DoH now dials Control D's shared anycast IPs (the resolver ID stays in the
URL path), fixing SERVFAIL on IPv6-only/NAT64 networks where the v4
anycast fallback isn't reachable.

Fixes #20430

Signed-off-by: Brendan Creane <bcreane@gmail.com>
This commit is contained in:
Brendan Creane
2026-07-13 16:47:23 -07:00
committed by GitHub
parent 55b1a4de74
commit 8eb18d902e
3 changed files with 96 additions and 32 deletions
+10 -26
View File
@@ -7,15 +7,12 @@ package publicdns
import (
"bytes"
"encoding/binary"
"encoding/hex"
"fmt"
"log"
"math/big"
"net/netip"
"slices"
"sort"
"strconv"
"strings"
"sync"
@@ -62,6 +59,11 @@ func DoHEndpointFromIP(ip netip.Addr) (dohBase string, dohOnly bool, ok bool) {
// 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
//
// 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
@@ -126,15 +128,12 @@ func DoHIPsOfBase(dohBase string) []netip.Addr {
}
}
}
if pathStr, ok := strings.CutPrefix(dohBase, controlDBase); ok {
if i := strings.IndexFunc(pathStr, isSlashOrQuestionMark); i != -1 {
pathStr = pathStr[:i]
}
if strings.HasPrefix(dohBase, controlDBase) {
return []netip.Addr{
controlDv4One,
controlDv4Two,
controlDv6Gen(controlDv6RangeA.Addr(), pathStr),
controlDv6Gen(controlDv6RangeB.Addr(), pathStr),
controlDv6One,
controlDv6Two,
}
}
return nil
@@ -330,6 +329,8 @@ var (
controlDv6RangeB = netip.MustParsePrefix("2606:1a40:1::/48")
controlDv4One = netip.MustParseAddr("76.76.2.22")
controlDv4Two = netip.MustParseAddr("76.76.10.22")
controlDv6One = netip.MustParseAddr("2606:1a40::22")
controlDv6Two = netip.MustParseAddr("2606:1a40:1::22")
)
// nextDNSv6Gen generates a NextDNS IPv6 address from the upper 8 bytes in the
@@ -343,23 +344,6 @@ func nextDNSv6Gen(ip netip.Addr, id []byte) netip.Addr {
return netip.AddrFrom16(a)
}
// controlDv6Gen generates a Control D IPv6 address from provided ip and id.
//
// The id is taken from the DoH query path component and represents a unique resolver configuration.
// e.g. https://dns.controld.com/hyq3ipr2ct
func controlDv6Gen(ip netip.Addr, id string) netip.Addr {
b := make([]byte, 8)
decoded, err := strconv.ParseUint(id, 36, 64)
if err != nil {
log.Printf("controlDv6Gen: failed to parse id %q: %v", id, err)
}
binary.BigEndian.PutUint64(b, decoded)
a := ip.AsSlice()
copy(a[6:14], b)
addr, _ := netip.AddrFromSlice(a)
return addr
}
// IPIsDoHOnlyServer reports whether ip is a DNS server that should only use
// DNS-over-HTTPS (not regular port 53 DNS).
func IPIsDoHOnlyServer(ip netip.Addr) bool {
+6 -6
View File
@@ -121,8 +121,8 @@ func TestDoHIPsOfBase(t *testing.T) {
want: ips(
"76.76.2.22",
"76.76.10.22",
"2606:1a40:0:6:7b5b:5949:35ad:0",
"2606:1a40:1:6:7b5b:5949:35ad:0",
"2606:1a40::22",
"2606:1a40:1::22",
),
},
{
@@ -130,8 +130,8 @@ func TestDoHIPsOfBase(t *testing.T) {
want: ips(
"76.76.2.22",
"76.76.10.22",
"2606:1a40:0:ffff:ffff:ffff:ffff:0",
"2606:1a40:1:ffff:ffff:ffff:ffff:0",
"2606:1a40::22",
"2606:1a40:1::22",
),
},
{
@@ -139,8 +139,8 @@ func TestDoHIPsOfBase(t *testing.T) {
want: ips(
"76.76.2.22",
"76.76.10.22",
"2606:1a40:0:6:7b5b:5949:35ad:0",
"2606:1a40:1:6:7b5b:5949:35ad:0",
"2606:1a40::22",
"2606:1a40:1::22",
),
},
}