diff --git a/docs/windows/policy/en-US/tailscale.adml b/docs/windows/policy/en-US/tailscale.adml
index eb6a520d1..fb71e521e 100644
--- a/docs/windows/policy/en-US/tailscale.adml
+++ b/docs/windows/policy/en-US/tailscale.adml
@@ -17,6 +17,7 @@
Tailscale version 1.74.0 and later
Tailscale version 1.78.0 and later
Tailscale version 1.82.0 and later
+ Tailscale version 1.84.0 and later
Tailscale
UI customization
Settings
@@ -147,6 +148,14 @@ If you disable this policy, then Use Tailscale Subnets is always disabled and th
If you do not configure this policy, then Use Tailscale Subnets depends on what is selected in the Preferences submenu.
See https://tailscale.com/kb/1315/mdm-keys#set-whether-the-device-accepts-tailscale-subnets or https://tailscale.com/kb/1019/subnets for more details.]]>
+ Always register
+ Use adapter properties
+ Register Tailscale IP addresses in DNS
+
Automatically install updates
Exit Node:
+
+ Registration mode:
+
Target IDs:
diff --git a/docs/windows/policy/tailscale.admx b/docs/windows/policy/tailscale.admx
index 0ff311b40..3db2108b4 100644
--- a/docs/windows/policy/tailscale.admx
+++ b/docs/windows/policy/tailscale.admx
@@ -58,6 +58,10 @@
displayName="$(string.SINCE_V1_82)">
+
+
+
@@ -193,6 +197,24 @@
never
+
+
+
+
+
+ -
+
+ always
+
+
+ -
+
+ user-decides
+
+
+
+
+
diff --git a/net/dns/manager_windows.go b/net/dns/manager_windows.go
index effdf23ca..6ed5d3ba6 100644
--- a/net/dns/manager_windows.go
+++ b/net/dns/manager_windows.go
@@ -29,6 +29,9 @@ import (
"tailscale.com/health"
"tailscale.com/types/logger"
"tailscale.com/util/dnsname"
+ "tailscale.com/util/syspolicy"
+ "tailscale.com/util/syspolicy/rsop"
+ "tailscale.com/util/syspolicy/setting"
"tailscale.com/util/winutil"
)
@@ -45,6 +48,8 @@ type windowsManager struct {
nrptDB *nrptRuleDatabase
wslManager *wslManager
+ unregisterPolicyChangeCb func() // called when the manager is closing
+
mu sync.Mutex
closing bool
}
@@ -64,6 +69,11 @@ func NewOSConfigurator(logf logger.Logf, health *health.Tracker, knobs *controlk
ret.nrptDB = newNRPTRuleDatabase(logf)
}
+ var err error
+ if ret.unregisterPolicyChangeCb, err = syspolicy.RegisterChangeCallback(ret.sysPolicyChanged); err != nil {
+ logf("error registering policy change callback: %v", err) // non-fatal
+ }
+
go func() {
// Log WSL status once at startup.
if distros, err := wslDistros(); err != nil {
@@ -362,11 +372,9 @@ func (m *windowsManager) SetDNS(cfg OSConfig) error {
// configuration only, routing one set of things to the "split"
// resolver and the rest to the primary.
- // Unconditionally disable dynamic DNS updates and NetBIOS on our
- // interfaces.
- if err := m.disableDynamicUpdates(); err != nil {
- m.logf("disableDynamicUpdates error: %v\n", err)
- }
+ // Reconfigure DNS registration according to the [syspolicy.DNSRegistration]
+ // policy setting, and unconditionally disable NetBIOS on our interfaces.
+ m.reconfigureDNSRegistration()
if err := m.disableNetBIOS(); err != nil {
m.logf("disableNetBIOS error: %v\n", err)
}
@@ -485,6 +493,10 @@ func (m *windowsManager) Close() error {
m.closing = true
m.mu.Unlock()
+ if m.unregisterPolicyChangeCb != nil {
+ m.unregisterPolicyChangeCb()
+ }
+
err := m.SetDNS(OSConfig{})
if m.nrptDB != nil {
m.nrptDB.Close()
@@ -493,15 +505,62 @@ func (m *windowsManager) Close() error {
return err
}
-// disableDynamicUpdates sets the appropriate registry values to prevent the
-// Windows DHCP client from sending dynamic DNS updates for our interface to
-// AD domain controllers.
-func (m *windowsManager) disableDynamicUpdates() error {
+// sysPolicyChanged is a callback triggered by [syspolicy] when it detects
+// a change in one or more syspolicy settings.
+func (m *windowsManager) sysPolicyChanged(policy *rsop.PolicyChange) {
+ if policy.HasChanged(syspolicy.EnableDNSRegistration) {
+ m.reconfigureDNSRegistration()
+ }
+}
+
+// reconfigureDNSRegistration configures the DNS registration settings
+// using the [syspolicy.DNSRegistration] policy setting, if it is set.
+// If the policy is not configured, it disables DNS registration.
+func (m *windowsManager) reconfigureDNSRegistration() {
+ // Disable DNS registration by default (if the policy setting is not configured).
+ // This is primarily for historical reasons and to avoid breaking existing
+ // setups that rely on this behavior.
+ enableDNSRegistration, err := syspolicy.GetPreferenceOptionOrDefault(syspolicy.EnableDNSRegistration, setting.NeverByPolicy)
+ if err != nil {
+ m.logf("error getting DNSRegistration policy setting: %v", err) // non-fatal; we'll use the default
+ }
+
+ if enableDNSRegistration.Show() {
+ // "Show" reports whether the policy setting is configured as "user-decides".
+ // The name is a bit unfortunate in this context, as we don't actually "show" anything.
+ // Still, if the admin configured the policy as "user-decides", we shouldn't modify
+ // the adapter's settings and should leave them up to the user (admin rights required)
+ // or the system defaults.
+ return
+ }
+
+ // Otherwise, if the policy setting is configured as "always" or "never",
+ // we should configure the adapter accordingly.
+ if err := m.configureDNSRegistration(enableDNSRegistration.IsAlways()); err != nil {
+ m.logf("error configuring DNS registration: %v", err)
+ }
+}
+
+// configureDNSRegistration sets the appropriate registry values to allow or prevent
+// the Windows DHCP client from registering Tailscale IP addresses with DNS
+// and sending dynamic updates for our interface to AD domain controllers.
+func (m *windowsManager) configureDNSRegistration(enabled bool) error {
prefixen := []winutil.RegistryPathPrefix{
winutil.IPv4TCPIPInterfacePrefix,
winutil.IPv6TCPIPInterfacePrefix,
}
+ var (
+ registrationEnabled = uint32(0)
+ disableDynamicUpdate = uint32(1)
+ maxNumberOfAddressesToRegister = uint32(0)
+ )
+ if enabled {
+ registrationEnabled = 1
+ disableDynamicUpdate = 0
+ maxNumberOfAddressesToRegister = 1
+ }
+
for _, prefix := range prefixen {
k, err := m.openInterfaceKey(prefix)
if err != nil {
@@ -509,13 +568,13 @@ func (m *windowsManager) disableDynamicUpdates() error {
}
defer k.Close()
- if err := k.SetDWordValue("RegistrationEnabled", 0); err != nil {
+ if err := k.SetDWordValue("RegistrationEnabled", registrationEnabled); err != nil {
return err
}
- if err := k.SetDWordValue("DisableDynamicUpdate", 1); err != nil {
+ if err := k.SetDWordValue("DisableDynamicUpdate", disableDynamicUpdate); err != nil {
return err
}
- if err := k.SetDWordValue("MaxNumberOfAddressesToRegister", 0); err != nil {
+ if err := k.SetDWordValue("MaxNumberOfAddressesToRegister", maxNumberOfAddressesToRegister); err != nil {
return err
}
}
diff --git a/util/syspolicy/policy_keys.go b/util/syspolicy/policy_keys.go
index 8da0e0cc8..29b2dfd28 100644
--- a/util/syspolicy/policy_keys.go
+++ b/util/syspolicy/policy_keys.go
@@ -63,6 +63,14 @@ const (
ExitNodeAllowLANAccess Key = "ExitNodeAllowLANAccess"
EnableTailscaleDNS Key = "UseTailscaleDNSSettings"
EnableTailscaleSubnets Key = "UseTailscaleSubnets"
+
+ // EnableDNSRegistration is a string value that can be set to "always", "never"
+ // or "user-decides". It controls whether DNS registration and dynamic DNS
+ // updates are enabled for the Tailscale interface. For historical reasons
+ // and to maintain compatibility with existing setups, the default is "never".
+ // It is only used on Windows.
+ EnableDNSRegistration Key = "EnableDNSRegistration"
+
// CheckUpdates is the key to signal if the updater should periodically
// check for updates.
CheckUpdates Key = "CheckUpdates"
@@ -168,6 +176,7 @@ var implicitDefinitions = []*setting.Definition{
setting.NewDefinition(CheckUpdates, setting.DeviceSetting, setting.PreferenceOptionValue),
setting.NewDefinition(ControlURL, setting.DeviceSetting, setting.StringValue),
setting.NewDefinition(DeviceSerialNumber, setting.DeviceSetting, setting.StringValue),
+ setting.NewDefinition(EnableDNSRegistration, setting.DeviceSetting, setting.PreferenceOptionValue),
setting.NewDefinition(EnableIncomingConnections, setting.DeviceSetting, setting.PreferenceOptionValue),
setting.NewDefinition(EnableRunExitNode, setting.DeviceSetting, setting.PreferenceOptionValue),
setting.NewDefinition(EnableServerMode, setting.DeviceSetting, setting.PreferenceOptionValue),
diff --git a/util/syspolicy/syspolicy.go b/util/syspolicy/syspolicy.go
index 5d5a283fb..afcc28ff1 100644
--- a/util/syspolicy/syspolicy.go
+++ b/util/syspolicy/syspolicy.go
@@ -90,6 +90,13 @@ func GetPreferenceOption(name Key) (setting.PreferenceOption, error) {
return getCurrentPolicySettingValue(name, setting.ShowChoiceByPolicy)
}
+// GetPreferenceOptionOrDefault is like [GetPreferenceOption], but allows
+// specifying a default value to return if the policy setting is not configured.
+// It can be used in situations where "user-decides" is not the default.
+func GetPreferenceOptionOrDefault(name Key, defaultValue setting.PreferenceOption) (setting.PreferenceOption, error) {
+ return getCurrentPolicySettingValue(name, defaultValue)
+}
+
// GetVisibility loads a policy from the registry that can be managed
// by an enterprise policy management system and describes show/hide decisions
// for UI elements. The registry value should be a string set to "show" (return