mirror of
https://github.com/tailscale/tailscale.git
synced 2024-12-05 16:05:36 +00:00
0aa4c6f147
Change-Id: I6b790e92dcc608515ac8b178f2271adc9fd98f78 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
845 lines
23 KiB
Go
845 lines
23 KiB
Go
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package resolver
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"math/rand"
|
|
"net"
|
|
"net/http"
|
|
"runtime"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
dns "golang.org/x/net/dns/dnsmessage"
|
|
"inet.af/netaddr"
|
|
"tailscale.com/hostinfo"
|
|
"tailscale.com/net/netns"
|
|
"tailscale.com/net/tsdial"
|
|
"tailscale.com/types/dnstype"
|
|
"tailscale.com/types/logger"
|
|
"tailscale.com/util/dnsname"
|
|
"tailscale.com/wgengine/monitor"
|
|
)
|
|
|
|
// headerBytes is the number of bytes in a DNS message header.
|
|
const headerBytes = 12
|
|
|
|
const (
|
|
// responseTimeout is the maximal amount of time to wait for a DNS response.
|
|
responseTimeout = 5 * time.Second
|
|
|
|
// dohTransportTimeout is how long to keep idle HTTP
|
|
// connections open to DNS-over-HTTPs servers. This is pretty
|
|
// arbitrary.
|
|
dohTransportTimeout = 30 * time.Second
|
|
|
|
// wellKnownHostBackupDelay is how long to artificially delay upstream
|
|
// DNS queries to the "fallback" DNS server IP for a known provider
|
|
// (e.g. how long to wait to query Google's 8.8.4.4 after 8.8.8.8).
|
|
wellKnownHostBackupDelay = 200 * time.Millisecond
|
|
)
|
|
|
|
var errNoUpstreams = errors.New("upstream nameservers not set")
|
|
|
|
// txid identifies a DNS transaction.
|
|
//
|
|
// As the standard DNS Request ID is only 16 bits, we extend it:
|
|
// the lower 32 bits are the zero-extended bits of the DNS Request ID;
|
|
// the upper 32 bits are the CRC32 checksum of the first question in the request.
|
|
// This makes probability of txid collision negligible.
|
|
type txid uint64
|
|
|
|
// getTxID computes the txid of the given DNS packet.
|
|
func getTxID(packet []byte) txid {
|
|
if len(packet) < headerBytes {
|
|
return 0
|
|
}
|
|
|
|
dnsid := binary.BigEndian.Uint16(packet[0:2])
|
|
// Previously, we hashed the question and combined it with the original txid
|
|
// which was useful when concurrent queries were multiplexed on a single
|
|
// local source port. We encountered some situations where the DNS server
|
|
// canonicalizes the question in the response (uppercase converted to
|
|
// lowercase in this case), which resulted in responses that we couldn't
|
|
// match to the original request due to hash mismatches.
|
|
return txid(dnsid)
|
|
}
|
|
|
|
func getRCode(packet []byte) dns.RCode {
|
|
if len(packet) < headerBytes {
|
|
// treat invalid packets as a refusal
|
|
return dns.RCode(5)
|
|
}
|
|
// get bottom 4 bits of 3rd byte
|
|
return dns.RCode(packet[3] & 0x0F)
|
|
}
|
|
|
|
// clampEDNSSize attempts to limit the maximum EDNS response size. This is not
|
|
// an exhaustive solution, instead only easy cases are currently handled in the
|
|
// interest of speed and reduced complexity. Only OPT records at the very end of
|
|
// the message with no option codes are addressed.
|
|
// TODO: handle more situations if we discover that they happen often
|
|
func clampEDNSSize(packet []byte, maxSize uint16) {
|
|
// optFixedBytes is the size of an OPT record with no option codes.
|
|
const optFixedBytes = 11
|
|
const edns0Version = 0
|
|
|
|
if len(packet) < headerBytes+optFixedBytes {
|
|
return
|
|
}
|
|
|
|
arCount := binary.BigEndian.Uint16(packet[10:12])
|
|
if arCount == 0 {
|
|
// OPT shows up in an AR, so there must be no OPT
|
|
return
|
|
}
|
|
|
|
// https://datatracker.ietf.org/doc/html/rfc6891#section-6.1.2
|
|
opt := packet[len(packet)-optFixedBytes:]
|
|
|
|
if opt[0] != 0 {
|
|
// OPT NAME must be 0 (root domain)
|
|
return
|
|
}
|
|
if dns.Type(binary.BigEndian.Uint16(opt[1:3])) != dns.TypeOPT {
|
|
// Not an OPT record
|
|
return
|
|
}
|
|
requestedSize := binary.BigEndian.Uint16(opt[3:5])
|
|
// Ignore extended RCODE in opt[5]
|
|
if opt[6] != edns0Version {
|
|
// Be conservative and don't touch unknown versions.
|
|
return
|
|
}
|
|
// Ignore flags in opt[6:9]
|
|
if binary.BigEndian.Uint16(opt[9:11]) != 0 {
|
|
// RDLEN must be 0 (no variable length data). We're at the end of the
|
|
// packet so this should be 0 anyway)..
|
|
return
|
|
}
|
|
|
|
if requestedSize <= maxSize {
|
|
return
|
|
}
|
|
|
|
// Clamp the maximum size
|
|
binary.BigEndian.PutUint16(opt[3:5], maxSize)
|
|
}
|
|
|
|
type route struct {
|
|
Suffix dnsname.FQDN
|
|
Resolvers []resolverAndDelay
|
|
}
|
|
|
|
// resolverAndDelay is an upstream DNS resolver and a delay for how
|
|
// long to wait before querying it.
|
|
type resolverAndDelay struct {
|
|
// name is the upstream resolver.
|
|
name dnstype.Resolver
|
|
|
|
// startDelay is an amount to delay this resolver at
|
|
// start. It's used when, say, there are four Google or
|
|
// Cloudflare DNS IPs (two IPv4 + two IPv6) and we don't want
|
|
// to race all four at once.
|
|
startDelay time.Duration
|
|
}
|
|
|
|
// forwarder forwards DNS packets to a number of upstream nameservers.
|
|
type forwarder struct {
|
|
logf logger.Logf
|
|
linkMon *monitor.Mon
|
|
linkSel ForwardLinkSelector // TODO(bradfitz): remove this when tsdial.Dialer absords it
|
|
dialer *tsdial.Dialer
|
|
dohSem chan struct{}
|
|
|
|
ctx context.Context // good until Close
|
|
ctxCancel context.CancelFunc // closes ctx
|
|
|
|
// responses is a channel by which responses are returned.
|
|
responses chan packet
|
|
|
|
mu sync.Mutex // guards following
|
|
|
|
dohClient map[string]*http.Client // urlBase -> client
|
|
|
|
// routes are per-suffix resolvers to use, with
|
|
// the most specific routes first.
|
|
routes []route
|
|
}
|
|
|
|
func init() {
|
|
rand.Seed(time.Now().UnixNano())
|
|
}
|
|
|
|
func maxDoHInFlight(goos string) int {
|
|
if goos != "ios" {
|
|
return 1000 // effectively unlimited
|
|
}
|
|
// iOS < 15 limits the memory to 15MB for NetworkExtensions.
|
|
// iOS >= 15 gives us 50MB.
|
|
// See: https://tailscale.com/blog/go-linker/
|
|
ver := hostinfo.GetOSVersion()
|
|
if ver == "" {
|
|
// Unknown iOS version, be cautious.
|
|
return 10
|
|
}
|
|
idx := strings.Index(ver, ".")
|
|
if idx == -1 {
|
|
// Unknown iOS version, be cautious.
|
|
return 10
|
|
}
|
|
major := ver[:idx]
|
|
if m, err := strconv.Atoi(major); err != nil || m < 15 {
|
|
return 10
|
|
}
|
|
return 1000
|
|
}
|
|
|
|
func newForwarder(logf logger.Logf, responses chan packet, linkMon *monitor.Mon, linkSel ForwardLinkSelector, dialer *tsdial.Dialer) *forwarder {
|
|
f := &forwarder{
|
|
logf: logger.WithPrefix(logf, "forward: "),
|
|
linkMon: linkMon,
|
|
linkSel: linkSel,
|
|
dialer: dialer,
|
|
responses: responses,
|
|
dohSem: make(chan struct{}, maxDoHInFlight(runtime.GOOS)),
|
|
}
|
|
f.ctx, f.ctxCancel = context.WithCancel(context.Background())
|
|
return f
|
|
}
|
|
|
|
func (f *forwarder) Close() error {
|
|
f.ctxCancel()
|
|
return nil
|
|
}
|
|
|
|
// resolversWithDelays maps from a set of DNS server names to a slice of
|
|
// a type that included a startDelay. So if resolvers contains e.g. four
|
|
// Google DNS IPs (two IPv4 + twoIPv6), this function partition adds
|
|
// delays to some.
|
|
func resolversWithDelays(resolvers []dnstype.Resolver) []resolverAndDelay {
|
|
type hostAndFam struct {
|
|
host string // some arbitrary string representing DNS host (currently the DoH base)
|
|
bits uint8 // either 32 or 128 for IPv4 vs IPv6s address family
|
|
}
|
|
|
|
// Track how many of each known resolver host are in the list,
|
|
// per address family.
|
|
total := map[hostAndFam]int{}
|
|
|
|
rr := make([]resolverAndDelay, len(resolvers))
|
|
for _, r := range resolvers {
|
|
if ip, err := netaddr.ParseIP(r.Addr); err == nil {
|
|
if host, ok := knownDoH[ip]; ok {
|
|
total[hostAndFam{host, ip.BitLen()}]++
|
|
}
|
|
}
|
|
}
|
|
|
|
done := map[hostAndFam]int{}
|
|
for i, r := range resolvers {
|
|
var startDelay time.Duration
|
|
if ip, err := netaddr.ParseIP(r.Addr); err == nil {
|
|
if host, ok := knownDoH[ip]; ok {
|
|
key4 := hostAndFam{host, 32}
|
|
key6 := hostAndFam{host, 128}
|
|
switch {
|
|
case ip.Is4():
|
|
if done[key4] > 0 {
|
|
startDelay += wellKnownHostBackupDelay
|
|
}
|
|
case ip.Is6():
|
|
total4 := total[key4]
|
|
if total4 >= 2 {
|
|
// If we have two IPv4 IPs of the same provider
|
|
// already in the set, delay the IPv6 queries
|
|
// until halfway through the timeout (so wait
|
|
// 2.5 seconds). Even the network is IPv6-only,
|
|
// the DoH dialer will fallback to IPv6
|
|
// immediately anyway.
|
|
startDelay = responseTimeout / 2
|
|
} else if total4 == 1 {
|
|
startDelay += wellKnownHostBackupDelay
|
|
}
|
|
if done[key6] > 0 {
|
|
startDelay += wellKnownHostBackupDelay
|
|
}
|
|
}
|
|
done[hostAndFam{host, ip.BitLen()}]++
|
|
}
|
|
}
|
|
rr[i] = resolverAndDelay{
|
|
name: r,
|
|
startDelay: startDelay,
|
|
}
|
|
}
|
|
return rr
|
|
}
|
|
|
|
// setRoutes sets the routes to use for DNS forwarding. It's called by
|
|
// Resolver.SetConfig on reconfig.
|
|
//
|
|
// The memory referenced by routesBySuffix should not be modified.
|
|
func (f *forwarder) setRoutes(routesBySuffix map[dnsname.FQDN][]dnstype.Resolver) {
|
|
routes := make([]route, 0, len(routesBySuffix))
|
|
for suffix, rs := range routesBySuffix {
|
|
routes = append(routes, route{
|
|
Suffix: suffix,
|
|
Resolvers: resolversWithDelays(rs),
|
|
})
|
|
}
|
|
// Sort from longest prefix to shortest.
|
|
sort.Slice(routes, func(i, j int) bool {
|
|
return routes[i].Suffix.NumLabels() > routes[j].Suffix.NumLabels()
|
|
})
|
|
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.routes = routes
|
|
}
|
|
|
|
var stdNetPacketListener packetListener = new(net.ListenConfig)
|
|
|
|
type packetListener interface {
|
|
ListenPacket(ctx context.Context, network, address string) (net.PacketConn, error)
|
|
}
|
|
|
|
func (f *forwarder) packetListener(ip netaddr.IP) (packetListener, error) {
|
|
if f.linkSel == nil || initListenConfig == nil {
|
|
return stdNetPacketListener, nil
|
|
}
|
|
linkName := f.linkSel.PickLink(ip)
|
|
if linkName == "" {
|
|
return stdNetPacketListener, nil
|
|
}
|
|
lc := new(net.ListenConfig)
|
|
if err := initListenConfig(lc, f.linkMon, linkName); err != nil {
|
|
return nil, err
|
|
}
|
|
return lc, nil
|
|
}
|
|
|
|
func (f *forwarder) getKnownDoHClient(ip netaddr.IP) (urlBase string, c *http.Client, ok bool) {
|
|
urlBase, ok = knownDoH[ip]
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if c, ok := f.dohClient[urlBase]; ok {
|
|
return urlBase, c, true
|
|
}
|
|
if f.dohClient == nil {
|
|
f.dohClient = map[string]*http.Client{}
|
|
}
|
|
nsDialer := netns.NewDialer(f.logf)
|
|
c = &http.Client{
|
|
Transport: &http.Transport{
|
|
IdleConnTimeout: dohTransportTimeout,
|
|
DialContext: func(ctx context.Context, netw, addr string) (net.Conn, error) {
|
|
if !strings.HasPrefix(netw, "tcp") {
|
|
return nil, fmt.Errorf("unexpected network %q", netw)
|
|
}
|
|
c, err := nsDialer.DialContext(ctx, "tcp", net.JoinHostPort(ip.String(), "443"))
|
|
// If v4 failed, try an equivalent v6 also in the time remaining.
|
|
if err != nil && ctx.Err() == nil {
|
|
if ip6, ok := dohV6(urlBase); ok && ip.Is4() {
|
|
if c6, err := nsDialer.DialContext(ctx, "tcp", net.JoinHostPort(ip6.String(), "443")); err == nil {
|
|
return c6, nil
|
|
}
|
|
}
|
|
}
|
|
return c, err
|
|
},
|
|
},
|
|
}
|
|
f.dohClient[urlBase] = c
|
|
return urlBase, c, true
|
|
}
|
|
|
|
const dohType = "application/dns-message"
|
|
|
|
func (f *forwarder) releaseDoHSem() { <-f.dohSem }
|
|
|
|
func (f *forwarder) sendDoH(ctx context.Context, urlBase string, c *http.Client, packet []byte) ([]byte, error) {
|
|
// Bound the number of HTTP requests in flight. This primarily
|
|
// matters for iOS where we're very memory constrained and
|
|
// HTTP requests are heavier on iOS where we don't include
|
|
// HTTP/2 for binary size reasons (as binaries on iOS linked
|
|
// with Go code cost memory proportional to the binary size,
|
|
// for reasons not fully understood).
|
|
select {
|
|
case f.dohSem <- struct{}{}:
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
}
|
|
defer f.releaseDoHSem()
|
|
|
|
metricDNSFwdDoH.Add(1)
|
|
req, err := http.NewRequestWithContext(ctx, "POST", urlBase, bytes.NewReader(packet))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Type", dohType)
|
|
// Note: we don't currently set the Accept header (which is
|
|
// only a SHOULD in the spec) as iOS doesn't use HTTP/2 and
|
|
// we'd rather save a few bytes on outgoing requests when
|
|
// empirically no provider cares about the Accept header's
|
|
// absence.
|
|
|
|
hres, err := c.Do(req)
|
|
if err != nil {
|
|
metricDNSFwdDoHErrorTransport.Add(1)
|
|
return nil, err
|
|
}
|
|
defer hres.Body.Close()
|
|
if hres.StatusCode != 200 {
|
|
metricDNSFwdDoHErrorStatus.Add(1)
|
|
return nil, errors.New(hres.Status)
|
|
}
|
|
if ct := hres.Header.Get("Content-Type"); ct != dohType {
|
|
metricDNSFwdDoHErrorCT.Add(1)
|
|
return nil, fmt.Errorf("unexpected response Content-Type %q", ct)
|
|
}
|
|
res, err := ioutil.ReadAll(hres.Body)
|
|
if err != nil {
|
|
metricDNSFwdDoHErrorBody.Add(1)
|
|
}
|
|
return res, err
|
|
}
|
|
|
|
// send sends packet to dst. It is best effort.
|
|
//
|
|
// send expects the reply to have the same txid as txidOut.
|
|
func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDelay) ([]byte, error) {
|
|
if strings.HasPrefix(rr.name.Addr, "http://") {
|
|
return f.sendDoH(ctx, rr.name.Addr, f.dialer.PeerAPIHTTPClient(), fq.packet)
|
|
}
|
|
if strings.HasPrefix(rr.name.Addr, "https://") {
|
|
metricDNSFwdErrorType.Add(1)
|
|
return nil, fmt.Errorf("https:// resolvers not supported yet")
|
|
}
|
|
if strings.HasPrefix(rr.name.Addr, "tls://") {
|
|
metricDNSFwdErrorType.Add(1)
|
|
return nil, fmt.Errorf("tls:// resolvers not supported yet")
|
|
}
|
|
ipp, err := netaddr.ParseIPPort(rr.name.Addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Upgrade known DNS IPs to DoH (DNS-over-HTTPs).
|
|
// All known DoH is over port 53.
|
|
if urlBase, dc, ok := f.getKnownDoHClient(ipp.IP()); ok {
|
|
res, err := f.sendDoH(ctx, urlBase, dc, fq.packet)
|
|
if err == nil || ctx.Err() != nil {
|
|
return res, err
|
|
}
|
|
f.logf("DoH error from %v: %v", ipp.IP(), err)
|
|
}
|
|
|
|
metricDNSFwdUDP.Add(1)
|
|
ln, err := f.packetListener(ipp.IP())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
conn, err := ln.ListenPacket(ctx, "udp", ":0")
|
|
if err != nil {
|
|
f.logf("ListenPacket failed: %v", err)
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
|
|
fq.closeOnCtxDone.Add(conn)
|
|
defer fq.closeOnCtxDone.Remove(conn)
|
|
|
|
if _, err := conn.WriteTo(fq.packet, ipp.UDPAddr()); err != nil {
|
|
metricDNSFwdUDPErrorWrite.Add(1)
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return nil, err
|
|
}
|
|
metricDNSFwdUDPWrote.Add(1)
|
|
|
|
// The 1 extra byte is to detect packet truncation.
|
|
out := make([]byte, maxResponseBytes+1)
|
|
n, _, err := conn.ReadFrom(out)
|
|
if err != nil {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if packetWasTruncated(err) {
|
|
err = nil
|
|
} else {
|
|
metricDNSFwdUDPErrorRead.Add(1)
|
|
return nil, err
|
|
}
|
|
}
|
|
truncated := n > maxResponseBytes
|
|
if truncated {
|
|
n = maxResponseBytes
|
|
}
|
|
if n < headerBytes {
|
|
f.logf("recv: packet too small (%d bytes)", n)
|
|
}
|
|
out = out[:n]
|
|
txid := getTxID(out)
|
|
if txid != fq.txid {
|
|
metricDNSFwdUDPErrorTxID.Add(1)
|
|
return nil, errors.New("txid doesn't match")
|
|
}
|
|
rcode := getRCode(out)
|
|
// don't forward transient errors back to the client when the server fails
|
|
if rcode == dns.RCodeServerFailure {
|
|
f.logf("recv: response code indicating server failure: %d", rcode)
|
|
metricDNSFwdUDPErrorServer.Add(1)
|
|
return nil, errors.New("response code indicates server issue")
|
|
}
|
|
|
|
if truncated {
|
|
const dnsFlagTruncated = 0x200
|
|
flags := binary.BigEndian.Uint16(out[2:4])
|
|
flags |= dnsFlagTruncated
|
|
binary.BigEndian.PutUint16(out[2:4], flags)
|
|
|
|
// TODO(#2067): Remove any incomplete records? RFC 1035 section 6.2
|
|
// states that truncation should head drop so that the authority
|
|
// section can be preserved if possible. However, the UDP read with
|
|
// a too-small buffer has already dropped the end, so that's the
|
|
// best we can do.
|
|
}
|
|
|
|
clampEDNSSize(out, maxResponseBytes)
|
|
metricDNSFwdUDPSuccess.Add(1)
|
|
return out, nil
|
|
}
|
|
|
|
// resolvers returns the resolvers to use for domain.
|
|
func (f *forwarder) resolvers(domain dnsname.FQDN) []resolverAndDelay {
|
|
f.mu.Lock()
|
|
routes := f.routes
|
|
f.mu.Unlock()
|
|
for _, route := range routes {
|
|
if route.Suffix == "." || route.Suffix.Contains(domain) {
|
|
return route.Resolvers
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// forwardQuery is information and state about a forwarded DNS query that's
|
|
// being sent to 1 or more upstreams.
|
|
//
|
|
// In the case of racing against multiple equivalent upstreams
|
|
// (e.g. Google or CloudFlare's 4 DNS IPs: 2 IPv4 + 2 IPv6), this type
|
|
// handles racing them more intelligently than just blasting away 4
|
|
// queries at once.
|
|
type forwardQuery struct {
|
|
txid txid
|
|
packet []byte
|
|
|
|
// closeOnCtxDone lets send register values to Close if the
|
|
// caller's ctx expires. This avoids send from allocating its
|
|
// own waiting goroutine to interrupt the ReadFrom, as memory
|
|
// is tight on iOS and we want the number of pending DNS
|
|
// lookups to be bursty without too much associated
|
|
// goroutine/memory cost.
|
|
closeOnCtxDone *closePool
|
|
|
|
// TODO(bradfitz): add race delay state:
|
|
// mu sync.Mutex
|
|
// ...
|
|
}
|
|
|
|
// forward forwards the query to all upstream nameservers and waits for
|
|
// the first response.
|
|
//
|
|
// It either sends to f.responses and returns nil, or returns a
|
|
// non-nil error (without sending to the channel).
|
|
func (f *forwarder) forward(query packet) error {
|
|
ctx, cancel := context.WithTimeout(f.ctx, responseTimeout)
|
|
defer cancel()
|
|
return f.forwardWithDestChan(ctx, query, f.responses)
|
|
}
|
|
|
|
// forwardWithDestChan forwards the query to all upstream nameservers
|
|
// and waits for the first response.
|
|
//
|
|
// It either sends to responseChan and returns nil, or returns a
|
|
// non-nil error (without sending to the channel).
|
|
//
|
|
// If resolvers is non-empty, it's used explicitly (notably, for exit
|
|
// node DNS proxy queries), otherwise f.resolvers is used.
|
|
func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, responseChan chan<- packet, resolvers ...resolverAndDelay) error {
|
|
metricDNSFwd.Add(1)
|
|
domain, err := nameFromQuery(query.bs)
|
|
if err != nil {
|
|
metricDNSFwdErrorName.Add(1)
|
|
return err
|
|
}
|
|
|
|
// Drop DNS service discovery spam, primarily for battery life
|
|
// on mobile. Things like Spotify on iOS generate this traffic,
|
|
// when browsing for LAN devices. But even when filtering this
|
|
// out, playing on Sonos still works.
|
|
if hasRDNSBonjourPrefix(domain) {
|
|
metricDNSFwdDropBonjour.Add(1)
|
|
res, err := nxDomainResponse(query)
|
|
if err != nil {
|
|
f.logf("error parsing bonjour query: %v", err)
|
|
return nil
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case responseChan <- res:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
if fl, ok := fwdLogAtomic.Load().(*fwdLog); ok {
|
|
fl.addName(string(domain))
|
|
}
|
|
|
|
clampEDNSSize(query.bs, maxResponseBytes)
|
|
|
|
if len(resolvers) == 0 {
|
|
resolvers = f.resolvers(domain)
|
|
if len(resolvers) == 0 {
|
|
metricDNSFwdErrorNoUpstream.Add(1)
|
|
return errNoUpstreams
|
|
}
|
|
}
|
|
|
|
fq := &forwardQuery{
|
|
txid: getTxID(query.bs),
|
|
packet: query.bs,
|
|
closeOnCtxDone: new(closePool),
|
|
}
|
|
defer fq.closeOnCtxDone.Close()
|
|
|
|
resc := make(chan []byte, 1)
|
|
var (
|
|
mu sync.Mutex
|
|
firstErr error
|
|
)
|
|
|
|
for i := range resolvers {
|
|
go func(rr *resolverAndDelay) {
|
|
if rr.startDelay > 0 {
|
|
timer := time.NewTimer(rr.startDelay)
|
|
select {
|
|
case <-timer.C:
|
|
case <-ctx.Done():
|
|
timer.Stop()
|
|
return
|
|
}
|
|
}
|
|
resb, err := f.send(ctx, fq, *rr)
|
|
if err != nil {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if firstErr == nil {
|
|
firstErr = err
|
|
}
|
|
return
|
|
}
|
|
select {
|
|
case resc <- resb:
|
|
default:
|
|
}
|
|
}(&resolvers[i])
|
|
}
|
|
|
|
select {
|
|
case v := <-resc:
|
|
select {
|
|
case <-ctx.Done():
|
|
metricDNSFwdErrorContext.Add(1)
|
|
return ctx.Err()
|
|
case responseChan <- packet{v, query.addr}:
|
|
metricDNSFwdSuccess.Add(1)
|
|
return nil
|
|
}
|
|
case <-ctx.Done():
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
metricDNSFwdErrorContext.Add(1)
|
|
if firstErr != nil {
|
|
metricDNSFwdErrorContextGotError.Add(1)
|
|
return firstErr
|
|
}
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
|
|
var initListenConfig func(_ *net.ListenConfig, _ *monitor.Mon, tunName string) error
|
|
|
|
// nameFromQuery extracts the normalized query name from bs.
|
|
func nameFromQuery(bs []byte) (dnsname.FQDN, error) {
|
|
var parser dns.Parser
|
|
|
|
hdr, err := parser.Start(bs)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if hdr.Response {
|
|
return "", errNotQuery
|
|
}
|
|
|
|
q, err := parser.Question()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
n := q.Name.Data[:q.Name.Length]
|
|
return dnsname.ToFQDN(rawNameToLower(n))
|
|
}
|
|
|
|
// nxDomainResponse returns an NXDomain DNS reply for the provided request.
|
|
func nxDomainResponse(req packet) (res packet, err error) {
|
|
p := dnsParserPool.Get().(*dnsParser)
|
|
defer dnsParserPool.Put(p)
|
|
|
|
if err := p.parseQuery(req.bs); err != nil {
|
|
return packet{}, err
|
|
}
|
|
|
|
h := p.Header
|
|
h.Response = true
|
|
h.RecursionAvailable = h.RecursionDesired
|
|
h.RCode = dns.RCodeNameError
|
|
b := dns.NewBuilder(nil, h)
|
|
// TODO(bradfitz): should we add an SOA record in the Authority
|
|
// section too? (for the nxdomain negative caching TTL)
|
|
// For which zone? Does iOS care?
|
|
res.bs, err = b.Finish()
|
|
res.addr = req.addr
|
|
return res, err
|
|
}
|
|
|
|
// closePool is a dynamic set of io.Closers to close as a group.
|
|
// It's intended to be Closed at most once.
|
|
//
|
|
// The zero value is ready for use.
|
|
type closePool struct {
|
|
mu sync.Mutex
|
|
m map[io.Closer]bool
|
|
closed bool
|
|
}
|
|
|
|
func (p *closePool) Add(c io.Closer) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
if p.closed {
|
|
c.Close()
|
|
return
|
|
}
|
|
if p.m == nil {
|
|
p.m = map[io.Closer]bool{}
|
|
}
|
|
p.m[c] = true
|
|
}
|
|
|
|
func (p *closePool) Remove(c io.Closer) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
if p.closed {
|
|
return
|
|
}
|
|
delete(p.m, c)
|
|
}
|
|
|
|
func (p *closePool) Close() error {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
if p.closed {
|
|
return nil
|
|
}
|
|
p.closed = true
|
|
for c := range p.m {
|
|
c.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var knownDoH = map[netaddr.IP]string{} // 8.8.8.8 => "https://..."
|
|
|
|
var dohIPsOfBase = map[string][]netaddr.IP{}
|
|
|
|
func addDoH(ipStr, base string) {
|
|
ip := netaddr.MustParseIP(ipStr)
|
|
knownDoH[ip] = base
|
|
dohIPsOfBase[base] = append(dohIPsOfBase[base], ip)
|
|
}
|
|
|
|
func dohV6(base string) (ip netaddr.IP, ok bool) {
|
|
for _, ip := range dohIPsOfBase[base] {
|
|
if ip.Is6() {
|
|
return ip, true
|
|
}
|
|
}
|
|
return ip, false
|
|
}
|
|
|
|
func init() {
|
|
// Cloudflare
|
|
addDoH("1.1.1.1", "https://cloudflare-dns.com/dns-query")
|
|
addDoH("1.0.0.1", "https://cloudflare-dns.com/dns-query")
|
|
addDoH("2606:4700:4700::1111", "https://cloudflare-dns.com/dns-query")
|
|
addDoH("2606:4700:4700::1001", "https://cloudflare-dns.com/dns-query")
|
|
|
|
// Cloudflare -Malware
|
|
addDoH("1.1.1.2", "https://security.cloudflare-dns.com/dns-query")
|
|
addDoH("1.0.0.2", "https://security.cloudflare-dns.com/dns-query")
|
|
addDoH("2606:4700:4700::1112", "https://security.cloudflare-dns.com/dns-query")
|
|
addDoH("2606:4700:4700::1002", "https://security.cloudflare-dns.com/dns-query")
|
|
|
|
// Cloudflare -Malware -Adult
|
|
addDoH("1.1.1.3", "https://family.cloudflare-dns.com/dns-query")
|
|
addDoH("1.0.0.3", "https://family.cloudflare-dns.com/dns-query")
|
|
addDoH("2606:4700:4700::1113", "https://family.cloudflare-dns.com/dns-query")
|
|
addDoH("2606:4700:4700::1003", "https://family.cloudflare-dns.com/dns-query")
|
|
|
|
// Google
|
|
addDoH("8.8.8.8", "https://dns.google/dns-query")
|
|
addDoH("8.8.4.4", "https://dns.google/dns-query")
|
|
addDoH("2001:4860:4860::8888", "https://dns.google/dns-query")
|
|
addDoH("2001:4860:4860::8844", "https://dns.google/dns-query")
|
|
|
|
// OpenDNS
|
|
// TODO(bradfitz): OpenDNS is unique amongst this current set in that
|
|
// its DoH DNS names resolve to different IPs than its normal DNS
|
|
// IPs. Support that later. For now we assume that they're the same.
|
|
// addDoH("208.67.222.222", "https://doh.opendns.com/dns-query")
|
|
// addDoH("208.67.220.220", "https://doh.opendns.com/dns-query")
|
|
// addDoH("208.67.222.123", "https://doh.familyshield.opendns.com/dns-query")
|
|
// addDoH("208.67.220.123", "https://doh.familyshield.opendns.com/dns-query")
|
|
|
|
// Quad9
|
|
addDoH("9.9.9.9", "https://dns.quad9.net/dns-query")
|
|
addDoH("149.112.112.112", "https://dns.quad9.net/dns-query")
|
|
addDoH("2620:fe::fe", "https://dns.quad9.net/dns-query")
|
|
addDoH("2620:fe::fe:9", "https://dns.quad9.net/dns-query")
|
|
|
|
// Quad9 -DNSSEC
|
|
addDoH("9.9.9.10", "https://dns10.quad9.net/dns-query")
|
|
addDoH("149.112.112.10", "https://dns10.quad9.net/dns-query")
|
|
addDoH("2620:fe::10", "https://dns10.quad9.net/dns-query")
|
|
addDoH("2620:fe::fe:10", "https://dns10.quad9.net/dns-query")
|
|
}
|