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:
parent
c2efe46f72
commit
4c7ee0c9f9
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue