net/dns: scope quad-100 on macOS so DoH profiles aren't shadowed (#20775)

* net/dns: scope quad-100 on macOS so DoH profiles aren't shadowed

On sandboxed macOS, an uncovered control ExtraRecord forced quad-100 to
be the primary resolver, proxying all public DNS and shadowing a user's
DoH system profile. Scope quad-100 to its match domains instead, adding
the uncovered host records to MatchDomains so they still resolve while
public names fall through to the OS resolver. quad-100 remains primary
only without a usable base resolver or with non-enumerable MagicDNS
host records.

fixes tailscale/corp#45534

Signed-off-by: Will Hannah <willh@tailscale.com>

* net/dns: move scoped DNS behind an envknob

updates tailscale/corp#45534

Given the sensitivity of this change, let's stuff it behind
a control knob for a release.

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>

---------

Signed-off-by: Will Hannah <willh@tailscale.com>
Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
Co-authored-by: Jonathan Nobels <jonathan@tailscale.com>
This commit is contained in:
Will HannahandJonathan Nobels authored and GitHub committed 2026-08-07 15:27:22 -04:00
1 parent 80522b814c
commit 15015f19bf
6 files changed
+258 -12

No files matched your search

+10
View File
@@ -140,6 +140,14 @@ 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
@@ -176,6 +184,7 @@ 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) {
@@ -210,6 +219,7 @@ 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