From da4cc8bbb49a5c56cb8a8c7a80716f1236d00632 Mon Sep 17 00:00:00 2001 From: David Anderson Date: Tue, 6 Apr 2021 22:00:59 -0700 Subject: [PATCH] net/dns: handle all possible translations of high-level DNS config. With this change, all OSes can sort-of do split DNS, except that the default upstream is hardcoded to 8.8.8.8 pending further plumbing. Additionally, Windows 8-10 can do split DNS fully correctly, without the 8.8.8.8 hack. Part of #953. Signed-off-by: David Anderson --- ipn/ipnlocal/local.go | 3 +- net/dns/config.go | 80 +++++++++++++++++++++ net/dns/manager.go | 161 ++++++++++++++++++++++++++++++++++-------- 3 files changed, 213 insertions(+), 31 deletions(-) diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index cc6122132..36bb1ec59 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -1618,10 +1618,9 @@ func (b *LocalBackend) initPeerAPIListener() { } // magicDNSRootDomains returns the subset of nm.DNS.Domains that are the search domains for MagicDNS. -// Each entry has a trailing period. func magicDNSRootDomains(nm *netmap.NetworkMap) []string { if v := nm.MagicDNSSuffix(); v != "" { - return []string{strings.Trim(v, ".") + "."} + return []string{strings.Trim(v, ".")} } return nil } diff --git a/net/dns/config.go b/net/dns/config.go index 527e74803..af3a01d7e 100644 --- a/net/dns/config.go +++ b/net/dns/config.go @@ -35,3 +35,83 @@ type Config struct { // return NXDOMAIN. AuthoritativeSuffixes []string } + +// needsAnyResolvers reports whether c requires a resolver to be set +// at the OS level. +func (c Config) needsOSResolver() bool { + return c.hasDefaultResolvers() || c.hasRoutes() || c.hasHosts() +} + +func (c Config) hasRoutes() bool { + return len(c.Routes) > 0 +} + +// hasDefaultResolversOnly reports whether the only resolvers in c are +// DefaultResolvers. +func (c Config) hasDefaultResolversOnly() bool { + return c.hasDefaultResolvers() && !c.hasRoutes() && !c.hasHosts() +} + +func (c Config) hasDefaultResolvers() bool { + return len(c.DefaultResolvers) > 0 +} + +// singleResolverSet returns the resolvers used by c.Routes if all +// routes use the same resolvers, or nil if multiple sets of resolvers +// are specified. +func (c Config) singleResolverSet() []netaddr.IPPort { + var first []netaddr.IPPort + for _, resolvers := range c.Routes { + if first == nil { + first = resolvers + continue + } + if !sameIPPorts(first, resolvers) { + return nil + } + } + return first +} + +// hasHosts reports whether c requires resolution of MagicDNS hosts or +// domains. +func (c Config) hasHosts() bool { + return len(c.Hosts) > 0 || len(c.AuthoritativeSuffixes) > 0 +} + +// matchDomains returns the list of match suffixes needed by Routes, +// AuthoritativeSuffixes. Hosts is not considered as we assume that +// they're covered by AuthoritativeSuffixes for now. +func (c Config) matchDomains() []string { + ret := make([]string, 0, len(c.Routes)+len(c.AuthoritativeSuffixes)) + seen := map[string]bool{} + for _, suffix := range c.AuthoritativeSuffixes { + if seen[suffix] { + continue + } + ret = append(ret, suffix) + seen[suffix] = true + } + for suffix := range c.Routes { + if seen[suffix] { + continue + } + ret = append(ret, suffix) + seen[suffix] = true + } + return ret +} + +func sameIPPorts(a, b []netaddr.IPPort) bool { + if len(a) != len(b) { + return false + } + + for i := range a { + if a[i] != b[i] { + return false + } + } + + return true +} diff --git a/net/dns/manager.go b/net/dns/manager.go index f62e9672d..d55e973cf 100644 --- a/net/dns/manager.go +++ b/net/dns/manager.go @@ -5,6 +5,7 @@ package dns import ( + "strings" "time" "inet.af/netaddr" @@ -50,49 +51,151 @@ func NewManager(logf logger.Logf, oscfg OSConfigurator, linkMon *monitor.Mon) *M return m } +// forceSplitDNSForTesting alters cfg to be a split DNS configuration +// that only captures search paths. It's intended for testing split +// DNS until the functionality is linked up in the admin panel. +func forceSplitDNSForTesting(cfg *Config) { + if len(cfg.DefaultResolvers) == 0 { + return + } + + if cfg.Routes == nil { + cfg.Routes = map[string][]netaddr.IPPort{} + } + for _, search := range cfg.SearchDomains { + cfg.Routes[search] = cfg.DefaultResolvers + } + cfg.DefaultResolvers = nil +} + func (m *Manager) Set(cfg Config) error { m.logf("Set: %+v", cfg) - if len(cfg.DefaultResolvers) == 0 { - // TODO: make other settings work even if you didn't set a - // default resolver. For now, no default resolvers == no - // managed DNS config. - cfg = Config{} + if false { + // Temporary, for danderson to test things. + forceSplitDNSForTesting(&cfg) } - resolverCfg := resolver.Config{ - Hosts: cfg.Hosts, - LocalDomains: cfg.AuthoritativeSuffixes, - Routes: map[string][]netaddr.IPPort{}, - } - osCfg := OSConfig{ - SearchDomains: cfg.SearchDomains, - } - // We must proxy through quad-100 if MagicDNS hosts are in - // use, or there are any per-domain routes. - mustProxy := len(cfg.Hosts) > 0 || len(cfg.Routes) > 0 - if mustProxy { - osCfg.Nameservers = []netaddr.IP{tsaddr.TailscaleServiceIP()} - resolverCfg.Routes["."] = cfg.DefaultResolvers - for suffix, resolvers := range cfg.Routes { - resolverCfg.Routes[suffix] = resolvers - } - } else { - for _, resolver := range cfg.DefaultResolvers { - osCfg.Nameservers = append(osCfg.Nameservers, resolver.IP) - } - } + rcfg, ocfg := m.compileConfig(cfg) - if err := m.resolver.SetConfig(resolverCfg); err != nil { + m.logf("Resolvercfg: %+v", rcfg) + m.logf("OScfg: %+v", ocfg) + + if err := m.resolver.SetConfig(rcfg); err != nil { return err } - if err := m.os.SetDNS(osCfg); err != nil { + if err := m.os.SetDNS(ocfg); err != nil { return err } return nil } +// compileConfig converts cfg into a quad-100 resolver configuration +// and an OS-level configuration. +func (m *Manager) compileConfig(cfg Config) (resolver.Config, OSConfig) { + // Deal with trivial configs first. + switch { + case !cfg.needsOSResolver(): + // Set search domains, but nothing else. This also covers the + // case where cfg is entirely zero, in which case these + // configs clear all Tailscale DNS settings. + return resolver.Config{}, OSConfig{ + SearchDomains: cfg.SearchDomains, + } + case cfg.hasDefaultResolversOnly(): + // Trivial CorpDNS configuration, just override the OS + // resolver. + return resolver.Config{}, OSConfig{ + Nameservers: toIPsOnly(cfg.DefaultResolvers), + SearchDomains: cfg.SearchDomains, + } + case cfg.hasDefaultResolvers(): + // Default resolvers plus other stuff always ends up proxying + // through quad-100. + rcfg := resolver.Config{ + Routes: map[string][]netaddr.IPPort{ + ".": cfg.DefaultResolvers, + }, + Hosts: cfg.Hosts, + LocalDomains: addFQDNDots(cfg.AuthoritativeSuffixes), + } + for suffix, resolvers := range cfg.Routes { + rcfg.Routes[suffix+"."] = resolvers + } + ocfg := OSConfig{ + Nameservers: []netaddr.IP{tsaddr.TailscaleServiceIP()}, + SearchDomains: cfg.SearchDomains, + } + return rcfg, ocfg + } + + // From this point on, we're figuring out split DNS + // configurations. The possible cases don't return directly any + // more, because as a final step we have to handle the case where + // the OS can't do split DNS. + var rcfg resolver.Config + var ocfg OSConfig + + if !cfg.hasHosts() && cfg.singleResolverSet() != nil && m.os.SupportsSplitDNS() { + // Split DNS configuration requested, where all split domains + // go to the same resolvers. We can let the OS do it. + return resolver.Config{}, OSConfig{ + Nameservers: toIPsOnly(cfg.singleResolverSet()), + SearchDomains: cfg.SearchDomains, + MatchDomains: cfg.matchDomains(), + } + } + + // Split DNS configuration with either multiple upstream routes, + // or routes + MagicDNS, or just MagicDNS, or on an OS that cannot + // split-DNS. Install a split config pointing at quad-100. + rcfg = resolver.Config{ + Routes: map[string][]netaddr.IPPort{}, + Hosts: cfg.Hosts, + LocalDomains: addFQDNDots(cfg.AuthoritativeSuffixes), + } + for suffix, resolvers := range cfg.Routes { + rcfg.Routes[suffix+"."] = resolvers + } + ocfg = OSConfig{ + Nameservers: []netaddr.IP{tsaddr.TailscaleServiceIP()}, + SearchDomains: cfg.SearchDomains, + } + + // If the OS can't do native split-dns, read out the underlying + // resolver config and blend it into our config. + // TODO: for now, use quad-8 as the upstream until more plumbing + // is done. + if m.os.SupportsSplitDNS() { + ocfg.MatchDomains = cfg.matchDomains() + } else { + rcfg.Routes["."] = []netaddr.IPPort{netaddr.MustParseIPPort("8.8.8.8:53")} + } + + return rcfg, ocfg +} + +func addFQDNDots(domains []string) []string { + ret := make([]string, 0, len(domains)) + for _, dom := range domains { + ret = append(ret, strings.TrimSuffix(dom, ".")+".") + } + return ret +} + +// toIPsOnly returns only the IP portion of ipps. +// TODO: this discards port information on the assumption that we're +// always pointing at port 53. +// https://github.com/tailscale/tailscale/issues/1666 tracks making +// that not true, if we ever want to. +func toIPsOnly(ipps []netaddr.IPPort) (ret []netaddr.IP) { + for _, ipp := range ipps { + ret = append(ret, ipp.IP) + } + return ret +} + func (m *Manager) EnqueueRequest(bs []byte, from netaddr.IPPort) error { return m.resolver.EnqueueRequest(bs, from) }