Add Control D support, along with DoH client metadata
Signed-off-by: alexelisenko <39712468+alexelisenko@users.noreply.github.com>pull/8417/head
parent
8b80d63b42
commit
77a12e58c5
|
@ -7,10 +7,13 @@ package publicdns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/big"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
@ -23,6 +26,9 @@ var dohOfIP = map[netip.Addr]string{} // 8.8.8.8 => "https://..."
|
||||||
var dohIPsOfBase = map[string][]netip.Addr{}
|
var dohIPsOfBase = map[string][]netip.Addr{}
|
||||||
var populateOnce sync.Once
|
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
|
// DoHEndpointFromIP returns the DNS-over-HTTPS base URL for a given IP
|
||||||
// and whether it's DoH-only (not speaking DNS on port 53).
|
// 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) {
|
if nextDNSv6RangeA.Contains(ip) || nextDNSv6RangeB.Contains(ip) {
|
||||||
a := ip.As16()
|
a := ip.As16()
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
const base = "https://dns.nextdns.io/"
|
sb.Grow(len(NextDNSBase) + 12)
|
||||||
sb.Grow(len(base) + 12)
|
sb.WriteString(NextDNSBase)
|
||||||
sb.WriteString(base)
|
|
||||||
for _, b := range bytes.TrimLeft(a[4:], "\x00") {
|
for _, b := range bytes.TrimLeft(a[4:], "\x00") {
|
||||||
fmt.Fprintf(&sb, "%02x", b)
|
fmt.Fprintf(&sb, "%02x", b)
|
||||||
}
|
}
|
||||||
return sb.String(), true, true
|
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
|
return "", false, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,7 +93,7 @@ func DoHIPsOfBase(dohBase string) []netip.Addr {
|
||||||
if s := dohIPsOfBase[dohBase]; len(s) > 0 {
|
if s := dohIPsOfBase[dohBase]; len(s) > 0 {
|
||||||
return s
|
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>...]
|
// The path is of the form /<profile-hex>[/<hostname>/<model>/<device id>...]
|
||||||
// or /<profile-hex>?<query params>
|
// or /<profile-hex>?<query params>
|
||||||
// but only the <profile-hex> is required. Ignore the rest:
|
// 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,6 +206,37 @@ func populate() {
|
||||||
addDoH("194.242.2.3", "https://adblock.doh.mullvad.net/dns-query")
|
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("193.19.108.3", "https://adblock.doh.mullvad.net/dns-query")
|
||||||
addDoH("2a07:e340::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 (
|
var (
|
||||||
|
@ -207,6 +259,13 @@ var (
|
||||||
nextDNSv4RangeB = netip.MustParsePrefix("45.90.30.0/24")
|
nextDNSv4RangeB = netip.MustParsePrefix("45.90.30.0/24")
|
||||||
nextDNSv4One = nextDNSv4RangeA.Addr()
|
nextDNSv4One = nextDNSv4RangeA.Addr()
|
||||||
nextDNSv4Two = nextDNSv4RangeB.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
|
// 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)
|
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
|
// IPIsDoHOnlyServer reports whether ip is a DNS server that should only use
|
||||||
// DNS-over-HTTPS (not regular port 53 DNS).
|
// DNS-over-HTTPS (not regular port 53 DNS).
|
||||||
func IPIsDoHOnlyServer(ip netip.Addr) bool {
|
func IPIsDoHOnlyServer(ip netip.Addr) bool {
|
||||||
return nextDNSv6RangeA.Contains(ip) || nextDNSv6RangeB.Contains(ip) ||
|
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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,6 +116,24 @@ func TestDoHIPsOfBase(t *testing.T) {
|
||||||
"2a07:a8c1::c3:a884",
|
"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 {
|
for _, tt := range tests {
|
||||||
got := DoHIPsOfBase(tt.base)
|
got := DoHIPsOfBase(tt.base)
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -24,6 +25,7 @@ import (
|
||||||
"tailscale.com/envknob"
|
"tailscale.com/envknob"
|
||||||
"tailscale.com/net/dns/publicdns"
|
"tailscale.com/net/dns/publicdns"
|
||||||
"tailscale.com/net/dnscache"
|
"tailscale.com/net/dnscache"
|
||||||
|
"tailscale.com/net/interfaces"
|
||||||
"tailscale.com/net/neterror"
|
"tailscale.com/net/neterror"
|
||||||
"tailscale.com/net/netmon"
|
"tailscale.com/net/netmon"
|
||||||
"tailscale.com/net/netns"
|
"tailscale.com/net/netns"
|
||||||
|
@ -200,6 +202,12 @@ type forwarder struct {
|
||||||
// /etc/resolv.conf is missing/corrupt, and the peerapi ExitDNS stub
|
// /etc/resolv.conf is missing/corrupt, and the peerapi ExitDNS stub
|
||||||
// resolver lookup.
|
// resolver lookup.
|
||||||
cloudHostFallback []resolverAndDelay
|
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() {
|
func init() {
|
||||||
|
@ -207,11 +215,32 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func newForwarder(logf logger.Logf, netMon *netmon.Monitor, linkSel ForwardLinkSelector, dialer *tsdial.Dialer) *forwarder {
|
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{
|
f := &forwarder{
|
||||||
logf: logger.WithPrefix(logf, "forward: "),
|
logf: logger.WithPrefix(logf, "forward: "),
|
||||||
netMon: netMon,
|
netMon: netMon,
|
||||||
linkSel: linkSel,
|
linkSel: linkSel,
|
||||||
dialer: dialer,
|
dialer: dialer,
|
||||||
|
// Control D specific DoH fields
|
||||||
|
dohClientHostname: hostname,
|
||||||
|
dohClientIP: tailscaleIP,
|
||||||
|
dohClientMac: clientMac,
|
||||||
}
|
}
|
||||||
f.ctx, f.ctxCancel = context.WithCancel(context.Background())
|
f.ctx, f.ctxCancel = context.WithCancel(context.Background())
|
||||||
return f
|
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("Accept", dohType)
|
||||||
req.Header.Set("User-Agent", "tailscaled/"+version.Long())
|
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)
|
hres, err := c.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
metricDNSFwdDoHErrorTransport.Add(1)
|
metricDNSFwdDoHErrorTransport.Add(1)
|
||||||
|
|
|
@ -87,6 +87,16 @@ func TestResolversWithDelays(t *testing.T) {
|
||||||
in: q("https://dns.nextdns.io/c3a884"),
|
in: q("https://dns.nextdns.io/c3a884"),
|
||||||
want: o("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 {
|
for _, tt := range tests {
|
||||||
|
|
Loading…
Reference in New Issue