net/dns,docs/windows/policy,util/syspolicy: register Tailscale IP addresses in AD DNS if required by policy

In this PR, we make DNS registration behavior configurable via the EnableDNSRegistration policy setting.
We keep the default behavior unchanged, but allow admins to either enforce DNS registration and dynamic
DNS updates for the Tailscale interface, or prevent Tailscale from modifying the settings configured in
the network adapter's properties or by other means.

Updates #14917

Signed-off-by: Nick Khyl <nickk@tailscale.com>
This commit is contained in:
Nick Khyl 2025-05-08 18:38:48 -05:00 committed by Nick Khyl
parent 2c16fcaa06
commit fb188c5b53
5 changed files with 121 additions and 12 deletions

View File

@ -17,6 +17,7 @@
<string id="SINCE_V1_74">Tailscale version 1.74.0 and later</string>
<string id="SINCE_V1_78">Tailscale version 1.78.0 and later</string>
<string id="SINCE_V1_82">Tailscale version 1.82.0 and later</string>
<string id="SINCE_V1_84">Tailscale version 1.84.0 and later</string>
<string id="Tailscale_Category">Tailscale</string>
<string id="UI_Category">UI customization</string>
<string id="Settings_Category">Settings</string>
@ -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.]]></string>
<string id="AlwaysRegister">Always register</string>
<string id="UseAdapterProperties">Use adapter properties</string>
<string id="DNSRegistration">Register Tailscale IP addresses in DNS</string>
<string id="DNSRegistration_Help"><![CDATA[This policy setting controls whether Tailscale IP addresses are registered in DNS and whether dynamic DNS updates are enabled for the Tailscale interface.
If you enable this policy, you can specify whether Tailscale IP addresses should always be registered in DNS or allow the settings to be changed in the network adapter's properties (admin rights required). The "Always" option is recommended in Active Directory domain environments where all devices, including Domain Controllers, have Tailscale installed and are reachable via their Tailscale IP addresses.
If you disable or do not configure this policy, Tailscale IP addresses will not be registered in DNS, and dynamic DNS updates will be disabled for the Tailscale interface. It can be used on non-domain-joined devices or in environments that primarily rely on subnet routers.]]></string>
<string id="InstallUpdates">Automatically install updates</string>
<string id="InstallUpdates_Help"><![CDATA[This policy can be used to require that Automatically Install Updates is configured a certain way.
@ -299,6 +308,9 @@ See https://tailscale.com/kb/1315/mdm-keys#set-your-organization-name for more d
<label>Exit Node:</label>
</textBox>
</presentation>
<presentation id="DNSRegistration">
<dropdownList refId="DNSRegistration_Mode" noSort="true" defaultItem="0">Registration mode:</dropdownList>
</presentation>
<presentation id="AllowedSuggestedExitNodes">
<listBox refId="AllowedSuggestedExitNodesList">Target IDs:</listBox>
</presentation>

View File

@ -58,6 +58,10 @@
displayName="$(string.SINCE_V1_82)">
<and><reference ref="TAILSCALE_PRODUCT"/></and>
</definition>
<definition name="SINCE_V1_84"
displayName="$(string.SINCE_V1_84)">
<and><reference ref="TAILSCALE_PRODUCT"/></and>
</definition>
</definitions>
</supportedOn>
<categories>
@ -193,6 +197,24 @@
<string>never</string>
</disabledValue>
</policy>
<policy name="DNSRegistration" class="Machine" displayName="$(string.DNSRegistration)" explainText="$(string.DNSRegistration_Help)" presentation="$(presentation.DNSRegistration)" key="Software\Policies\Tailscale" valueName="EnableDNSRegistration">
<parentCategory ref="Settings_Category" />
<supportedOn ref="SINCE_V1_84" />
<elements>
<enum id="DNSRegistration_Mode" valueName="EnableDNSRegistration">
<item displayName="$(string.AlwaysRegister)">
<value>
<string>always</string>
</value>
</item>
<item displayName="$(string.UseAdapterProperties)">
<value>
<string>user-decides</string>
</value>
</item>
</enum>
</elements>
</policy>
<policy name="InstallUpdates" class="Machine" displayName="$(string.InstallUpdates)" explainText="$(string.InstallUpdates_Help)" key="Software\Policies\Tailscale" valueName="InstallUpdates">
<parentCategory ref="Settings_Category" />
<supportedOn ref="PARTIAL_FULL_SINCE_V1_56" />

View File

@ -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
}
}

View File

@ -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),

View File

@ -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