mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-30 05:25:35 +00:00
dd77111462
Updates https://github.com/tailscale/corp/issues/15802. On iOS exclusively, this PR adds logic to use a split DNS configuration in more cases, with the goal of improving battery life. Acting as the global DNS resolver on iOS should be avoided, as it leads to frequent wakes of IPNExtension. We try to determine if we can have Tailscale only handle DNS queries for resources inside the tailnet, that is, all routes in the DNS configuration do not require a custom resolver (this is the case for app connectors, for instance). If so, we set all Routes as MatchDomains. This enables a split DNS configuration which will help preserve battery life. Effectively, for the average Tailscale user who only relies on MagicDNS to resolve *.ts.net domains, this means that Tailscale DNS will only be used for those domains. This PR doesn't affect users with Override Local DNS enabled. For these users, there should be no difference and Tailscale will continue acting as a global DNS resolver. Signed-off-by: Andrea Gottardo <andrea@tailscale.com>
1371 lines
41 KiB
Go
1371 lines
41 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
// Package resolver implements a stub DNS resolver that can also serve
|
|
// records out of an internal local zone.
|
|
package resolver
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/netip"
|
|
"os"
|
|
"runtime"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
dns "golang.org/x/net/dns/dnsmessage"
|
|
"tailscale.com/control/controlknobs"
|
|
"tailscale.com/envknob"
|
|
"tailscale.com/net/dns/resolvconffile"
|
|
"tailscale.com/net/netaddr"
|
|
"tailscale.com/net/netmon"
|
|
"tailscale.com/net/tsaddr"
|
|
"tailscale.com/net/tsdial"
|
|
"tailscale.com/syncs"
|
|
"tailscale.com/types/dnstype"
|
|
"tailscale.com/types/logger"
|
|
"tailscale.com/util/clientmetric"
|
|
"tailscale.com/util/cloudenv"
|
|
"tailscale.com/util/dnsname"
|
|
)
|
|
|
|
const dnsSymbolicFQDN = "magicdns.localhost-tailscale-daemon."
|
|
|
|
// maxResponseBytes is the maximum size of a response from a Resolver. The
|
|
// actual buffer size will be one larger than this so that we can detect
|
|
// truncation in a platform-agnostic way.
|
|
const maxResponseBytes = 4095
|
|
|
|
// defaultTTL is the TTL of all responses from Resolver.
|
|
const defaultTTL = 600 * time.Second
|
|
|
|
var (
|
|
errNotQuery = errors.New("not a DNS query")
|
|
errNotOurName = errors.New("not a Tailscale DNS name")
|
|
)
|
|
|
|
type packet struct {
|
|
bs []byte
|
|
family string // either "tcp" or "udp"
|
|
addr netip.AddrPort // src for a request, dst for a response
|
|
}
|
|
|
|
// Config is a resolver configuration.
|
|
// Given a Config, queries are resolved in the following order:
|
|
// If the query is an exact match for an entry in LocalHosts, return that.
|
|
// Else if the query suffix matches an entry in LocalDomains, return NXDOMAIN.
|
|
// Else forward the query to the most specific matching entry in Routes.
|
|
// Else return SERVFAIL.
|
|
type Config struct {
|
|
// Routes is a map of DNS name suffix to the resolvers to use for
|
|
// queries within that suffix.
|
|
// Queries only match the most specific suffix.
|
|
// To register a "default route", add an entry for ".".
|
|
Routes map[dnsname.FQDN][]*dnstype.Resolver
|
|
// LocalHosts is a map of FQDNs to corresponding IPs.
|
|
Hosts map[dnsname.FQDN][]netip.Addr
|
|
// LocalDomains is a list of DNS name suffixes that should not be
|
|
// routed to upstream resolvers.
|
|
LocalDomains []dnsname.FQDN
|
|
}
|
|
|
|
// WriteToBufioWriter write a debug version of c for logs to w, omitting
|
|
// spammy stuff like *.arpa entries and replacing it with a total count.
|
|
func (c *Config) WriteToBufioWriter(w *bufio.Writer) {
|
|
w.WriteString("{Routes:")
|
|
WriteRoutes(w, c.Routes)
|
|
fmt.Fprintf(w, " Hosts:%v LocalDomains:[", len(c.Hosts))
|
|
space := false
|
|
arpa := 0
|
|
for _, d := range c.LocalDomains {
|
|
if strings.HasSuffix(string(d), ".arpa.") {
|
|
arpa++
|
|
continue
|
|
}
|
|
if space {
|
|
w.WriteByte(' ')
|
|
}
|
|
w.WriteString(string(d))
|
|
space = true
|
|
}
|
|
w.WriteString("]")
|
|
if arpa > 0 {
|
|
fmt.Fprintf(w, "+%darpa", arpa)
|
|
}
|
|
if c := cloudenv.Get(); c != "" {
|
|
fmt.Fprintf(w, ", cloud=%q", string(c))
|
|
}
|
|
w.WriteString("}")
|
|
}
|
|
|
|
// WriteIPPorts writes vv to w.
|
|
func WriteIPPorts(w *bufio.Writer, vv []netip.AddrPort) {
|
|
w.WriteByte('[')
|
|
var b []byte
|
|
for i, v := range vv {
|
|
if i > 0 {
|
|
w.WriteByte(' ')
|
|
}
|
|
b = v.AppendTo(b[:0])
|
|
w.Write(b)
|
|
}
|
|
w.WriteByte(']')
|
|
}
|
|
|
|
// WriteDNSResolver writes r to w.
|
|
func WriteDNSResolver(w *bufio.Writer, r *dnstype.Resolver) {
|
|
io.WriteString(w, r.Addr)
|
|
if len(r.BootstrapResolution) > 0 {
|
|
w.WriteByte('(')
|
|
var b []byte
|
|
for _, ip := range r.BootstrapResolution {
|
|
ip.AppendTo(b[:0])
|
|
w.Write(b)
|
|
}
|
|
w.WriteByte(')')
|
|
}
|
|
}
|
|
|
|
// WriteDNSResolvers writes resolvers to w.
|
|
func WriteDNSResolvers(w *bufio.Writer, resolvers []*dnstype.Resolver) {
|
|
w.WriteByte('[')
|
|
for i, r := range resolvers {
|
|
if i > 0 {
|
|
w.WriteByte(' ')
|
|
}
|
|
WriteDNSResolver(w, r)
|
|
}
|
|
w.WriteByte(']')
|
|
}
|
|
|
|
// WriteRoutes writes routes to w, omitting *.arpa routes and instead
|
|
// summarizing how many of them there were.
|
|
func WriteRoutes(w *bufio.Writer, routes map[dnsname.FQDN][]*dnstype.Resolver) {
|
|
var kk []dnsname.FQDN
|
|
arpa := 0
|
|
for k := range routes {
|
|
if strings.HasSuffix(string(k), ".arpa.") {
|
|
arpa++
|
|
continue
|
|
}
|
|
kk = append(kk, k)
|
|
}
|
|
sort.Slice(kk, func(i, j int) bool { return kk[i] < kk[j] })
|
|
w.WriteByte('{')
|
|
for i, k := range kk {
|
|
if i > 0 {
|
|
w.WriteByte(' ')
|
|
}
|
|
w.WriteString(string(k))
|
|
w.WriteByte(':')
|
|
WriteDNSResolvers(w, routes[k])
|
|
}
|
|
w.WriteByte('}')
|
|
if arpa > 0 {
|
|
fmt.Fprintf(w, "+%darpa", arpa)
|
|
}
|
|
}
|
|
|
|
// RoutesRequireNoCustomResolvers returns true if this resolver.Config only contains routes
|
|
// that do not specify a set of custom resolver(s), i.e. they can be resolved by the local
|
|
// upstream DNS resolver.
|
|
func (c *Config) RoutesRequireNoCustomResolvers() bool {
|
|
for route, resolvers := range c.Routes {
|
|
if route.WithoutTrailingDot() == "ts.net" {
|
|
// Ignore the "ts.net" route here. It always specifies the corp resolvers but
|
|
// its presence is not an issue, as ts.net will be a search domain.
|
|
continue
|
|
}
|
|
if len(resolvers) != 0 {
|
|
// Found a route with custom resolvers.
|
|
return false
|
|
}
|
|
}
|
|
// No routes other than ts.net have specified one or more resolvers.
|
|
return true
|
|
}
|
|
|
|
// Resolver is a DNS resolver for nodes on the Tailscale network,
|
|
// associating them with domain names of the form <mynode>.<mydomain>.<root>.
|
|
// If it is asked to resolve a domain that is not of that form,
|
|
// it delegates to upstream nameservers if any are set.
|
|
type Resolver struct {
|
|
logf logger.Logf
|
|
netMon *netmon.Monitor // non-nil
|
|
dialer *tsdial.Dialer // non-nil
|
|
saveConfigForTests func(cfg Config) // used in tests to capture resolver config
|
|
// forwarder forwards requests to upstream nameservers.
|
|
forwarder *forwarder
|
|
|
|
// closed signals all goroutines to stop.
|
|
closed chan struct{}
|
|
|
|
// mu guards the following fields from being updated while used.
|
|
mu sync.Mutex
|
|
localDomains []dnsname.FQDN
|
|
hostToIP map[dnsname.FQDN][]netip.Addr
|
|
ipToHost map[netip.Addr]dnsname.FQDN
|
|
}
|
|
|
|
type ForwardLinkSelector interface {
|
|
// PickLink returns which network device should be used to query
|
|
// the DNS server at the given IP.
|
|
// The empty string means to use an unspecified default.
|
|
PickLink(netip.Addr) (linkName string)
|
|
}
|
|
|
|
// New returns a new resolver.
|
|
func New(logf logger.Logf, linkSel ForwardLinkSelector, dialer *tsdial.Dialer, knobs *controlknobs.Knobs) *Resolver {
|
|
if dialer == nil {
|
|
panic("nil Dialer")
|
|
}
|
|
netMon := dialer.NetMon()
|
|
if netMon == nil {
|
|
logf("nil netMon")
|
|
}
|
|
r := &Resolver{
|
|
logf: logger.WithPrefix(logf, "resolver: "),
|
|
netMon: netMon,
|
|
closed: make(chan struct{}),
|
|
hostToIP: map[dnsname.FQDN][]netip.Addr{},
|
|
ipToHost: map[netip.Addr]dnsname.FQDN{},
|
|
dialer: dialer,
|
|
}
|
|
r.forwarder = newForwarder(r.logf, netMon, linkSel, dialer, knobs)
|
|
return r
|
|
}
|
|
|
|
func (r *Resolver) TestOnlySetHook(hook func(Config)) { r.saveConfigForTests = hook }
|
|
|
|
func (r *Resolver) SetConfig(cfg Config) error {
|
|
if r.saveConfigForTests != nil {
|
|
r.saveConfigForTests(cfg)
|
|
}
|
|
|
|
reverse := make(map[netip.Addr]dnsname.FQDN, len(cfg.Hosts))
|
|
|
|
for host, ips := range cfg.Hosts {
|
|
for _, ip := range ips {
|
|
reverse[ip] = host
|
|
}
|
|
}
|
|
|
|
r.forwarder.setRoutes(cfg.Routes)
|
|
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.localDomains = cfg.LocalDomains
|
|
r.hostToIP = cfg.Hosts
|
|
r.ipToHost = reverse
|
|
return nil
|
|
}
|
|
|
|
// Close shuts down the resolver and ensures poll goroutines have exited.
|
|
// The Resolver cannot be used again after Close is called.
|
|
func (r *Resolver) Close() {
|
|
select {
|
|
case <-r.closed:
|
|
return
|
|
default:
|
|
// continue
|
|
}
|
|
close(r.closed)
|
|
|
|
r.forwarder.Close()
|
|
}
|
|
|
|
// dnsQueryTimeout is not intended to be user-visible (the users
|
|
// DNS resolver will retry well before that), just put an upper
|
|
// bound on per-query resource usage.
|
|
const dnsQueryTimeout = 10 * time.Second
|
|
|
|
func (r *Resolver) Query(ctx context.Context, bs []byte, family string, from netip.AddrPort) ([]byte, error) {
|
|
metricDNSQueryLocal.Add(1)
|
|
select {
|
|
case <-r.closed:
|
|
metricDNSQueryErrorClosed.Add(1)
|
|
return nil, net.ErrClosed
|
|
default:
|
|
}
|
|
|
|
out, err := r.respond(bs)
|
|
if err == errNotOurName {
|
|
responses := make(chan packet, 1)
|
|
ctx, cancel := context.WithTimeout(ctx, dnsQueryTimeout)
|
|
defer close(responses)
|
|
defer cancel()
|
|
err = r.forwarder.forwardWithDestChan(ctx, packet{bs, family, from}, responses)
|
|
if err != nil {
|
|
select {
|
|
// Best effort: use any error response sent by forwardWithDestChan.
|
|
// This is present in some errors paths, such as when all upstream
|
|
// DNS servers replied with an error.
|
|
case resp := <-responses:
|
|
return resp.bs, err
|
|
default:
|
|
return nil, err
|
|
}
|
|
}
|
|
return (<-responses).bs, nil
|
|
}
|
|
|
|
return out, err
|
|
}
|
|
|
|
// parseExitNodeQuery parses a DNS request packet.
|
|
// It returns nil if it's malformed or lacking a question.
|
|
func parseExitNodeQuery(q []byte) *response {
|
|
p := dnsParserPool.Get().(*dnsParser)
|
|
defer dnsParserPool.Put(p)
|
|
p.zeroParser()
|
|
defer p.zeroParser()
|
|
if err := p.parseQuery(q); err != nil {
|
|
return nil
|
|
}
|
|
return p.response()
|
|
}
|
|
|
|
// HandlePeerDNSQuery handles a DNS query that arrived from a peer
|
|
// via the peerapi's DoH server. This is used when the local
|
|
// node is being an exit node or an app connector.
|
|
//
|
|
// The provided allowName callback is whether a DNS query for a name
|
|
// (as found by parsing q) is allowed.
|
|
//
|
|
// In most (all?) cases, err will be nil. A bogus DNS query q will
|
|
// still result in a response DNS packet (saying there's a failure)
|
|
// and a nil error.
|
|
// TODO: figure out if we even need an error result.
|
|
func (r *Resolver) HandlePeerDNSQuery(ctx context.Context, q []byte, from netip.AddrPort, allowName func(name string) bool) (res []byte, err error) {
|
|
metricDNSExitProxyQuery.Add(1)
|
|
ch := make(chan packet, 1)
|
|
|
|
resp := parseExitNodeQuery(q)
|
|
if resp == nil {
|
|
return nil, errors.New("bad query")
|
|
}
|
|
name := resp.Question.Name.String()
|
|
if !allowName(name) {
|
|
metricDNSExitProxyErrorName.Add(1)
|
|
resp.Header.RCode = dns.RCodeRefused
|
|
return marshalResponse(resp)
|
|
}
|
|
|
|
switch runtime.GOOS {
|
|
default:
|
|
return nil, errors.New("unsupported exit node OS")
|
|
case "windows", "android":
|
|
return handleExitNodeDNSQueryWithNetPkg(ctx, r.logf, nil, resp)
|
|
case "darwin":
|
|
// /etc/resolv.conf is a lie and only says one upstream DNS
|
|
// but for now that's probably good enough. Later we'll
|
|
// want to blend in everything from scutil --dns.
|
|
fallthrough
|
|
case "linux", "freebsd", "openbsd", "illumos", "ios":
|
|
nameserver, err := stubResolverForOS()
|
|
if err != nil {
|
|
r.logf("stubResolverForOS: %v", err)
|
|
metricDNSExitProxyErrorResolvConf.Add(1)
|
|
return nil, err
|
|
}
|
|
// TODO: more than 1 resolver from /etc/resolv.conf?
|
|
|
|
var resolvers []resolverAndDelay
|
|
switch nameserver {
|
|
case tsaddr.TailscaleServiceIP(), tsaddr.TailscaleServiceIPv6():
|
|
// If resolv.conf says 100.100.100.100, it's coming right back to us anyway
|
|
// so avoid the loop through the kernel and just do what we
|
|
// would've done anyway. By not passing any resolvers, the forwarder
|
|
// will use its default ones from our DNS config.
|
|
case netip.Addr{}:
|
|
// Likewise, if the platform has no resolv.conf, just use our defaults.
|
|
default:
|
|
resolvers = []resolverAndDelay{{
|
|
name: &dnstype.Resolver{Addr: net.JoinHostPort(nameserver.String(), "53")},
|
|
}}
|
|
}
|
|
|
|
err = r.forwarder.forwardWithDestChan(ctx, packet{q, "tcp", from}, ch, resolvers...)
|
|
if err != nil {
|
|
metricDNSExitProxyErrorForward.Add(1)
|
|
return nil, err
|
|
}
|
|
}
|
|
select {
|
|
case p, ok := <-ch:
|
|
if ok {
|
|
return p.bs, nil
|
|
}
|
|
panic("unexpected close chan")
|
|
default:
|
|
panic("unexpected unreadable chan")
|
|
}
|
|
}
|
|
|
|
var debugExitNodeDNSNetPkg = envknob.RegisterBool("TS_DEBUG_EXIT_NODE_DNS_NET_PKG")
|
|
|
|
// handleExitNodeDNSQueryWithNetPkg takes a DNS query message in q and
|
|
// return a reply (for the ExitDNS DoH service) using the net package's
|
|
// native APIs.
|
|
//
|
|
// If resolver is nil, the net.Resolver zero value is used.
|
|
//
|
|
// response contains the pre-serialized response, which notably
|
|
// includes the original question and its header.
|
|
func handleExitNodeDNSQueryWithNetPkg(ctx context.Context, logf logger.Logf, resolver *net.Resolver, resp *response) (res []byte, err error) {
|
|
logf = logger.WithPrefix(logf, "exitNodeDNSQueryWithNetPkg: ")
|
|
if resp.Question.Class != dns.ClassINET {
|
|
return nil, errors.New("unsupported class")
|
|
}
|
|
|
|
r := resolver
|
|
if r == nil {
|
|
r = new(net.Resolver)
|
|
}
|
|
name := resp.Question.Name.String()
|
|
|
|
handleError := func(err error) (res []byte, _ error) {
|
|
if isGoNoSuchHostError(err) {
|
|
if debugExitNodeDNSNetPkg() {
|
|
logf(`converting Go "no such host" error to a NXDOMAIN: %v`, err)
|
|
}
|
|
resp.Header.RCode = dns.RCodeNameError
|
|
return marshalResponse(resp)
|
|
}
|
|
|
|
if debugExitNodeDNSNetPkg() {
|
|
logf("returning error: %v", err)
|
|
}
|
|
// TODO: map other errors to RCodeServerFailure?
|
|
// Or I guess our caller should do that?
|
|
return nil, err
|
|
}
|
|
|
|
resp.Header.RCode = dns.RCodeSuccess // unless changed below
|
|
|
|
switch resp.Question.Type {
|
|
case dns.TypeA, dns.TypeAAAA:
|
|
network := "ip4"
|
|
if resp.Question.Type == dns.TypeAAAA {
|
|
network = "ip6"
|
|
}
|
|
if debugExitNodeDNSNetPkg() {
|
|
logf("resolving %s %q", network, name)
|
|
}
|
|
ips, err := r.LookupIP(ctx, network, name)
|
|
if err != nil {
|
|
return handleError(err)
|
|
}
|
|
for _, stdIP := range ips {
|
|
if ip, ok := netip.AddrFromSlice(stdIP); ok {
|
|
resp.IPs = append(resp.IPs, ip.Unmap())
|
|
}
|
|
}
|
|
case dns.TypeTXT:
|
|
if debugExitNodeDNSNetPkg() {
|
|
logf("resolving TXT %q", name)
|
|
}
|
|
strs, err := r.LookupTXT(ctx, name)
|
|
if err != nil {
|
|
return handleError(err)
|
|
}
|
|
resp.TXT = strs
|
|
case dns.TypePTR:
|
|
ipStr, ok := unARPA(name)
|
|
if !ok {
|
|
// TODO: is this RCodeFormatError?
|
|
return nil, errors.New("bogus PTR name")
|
|
}
|
|
if debugExitNodeDNSNetPkg() {
|
|
logf("resolving PTR %q", ipStr)
|
|
}
|
|
addrs, err := r.LookupAddr(ctx, ipStr)
|
|
if err != nil {
|
|
return handleError(err)
|
|
}
|
|
if len(addrs) > 0 {
|
|
resp.Name, _ = dnsname.ToFQDN(addrs[0])
|
|
}
|
|
case dns.TypeCNAME:
|
|
if debugExitNodeDNSNetPkg() {
|
|
logf("resolving CNAME %q", name)
|
|
}
|
|
cname, err := r.LookupCNAME(ctx, name)
|
|
if err != nil {
|
|
return handleError(err)
|
|
}
|
|
resp.CNAME = cname
|
|
case dns.TypeSRV:
|
|
if debugExitNodeDNSNetPkg() {
|
|
logf("resolving SRV %q", name)
|
|
}
|
|
// Thanks, Go: "To accommodate services publishing SRV
|
|
// records under non-standard names, if both service
|
|
// and proto are empty strings, LookupSRV looks up
|
|
// name directly."
|
|
_, srvs, err := r.LookupSRV(ctx, "", "", name)
|
|
if err != nil {
|
|
return handleError(err)
|
|
}
|
|
resp.SRVs = srvs
|
|
case dns.TypeNS:
|
|
if debugExitNodeDNSNetPkg() {
|
|
logf("resolving NS %q", name)
|
|
}
|
|
nss, err := r.LookupNS(ctx, name)
|
|
if err != nil {
|
|
return handleError(err)
|
|
}
|
|
resp.NSs = nss
|
|
default:
|
|
return nil, fmt.Errorf("unsupported record type %v", resp.Question.Type)
|
|
}
|
|
return marshalResponse(resp)
|
|
}
|
|
|
|
func isGoNoSuchHostError(err error) bool {
|
|
if de, ok := err.(*net.DNSError); ok {
|
|
return de.IsNotFound
|
|
}
|
|
return false
|
|
}
|
|
|
|
type resolvConfCache struct {
|
|
mod time.Time
|
|
size int64
|
|
ip netip.Addr
|
|
// TODO: inode/dev?
|
|
}
|
|
|
|
// resolvConfCacheValue contains the most recent stat metadata and parsed
|
|
// version of /etc/resolv.conf.
|
|
var resolvConfCacheValue syncs.AtomicValue[resolvConfCache]
|
|
|
|
var errEmptyResolvConf = errors.New("resolv.conf has no nameservers")
|
|
|
|
// stubResolverForOS returns the IP address of the first nameserver in
|
|
// /etc/resolv.conf.
|
|
//
|
|
// It may also return the netip.Addr zero value and a nil error to mean
|
|
// that the platform has no resolv.conf.
|
|
func stubResolverForOS() (ip netip.Addr, err error) {
|
|
if runtime.GOOS == "ios" {
|
|
return netip.Addr{}, nil // no resolv.conf on iOS
|
|
}
|
|
fi, err := os.Stat(resolvconffile.Path)
|
|
if err != nil {
|
|
return netip.Addr{}, err
|
|
}
|
|
cur := resolvConfCache{
|
|
mod: fi.ModTime(),
|
|
size: fi.Size(),
|
|
}
|
|
if c, ok := resolvConfCacheValue.LoadOk(); ok && c.mod == cur.mod && c.size == cur.size {
|
|
return c.ip, nil
|
|
}
|
|
conf, err := resolvconffile.ParseFile(resolvconffile.Path)
|
|
if err != nil {
|
|
return netip.Addr{}, err
|
|
}
|
|
if len(conf.Nameservers) == 0 {
|
|
return netip.Addr{}, errEmptyResolvConf
|
|
}
|
|
ip = conf.Nameservers[0]
|
|
cur.ip = ip
|
|
resolvConfCacheValue.Store(cur)
|
|
return ip, nil
|
|
}
|
|
|
|
// resolveLocal returns an IP for the given domain, if domain is in
|
|
// the local hosts map and has an IP corresponding to the requested
|
|
// typ (A, AAAA, ALL).
|
|
// Returns dns.RCodeRefused to indicate that the local map is not
|
|
// authoritative for domain.
|
|
func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netip.Addr, dns.RCode) {
|
|
metricDNSResolveLocal.Add(1)
|
|
// Reject .onion domains per RFC 7686.
|
|
if dnsname.HasSuffix(domain.WithoutTrailingDot(), ".onion") {
|
|
metricDNSResolveLocalErrorOnion.Add(1)
|
|
return netip.Addr{}, dns.RCodeNameError
|
|
}
|
|
|
|
// We return a symbolic domain if someone does a reverse lookup on the
|
|
// DNS endpoint. To round out this special case, we also do the inverse
|
|
// (returning the endpoint IP if someone looks up the symbolic domain).
|
|
if domain == dnsSymbolicFQDN {
|
|
switch typ {
|
|
case dns.TypeA:
|
|
return tsaddr.TailscaleServiceIP(), dns.RCodeSuccess
|
|
case dns.TypeAAAA:
|
|
return tsaddr.TailscaleServiceIPv6(), dns.RCodeSuccess
|
|
}
|
|
}
|
|
// Special-case: 4via6 DNS names.
|
|
if ip, ok := r.parseViaDomain(domain, typ); ok {
|
|
return ip, dns.RCodeSuccess
|
|
}
|
|
|
|
r.mu.Lock()
|
|
hosts := r.hostToIP
|
|
localDomains := r.localDomains
|
|
r.mu.Unlock()
|
|
|
|
addrs, found := hosts[domain]
|
|
if !found {
|
|
for _, suffix := range localDomains {
|
|
if suffix.Contains(domain) {
|
|
// We are authoritative for the queried domain.
|
|
metricDNSResolveLocalErrorMissing.Add(1)
|
|
return netip.Addr{}, dns.RCodeNameError
|
|
}
|
|
}
|
|
// Not authoritative, signal that forwarding is advisable.
|
|
metricDNSResolveLocalErrorRefused.Add(1)
|
|
return netip.Addr{}, dns.RCodeRefused
|
|
}
|
|
|
|
// Refactoring note: this must happen after we check suffixes,
|
|
// otherwise we will respond with NOTIMP to requests that should be forwarded.
|
|
//
|
|
// DNS semantics subtlety: when a DNS name exists, but no records
|
|
// are available for the requested record type, we must return
|
|
// RCodeSuccess with no data, not NXDOMAIN.
|
|
switch typ {
|
|
case dns.TypeA:
|
|
for _, ip := range addrs {
|
|
if ip.Is4() {
|
|
metricDNSResolveLocalOKA.Add(1)
|
|
return ip, dns.RCodeSuccess
|
|
}
|
|
}
|
|
metricDNSResolveLocalNoA.Add(1)
|
|
return netip.Addr{}, dns.RCodeSuccess
|
|
case dns.TypeAAAA:
|
|
for _, ip := range addrs {
|
|
if ip.Is6() {
|
|
metricDNSResolveLocalOKAAAA.Add(1)
|
|
return ip, dns.RCodeSuccess
|
|
}
|
|
}
|
|
metricDNSResolveLocalNoAAAA.Add(1)
|
|
return netip.Addr{}, dns.RCodeSuccess
|
|
case dns.TypeALL:
|
|
// Answer with whatever we've got.
|
|
// It could be IPv4, IPv6, or a zero addr.
|
|
// TODO: Return all available resolutions (A and AAAA, if we have them).
|
|
if len(addrs) == 0 {
|
|
metricDNSResolveLocalNoAll.Add(1)
|
|
return netip.Addr{}, dns.RCodeSuccess
|
|
}
|
|
metricDNSResolveLocalOKAll.Add(1)
|
|
return addrs[0], dns.RCodeSuccess
|
|
|
|
// Leave some record types explicitly unimplemented.
|
|
// These types relate to recursive resolution or special
|
|
// DNS semantics and might be implemented in the future.
|
|
case dns.TypeNS, dns.TypeSOA, dns.TypeAXFR, dns.TypeHINFO:
|
|
metricDNSResolveNotImplType.Add(1)
|
|
return netip.Addr{}, dns.RCodeNotImplemented
|
|
|
|
// For everything except for the few types above that are explicitly not implemented, return no records.
|
|
// This is what other DNS systems do: always return NOERROR
|
|
// without any records whenever the requested record type is unknown.
|
|
// You can try this with:
|
|
// dig -t TYPE9824 example.com
|
|
// and note that NOERROR is returned, despite that record type being made up.
|
|
default:
|
|
metricDNSResolveNoRecordType.Add(1)
|
|
// The name exists, but no records exist of the requested type.
|
|
return netip.Addr{}, dns.RCodeSuccess
|
|
}
|
|
}
|
|
|
|
// parseViaDomain synthesizes an IP address for quad-A DNS requests of the form
|
|
// `<IPv4-address-with-hypens-instead-of-dots>-via-<siteid>[.*]`. Two prior formats that
|
|
// didn't pan out (due to a Chrome issue and DNS search ndots issues) were
|
|
// `<IPv4-address>.via-<X>` and the older `via-<X>.<IPv4-address>`,
|
|
// where X is a decimal, or hex-encoded number with a '0x' prefix.
|
|
//
|
|
// This exists as a convenient mapping into Tailscales 'Via Range'.
|
|
//
|
|
// TODO(maisem/bradfitz/tom): `<IPv4-address>.via-<X>` was introduced
|
|
// (2022-06-02) to work around an issue in Chrome where it would treat
|
|
// "http://via-1.1.2.3.4" as a search string instead of a URL. We should rip out
|
|
// the old format in early 2023.
|
|
func (r *Resolver) parseViaDomain(domain dnsname.FQDN, typ dns.Type) (netip.Addr, bool) {
|
|
fqdn := string(domain.WithoutTrailingDot())
|
|
if typ != dns.TypeAAAA {
|
|
return netip.Addr{}, false
|
|
}
|
|
if len(fqdn) < len("via-X.0.0.0.0") {
|
|
return netip.Addr{}, false // too short to be valid
|
|
}
|
|
|
|
var siteID string
|
|
var ip4Str string
|
|
switch {
|
|
case strings.Contains(fqdn, "-via-"):
|
|
// Format number 3: "192-168-1-2-via-7" or "192-168-1-2-via-7.foo.ts.net."
|
|
// Third time's a charm. The earlier two formats follow after this block.
|
|
firstLabel, domain, _ := strings.Cut(fqdn, ".") // "192-168-1-2-via-7"
|
|
if !(domain == "" || dnsname.HasSuffix(domain, "ts.net") || dnsname.HasSuffix(domain, "tailscale.net")) {
|
|
return netip.Addr{}, false
|
|
}
|
|
v4hyphens, suffix, ok := strings.Cut(firstLabel, "-via-")
|
|
if !ok {
|
|
return netip.Addr{}, false
|
|
}
|
|
siteID = suffix
|
|
ip4Str = strings.ReplaceAll(v4hyphens, "-", ".")
|
|
case strings.HasPrefix(fqdn, "via-"):
|
|
firstDot := strings.Index(fqdn, ".")
|
|
if firstDot < 0 {
|
|
return netip.Addr{}, false // missing dot delimiters
|
|
}
|
|
siteID = fqdn[len("via-"):firstDot]
|
|
ip4Str = fqdn[firstDot+1:]
|
|
default:
|
|
lastDot := strings.LastIndex(fqdn, ".")
|
|
if lastDot < 0 {
|
|
return netip.Addr{}, false // missing dot delimiters
|
|
}
|
|
suffix := fqdn[lastDot+1:]
|
|
if !strings.HasPrefix(suffix, "via-") {
|
|
return netip.Addr{}, false
|
|
}
|
|
siteID = suffix[len("via-"):]
|
|
ip4Str = fqdn[:lastDot]
|
|
}
|
|
|
|
ip4, err := netip.ParseAddr(ip4Str)
|
|
if err != nil {
|
|
return netip.Addr{}, false // badly formed, don't respond
|
|
}
|
|
|
|
prefix, err := strconv.ParseUint(siteID, 0, 32)
|
|
if err != nil {
|
|
return netip.Addr{}, false // badly formed, don't respond
|
|
}
|
|
|
|
// MapVia will never error when given an IPv4 netip.Prefix.
|
|
out, _ := tsaddr.MapVia(uint32(prefix), netip.PrefixFrom(ip4, ip4.BitLen()))
|
|
return out.Addr(), true
|
|
}
|
|
|
|
// resolveReverse returns the unique domain name that maps to the given address.
|
|
func (r *Resolver) resolveLocalReverse(name dnsname.FQDN) (dnsname.FQDN, dns.RCode) {
|
|
var ip netip.Addr
|
|
var ok bool
|
|
switch {
|
|
case strings.HasSuffix(name.WithTrailingDot(), rdnsv4Suffix):
|
|
ip, ok = rdnsNameToIPv4(name)
|
|
case strings.HasSuffix(name.WithTrailingDot(), rdnsv6Suffix):
|
|
ip, ok = rdnsNameToIPv6(name)
|
|
}
|
|
if !ok {
|
|
// This isn't a well-formed in-addr.arpa or ip6.arpa name, but
|
|
// who knows what upstreams might do, try kicking it up to
|
|
// them. We definitely won't handle it.
|
|
return "", dns.RCodeRefused
|
|
}
|
|
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
// If the requested IP is part of the IPv6 4-to-6 range, it might
|
|
// correspond to an IPv4 address (assuming IPv4 is enabled).
|
|
if ip4, ok := tsaddr.Tailscale6to4(ip); ok {
|
|
fqdn, code := r.fqdnForIPLocked(ip4, name)
|
|
if code == dns.RCodeSuccess {
|
|
return fqdn, code
|
|
}
|
|
}
|
|
return r.fqdnForIPLocked(ip, name)
|
|
}
|
|
|
|
// r.mu must be held.
|
|
func (r *Resolver) fqdnForIPLocked(ip netip.Addr, name dnsname.FQDN) (dnsname.FQDN, dns.RCode) {
|
|
// If someone curiously does a reverse lookup on the DNS IP, we
|
|
// return a domain that helps indicate that Tailscale is using
|
|
// this IP for a special purpose and it is not a node on their
|
|
// tailnet.
|
|
if ip == tsaddr.TailscaleServiceIP() || ip == tsaddr.TailscaleServiceIPv6() {
|
|
return dnsSymbolicFQDN, dns.RCodeSuccess
|
|
}
|
|
|
|
ret, ok := r.ipToHost[ip]
|
|
if !ok {
|
|
for _, suffix := range r.localDomains {
|
|
if suffix.Contains(name) {
|
|
// We are authoritative for this chunk of IP space.
|
|
return "", dns.RCodeNameError
|
|
}
|
|
}
|
|
// Not authoritative, signal that forwarding is advisable.
|
|
return "", dns.RCodeRefused
|
|
}
|
|
return ret, dns.RCodeSuccess
|
|
}
|
|
|
|
type response struct {
|
|
Header dns.Header
|
|
Question dns.Question
|
|
|
|
// Name is the response to a PTR query.
|
|
Name dnsname.FQDN
|
|
|
|
// IP and IPs are the responses to an A, AAAA, or ALL query.
|
|
// Either/both/neither can be populated.
|
|
IP netip.Addr
|
|
IPs []netip.Addr
|
|
|
|
// TXT is the response to a TXT query.
|
|
// Each one is its own RR with one string.
|
|
TXT []string
|
|
|
|
// CNAME is the response to a CNAME query.
|
|
CNAME string
|
|
|
|
// SRVs are the responses to a SRV query.
|
|
SRVs []*net.SRV
|
|
|
|
// NSs are the responses to an NS query.
|
|
NSs []*net.NS
|
|
}
|
|
|
|
var dnsParserPool = &sync.Pool{
|
|
New: func() any {
|
|
return new(dnsParser)
|
|
},
|
|
}
|
|
|
|
// dnsParser parses DNS queries using x/net/dns/dnsmessage.
|
|
// These structs are pooled with dnsParserPool.
|
|
type dnsParser struct {
|
|
Header dns.Header
|
|
Question dns.Question
|
|
|
|
parser dns.Parser
|
|
}
|
|
|
|
func (p *dnsParser) response() *response {
|
|
return &response{Header: p.Header, Question: p.Question}
|
|
}
|
|
|
|
// zeroParser clears parser so it doesn't retain its most recently
|
|
// parsed DNS query's []byte while it's sitting in a sync.Pool.
|
|
// It's not useful to keep anyway: the next Start will do the same.
|
|
func (p *dnsParser) zeroParser() { p.parser = dns.Parser{} }
|
|
|
|
// parseQuery parses the query in given packet into p.Header and
|
|
// p.Question.
|
|
func (p *dnsParser) parseQuery(query []byte) error {
|
|
defer p.zeroParser()
|
|
p.zeroParser()
|
|
var err error
|
|
p.Header, err = p.parser.Start(query)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if p.Header.Response {
|
|
return errNotQuery
|
|
}
|
|
p.Question, err = p.parser.Question()
|
|
return err
|
|
}
|
|
|
|
// marshalARecord serializes an A record into an active builder.
|
|
// The caller may continue using the builder following the call.
|
|
func marshalARecord(name dns.Name, ip netip.Addr, builder *dns.Builder) error {
|
|
var answer dns.AResource
|
|
|
|
answerHeader := dns.ResourceHeader{
|
|
Name: name,
|
|
Type: dns.TypeA,
|
|
Class: dns.ClassINET,
|
|
TTL: uint32(defaultTTL / time.Second),
|
|
}
|
|
ipbytes := ip.As4()
|
|
copy(answer.A[:], ipbytes[:])
|
|
return builder.AResource(answerHeader, answer)
|
|
}
|
|
|
|
// marshalAAAARecord serializes an AAAA record into an active builder.
|
|
// The caller may continue using the builder following the call.
|
|
func marshalAAAARecord(name dns.Name, ip netip.Addr, builder *dns.Builder) error {
|
|
var answer dns.AAAAResource
|
|
|
|
answerHeader := dns.ResourceHeader{
|
|
Name: name,
|
|
Type: dns.TypeAAAA,
|
|
Class: dns.ClassINET,
|
|
TTL: uint32(defaultTTL / time.Second),
|
|
}
|
|
ipbytes := ip.As16()
|
|
copy(answer.AAAA[:], ipbytes[:])
|
|
return builder.AAAAResource(answerHeader, answer)
|
|
}
|
|
|
|
func marshalIP(name dns.Name, ip netip.Addr, builder *dns.Builder) error {
|
|
if ip.Is4() {
|
|
return marshalARecord(name, ip, builder)
|
|
}
|
|
if ip.Is6() {
|
|
return marshalAAAARecord(name, ip, builder)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// marshalPTRRecord serializes a PTR record into an active builder.
|
|
// The caller may continue using the builder following the call.
|
|
func marshalPTRRecord(queryName dns.Name, name dnsname.FQDN, builder *dns.Builder) error {
|
|
var answer dns.PTRResource
|
|
var err error
|
|
|
|
answerHeader := dns.ResourceHeader{
|
|
Name: queryName,
|
|
Type: dns.TypePTR,
|
|
Class: dns.ClassINET,
|
|
TTL: uint32(defaultTTL / time.Second),
|
|
}
|
|
answer.PTR, err = dns.NewName(name.WithTrailingDot())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return builder.PTRResource(answerHeader, answer)
|
|
}
|
|
|
|
func marshalTXT(queryName dns.Name, txts []string, builder *dns.Builder) error {
|
|
for _, txt := range txts {
|
|
if err := builder.TXTResource(dns.ResourceHeader{
|
|
Name: queryName,
|
|
Type: dns.TypeTXT,
|
|
Class: dns.ClassINET,
|
|
TTL: uint32(defaultTTL / time.Second),
|
|
}, dns.TXTResource{
|
|
TXT: []string{txt},
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func marshalCNAME(queryName dns.Name, cname string, builder *dns.Builder) error {
|
|
if cname == "" {
|
|
return nil
|
|
}
|
|
name, err := dns.NewName(cname)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return builder.CNAMEResource(dns.ResourceHeader{
|
|
Name: queryName,
|
|
Type: dns.TypeCNAME,
|
|
Class: dns.ClassINET,
|
|
TTL: uint32(defaultTTL / time.Second),
|
|
}, dns.CNAMEResource{
|
|
CNAME: name,
|
|
})
|
|
}
|
|
|
|
func marshalNS(queryName dns.Name, nss []*net.NS, builder *dns.Builder) error {
|
|
for _, ns := range nss {
|
|
name, err := dns.NewName(ns.Host)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = builder.NSResource(dns.ResourceHeader{
|
|
Name: queryName,
|
|
Type: dns.TypeNS,
|
|
Class: dns.ClassINET,
|
|
TTL: uint32(defaultTTL / time.Second),
|
|
}, dns.NSResource{NS: name})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func marshalSRV(queryName dns.Name, srvs []*net.SRV, builder *dns.Builder) error {
|
|
for _, s := range srvs {
|
|
srvName, err := dns.NewName(s.Target)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = builder.SRVResource(dns.ResourceHeader{
|
|
Name: queryName,
|
|
Type: dns.TypeSRV,
|
|
Class: dns.ClassINET,
|
|
TTL: uint32(defaultTTL / time.Second),
|
|
}, dns.SRVResource{
|
|
Target: srvName,
|
|
Priority: s.Priority,
|
|
Port: s.Port,
|
|
Weight: s.Weight,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// marshalResponse serializes the DNS response into a new buffer.
|
|
func marshalResponse(resp *response) ([]byte, error) {
|
|
resp.Header.Response = true
|
|
resp.Header.Authoritative = true
|
|
if resp.Header.RecursionDesired {
|
|
resp.Header.RecursionAvailable = true
|
|
}
|
|
|
|
builder := dns.NewBuilder(nil, resp.Header)
|
|
|
|
// TODO(bradfitz): I'm not sure why this wasn't enabled
|
|
// before, but for now (2021-12-09) enable it at least when
|
|
// there's more than 1 record (which was never the case
|
|
// before), where it really helps.
|
|
if len(resp.IPs) > 1 {
|
|
builder.EnableCompression()
|
|
}
|
|
|
|
isSuccess := resp.Header.RCode == dns.RCodeSuccess
|
|
|
|
if resp.Question.Type != 0 || isSuccess {
|
|
err := builder.StartQuestions()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = builder.Question(resp.Question)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Only successful responses contain answers.
|
|
if !isSuccess {
|
|
return builder.Finish()
|
|
}
|
|
|
|
err := builder.StartAnswers()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch resp.Question.Type {
|
|
case dns.TypeA, dns.TypeAAAA, dns.TypeALL:
|
|
if err := marshalIP(resp.Question.Name, resp.IP, &builder); err != nil {
|
|
return nil, err
|
|
}
|
|
for _, ip := range resp.IPs {
|
|
if err := marshalIP(resp.Question.Name, ip, &builder); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
case dns.TypePTR:
|
|
err = marshalPTRRecord(resp.Question.Name, resp.Name, &builder)
|
|
case dns.TypeTXT:
|
|
err = marshalTXT(resp.Question.Name, resp.TXT, &builder)
|
|
case dns.TypeCNAME:
|
|
err = marshalCNAME(resp.Question.Name, resp.CNAME, &builder)
|
|
case dns.TypeSRV:
|
|
err = marshalSRV(resp.Question.Name, resp.SRVs, &builder)
|
|
case dns.TypeNS:
|
|
err = marshalNS(resp.Question.Name, resp.NSs, &builder)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return builder.Finish()
|
|
}
|
|
|
|
const (
|
|
rdnsv4Suffix = ".in-addr.arpa."
|
|
rdnsv6Suffix = ".ip6.arpa."
|
|
)
|
|
|
|
// hasRDNSBonjourPrefix reports whether name has a Bonjour Service Prefix..
|
|
//
|
|
// https://tools.ietf.org/html/rfc6763 lists
|
|
// "five special RR names" for Bonjour service discovery:
|
|
//
|
|
// b._dns-sd._udp.<domain>.
|
|
// db._dns-sd._udp.<domain>.
|
|
// r._dns-sd._udp.<domain>.
|
|
// dr._dns-sd._udp.<domain>.
|
|
// lb._dns-sd._udp.<domain>.
|
|
func hasRDNSBonjourPrefix(name dnsname.FQDN) bool {
|
|
s := name.WithTrailingDot()
|
|
base, rest, ok := strings.Cut(s, ".")
|
|
if !ok {
|
|
return false // shouldn't happen
|
|
}
|
|
switch base {
|
|
case "b", "db", "r", "dr", "lb":
|
|
default:
|
|
return false
|
|
}
|
|
|
|
return strings.HasPrefix(rest, "_dns-sd._udp.")
|
|
}
|
|
|
|
// rawNameToLower converts a raw DNS name to a string, lowercasing it.
|
|
func rawNameToLower(name []byte) string {
|
|
var sb strings.Builder
|
|
sb.Grow(len(name))
|
|
|
|
for _, b := range name {
|
|
if 'A' <= b && b <= 'Z' {
|
|
b = b - 'A' + 'a'
|
|
}
|
|
sb.WriteByte(b)
|
|
}
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
// ptrNameToIPv4 transforms a PTR name representing an IPv4 address to said address.
|
|
// Such names are IPv4 labels in reverse order followed by .in-addr.arpa.
|
|
// For example,
|
|
//
|
|
// 4.3.2.1.in-addr.arpa
|
|
//
|
|
// is transformed to
|
|
//
|
|
// 1.2.3.4
|
|
func rdnsNameToIPv4(name dnsname.FQDN) (ip netip.Addr, ok bool) {
|
|
s := strings.TrimSuffix(name.WithTrailingDot(), rdnsv4Suffix)
|
|
ip, err := netip.ParseAddr(s)
|
|
if err != nil {
|
|
return netip.Addr{}, false
|
|
}
|
|
if !ip.Is4() {
|
|
return netip.Addr{}, false
|
|
}
|
|
b := ip.As4()
|
|
return netaddr.IPv4(b[3], b[2], b[1], b[0]), true
|
|
}
|
|
|
|
// ptrNameToIPv6 transforms a PTR name representing an IPv6 address to said address.
|
|
// Such names are dot-separated nibbles in reverse order followed by .ip6.arpa.
|
|
// For example,
|
|
//
|
|
// b.a.9.8.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.
|
|
//
|
|
// is transformed to
|
|
//
|
|
// 2001:db8::567:89ab
|
|
func rdnsNameToIPv6(name dnsname.FQDN) (ip netip.Addr, ok bool) {
|
|
var b [32]byte
|
|
var ipb [16]byte
|
|
|
|
s := strings.TrimSuffix(name.WithTrailingDot(), rdnsv6Suffix)
|
|
// 32 nibbles and 31 dots between them.
|
|
if len(s) != 63 {
|
|
return netip.Addr{}, false
|
|
}
|
|
|
|
// Dots and hex digits alternate.
|
|
prevDot := true
|
|
// i ranges over name backward; j ranges over b forward.
|
|
for i, j := len(s)-1, 0; i >= 0; i-- {
|
|
thisDot := (s[i] == '.')
|
|
if prevDot == thisDot {
|
|
return netip.Addr{}, false
|
|
}
|
|
prevDot = thisDot
|
|
|
|
if !thisDot {
|
|
// This is safe assuming alternation.
|
|
// We do not check that non-dots are hex digits: hex.Decode below will do that.
|
|
b[j] = s[i]
|
|
j++
|
|
}
|
|
}
|
|
|
|
_, err := hex.Decode(ipb[:], b[:])
|
|
if err != nil {
|
|
return netip.Addr{}, false
|
|
}
|
|
|
|
return netip.AddrFrom16(ipb), true
|
|
}
|
|
|
|
// respondReverse returns a DNS response to a PTR query.
|
|
// It is assumed that resp.Question is populated by respond before this is called.
|
|
func (r *Resolver) respondReverse(query []byte, name dnsname.FQDN, resp *response) ([]byte, error) {
|
|
if hasRDNSBonjourPrefix(name) {
|
|
metricDNSReverseMissBonjour.Add(1)
|
|
return nil, errNotOurName
|
|
}
|
|
|
|
resp.Name, resp.Header.RCode = r.resolveLocalReverse(name)
|
|
if resp.Header.RCode == dns.RCodeRefused {
|
|
metricDNSReverseMissOther.Add(1)
|
|
return nil, errNotOurName
|
|
}
|
|
|
|
metricDNSMagicDNSSuccessReverse.Add(1)
|
|
return marshalResponse(resp)
|
|
}
|
|
|
|
// respond returns a DNS response to query if it can be resolved locally.
|
|
// Otherwise, it returns errNotOurName.
|
|
func (r *Resolver) respond(query []byte) ([]byte, error) {
|
|
parser := dnsParserPool.Get().(*dnsParser)
|
|
defer dnsParserPool.Put(parser)
|
|
|
|
// ParseQuery is sufficiently fast to run on every DNS packet.
|
|
// This is considerably simpler than extracting the name by hand
|
|
// to shave off microseconds in case of delegation.
|
|
err := parser.parseQuery(query)
|
|
// We will not return this error: it is the sender's fault.
|
|
if err != nil {
|
|
if errors.Is(err, dns.ErrSectionDone) {
|
|
metricDNSErrorParseNoQ.Add(1)
|
|
r.logf("parseQuery(%02x): no DNS questions", query)
|
|
} else {
|
|
metricDNSErrorParseQuery.Add(1)
|
|
r.logf("parseQuery(%02x): %v", query, err)
|
|
}
|
|
resp := parser.response()
|
|
resp.Header.RCode = dns.RCodeFormatError
|
|
return marshalResponse(resp)
|
|
}
|
|
rawName := parser.Question.Name.Data[:parser.Question.Name.Length]
|
|
name, err := dnsname.ToFQDN(rawNameToLower(rawName))
|
|
if err != nil {
|
|
metricDNSErrorNotFQDN.Add(1)
|
|
// DNS packet unexpectedly contains an invalid FQDN.
|
|
resp := parser.response()
|
|
resp.Header.RCode = dns.RCodeFormatError
|
|
return marshalResponse(resp)
|
|
}
|
|
|
|
// Always try to handle reverse lookups; delegate inside when not found.
|
|
// This way, queries for existent nodes do not leak,
|
|
// but we behave gracefully if non-Tailscale nodes exist in CGNATRange.
|
|
if parser.Question.Type == dns.TypePTR {
|
|
return r.respondReverse(query, name, parser.response())
|
|
}
|
|
|
|
ip, rcode := r.resolveLocal(name, parser.Question.Type)
|
|
if rcode == dns.RCodeRefused {
|
|
return nil, errNotOurName // sentinel error return value: it requests forwarding
|
|
}
|
|
|
|
resp := parser.response()
|
|
resp.Header.RCode = rcode
|
|
resp.IP = ip
|
|
metricDNSMagicDNSSuccessName.Add(1)
|
|
return marshalResponse(resp)
|
|
}
|
|
|
|
// unARPA maps from "4.4.8.8.in-addr.arpa." to "8.8.4.4", etc.
|
|
func unARPA(a string) (ipStr string, ok bool) {
|
|
const suf4 = ".in-addr.arpa."
|
|
if s, ok := strings.CutSuffix(a, suf4); ok {
|
|
// Parse and reverse octets.
|
|
ip, err := netip.ParseAddr(s)
|
|
if err != nil || !ip.Is4() {
|
|
return "", false
|
|
}
|
|
a4 := ip.As4()
|
|
return netaddr.IPv4(a4[3], a4[2], a4[1], a4[0]).String(), true
|
|
}
|
|
const suf6 = ".ip6.arpa."
|
|
if len(a) == len("e.0.0.2.0.0.0.0.0.0.0.0.0.0.0.0.b.0.8.0.a.0.0.4.0.b.8.f.7.0.6.2.ip6.arpa.") &&
|
|
strings.HasSuffix(a, suf6) {
|
|
var hx [32]byte
|
|
var a16 [16]byte
|
|
for i := range hx {
|
|
hx[31-i] = a[i*2]
|
|
if a[i*2+1] != '.' {
|
|
return "", false
|
|
}
|
|
}
|
|
hex.Decode(a16[:], hx[:])
|
|
return netip.AddrFrom16(a16).Unmap().String(), true
|
|
}
|
|
return "", false
|
|
|
|
}
|
|
|
|
var (
|
|
metricDNSQueryLocal = clientmetric.NewCounter("dns_query_local")
|
|
metricDNSQueryErrorClosed = clientmetric.NewCounter("dns_query_local_error_closed")
|
|
|
|
metricDNSErrorParseNoQ = clientmetric.NewCounter("dns_query_respond_error_no_question")
|
|
metricDNSErrorParseQuery = clientmetric.NewCounter("dns_query_respond_error_parse")
|
|
metricDNSErrorNotFQDN = clientmetric.NewCounter("dns_query_respond_error_not_fqdn")
|
|
|
|
metricDNSMagicDNSSuccessName = clientmetric.NewCounter("dns_query_magic_success_name")
|
|
metricDNSMagicDNSSuccessReverse = clientmetric.NewCounter("dns_query_magic_success_reverse")
|
|
|
|
metricDNSExitProxyQuery = clientmetric.NewCounter("dns_exit_node_query")
|
|
metricDNSExitProxyErrorName = clientmetric.NewCounter("dns_exit_node_error_name")
|
|
metricDNSExitProxyErrorForward = clientmetric.NewCounter("dns_exit_node_error_forward")
|
|
metricDNSExitProxyErrorResolvConf = clientmetric.NewCounter("dns_exit_node_error_resolvconf")
|
|
|
|
metricDNSFwd = clientmetric.NewCounter("dns_query_fwd")
|
|
metricDNSFwdDropBonjour = clientmetric.NewCounter("dns_query_fwd_drop_bonjour")
|
|
metricDNSFwdErrorName = clientmetric.NewCounter("dns_query_fwd_error_name")
|
|
metricDNSFwdErrorNoUpstream = clientmetric.NewCounter("dns_query_fwd_error_no_upstream")
|
|
metricDNSFwdSuccess = clientmetric.NewCounter("dns_query_fwd_success")
|
|
metricDNSFwdErrorContext = clientmetric.NewCounter("dns_query_fwd_error_context")
|
|
metricDNSFwdErrorContextGotError = clientmetric.NewCounter("dns_query_fwd_error_context_got_error")
|
|
|
|
metricDNSFwdErrorType = clientmetric.NewCounter("dns_query_fwd_error_type")
|
|
metricDNSFwdTruncated = clientmetric.NewCounter("dns_query_fwd_truncated")
|
|
|
|
metricDNSFwdUDP = clientmetric.NewCounter("dns_query_fwd_udp") // on entry
|
|
metricDNSFwdUDPWrote = clientmetric.NewCounter("dns_query_fwd_udp_wrote") // sent UDP packet
|
|
metricDNSFwdUDPErrorWrite = clientmetric.NewCounter("dns_query_fwd_udp_error_write")
|
|
metricDNSFwdUDPErrorServer = clientmetric.NewCounter("dns_query_fwd_udp_error_server")
|
|
metricDNSFwdUDPErrorTxID = clientmetric.NewCounter("dns_query_fwd_udp_error_txid")
|
|
metricDNSFwdUDPErrorRead = clientmetric.NewCounter("dns_query_fwd_udp_error_read")
|
|
metricDNSFwdUDPSuccess = clientmetric.NewCounter("dns_query_fwd_udp_success")
|
|
|
|
metricDNSFwdTCP = clientmetric.NewCounter("dns_query_fwd_tcp") // on entry
|
|
metricDNSFwdTCPWrote = clientmetric.NewCounter("dns_query_fwd_tcp_wrote") // sent TCP packet
|
|
metricDNSFwdTCPErrorWrite = clientmetric.NewCounter("dns_query_fwd_tcp_error_write")
|
|
metricDNSFwdTCPErrorServer = clientmetric.NewCounter("dns_query_fwd_tcp_error_server")
|
|
metricDNSFwdTCPErrorTxID = clientmetric.NewCounter("dns_query_fwd_tcp_error_txid")
|
|
metricDNSFwdTCPErrorRead = clientmetric.NewCounter("dns_query_fwd_tcp_error_read")
|
|
metricDNSFwdTCPSuccess = clientmetric.NewCounter("dns_query_fwd_tcp_success")
|
|
|
|
metricDNSFwdDoH = clientmetric.NewCounter("dns_query_fwd_doh")
|
|
metricDNSFwdDoHErrorStatus = clientmetric.NewCounter("dns_query_fwd_doh_error_status")
|
|
metricDNSFwdDoHErrorCT = clientmetric.NewCounter("dns_query_fwd_doh_error_content_type")
|
|
metricDNSFwdDoHErrorTransport = clientmetric.NewCounter("dns_query_fwd_doh_error_transport")
|
|
metricDNSFwdDoHErrorBody = clientmetric.NewCounter("dns_query_fwd_doh_error_body")
|
|
|
|
metricDNSResolveLocal = clientmetric.NewCounter("dns_resolve_local")
|
|
metricDNSResolveLocalErrorOnion = clientmetric.NewCounter("dns_resolve_local_error_onion")
|
|
metricDNSResolveLocalErrorMissing = clientmetric.NewCounter("dns_resolve_local_error_missing")
|
|
metricDNSResolveLocalErrorRefused = clientmetric.NewCounter("dns_resolve_local_error_refused")
|
|
metricDNSResolveLocalOKA = clientmetric.NewCounter("dns_resolve_local_ok_a")
|
|
metricDNSResolveLocalOKAAAA = clientmetric.NewCounter("dns_resolve_local_ok_aaaa")
|
|
metricDNSResolveLocalOKAll = clientmetric.NewCounter("dns_resolve_local_ok_all")
|
|
metricDNSResolveLocalNoA = clientmetric.NewCounter("dns_resolve_local_no_a")
|
|
metricDNSResolveLocalNoAAAA = clientmetric.NewCounter("dns_resolve_local_no_aaaa")
|
|
metricDNSResolveLocalNoAll = clientmetric.NewCounter("dns_resolve_local_no_all")
|
|
metricDNSResolveNotImplType = clientmetric.NewCounter("dns_resolve_local_not_impl_type")
|
|
metricDNSResolveNoRecordType = clientmetric.NewCounter("dns_resolve_local_no_record_type")
|
|
|
|
metricDNSReverseMissBonjour = clientmetric.NewCounter("dns_reverse_miss_bonjour")
|
|
metricDNSReverseMissOther = clientmetric.NewCounter("dns_reverse_miss_other")
|
|
)
|