diff --git a/control/controlknobs/controlknobs.go b/control/controlknobs/controlknobs.go index 45bfde0fa..280953cd9 100644 --- a/control/controlknobs/controlknobs.go +++ b/control/controlknobs/controlknobs.go @@ -140,14 +140,6 @@ type Knobs struct { // maps and use them to establish peer connectivity on start, if doing so // is supported by the client and storage is available. CacheNetworkMaps atomic.Bool - - // ScopeQuad100OnMacOS is whether sandboxed macOS should scope quad-100 to - // its match domains rather than installing it as the OS's primary resolver, - // so a user's DoH system profile isn't shadowed. It has no effect on other - // platforms. Off by default; when off, sandboxed macOS keeps the older - // behavior of making quad-100 the default resolver, as iOS still does. - // See tailscale/corp#45534. - ScopeQuad100OnMacOS atomic.Bool } // UpdateFromNodeAttributes updates k (if non-nil) based on the provided self @@ -184,7 +176,6 @@ func (k *Knobs) UpdateFromNodeAttributes(capMap tailcfg.NodeCapMap) { disableTUNTCPGRO = has(tailcfg.NodeAttrDisableTUNTCPGRO) neverGSOEqualTail = has(tailcfg.NodeAttrNeverGSOEqualTail) cacheNetworkMaps = has(tailcfg.NodeAttrCacheNetworkMaps) - scopeQuad100OnMacOS = has(tailcfg.NodeAttrScopeQuad100OnMacOS) ) if has(tailcfg.NodeAttrOneCGNATEnable) { @@ -219,7 +210,6 @@ func (k *Knobs) UpdateFromNodeAttributes(capMap tailcfg.NodeCapMap) { k.DisableTUNTCPGRO.Store(disableTUNTCPGRO) k.NeverGSOEqualTail.Store(neverGSOEqualTail) k.CacheNetworkMaps.Store(cacheNetworkMaps) - k.ScopeQuad100OnMacOS.Store(scopeQuad100OnMacOS) } // AsDebugJSON returns k as something that can be marshalled with json.Marshal diff --git a/ipn/ipnlocal/node_backend.go b/ipn/ipnlocal/node_backend.go index bf79600b0..fbc6317c0 100644 --- a/ipn/ipnlocal/node_backend.go +++ b/ipn/ipnlocal/node_backend.go @@ -1503,9 +1503,6 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg. set(peer.Name(), peer.Addresses()) } } - // extraRecordNames are the ExtraRecord FQDNs, tracked separately from - // dcfg.Hosts because on Windows that map also holds every node's records. - var extraRecordNames []dnsname.FQDN for _, rec := range nm.DNS.ExtraRecords { switch rec.Type { case "", "A", "AAAA": @@ -1523,9 +1520,6 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg. if err != nil { continue } - if !slices.Contains(extraRecordNames, fqdn) { - extraRecordNames = append(extraRecordNames, fqdn) - } dcfg.Hosts[fqdn] = append(dcfg.Hosts[fqdn], ip) } @@ -1574,34 +1568,6 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg. } } - // coverExtraRecords adds an authoritative (resolver-less) route for each - // ExtraRecord name not already covered by one, so dns.Manager can scope - // quad-100 to those names instead of having to install it as the OS's - // primary resolver just to answer them. This is the same convention - // addSplitDNSRoutes implements for control-sent empty routes (Issue 2706); - // here we apply it to records control sent without a matching route. - // - // Only ExtraRecords, not all of dcfg.Hosts: on Windows Hosts also carries - // every node's records, and routing each one individually would mean a - // per-node NRPT rule. - // - // Must run after all addSplitDNSRoutes calls so a control-sent route for - // the same suffix wins. - coverExtraRecords := func() { - for _, fqdn := range extraRecordNames { - covered := false - for route := range dcfg.Routes { - if route.Contains(fqdn) { - covered = true - break - } - } - if !covered { - dcfg.Routes[fqdn] = nil - } - } - } - // conn25 split DNS routes are calculated from the domains in the SelfNode.CapMap // section of the netmap, so need to be assembled separately. // TODO(tailscale/corp#37125): make this a hook the extension can add @@ -1627,7 +1593,6 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg. addSplitDNSRoutes(useWithExitNodeRoutes(nm.DNS.Routes)) addSplitDNSRoutes(useWithExitNodeRoutes(conn25AppRoutes)) - coverExtraRecords() return dcfg } } @@ -1646,7 +1611,6 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg. // Add split DNS routes, with no regard to exit node configuration. addSplitDNSRoutes(nm.DNS.Routes) addSplitDNSRoutes(conn25AppRoutes) - coverExtraRecords() // Set FallbackResolvers as the default resolvers in the // scenarios that can't handle a purely split-DNS config. See diff --git a/net/dns/config.go b/net/dns/config.go index 8e82a3247..da19e7035 100644 --- a/net/dns/config.go +++ b/net/dns/config.go @@ -150,19 +150,6 @@ func (c Config) hasHostsWithoutSplitDNSRoutes() bool { return false } -// requiresPrimaryResolver reports whether c can only be served correctly with -// quad-100 installed as the OS's primary (catch-all) resolver, rather than -// scoped to a set of match domains. -// -// That's the case when c has names quad-100 must answer that no route suffix -// covers, so there is no suffix to scope to. dnsConfigForNetmap pairs every -// ExtraRecord it emits with a route, so in practice this is the -// MagicDNS-names-present but MagicDNS-domain-routing-off case -// (MagicDNSHostsUnrouted). -func (c Config) requiresPrimaryResolver() bool { - return c.hasHostsWithoutSplitDNSRoutes() -} - // hasSplitDNSRouteForHost reports whether c contains a SplitDNS route // that contains hosts. func (c Config) hasSplitDNSRouteForHost(host dnsname.FQDN) bool { diff --git a/net/dns/manager.go b/net/dns/manager.go index 44a0782d7..b34c5b632 100644 --- a/net/dns/manager.go +++ b/net/dns/manager.go @@ -20,7 +20,6 @@ "time" "tailscale.com/control/controlknobs" - "tailscale.com/envknob" "tailscale.com/feature/buildfeatures" "tailscale.com/health" "tailscale.com/net/dns/resolver" @@ -321,9 +320,6 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig routes[suffix] = resolvers } } - // LocalDomains is an unordered suffix set, but it comes out of map - // iteration; sort it so equal configs compare and log equal. - slices.Sort(rcfg.LocalDomains) // Similarly, the OS always gets search paths. ocfg.SearchDomains = cfg.SearchDomains @@ -382,13 +378,11 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig // workaround. isWindows := m.goos == "windows" isIOS := m.goos == "ios" - isSandboxedMac := m.goos == "darwin" && isSandboxedMacOS() supportsSplitDNS := m.os.SupportsSplitDNS() - isSandboxedApple := isIOS || isSandboxedMac - // Apple platforms keep split-domain traffic pointed at quad-100 rather than - // handing the upstream resolvers to the OS directly, because those resolvers - // may only be reachable through the tunnel. - if supportsSplitDNS && !isWindows && !isSandboxedApple { + // 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 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. @@ -404,15 +398,7 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig rcfg.Routes = routes ocfg.Nameservers = cfg.serviceIPs(m.knobs) - // usePrimaryResolver forces quad-100 to be installed as the OS's primary - // (catch-all) resolver rather than scoped to the match domains. iOS always - // does this (it has no way to selectively answer ExtraRecords). Sandboxed - // macOS did too until control opts it into scoping via - // NodeAttrScopeQuad100OnMacOS, so that a user's DoH system profile isn't - // shadowed by quad-100. See tailscale/corp#45534. - usePrimaryResolver := isIOS || (isSandboxedMac && !m.scopeQuad100OnMacOS()) - - if supportsSplitDNS && !usePrimaryResolver && !cfg.requiresPrimaryResolver() { + if supportsSplitDNS && !appleSplitDNSWorkaround { ocfg.MatchDomains = cfg.matchDomains() return rcfg, ocfg, nil } @@ -478,19 +464,6 @@ func (m *Manager) disableSplitDNSOptimization() bool { return m.knobs != nil && m.knobs.DisableSplitDNSWhenNoCustomResolvers.Load() } -var scopeQuad100OnMacOSEnv = envknob.RegisterOptBool("TS_DEBUG_SCOPE_QUAD100_MACOS") - -// scopeQuad100OnMacOS reports whether sandboxed macOS should scope quad-100 to -// its match domains rather than installing it as the OS's primary resolver. -// Off (false) unless control sets NodeAttrScopeQuad100OnMacOS, or the -// TS_DEBUG_SCOPE_QUAD100_MACOS env override is set. See tailscale/corp#45534. -func (m *Manager) scopeQuad100OnMacOS() bool { - if v, ok := scopeQuad100OnMacOSEnv().Get(); ok { - return v - } - return m.knobs != nil && m.knobs.ScopeQuad100OnMacOS.Load() -} - var isSandboxedMacOS = version.IsSandboxedMacOS // toIPsOnly returns only the IP portion of dnstype.Resolver. diff --git a/net/dns/manager_test.go b/net/dns/manager_test.go index efd9d2c6f..b10c89056 100644 --- a/net/dns/manager_test.go +++ b/net/dns/manager_test.go @@ -175,15 +175,6 @@ func TestCompileHostEntries(t *testing.T) { var serviceAddr46 = []netip.Addr{tsaddr.TailscaleServiceIP(), tsaddr.TailscaleServiceIPv6()} -// scopeQuad100Knobs returns Knobs with ScopeQuad100OnMacOS set, i.e. the -// NodeAttrScopeQuad100OnMacOS opt-in that lets sandboxed macOS scope quad-100 -// to its match domains instead of installing it as the primary resolver. -func scopeQuad100Knobs() *controlknobs.Knobs { - k := new(controlknobs.Knobs) - k.ScopeQuad100OnMacOS.Store(true) - return k -} - func TestManager(t *testing.T) { if runtime.GOOS == "windows" { t.Skipf("test's assumptions break because of https://github.com/tailscale/corp/issues/1662") @@ -461,49 +452,15 @@ func TestManager(t *testing.T) { }, }, { - // Sandboxed macOS app builds use NetworkExtension DNS settings - // rather than tailscaled's /etc/resolver configurator, so split - // traffic stays pointed at quad-100 instead of handing 2.2.2.2 to - // the OS directly (it may only be reachable via the tunnel). With - // NodeAttrScopeQuad100OnMacOS set, quad-100 is scoped to the match - // domains, so public names fall through to the OS resolver -- e.g. a - // DoH profile -- rather than being shadowed by a "." route. See the - // -no-knob variant below for the default behavior. tailscale/corp#45534. + // Sandboxed macOS app builds use NetworkExtension DNS settings, not + // tailscaled's /etc/resolver configurator, so they keep the older + // Apple base-config behavior. name: "routes-split-sandboxed-darwin", in: Config{ Routes: upstreams("corp.com", "2.2.2.2"), SearchDomains: fqdns("tailscale.com", "universe.tf"), }, split: true, - knobs: scopeQuad100Knobs(), - bs: OSConfig{ - Nameservers: mustIPs("8.8.8.8"), - SearchDomains: fqdns("coffee.shop"), - }, - os: OSConfig{ - Nameservers: serviceAddr46, - SearchDomains: fqdns("tailscale.com", "universe.tf"), - MatchDomains: fqdns("corp.com"), - }, - rs: resolver.Config{ - Routes: upstreams( - "corp.com.", "2.2.2.2"), - }, - goos: "darwin", - sandboxedMacOS: true, - }, - { - // Same as above but without NodeAttrScopeQuad100OnMacOS: the default - // on sandboxed macOS, matching iOS. quad-100 is installed as the OS - // primary resolver (a "." route to the base config's 8.8.8.8), so it - // proxies all public DNS -- shadowing any DoH profile -- and is not - // scoped to MatchDomains. tailscale/corp#45534. - name: "routes-split-sandboxed-darwin-no-knob", - in: Config{ - Routes: upstreams("corp.com", "2.2.2.2"), - SearchDomains: fqdns("tailscale.com", "universe.tf"), - }, - split: true, bs: OSConfig{ Nameservers: mustIPs("8.8.8.8"), SearchDomains: fqdns("coffee.shop"), @@ -520,85 +477,6 @@ func TestManager(t *testing.T) { goos: "darwin", sandboxedMacOS: true, }, - { - // An ExtraRecord that dnsConfigForNetmap has paired with an - // authoritative (resolver-less) route is scoped like any other - // split domain, on every platform. Nothing here is Apple-specific: - // the darwin and linux variants below must agree. - name: "extra-record-routed-scopes-quad100", - in: Config{ - Hosts: hosts("extra.example.com.", "100.64.0.9"), - Routes: upstreams("corp.ts.net.", "", "extra.example.com.", ""), - SearchDomains: fqdns("corp.ts.net"), - }, - split: true, - knobs: scopeQuad100Knobs(), - bs: OSConfig{ - Nameservers: mustIPs("8.8.8.8"), - }, - os: OSConfig{ - Nameservers: serviceAddr46, - SearchDomains: fqdns("corp.ts.net"), - MatchDomains: fqdns("corp.ts.net", "extra.example.com"), - }, - rs: resolver.Config{ - Hosts: hosts("extra.example.com.", "100.64.0.9"), - LocalDomains: fqdns("corp.ts.net.", "extra.example.com."), - }, - goos: "darwin", - sandboxedMacOS: true, - }, - { - name: "extra-record-routed-scopes-quad100-linux", - in: Config{ - Hosts: hosts("extra.example.com.", "100.64.0.9"), - Routes: upstreams("corp.ts.net.", "", "extra.example.com.", ""), - SearchDomains: fqdns("corp.ts.net"), - }, - split: true, - bs: OSConfig{ - Nameservers: mustIPs("8.8.8.8"), - }, - os: OSConfig{ - Nameservers: serviceAddr46, - SearchDomains: fqdns("corp.ts.net"), - MatchDomains: fqdns("corp.ts.net", "extra.example.com"), - }, - rs: resolver.Config{ - Hosts: hosts("extra.example.com.", "100.64.0.9"), - LocalDomains: fqdns("corp.ts.net.", "extra.example.com."), - }, - }, - { - // MagicDNS names exist but MagicDNS domain routing is off, so no - // route suffix covers them and there is nothing to scope to. - // quad-100 must stay primary or those names stop resolving -- - // requiresPrimaryResolver overrides NodeAttrScopeQuad100OnMacOS even - // when it's set. The DoH profile is still shadowed here; that is the - // cost of serving unrouted names. - name: "unrouted-magicdns-hosts-keep-quad100-primary", - in: Config{ - Routes: upstreams("corp.ts.net.", "1.2.3.4"), - SearchDomains: fqdns("corp.ts.net"), - MagicDNSHostsUnrouted: true, - }, - split: true, - knobs: scopeQuad100Knobs(), - bs: OSConfig{ - Nameservers: mustIPs("192.168.1.1"), - }, - os: OSConfig{ - Nameservers: serviceAddr46, - SearchDomains: fqdns("corp.ts.net"), - }, - rs: resolver.Config{ - Routes: upstreams( - ".", "192.168.1.1", - "corp.ts.net.", "1.2.3.4"), - }, - goos: "darwin", - sandboxedMacOS: true, - }, { name: "routes-multi", in: Config{ diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 99276d7ae..5923e1a6d 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -191,8 +191,7 @@ // - 142: 2026-07-06: Client understands c2n /remoteapi/localapi/* proxy // - 143: 2026-07-22: Client correctly ignores conn25 node attributes when not enabled by environment variable // - 144: 2026-07-31: Client sends [packet.TSMPDiscoKeyAdvertisement] around WireGuard handshakes -// - 145: 2026-08-04: Client understands [NodeAttrScopeQuad100OnMacOS] -const CurrentCapabilityVersion CapabilityVersion = 145 +const CurrentCapabilityVersion CapabilityVersion = 144 // ID is an integer ID for a user, node, or login allocated by the // control plane. @@ -2698,15 +2697,6 @@ func (p NodeCapabilityPrefix) ToAttribute(value string) NodeCapability { // if needed. NodeAttrDisableSplitDNSWhenNoCustomResolvers NodeCapability = "disable-split-dns-when-no-custom-resolvers" - // NodeAttrScopeQuad100OnMacOS makes sandboxed macOS clients scope quad-100 - // to its match domains instead of installing it as the OS's primary - // (catch-all) resolver, so that public names fall through to the OS - // resolver -- e.g. a user's DoH system profile -- rather than being - // shadowed. It has no effect on any other platform. Without this attribute, - // sandboxed macOS keeps the older behavior of making quad-100 the default - // resolver, as iOS still does. See tailscale/corp#45534. - NodeAttrScopeQuad100OnMacOS NodeCapability = "scope-quad100-macos" - // NodeAttrDisableLocalDNSOverrideViaNRPT indicates that the node's DNS manager should not // create a default (catch-all) Windows NRPT rule when "Override local DNS" is enabled. // Without this rule, Windows 8.1 and newer devices issue parallel DNS requests to DNS servers