diff --git a/net/dns/debian_resolvconf.go b/net/dns/debian_resolvconf.go index ab9a7cf79..8e170f6bd 100644 --- a/net/dns/debian_resolvconf.go +++ b/net/dns/debian_resolvconf.go @@ -18,6 +18,7 @@ "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 diff --git a/net/dns/direct.go b/net/dns/direct.go index 388eda68f..22b85436d 100644 --- a/net/dns/direct.go +++ b/net/dns/direct.go @@ -18,6 +18,7 @@ "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) { diff --git a/net/dns/manager.go b/net/dns/manager.go index 6fd6d8f3a..928d20a45 100644 --- a/net/dns/manager.go +++ b/net/dns/manager.go @@ -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) diff --git a/net/dns/manager_test.go b/net/dns/manager_test.go index 05ec7c06f..a42da2919 100644 --- a/net/dns/manager_test.go +++ b/net/dns/manager_test.go @@ -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"}}, }, }, { @@ -293,7 +299,8 @@ func TestManager(t *testing.T) { Hosts: hosts( "dave.ts.com.", "1.2.3.4", "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( "dave.ts.com.", "1.2.3.4", "bradfitz.ts.com.", "2.3.4.5"), - LocalDomains: fqdns("ts.com."), + LocalDomains: fqdns("ts.com."), + ExitNodeBackupResolvers: []dnstype.Resolver{{Addr: "8.8.8.8:53"}}, }, }, { diff --git a/net/dns/manager_windows.go b/net/dns/manager_windows.go index f6eb7b3ff..cea204ed0 100644 --- a/net/dns/manager_windows.go +++ b/net/dns/manager_windows.go @@ -17,6 +17,7 @@ "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 diff --git a/net/dns/nm.go b/net/dns/nm.go index 5733098d2..e5386adc8 100644 --- a/net/dns/nm.go +++ b/net/dns/nm.go @@ -16,6 +16,7 @@ "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 @@ type dnsPrio struct { 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. diff --git a/net/dns/noop.go b/net/dns/noop.go index f08ecc036..23fc49399 100644 --- a/net/dns/noop.go +++ b/net/dns/noop.go @@ -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 diff --git a/net/dns/openresolv.go b/net/dns/openresolv.go index 13b72438f..34f2da5b3 100644 --- a/net/dns/openresolv.go +++ b/net/dns/openresolv.go @@ -12,6 +12,8 @@ "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() } diff --git a/net/dns/osconfig.go b/net/dns/osconfig.go index 866760a8e..e40a7e8e2 100644 --- a/net/dns/osconfig.go +++ b/net/dns/osconfig.go @@ -8,6 +8,7 @@ "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 +} diff --git a/net/dns/resolved.go b/net/dns/resolved.go index 9cd859e79..024717203 100644 --- a/net/dns/resolved.go +++ b/net/dns/resolved.go @@ -19,6 +19,7 @@ "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() diff --git a/net/dns/resolver/tsdns.go b/net/dns/resolver/tsdns.go index ee67995e4..a5b60dbbd 100644 --- a/net/dns/resolver/tsdns.go +++ b/net/dns/resolver/tsdns.go @@ -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 @@ -202,10 +210,11 @@ type Resolver struct { wg sync.WaitGroup // mu guards the following fields from being updated while used. - mu sync.Mutex - localDomains []dnsname.FQDN - hostToIP map[dnsname.FQDN][]netaddr.IP - ipToHost map[netaddr.IP]dnsname.FQDN + mu sync.Mutex + 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 {