diff --git a/control/controlknobs/controlknobs.go b/control/controlknobs/controlknobs.go index 708840155..0f85e8236 100644 --- a/control/controlknobs/controlknobs.go +++ b/control/controlknobs/controlknobs.go @@ -107,6 +107,12 @@ type Knobs struct { // of queued netmap.NetworkMap between the controlclient and LocalBackend. // See tailscale/tailscale#14768. DisableSkipStatusQueue atomic.Bool + + // DisableHostsFileUpdates indicates that the node's DNS manager should not create + // hosts file entries when it normally would, such as when we're not the primary + // resolver on Windows or when the host is domain-joined and its primary domain + // takes precedence over MagicDNS. As of 2026-02-13, it is only used on Windows. + DisableHostsFileUpdates atomic.Bool } // UpdateFromNodeAttributes updates k (if non-nil) based on the provided self @@ -137,6 +143,7 @@ func (k *Knobs) UpdateFromNodeAttributes(capMap tailcfg.NodeCapMap) { disableLocalDNSOverrideViaNRPT = has(tailcfg.NodeAttrDisableLocalDNSOverrideViaNRPT) disableCaptivePortalDetection = has(tailcfg.NodeAttrDisableCaptivePortalDetection) disableSkipStatusQueue = has(tailcfg.NodeAttrDisableSkipStatusQueue) + disableHostsFileUpdates = has(tailcfg.NodeAttrDisableHostsFileUpdates) ) if has(tailcfg.NodeAttrOneCGNATEnable) { @@ -163,6 +170,7 @@ func (k *Knobs) UpdateFromNodeAttributes(capMap tailcfg.NodeCapMap) { k.DisableLocalDNSOverrideViaNRPT.Store(disableLocalDNSOverrideViaNRPT) k.DisableCaptivePortalDetection.Store(disableCaptivePortalDetection) k.DisableSkipStatusQueue.Store(disableSkipStatusQueue) + k.DisableHostsFileUpdates.Store(disableHostsFileUpdates) // If both attributes are present, then "enable" should win. This reflects // the history of seamless key renewal. diff --git a/net/dns/manager_windows.go b/net/dns/manager_windows.go index bc1e64560..1e412b2d2 100644 --- a/net/dns/manager_windows.go +++ b/net/dns/manager_windows.go @@ -34,6 +34,7 @@ "tailscale.com/util/syspolicy/policyclient" "tailscale.com/util/syspolicy/ptype" "tailscale.com/util/winutil" + "tailscale.com/util/winutil/winenv" ) const ( @@ -354,6 +355,10 @@ func (m *windowsManager) disableLocalDNSOverrideViaNRPT() bool { return m.knobs != nil && m.knobs.DisableLocalDNSOverrideViaNRPT.Load() } +func (m *windowsManager) disableHostsFileUpdates() bool { + return m.knobs != nil && m.knobs.DisableHostsFileUpdates.Load() +} + func (m *windowsManager) SetDNS(cfg OSConfig) error { // We can configure Windows DNS in one of two ways: // @@ -400,7 +405,7 @@ func (m *windowsManager) SetDNS(cfg OSConfig) error { return err } var hosts []*HostEntry - if winenv.IsDomainJoined() { + if !m.disableHostsFileUpdates() && winenv.IsDomainJoined() { // On domain-joined Windows devices the primary search domain (the one the device is joined to) // always takes precedence over other search domains. This breaks MagicDNS when we are the primary // resolver on the device (see #18712). To work around this Windows behavior, we should write MagicDNS @@ -429,12 +434,14 @@ func (m *windowsManager) SetDNS(cfg OSConfig) error { return err } - // As we are not the primary resolver in this setup, we need to - // explicitly set some single name hosts to ensure that we can resolve - // them quickly and get around the 2.3s delay that otherwise occurs due - // to multicast timeouts. - if err := m.setHosts(cfg.Hosts); err != nil { - return err + if !m.disableHostsFileUpdates() { + // As we are not the primary resolver in this setup, we need to + // explicitly set some single name hosts to ensure that we can resolve + // them quickly and get around the 2.3s delay that otherwise occurs due + // to multicast timeouts. + if err := m.setHosts(cfg.Hosts); err != nil { + return err + } } } diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 171f88fd7..69ca20a94 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -178,7 +178,8 @@ // - 129: 2025-10-04: Fixed sleep/wake deadlock in magicsock when using peer relay (PR #17449) // - 130: 2025-10-06: client can send key.HardwareAttestationPublic and key.HardwareAttestationKeySignature in MapRequest // - 131: 2025-11-25: client respects [NodeAttrDefaultAutoUpdate] -const CurrentCapabilityVersion CapabilityVersion = 131 +// - 132: 2026-02-13: client respects [NodeAttrDisableHostsFileUpdates] +const CurrentCapabilityVersion CapabilityVersion = 132 // ID is an integer ID for a user, node, or login allocated by the // control plane. @@ -2740,6 +2741,13 @@ type Oauth2Token struct { // // The value of the key in [NodeCapMap] is a JSON boolean. NodeAttrDefaultAutoUpdate NodeCapability = "default-auto-update" + + // NodeAttrDisableHostsFileUpdates indicates that the node's DNS manager should + // not create hosts file entries when it normally would, such as when we're not + // the primary resolver on Windows or when the host is domain-joined and its + // primary domain takes precedence over MagicDNS. As of 2026-02-12, it is only + // used on Windows. + NodeAttrDisableHostsFileUpdates NodeCapability = "disable-hosts-file-updates" ) // SetDNSRequest is a request to add a DNS record.