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

View File

@ -18,6 +18,7 @@
"strings" "strings"
"inet.af/netaddr" "inet.af/netaddr"
"tailscale.com/types/dnstype"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/util/dnsname" "tailscale.com/util/dnsname"
) )
@ -183,6 +184,24 @@ func (m *directManager) readResolvFile(path string) (OSConfig, error) {
return readResolv(bytes.NewReader(b)) 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 // ownedByTailscale reports whether /etc/resolv.conf seems to be a
// tailscale-managed file. // tailscale-managed file.
func (m *directManager) ownedByTailscale() (bool, error) { func (m *directManager) ownedByTailscale() (bool, error) {

View File

@ -62,6 +62,11 @@ func (m *Manager) Set(cfg Config) error {
if err != nil { if err != nil {
return err 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) { m.logf("Resolvercfg: %v", logger.ArgWriter(func(w *bufio.Writer) {
rcfg.WriteToBufioWriter(w) rcfg.WriteToBufioWriter(w)

View File

@ -45,6 +45,10 @@ func (c *fakeOSConfigurator) GetBaseConfig() (OSConfig, error) {
return c.BaseConfig, nil return c.BaseConfig, nil
} }
func (c *fakeOSConfigurator) GetExitNodeForwardResolver() ([]dnstype.Resolver, error) {
return getExitNodeForwardResolverFromBaseConfig(c)
}
func (c *fakeOSConfigurator) Close() error { return nil } func (c *fakeOSConfigurator) Close() error { return nil }
func TestManager(t *testing.T) { func TestManager(t *testing.T) {
@ -213,6 +217,7 @@ func TestManager(t *testing.T) {
Routes: upstreams( Routes: upstreams(
".", "8.8.8.8:53", ".", "8.8.8.8:53",
"corp.com.", "2.2.2.2: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", ".", "8.8.8.8:53",
"corp.com.", "2.2.2.2:53", "corp.com.", "2.2.2.2:53",
"bigco.net.", "3.3.3.3:53"), "bigco.net.", "3.3.3.3:53"),
ExitNodeBackupResolvers: []dnstype.Resolver{{Addr: "8.8.8.8:53"}},
}, },
}, },
{ {
@ -293,7 +299,8 @@ func TestManager(t *testing.T) {
Hosts: hosts( Hosts: hosts(
"dave.ts.com.", "1.2.3.4", "dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"), "bradfitz.ts.com.", "2.3.4.5"),
LocalDomains: fqdns("ts.com."), LocalDomains: fqdns("ts.com."),
ExitNodeBackupResolvers: []dnstype.Resolver{{Addr: "8.8.8.8:53"}},
}, },
}, },
{ {
@ -342,7 +349,8 @@ func TestManager(t *testing.T) {
Hosts: hosts( Hosts: hosts(
"dave.ts.com.", "1.2.3.4", "dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"), "bradfitz.ts.com.", "2.3.4.5"),
LocalDomains: fqdns("ts.com."), LocalDomains: fqdns("ts.com."),
ExitNodeBackupResolvers: []dnstype.Resolver{{Addr: "8.8.8.8:53"}},
}, },
}, },
{ {

View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,7 @@
"errors" "errors"
"inet.af/netaddr" "inet.af/netaddr"
"tailscale.com/types/dnstype"
"tailscale.com/util/dnsname" "tailscale.com/util/dnsname"
) )
@ -18,20 +19,35 @@ type OSConfigurator interface {
// configuration is removed. // configuration is removed.
// SetDNS must not be called after Close. // SetDNS must not be called after Close.
SetDNS(cfg OSConfig) error SetDNS(cfg OSConfig) error
// SupportsSplitDNS reports whether the configurator is capable of // SupportsSplitDNS reports whether the configurator is capable of
// installing a resolver only for specific DNS suffixes. If false, // installing a resolver only for specific DNS suffixes. If false,
// the configurator can only set a global resolver. // the configurator can only set a global resolver.
SupportsSplitDNS() bool SupportsSplitDNS() bool
// GetBaseConfig returns the OS's "base" configuration, i.e. the // GetBaseConfig returns the OS's "base" configuration, i.e. the
// resolver settings the OS would use without Tailscale // resolver settings the OS would use without Tailscale
// contributing any configuration. // contributing any configuration.
// GetBaseConfig must return the tailscale-free base config even // GetBaseConfig must return the tailscale-free base config even
// after SetDNS has been called to set a Tailscale configuration. // after SetDNS has been called to set a Tailscale configuration.
// Only works when SupportsSplitDNS=false. // Only works when SupportsSplitDNS=false.
//
// Implementations that don't support getting the base config must // Implementations that don't support getting the base config must
// return ErrGetBaseConfigNotSupported. // return ErrGetBaseConfigNotSupported.
GetBaseConfig() (OSConfig, error) 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 removes Tailscale-related DNS configuration from the OS.
Close() error Close() error
} }
@ -90,3 +106,16 @@ func (a OSConfig) Equal(b OSConfig) bool {
// OSConfigurator.GetBaseConfig returns when the OSConfigurator // OSConfigurator.GetBaseConfig returns when the OSConfigurator
// doesn't support reading the underlying configuration out of the OS. // doesn't support reading the underlying configuration out of the OS.
var ErrGetBaseConfigNotSupported = errors.New("getting OS base config is not supported") 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 @@
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"inet.af/netaddr" "inet.af/netaddr"
"tailscale.com/health" "tailscale.com/health"
"tailscale.com/types/dnstype"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/util/dnsname" "tailscale.com/util/dnsname"
) )
@ -332,6 +333,10 @@ func (m *resolvedManager) GetBaseConfig() (OSConfig, error) {
return OSConfig{}, ErrGetBaseConfigNotSupported 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 { func (m *resolvedManager) Close() error {
m.cancelSyncer() m.cancelSyncer()

View File

@ -83,6 +83,14 @@ type Config struct {
// LocalDomains is a list of DNS name suffixes that should not be // LocalDomains is a list of DNS name suffixes that should not be
// routed to upstream resolvers. // routed to upstream resolvers.
LocalDomains []dnsname.FQDN 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 // WriteToBufioWriter write a debug version of c for logs to w, omitting
@ -202,10 +210,11 @@ type Resolver struct {
wg sync.WaitGroup wg sync.WaitGroup
// mu guards the following fields from being updated while used. // mu guards the following fields from being updated while used.
mu sync.Mutex mu sync.Mutex
localDomains []dnsname.FQDN localDomains []dnsname.FQDN
hostToIP map[dnsname.FQDN][]netaddr.IP hostToIP map[dnsname.FQDN][]netaddr.IP
ipToHost map[netaddr.IP]dnsname.FQDN ipToHost map[netaddr.IP]dnsname.FQDN
exitNodeBackupResolvers []dnstype.Resolver
} }
type ForwardLinkSelector interface { type ForwardLinkSelector interface {
@ -253,9 +262,16 @@ func (r *Resolver) SetConfig(cfg Config) error {
r.localDomains = cfg.LocalDomains r.localDomains = cfg.LocalDomains
r.hostToIP = cfg.Hosts r.hostToIP = cfg.Hosts
r.ipToHost = reverse r.ipToHost = reverse
r.exitNodeBackupResolvers = append([]dnstype.Resolver(nil), cfg.ExitNodeBackupResolvers...)
return nil 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. // Close shuts down the resolver and ensures poll goroutines have exited.
// The Resolver cannot be used again after Close is called. // The Resolver cannot be used again after Close is called.
func (r *Resolver) Close() { 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) err = r.forwarder.forwardWithDestChan(ctx, packet{q, from}, ch)
if err == errNoUpstreams { if err == errNoUpstreams {
// Handle to the system resolver. backup := r.exitNodeForwardResolvers()
switch runtime.GOOS { if len(backup) > 0 {
case "linux": var extra []resolverAndDelay
// Assume for now that we don't have an upstream because for _, v := range backup {
// they're using systemd-resolved and we're in Split DNS mode extra = append(extra, resolverAndDelay{name: v})
// where we don't know the base config. }
// err = r.forwarder.forwardWithDestChan(ctx, packet{q, from}, ch, extra...)
// 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
} }
} }
if err != nil { if err != nil {