diff --git a/net/dns/manager.go b/net/dns/manager.go index 6810d5a6b..14e297408 100644 --- a/net/dns/manager.go +++ b/net/dns/manager.go @@ -262,6 +262,18 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig // config is empty, then we need to fallback to SplitDNS mode. ocfg.MatchDomains = cfg.matchDomains() } else { + // On iOS only (for now), check if all route names point to resources inside the tailnet. + // If so, we can set those names as MatchDomains to enable a split DNS configuration + // which will help preserve battery life. + // Because on iOS MatchDomains must equal SearchDomains, we cannot do this when + // we have any Routes outside the tailnet. Otherwise when app connectors are enabled, + // a query for 'work-laptop' might lead to search domain expansion, resolving + // as 'work-laptop.aws.com' for example. + if runtime.GOOS == "ios" && rcfg.RoutesRequireNoCustomResolvers() { + for r := range rcfg.Routes { + ocfg.MatchDomains = append(ocfg.MatchDomains, r) + } + } var defaultRoutes []*dnstype.Resolver for _, ip := range baseCfg.Nameservers { defaultRoutes = append(defaultRoutes, &dnstype.Resolver{Addr: ip.String()}) diff --git a/net/dns/resolver/tsdns.go b/net/dns/resolver/tsdns.go index c44565ee1..dd0504baf 100644 --- a/net/dns/resolver/tsdns.go +++ b/net/dns/resolver/tsdns.go @@ -175,6 +175,25 @@ func WriteRoutes(w *bufio.Writer, routes map[dnsname.FQDN][]*dnstype.Resolver) { } } +// RoutesRequireNoCustomResolvers returns true if this resolver.Config only contains routes +// that do not specify a set of custom resolver(s), i.e. they can be resolved by the local +// upstream DNS resolver. +func (c *Config) RoutesRequireNoCustomResolvers() bool { + for route, resolvers := range c.Routes { + if route.WithoutTrailingDot() == "ts.net" { + // Ignore the "ts.net" route here. It always specifies the corp resolvers but + // its presence is not an issue, as ts.net will be a search domain. + continue + } + if len(resolvers) != 0 { + // Found a route with custom resolvers. + return false + } + } + // No routes other than ts.net have specified one or more resolvers. + return true +} + // Resolver is a DNS resolver for nodes on the Tailscale network, // associating them with domain names of the form ... // If it is asked to resolve a domain that is not of that form, diff --git a/net/dns/resolver/tsdns_test.go b/net/dns/resolver/tsdns_test.go index a0f625b20..fb33b49a8 100644 --- a/net/dns/resolver/tsdns_test.go +++ b/net/dns/resolver/tsdns_test.go @@ -243,6 +243,43 @@ func mustIP(str string) netip.Addr { return ip } +func TestRoutesRequireNoCustomResolvers(t *testing.T) { + tests := []struct { + name string + config Config + expected bool + }{ + {"noRoutes", Config{Routes: map[dnsname.FQDN][]*dnstype.Resolver{}}, true}, + {"onlyDefault", Config{Routes: map[dnsname.FQDN][]*dnstype.Resolver{ + "ts.net.": { + {}, + }, + }}, true}, + {"oneOther", Config{Routes: map[dnsname.FQDN][]*dnstype.Resolver{ + "example.com.": { + {}, + }, + }}, false}, + {"defaultAndOneOther", Config{Routes: map[dnsname.FQDN][]*dnstype.Resolver{ + "ts.net.": { + {}, + }, + "example.com.": { + {}, + }, + }}, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.config.RoutesRequireNoCustomResolvers() + if result != tt.expected { + t.Errorf("result = %v; want %v", result, tt.expected) + } + }) + } +} + func TestRDNSNameToIPv4(t *testing.T) { tests := []struct { name string