net/dns: make exit node DNS ask OSConfigurator for backup resolvers

Updates #1713

Change-Id: I7be9dab2b2c03749b4c2d99f9f45c11422ac915a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2021-11-24 14:42:33 -08:00
parent c2efe46f72
commit 4c7ee0c9f9
11 changed files with 124 additions and 35 deletions

View File

@ -18,6 +18,7 @@ import (
"path/filepath"
"tailscale.com/atomicfile"
"tailscale.com/types/dnstype"
"tailscale.com/types/logger"
)
@ -173,6 +174,10 @@ func (m *resolvconfManager) GetBaseConfig() (OSConfig, error) {
return readResolv(&conf)
}
func (m *resolvconfManager) GetExitNodeForwardResolver() ([]dnstype.Resolver, error) {
return getExitNodeForwardResolverFromBaseConfig(m)
}
func (m *resolvconfManager) Close() error {
if err := m.deleteTailscaleConfig(); err != nil {
return err

View File

@ -18,6 +18,7 @@ import (
"strings"
"inet.af/netaddr"
"tailscale.com/types/dnstype"
"tailscale.com/types/logger"
"tailscale.com/util/dnsname"
)
@ -183,6 +184,24 @@ func (m *directManager) readResolvFile(path string) (OSConfig, error) {
return readResolv(bytes.NewReader(b))
}
func (m *directManager) GetExitNodeForwardResolver() (ret []dnstype.Resolver, retErr error) {
for _, filename := range []string{backupConf, resolvConf} {
if oc, err := m.readResolvFile(filename); err == nil {
for _, ip := range oc.Nameservers {
if ip != netaddr.IPv4(100, 100, 100, 100) {
ret = append(ret, dnstype.Resolver{Addr: netaddr.IPPortFrom(ip, 53).String()})
}
}
if len(ret) > 0 {
return ret, nil
}
} else if !os.IsNotExist(err) && retErr == nil {
retErr = err
}
}
return nil, retErr
}
// ownedByTailscale reports whether /etc/resolv.conf seems to be a
// tailscale-managed file.
func (m *directManager) ownedByTailscale() (bool, error) {

View File

@ -62,6 +62,11 @@ func (m *Manager) Set(cfg Config) error {
if err != nil {
return err
}
exitNodeBackupResolvers, err := m.os.GetExitNodeForwardResolver()
if err != nil {
return err
}
rcfg.ExitNodeBackupResolvers = exitNodeBackupResolvers
m.logf("Resolvercfg: %v", logger.ArgWriter(func(w *bufio.Writer) {
rcfg.WriteToBufioWriter(w)

View File

@ -45,6 +45,10 @@ func (c *fakeOSConfigurator) GetBaseConfig() (OSConfig, error) {
return c.BaseConfig, nil
}
func (c *fakeOSConfigurator) GetExitNodeForwardResolver() ([]dnstype.Resolver, error) {
return getExitNodeForwardResolverFromBaseConfig(c)
}
func (c *fakeOSConfigurator) Close() error { return nil }
func TestManager(t *testing.T) {
@ -213,6 +217,7 @@ func TestManager(t *testing.T) {
Routes: upstreams(
".", "8.8.8.8:53",
"corp.com.", "2.2.2.2:53"),
ExitNodeBackupResolvers: []dnstype.Resolver{{Addr: "8.8.8.8:53"}},
},
},
{
@ -249,6 +254,7 @@ func TestManager(t *testing.T) {
".", "8.8.8.8:53",
"corp.com.", "2.2.2.2:53",
"bigco.net.", "3.3.3.3:53"),
ExitNodeBackupResolvers: []dnstype.Resolver{{Addr: "8.8.8.8:53"}},
},
},
{
@ -294,6 +300,7 @@ func TestManager(t *testing.T) {
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
LocalDomains: fqdns("ts.com."),
ExitNodeBackupResolvers: []dnstype.Resolver{{Addr: "8.8.8.8:53"}},
},
},
{
@ -343,6 +350,7 @@ func TestManager(t *testing.T) {
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
LocalDomains: fqdns("ts.com."),
ExitNodeBackupResolvers: []dnstype.Resolver{{Addr: "8.8.8.8:53"}},
},
},
{

View File

@ -17,6 +17,7 @@ import (
"golang.org/x/sys/windows/registry"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"inet.af/netaddr"
"tailscale.com/types/dnstype"
"tailscale.com/types/logger"
"tailscale.com/util/dnsname"
)
@ -340,6 +341,10 @@ func (m windowsManager) GetBaseConfig() (OSConfig, error) {
}, nil
}
func (m windowsManager) GetExitNodeForwardResolver() ([]dnstype.Resolver, error) {
return getExitNodeForwardResolverFromBaseConfig(m)
}
// getBasePrimaryResolver returns a guess of the non-Tailscale primary
// resolver on the system.
// It's used on Windows 7 to emulate split DNS by trying to figure out

View File

@ -16,6 +16,7 @@ import (
"github.com/godbus/dbus/v5"
"inet.af/netaddr"
"tailscale.com/net/interfaces"
"tailscale.com/types/dnstype"
"tailscale.com/util/dnsname"
"tailscale.com/util/endian"
)
@ -374,6 +375,10 @@ func (m *nmManager) GetBaseConfig() (OSConfig, error) {
return ret, nil
}
func (m *nmManager) GetExitNodeForwardResolver() ([]dnstype.Resolver, error) {
return getExitNodeForwardResolverFromBaseConfig(m)
}
func (m *nmManager) Close() error {
// No need to do anything on close, NetworkManager will delete our
// settings when the tailscale interface goes away.

View File

@ -4,14 +4,21 @@
package dns
import "tailscale.com/types/dnstype"
type noopManager struct{}
var _ OSConfigurator = noopManager{}
func (m noopManager) SetDNS(OSConfig) error { return nil }
func (m noopManager) SupportsSplitDNS() bool { return false }
func (m noopManager) Close() error { return nil }
func (m noopManager) GetBaseConfig() (OSConfig, error) {
return OSConfig{}, ErrGetBaseConfigNotSupported
}
func (m noopManager) GetExitNodeForwardResolver() ([]dnstype.Resolver, error) {
return nil, nil
}
func NewNoopManager() (noopManager, error) {
return noopManager{}, nil

View File

@ -12,6 +12,8 @@ import (
"fmt"
"os/exec"
"strings"
"tailscale.com/types/dnstype"
)
// openresolvManager manages DNS configuration using the openresolv
@ -90,6 +92,10 @@ func (m openresolvManager) GetBaseConfig() (OSConfig, error) {
return readResolv(&buf)
}
func (m openresolvManager) GetExitNodeForwardResolver() ([]dnstype.Resolver, error) {
return getExitNodeForwardResolverFromBaseConfig(m)
}
func (m openresolvManager) Close() error {
return m.deleteTailscaleConfig()
}

View File

@ -8,6 +8,7 @@ import (
"errors"
"inet.af/netaddr"
"tailscale.com/types/dnstype"
"tailscale.com/util/dnsname"
)
@ -18,20 +19,35 @@ type OSConfigurator interface {
// configuration is removed.
// SetDNS must not be called after Close.
SetDNS(cfg OSConfig) error
// SupportsSplitDNS reports whether the configurator is capable of
// installing a resolver only for specific DNS suffixes. If false,
// the configurator can only set a global resolver.
SupportsSplitDNS() bool
// GetBaseConfig returns the OS's "base" configuration, i.e. the
// resolver settings the OS would use without Tailscale
// contributing any configuration.
// GetBaseConfig must return the tailscale-free base config even
// after SetDNS has been called to set a Tailscale configuration.
// Only works when SupportsSplitDNS=false.
//
// Implementations that don't support getting the base config must
// return ErrGetBaseConfigNotSupported.
GetBaseConfig() (OSConfig, error)
// GetExitNodeForwardResolver returns the resolver(s) that should
// be used as a fallback for the exit node's DNS-over-HTTP peerapi
// to send DNS queries from peers on to, in the case where the tailnet
// doesn't have global DNS servers configured.
//
// For example, on Linux with systemd-resolved, this will
// return 127.0.0.53:53.
//
// On other systems, it'll usually be the value of
// GetBaseConfig.Nameservers.
GetExitNodeForwardResolver() ([]dnstype.Resolver, error)
// Close removes Tailscale-related DNS configuration from the OS.
Close() error
}
@ -90,3 +106,16 @@ func (a OSConfig) Equal(b OSConfig) bool {
// OSConfigurator.GetBaseConfig returns when the OSConfigurator
// doesn't support reading the underlying configuration out of the OS.
var ErrGetBaseConfigNotSupported = errors.New("getting OS base config is not supported")
func getExitNodeForwardResolverFromBaseConfig(o OSConfigurator) (ret []dnstype.Resolver, retErr error) {
oc, err := o.GetBaseConfig()
if err != nil {
return nil, err
}
for _, ip := range oc.Nameservers {
if ip != netaddr.IPv4(100, 100, 100, 100) {
ret = append(ret, dnstype.Resolver{Addr: netaddr.IPPortFrom(ip, 53).String()})
}
}
return ret, nil
}

View File

@ -19,6 +19,7 @@ import (
"golang.org/x/sys/unix"
"inet.af/netaddr"
"tailscale.com/health"
"tailscale.com/types/dnstype"
"tailscale.com/types/logger"
"tailscale.com/util/dnsname"
)
@ -332,6 +333,10 @@ func (m *resolvedManager) GetBaseConfig() (OSConfig, error) {
return OSConfig{}, ErrGetBaseConfigNotSupported
}
func (m *resolvedManager) GetExitNodeForwardResolver() ([]dnstype.Resolver, error) {
return []dnstype.Resolver{{Addr: "127.0.0.53:53"}}, nil
}
func (m *resolvedManager) Close() error {
m.cancelSyncer()

View File

@ -83,6 +83,14 @@ type Config struct {
// LocalDomains is a list of DNS name suffixes that should not be
// routed to upstream resolvers.
LocalDomains []dnsname.FQDN
// ExitNodeBackupResolvers are where the local node when
// acting as an exit node and serving a DNS proxy should
// forward DNS requests to in the case where there are no
// routes found. For example, for Linux systemd-resolved
// machines this is likely 127.0.0.53:53.
// If it's empty, there are no backups and the OS should
// be queried directly using its OS-level DNS APIs.
ExitNodeBackupResolvers []dnstype.Resolver
}
// WriteToBufioWriter write a debug version of c for logs to w, omitting
@ -206,6 +214,7 @@ type Resolver struct {
localDomains []dnsname.FQDN
hostToIP map[dnsname.FQDN][]netaddr.IP
ipToHost map[netaddr.IP]dnsname.FQDN
exitNodeBackupResolvers []dnstype.Resolver
}
type ForwardLinkSelector interface {
@ -253,9 +262,16 @@ func (r *Resolver) SetConfig(cfg Config) error {
r.localDomains = cfg.LocalDomains
r.hostToIP = cfg.Hosts
r.ipToHost = reverse
r.exitNodeBackupResolvers = append([]dnstype.Resolver(nil), cfg.ExitNodeBackupResolvers...)
return nil
}
func (r *Resolver) exitNodeForwardResolvers() []dnstype.Resolver {
r.mu.Lock()
defer r.mu.Unlock()
return r.exitNodeBackupResolvers
}
// 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() {
@ -312,34 +328,13 @@ func (r *Resolver) HandleExitNodeDNSQuery(ctx context.Context, q []byte, from ne
err = r.forwarder.forwardWithDestChan(ctx, packet{q, from}, ch)
if err == errNoUpstreams {
// Handle to the system resolver.
switch runtime.GOOS {
case "linux":
// Assume for now that we don't have an upstream because
// they're using systemd-resolved and we're in Split DNS mode
// where we don't know the base config.
//
// TODO(bradfitz): this is a lazy assumption. Do better, and
// maybe move the HandleExitNodeDNSQuery method to the dns.Manager
// instead? But this works for now.
err = r.forwarder.forwardWithDestChan(ctx, packet{q, from}, ch, resolverAndDelay{
name: dnstype.Resolver{
Addr: "127.0.0.1:53",
},
})
default:
// TODO(bradfitz): if we're on an exit node
// on, say, Windows, we need to parse the DNS
// packet in q and call OS-native APIs for
// each question. But we'll want to strip out
// questions for MagicDNS names probably, so
// they don't loop back into
// 100.100.100.100. We don't want to resolve
// MagicDNS names across Tailnets once we
// permit sharing exit nodes.
//
// For now, just return an error.
return nil, err
backup := r.exitNodeForwardResolvers()
if len(backup) > 0 {
var extra []resolverAndDelay
for _, v := range backup {
extra = append(extra, resolverAndDelay{name: v})
}
err = r.forwarder.forwardWithDestChan(ctx, packet{q, from}, ch, extra...)
}
}
if err != nil {