This commit is contained in:
bluepython508 committed 2026-10-22 12:27:19 +01:00
1 parent 3530afb854
commit 6903fcff27
2146 files changed
+112256 -190703

No files matched your search

+73 -34
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package publicdns contains mapping and helpers for working with
@@ -7,18 +7,16 @@ package publicdns
import (
"bytes"
"encoding/binary"
"encoding/hex"
"fmt"
"log"
"math/big"
"net/netip"
"slices"
"sort"
"strconv"
"strings"
"sync"
"tailscale.com/feature/buildfeatures"
"tailscale.com/util/testenv"
)
// dohOfIP maps from public DNS IPs to their DoH base URL.
@@ -58,12 +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
if controlDv6RangeA.Contains(ip) || controlDv6RangeB.Contains(ip) {
path := big.NewInt(0).SetBytes(ip.AsSlice()[6:14]).Text(36)
return controlDBase + path, true, true
}
// 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.
//
// 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/<id> URL path (see DoHIPsOfBase).
return "", false, false
}
@@ -124,15 +125,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
@@ -275,6 +273,26 @@ func populate() {
addDoH("76.76.10.4", "https://freedns.controld.com/family")
addDoH("2606:1a40::4", "https://freedns.controld.com/family")
addDoH("2606:1a40:1::4", "https://freedns.controld.com/family")
// CIRA Canadian Shield: https://www.cira.ca/en/canadian-shield/configure/summary-cira-canadian-shield-dns-resolver-addresses/
// CIRA Canadian Shield Private (DNS resolution only)
addDoH("149.112.121.10", "https://private.canadianshield.cira.ca/dns-query")
addDoH("149.112.122.10", "https://private.canadianshield.cira.ca/dns-query")
addDoH("2620:10a:80bb::10", "https://private.canadianshield.cira.ca/dns-query")
addDoH("2620:10a:80bc::10", "https://private.canadianshield.cira.ca/dns-query")
// CIRA Canadian Shield Protected (Malware and phishing protection)
addDoH("149.112.121.20", "https://protected.canadianshield.cira.ca/dns-query")
addDoH("149.112.122.20", "https://protected.canadianshield.cira.ca/dns-query")
addDoH("2620:10a:80bb::20", "https://protected.canadianshield.cira.ca/dns-query")
addDoH("2620:10a:80bc::20", "https://protected.canadianshield.cira.ca/dns-query")
// CIRA Canadian Shield Family (Protected + blocking adult content)
addDoH("149.112.121.30", "https://family.canadianshield.cira.ca/dns-query")
addDoH("149.112.122.30", "https://family.canadianshield.cira.ca/dns-query")
addDoH("2620:10a:80bb::30", "https://family.canadianshield.cira.ca/dns-query")
addDoH("2620:10a:80bc::30", "https://family.canadianshield.cira.ca/dns-query")
}
var (
@@ -308,6 +326,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
@@ -321,23 +341,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 {
@@ -347,3 +350,39 @@ func IPIsDoHOnlyServer(ip netip.Addr) bool {
controlDv6RangeA.Contains(ip) || controlDv6RangeB.Contains(ip) ||
ip == controlDv4One || ip == controlDv4Two
}
var testMu sync.Mutex
// RegisterTestDoHEndpoint registers a test DoH endpoint mapping for use in tests.
// It maps the given IP to the DoH base URL, and the URL back to the IP.
//
// This function panics if called outside of tests, and cannot be called
// concurrently with any usage of this package (i.e. before any DNS forwarders
// are created). It is safe to call concurrently with itself.
//
// It returns a cleanup function that removes the registration.
func RegisterTestDoHEndpoint(ip netip.Addr, dohBase string) func() {
if !testenv.InTest() {
panic("RegisterTestDoHEndpoint called outside of tests")
}
populateOnce.Do(populate)
testMu.Lock()
defer testMu.Unlock()
dohOfIP[ip] = dohBase
dohIPsOfBase[dohBase] = append(dohIPsOfBase[dohBase], ip)
return func() {
testMu.Lock()
defer testMu.Unlock()
delete(dohOfIP, ip)
dohIPsOfBase[dohBase] = slices.DeleteFunc(dohIPsOfBase[dohBase], func(addr netip.Addr) bool {
return addr == ip
})
if len(dohIPsOfBase[dohBase]) == 0 {
delete(dohIPsOfBase, dohBase)
}
}
}