Add Control D support, along with DoH client metadata

Signed-off-by: alexelisenko <39712468+alexelisenko@users.noreply.github.com>
pull/8417/head
alexelisenko 2023-06-22 20:47:36 -04:00
parent 8b80d63b42
commit 77a12e58c5
4 changed files with 148 additions and 5 deletions

View File

@ -7,10 +7,13 @@ package publicdns
import (
"bytes"
"encoding/binary"
"encoding/hex"
"fmt"
"math/big"
"net/netip"
"sort"
"strconv"
"strings"
"sync"
)
@ -23,6 +26,9 @@ var dohOfIP = map[netip.Addr]string{} // 8.8.8.8 => "https://..."
var dohIPsOfBase = map[string][]netip.Addr{}
var populateOnce sync.Once
const NextDNSBase = "https://dns.nextdns.io/"
const ControlDBase = "https://dns.controld.com/"
// DoHEndpointFromIP returns the DNS-over-HTTPS base URL for a given IP
// and whether it's DoH-only (not speaking DNS on port 53).
//
@ -39,14 +45,21 @@ func DoHEndpointFromIP(ip netip.Addr) (dohBase string, dohOnly bool, ok bool) {
if nextDNSv6RangeA.Contains(ip) || nextDNSv6RangeB.Contains(ip) {
a := ip.As16()
var sb strings.Builder
const base = "https://dns.nextdns.io/"
sb.Grow(len(base) + 12)
sb.WriteString(base)
sb.Grow(len(NextDNSBase) + 12)
sb.WriteString(NextDNSBase)
for _, b := range bytes.TrimLeft(a[4:], "\x00") {
fmt.Fprintf(&sb, "%02x", b)
}
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
}
return "", false, false
}
@ -80,7 +93,7 @@ func DoHIPsOfBase(dohBase string) []netip.Addr {
if s := dohIPsOfBase[dohBase]; len(s) > 0 {
return s
}
if hexStr, ok := strings.CutPrefix(dohBase, "https://dns.nextdns.io/"); ok {
if hexStr, ok := strings.CutPrefix(dohBase, NextDNSBase); ok {
// The path is of the form /<profile-hex>[/<hostname>/<model>/<device id>...]
// or /<profile-hex>?<query params>
// but only the <profile-hex> is required. Ignore the rest:
@ -106,6 +119,14 @@ func DoHIPsOfBase(dohBase string) []netip.Addr {
}
}
}
if pathStr, ok := strings.CutPrefix(dohBase, ControlDBase); ok {
return []netip.Addr{
controlDv4One,
controlDv4Two,
controlDv6Gen(nextDNSv6RangeA.Addr(), pathStr),
controlDv6Gen(nextDNSv6RangeB.Addr(), pathStr),
}
}
return nil
}
@ -185,6 +206,37 @@ func populate() {
addDoH("194.242.2.3", "https://adblock.doh.mullvad.net/dns-query")
addDoH("193.19.108.3", "https://adblock.doh.mullvad.net/dns-query")
addDoH("2a07:e340::3", "https://adblock.doh.mullvad.net/dns-query")
// Control D
addDoH("76.76.2.0", "https://freedns.controld.com/p0")
addDoH("76.76.10.0", "https://freedns.controld.com/p0")
addDoH("2606:1a40::", "https://freedns.controld.com/p0")
addDoH("2606:1a40:1::", "https://freedns.controld.com/p0")
// Control D -Malware
addDoH("76.76.2.1", "https://freedns.controld.com/p1")
addDoH("76.76.10.1", "https://freedns.controld.com/p1")
addDoH("2606:1a40::1", "https://freedns.controld.com/p1")
addDoH("2606:1a40:1::1", "https://freedns.controld.com/p1")
// Control D -Malware + Ads
addDoH("76.76.2.2", "https://freedns.controld.com/p2")
addDoH("76.76.10.2", "https://freedns.controld.com/p2")
addDoH("2606:1a40::2", "https://freedns.controld.com/p2")
addDoH("2606:1a40:1::2", "https://freedns.controld.com/p2")
// Control D -Malware + Ads + Social
addDoH("76.76.2.3", "https://freedns.controld.com/p3")
addDoH("76.76.10.3", "https://freedns.controld.com/p3")
addDoH("2606:1a40::3", "https://freedns.controld.com/p3")
addDoH("2606:1a40:1::3", "https://freedns.controld.com/p3")
// Control D -Malware + Ads + Adult
addDoH("76.76.2.4", "https://freedns.controld.com/family")
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")
}
var (
@ -207,6 +259,13 @@ var (
nextDNSv4RangeB = netip.MustParsePrefix("45.90.30.0/24")
nextDNSv4One = nextDNSv4RangeA.Addr()
nextDNSv4Two = nextDNSv4RangeB.Addr()
// The Control D IPv6 ranges (primary and secondary). The customer ID is
// encoded in the ipv6 address is used (in base 36 form) as the DoH query
controlDv6RangeA = netip.MustParsePrefix("2606:1a40::/48")
controlDv6RangeB = netip.MustParsePrefix("2606:1a40:1::/48")
controlDv4One = netip.MustParseAddr("76.76.2.22")
controlDv4Two = netip.MustParseAddr("76.76.10.22")
)
// nextDNSv6Gen generates a NextDNS IPv6 address from the upper 8 bytes in the
@ -220,9 +279,22 @@ 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.
func controlDv6Gen(ip netip.Addr, id string) netip.Addr {
b := make([]byte, 8)
decoded, _ := strconv.ParseUint(id, 36, 64)
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 {
return nextDNSv6RangeA.Contains(ip) || nextDNSv6RangeB.Contains(ip) ||
nextDNSv4RangeA.Contains(ip) || nextDNSv4RangeB.Contains(ip)
nextDNSv4RangeA.Contains(ip) || nextDNSv4RangeB.Contains(ip) ||
controlDv6RangeA.Contains(ip) || controlDv6RangeB.Contains(ip) ||
ip.String() == controlDv4One.String() || ip.String() == controlDv4Two.String()
}

View File

@ -116,6 +116,24 @@ func TestDoHIPsOfBase(t *testing.T) {
"2a07:a8c1::c3:a884",
),
},
{
base: "https://dns.controld.com/hyq3ipr2ct",
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",
),
},
{
base: "https://dns.controld.com/112233445566778899aabbcc",
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",
),
},
}
for _, tt := range tests {
got := DoHIPsOfBase(tt.base)

View File

@ -15,6 +15,7 @@ import (
"net/http"
"net/netip"
"net/url"
"os"
"sort"
"strings"
"sync"
@ -24,6 +25,7 @@ import (
"tailscale.com/envknob"
"tailscale.com/net/dns/publicdns"
"tailscale.com/net/dnscache"
"tailscale.com/net/interfaces"
"tailscale.com/net/neterror"
"tailscale.com/net/netmon"
"tailscale.com/net/netns"
@ -200,6 +202,12 @@ type forwarder struct {
// /etc/resolv.conf is missing/corrupt, and the peerapi ExitDNS stub
// resolver lookup.
cloudHostFallback []resolverAndDelay
// Control D specific fields. These allow the admin to destinguish between
// different clients using the same DoH resolver URL
dohClientHostname string // Hostname to send in Control D DoH requests
dohClientMac string // MAC address to send in Control D DoH requests
dohClientIP string // IP address to send in Control D DoH requests
}
func init() {
@ -207,11 +215,32 @@ func init() {
}
func newForwarder(logf logger.Logf, netMon *netmon.Monitor, linkSel ForwardLinkSelector, dialer *tsdial.Dialer) *forwarder {
hostname, _ := os.Hostname()
tailscaleIP := ""
clientMac := ""
// TODO: is this the best way to determine the tailscale IP of this node?
if tsIPs, _, err := interfaces.Tailscale(); err == nil && len(tsIPs) > 0 {
tailscaleIP = tsIPs[0].String()
}
// TODO: is this the best way to determine the MAC address of this node?
if idx, err := interfaces.DefaultRouteInterfaceIndex(); err == nil {
iface, err := net.InterfaceByIndex(idx)
if err == nil {
clientMac = iface.HardwareAddr.String()
}
}
f := &forwarder{
logf: logger.WithPrefix(logf, "forward: "),
netMon: netMon,
linkSel: linkSel,
dialer: dialer,
// Control D specific DoH fields
dohClientHostname: hostname,
dohClientIP: tailscaleIP,
dohClientMac: clientMac,
}
f.ctx, f.ctxCancel = context.WithCancel(context.Background())
return f
@ -419,6 +448,20 @@ func (f *forwarder) sendDoH(ctx context.Context, urlBase string, c *http.Client,
req.Header.Set("Accept", dohType)
req.Header.Set("User-Agent", "tailscaled/"+version.Long())
// If this is a Control D request, attach the hostname, mac address and IP address
// this allows for additional DNS analytics and client specific DNS policies
if strings.HasPrefix(urlBase, publicdns.ControlDBase) {
if f.dohClientHostname != "" {
req.Header.Set("X-Cd-Host", f.dohClientHostname)
}
if f.dohClientMac != "" {
req.Header.Set("X-Cd-Mac", f.dohClientMac)
}
if f.dohClientIP != "" {
req.Header.Set("X-Cd-IP", f.dohClientIP)
}
}
hres, err := c.Do(req)
if err != nil {
metricDNSFwdDoHErrorTransport.Add(1)

View File

@ -87,6 +87,16 @@ func TestResolversWithDelays(t *testing.T) {
in: q("https://dns.nextdns.io/c3a884"),
want: o("https://dns.nextdns.io/c3a884"),
},
{
name: "controld-ipv6-expand",
in: q("2606:1a40:0:6:7b5b:5949:35ad:0"),
want: o("https://dns.controld.com/hyq3ipr2ct"),
},
{
name: "controld-doh-input",
in: q("https://dns.controld.com/hyq3ipr2ct"),
want: o("https://dns.controld.com/hyq3ipr2ct"),
},
}
for _, tt := range tests {