net/dns: simplify split DNS compile path

Cache the OS split-DNS capability while compiling DNS config and return directly for split-capable platforms that do not need the Apple base-config workaround.

This removes the base-config sentinel pointer and keeps the iOS and sandboxed macOS fallback path explicit.

Updates #1338

Change-Id: I836417c8fa775b35d3be9bc80cf6841d30cec222

Signed-off-by: Maisem Ali <maisem@bold.dev>
This commit is contained in:
Maisem Ali
2026-07-02 12:21:54 -07:00
committed by Brad Fitzpatrick
parent 10672a63f4
commit 9013b6ec1b
+46 -46
View File
@@ -378,10 +378,11 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig
// workaround.
isWindows := m.goos == "windows"
isIOS := m.goos == "ios"
supportsSplitDNS := m.os.SupportsSplitDNS()
// Sandboxed macOS builds use NetworkExtension DNS settings, not
// tailscaled's /etc/resolver configurator, so keep the Apple workaround.
appleSplitDNSWorkaround := isIOS || (m.goos == "darwin" && isSandboxedMacOS())
if m.os.SupportsSplitDNS() && !isWindows && !appleSplitDNSWorkaround {
if supportsSplitDNS && !isWindows && !appleSplitDNSWorkaround {
if srs := toIPsOnly(cfg.singleResolverSet()); len(srs) > 0 {
// Split DNS configuration requested, where all split domains
// go to the same resolvers. We can let the OS do it.
@@ -397,63 +398,62 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig
rcfg.Routes = routes
ocfg.Nameservers = cfg.serviceIPs(m.knobs)
var baseCfg *OSConfig // base config; non-nil if/when known
if supportsSplitDNS && !appleSplitDNSWorkaround {
ocfg.MatchDomains = cfg.matchDomains()
return rcfg, ocfg, nil
}
// Even though iOS devices can do split DNS, they don't provide a way to
// selectively answer ExtraRecords, and ignore other DNS traffic. As a
// workaround, we read the existing default resolver configuration and use
// that as the forwarder for all DNS traffic that quad-100 doesn't handle.
if appleSplitDNSWorkaround || !m.os.SupportsSplitDNS() {
// If the OS can't do native split-dns, read out the underlying
// resolver config and blend it into our config. On apple platforms, [OSConfigurator.GetBaseConfig]
// has a tendency to temporarily fail if called immediately following
// an interface change. These failures should be retried if/when the OS
// indicates that the DNS configuration has changed via [RecompileDNSConfig].
cfg, err := m.os.GetBaseConfig()
if err == nil {
baseCfg = &cfg
} else if (isIOS || isNoopManager(m.os)) && err == ErrGetBaseConfigNotSupported {
//
// If the OS can't do native split-DNS, read out the underlying resolver
// config and blend it into our config. On iOS, [OSConfigurator.GetBaseConfig]
// has a tendency to temporarily fail if called immediately following an
// interface change. These failures should be retried if/when the OS indicates
// that the DNS configuration has changed via [RecompileDNSConfig].
base, err := m.os.GetBaseConfig()
if err != nil {
if (isIOS || isNoopManager(m.os)) && err == ErrGetBaseConfigNotSupported {
// Expected when using noopManager (userspace networking) or on
// certain iOS builds. Continue without base config.
} else {
m.health.SetUnhealthy(osConfigurationReadWarnable, health.Args{health.ArgError: err.Error()})
return resolver.Config{}, OSConfig{}, err
m.health.SetHealthy(osConfigurationReadWarnable)
ocfg.MatchDomains = cfg.matchDomains()
return rcfg, ocfg, nil
}
m.health.SetHealthy(osConfigurationReadWarnable)
m.health.SetUnhealthy(osConfigurationReadWarnable, health.Args{health.ArgError: err.Error()})
return resolver.Config{}, OSConfig{}, err
}
m.health.SetHealthy(osConfigurationReadWarnable)
if baseCfg == nil {
// If there was no base config, 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 m.goos == "ios" && rcfg.RoutesRequireNoCustomResolvers() {
if !m.disableSplitDNSOptimization() {
for r := range rcfg.Routes {
ocfg.MatchDomains = append(ocfg.MatchDomains, r)
}
} else {
m.logf("iOS split DNS is disabled by nodeattr")
// 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 isIOS && rcfg.RoutesRequireNoCustomResolvers() {
if !m.disableSplitDNSOptimization() {
for r := range rcfg.Routes {
ocfg.MatchDomains = append(ocfg.MatchDomains, r)
}
} else {
m.logf("iOS split DNS is disabled by nodeattr")
}
var defaultRoutes []*dnstype.Resolver
for _, ip := range baseCfg.Nameservers {
defaultRoutes = append(defaultRoutes, &dnstype.Resolver{Addr: ip.String()})
}
rcfg.Routes["."] = defaultRoutes
// Append base config search domains, but only if not already present.
// This prevents duplicates when GetBaseConfig() reads back domains that
// Tailscale itself previously wrote to resolv.conf.
for _, domain := range baseCfg.SearchDomains {
if !slices.Contains(ocfg.SearchDomains, domain) {
ocfg.SearchDomains = append(ocfg.SearchDomains, domain)
}
}
var defaultRoutes []*dnstype.Resolver
for _, ip := range base.Nameservers {
defaultRoutes = append(defaultRoutes, &dnstype.Resolver{Addr: ip.String()})
}
rcfg.Routes["."] = defaultRoutes
// Append base config search domains, but only if not already present.
// This prevents duplicates when GetBaseConfig() reads back domains that
// Tailscale itself previously wrote to resolv.conf.
for _, domain := range base.SearchDomains {
if !slices.Contains(ocfg.SearchDomains, domain) {
ocfg.SearchDomains = append(ocfg.SearchDomains, domain)
}
}