mirror of
https://github.com/tailscale/tailscale.git
synced 2025-12-03 02:21:58 +00:00
net/dns: update Windows split DNS settings to work alongside other NRPT entries set by group policy.
When there are group policy entries for the NRPT that do not belong to Tailscale, we recognize that we need to add ourselves to group policy and use that registry key instead of the local one. We also refresh the group policy settings as necessary to ensure that our changes take effect immediately. Fixes https://github.com/tailscale/tailscale/issues/4607 Signed-off-by: Aaron Klotz <aaron@tailscale.com>
This commit is contained in:
@@ -20,27 +20,12 @@ import (
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/winutil"
|
||||
)
|
||||
|
||||
const (
|
||||
ipv4RegBase = `SYSTEM\CurrentControlSet\Services\Tcpip\Parameters`
|
||||
ipv6RegBase = `SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters`
|
||||
|
||||
nrptBase = `SYSTEM\CurrentControlSet\Services\Dnscache\Parameters\DnsPolicyConfig\`
|
||||
nrptOverrideDNS = 0x8 // bitmask value for "use the provided override DNS resolvers"
|
||||
|
||||
// This is the legacy rule ID that previous versions used when we supported
|
||||
// only a single rule. Now that we support multiple rules are required, we
|
||||
// generate their GUIDs and store them under the Tailscale registry key.
|
||||
nrptSingleRuleID = `{5abe529b-675b-4486-8459-25a634dacc23}`
|
||||
// Apparently NRPT rules cannot handle > 50 domains.
|
||||
nrptMaxDomainsPerRule = 50
|
||||
|
||||
// This is the name of the registry value we use to save Rule IDs under
|
||||
// the Tailscale registry key.
|
||||
nrptRuleIDValueName = `NRPTRuleIDs`
|
||||
|
||||
versionKey = `SOFTWARE\Microsoft\Windows NT\CurrentVersion`
|
||||
)
|
||||
|
||||
@@ -49,35 +34,19 @@ var configureWSL = envknob.Bool("TS_DEBUG_CONFIGURE_WSL")
|
||||
type windowsManager struct {
|
||||
logf logger.Logf
|
||||
guid string
|
||||
nrptWorks bool
|
||||
nrptDB *nrptRuleDatabase
|
||||
wslManager *wslManager
|
||||
}
|
||||
|
||||
func loadRuleSubkeyNames() []string {
|
||||
result := winutil.GetRegStrings(nrptRuleIDValueName, nil)
|
||||
if result == nil {
|
||||
// Use the legacy rule ID if none are specified in our registry key
|
||||
result = []string{nrptSingleRuleID}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func NewOSConfigurator(logf logger.Logf, interfaceName string) (OSConfigurator, error) {
|
||||
ret := windowsManager{
|
||||
logf: logf,
|
||||
guid: interfaceName,
|
||||
nrptWorks: isWindows10OrBetter(),
|
||||
wslManager: newWSLManager(logf),
|
||||
}
|
||||
|
||||
// Best-effort: if our NRPT rule exists, try to delete it. Unlike
|
||||
// per-interface configuration, NRPT rules survive the unclean
|
||||
// termination of the Tailscale process, and depending on the
|
||||
// rule, it may prevent us from reaching login.tailscale.com to
|
||||
// boot up. The bootstrap resolver logic will save us, but it
|
||||
// slows down start-up a bunch.
|
||||
if ret.nrptWorks {
|
||||
ret.delAllRuleKeys()
|
||||
if isWindows10OrBetter() {
|
||||
ret.nrptDB = newNRPTRuleDatabase(logf)
|
||||
}
|
||||
|
||||
// Log WSL status once at startup.
|
||||
@@ -108,29 +77,6 @@ func (m windowsManager) ifPath(basePath string) string {
|
||||
return fmt.Sprintf(`%s\Interfaces\%s`, basePath, m.guid)
|
||||
}
|
||||
|
||||
func (m windowsManager) delAllRuleKeys() error {
|
||||
nrptRuleIDs := loadRuleSubkeyNames()
|
||||
if err := m.delRuleKeys(nrptRuleIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := winutil.DeleteRegValue(nrptRuleIDValueName); err != nil {
|
||||
m.logf("Error deleting registry value %q: %v", nrptRuleIDValueName, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m windowsManager) delRuleKeys(nrptRuleIDs []string) error {
|
||||
for _, rid := range nrptRuleIDs {
|
||||
keyName := nrptBase + rid
|
||||
if err := registry.DeleteKey(registry.LOCAL_MACHINE, keyName); err != nil && err != registry.ErrNotExist {
|
||||
m.logf("Error deleting NRPT rule key %q: %v", keyName, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func delValue(key registry.Key, name string) error {
|
||||
if err := key.DeleteValue(name); err != nil && err != registry.ErrNotExist {
|
||||
return err
|
||||
@@ -144,8 +90,17 @@ func delValue(key registry.Key, name string) error {
|
||||
//
|
||||
// If no resolvers are provided, the Tailscale NRPT rules are deleted.
|
||||
func (m windowsManager) setSplitDNS(resolvers []netaddr.IP, domains []dnsname.FQDN) error {
|
||||
if m.nrptDB == nil {
|
||||
if resolvers == nil {
|
||||
// Just a no-op in this case.
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("Split DNS unsupported on this Windows version")
|
||||
}
|
||||
|
||||
defer m.nrptDB.Refresh()
|
||||
if len(resolvers) == 0 {
|
||||
return m.delAllRuleKeys()
|
||||
return m.nrptDB.DelAllRuleKeys()
|
||||
}
|
||||
|
||||
servers := make([]string, 0, len(resolvers))
|
||||
@@ -153,92 +108,7 @@ func (m windowsManager) setSplitDNS(resolvers []netaddr.IP, domains []dnsname.FQ
|
||||
servers = append(servers, resolver.String())
|
||||
}
|
||||
|
||||
// NRPT has an undocumented restriction that each rule may only be associated
|
||||
// with a maximum of 50 domains. If we are setting rules for more domains
|
||||
// than that, we need to split domains into chunks and write out a rule per chunk.
|
||||
dq := len(domains) / nrptMaxDomainsPerRule
|
||||
dr := len(domains) % nrptMaxDomainsPerRule
|
||||
|
||||
domainRulesLen := dq
|
||||
if dr > 0 {
|
||||
domainRulesLen++
|
||||
}
|
||||
|
||||
nrptRuleIDs := loadRuleSubkeyNames()
|
||||
for len(nrptRuleIDs) < domainRulesLen {
|
||||
guid, err := windows.GenerateGUID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nrptRuleIDs = append(nrptRuleIDs, guid.String())
|
||||
}
|
||||
|
||||
// Remove any surplus rules that are no longer needed.
|
||||
ruleIDsToRemove := nrptRuleIDs[domainRulesLen:]
|
||||
m.delRuleKeys(ruleIDsToRemove)
|
||||
|
||||
// We need to save the list of rule IDs to our Tailscale registry key so that
|
||||
// we know which rules are ours during subsequent modifications to NRPT rules.
|
||||
ruleIDsToWrite := nrptRuleIDs[:domainRulesLen]
|
||||
if len(ruleIDsToWrite) > 0 {
|
||||
if err := winutil.SetRegStrings(nrptRuleIDValueName, ruleIDsToWrite); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := winutil.DeleteRegValue(nrptRuleIDValueName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
doms := make([]string, 0, nrptMaxDomainsPerRule)
|
||||
for i := 0; i < domainRulesLen; i++ {
|
||||
// Each iteration consumes nrptMaxDomainsPerRule domains...
|
||||
curLen := nrptMaxDomainsPerRule
|
||||
// Except for the final iteration: when we have a remainder, use that instead.
|
||||
if i == domainRulesLen-1 && dr > 0 {
|
||||
curLen = dr
|
||||
}
|
||||
|
||||
// Obtain the slice of domains to consume within the current iteration.
|
||||
start := i * nrptMaxDomainsPerRule
|
||||
end := start + curLen
|
||||
for _, domain := range domains[start:end] {
|
||||
// NRPT rules must have a leading dot, which is not usual for
|
||||
// DNS search paths.
|
||||
doms = append(doms, "."+domain.WithoutTrailingDot())
|
||||
}
|
||||
|
||||
if err := writeNRPTRule(nrptRuleIDs[i], doms, servers); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
doms = doms[:0]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeNRPTRule(ruleID string, doms, servers []string) error {
|
||||
// CreateKey is actually open-or-create, which suits us fine.
|
||||
key, _, err := registry.CreateKey(registry.LOCAL_MACHINE, nrptBase+ruleID, registry.SET_VALUE)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening %s: %w", nrptBase+ruleID, err)
|
||||
}
|
||||
defer key.Close()
|
||||
if err := key.SetDWordValue("Version", 1); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := key.SetStringsValue("Name", doms); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := key.SetStringValue("GenericDNSServers", strings.Join(servers, "; ")); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := key.SetDWordValue("ConfigOptions", nrptOverrideDNS); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return m.nrptDB.WriteSplitDNSConfig(servers, domains)
|
||||
}
|
||||
|
||||
// setPrimaryDNS sets the given resolvers and domains as the Tailscale
|
||||
@@ -352,7 +222,7 @@ func (m windowsManager) SetDNS(cfg OSConfig) error {
|
||||
if err := m.setPrimaryDNS(cfg.Nameservers, cfg.SearchDomains); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if !m.nrptWorks {
|
||||
} else if m.nrptDB == nil {
|
||||
return errors.New("cannot set per-domain resolvers on Windows 7")
|
||||
} else {
|
||||
if err := m.setSplitDNS(cfg.Nameservers, cfg.MatchDomains); err != nil {
|
||||
@@ -418,7 +288,7 @@ func (m windowsManager) SetDNS(cfg OSConfig) error {
|
||||
}
|
||||
|
||||
func (m windowsManager) SupportsSplitDNS() bool {
|
||||
return m.nrptWorks
|
||||
return m.nrptDB != nil
|
||||
}
|
||||
|
||||
func (m windowsManager) Close() error {
|
||||
|
||||
Reference in New Issue
Block a user