net/dns, util/winutil: improve detection of group policy affecting NRPT
Due to a customer issue, I investigated the Windows Dnscache service more intensively. I learned that the only time it attempts to read the NRPT from group policy is in response to a group policy change notification. Under the hypothesis that policy refresh is not effectively delivering GP notifications due to its dependency on reaching a DC, I replaced our use of the RefreshPolicyEx with the quasi-documented GenerateGPNotification API. Tests have been updated to ensure they check that they are running as LocalSystem, which is required for GenerateGPNotification. Fixes #20187 Signed-off-by: Aaron Klotz <aaron@tailscale.com>
This commit is contained in:
+26
-32
@@ -7,7 +7,6 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
@@ -19,7 +18,6 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
dnsBaseGP = `SOFTWARE\Policies\Microsoft\Windows NT\DNSClient`
|
||||
nrptBaseLocal = `SYSTEM\CurrentControlSet\Services\Dnscache\Parameters\DnsPolicyConfig`
|
||||
nrptBaseGP = `SOFTWARE\Policies\Microsoft\Windows NT\DNSClient\DnsPolicyConfig`
|
||||
|
||||
@@ -53,13 +51,12 @@ const (
|
||||
// nrptRuleDatabase encapsulates access to the Windows Name Resolution Policy
|
||||
// Table (NRPT).
|
||||
type nrptRuleDatabase struct {
|
||||
logf logger.Logf
|
||||
watcher *gp.ChangeWatcher
|
||||
isGPRefreshPending atomic.Bool
|
||||
mu sync.Mutex // protects the fields below
|
||||
ruleIDs []string
|
||||
isGPDirty bool
|
||||
writeAsGP bool
|
||||
logf logger.Logf
|
||||
watcher *gp.ChangeWatcher
|
||||
mu sync.Mutex // protects the fields below
|
||||
ruleIDs []string
|
||||
isGPDirty bool
|
||||
writeAsGP bool
|
||||
}
|
||||
|
||||
func newNRPTRuleDatabase(logf logger.Logf) *nrptRuleDatabase {
|
||||
@@ -141,6 +138,12 @@ func (db *nrptRuleDatabase) detectWriteAsGP() {
|
||||
writeAsGP = len(gpSubkeyMap) > 0
|
||||
}
|
||||
|
||||
func regKeyHasNoValues(ki *registry.KeyInfo) bool {
|
||||
// Either the key has no values, or there is one value: an empty default value.
|
||||
return ki.ValueCount == 0 ||
|
||||
ki.ValueCount == 1 && ki.MaxValueNameLen == 0 && ki.MaxValueLen == 0
|
||||
}
|
||||
|
||||
// DelAllRuleKeys removes any and all NRPT rules that are owned by Tailscale.
|
||||
func (db *nrptRuleDatabase) DelAllRuleKeys() error {
|
||||
db.mu.Lock()
|
||||
@@ -171,7 +174,7 @@ func (db *nrptRuleDatabase) delRuleKeys(nrptRuleIDs []string) error {
|
||||
keyNameGP := nrptBaseGP + `\` + rid
|
||||
err := registry.DeleteKey(registry.LOCAL_MACHINE, keyNameGP)
|
||||
if err == nil {
|
||||
// If this deleted subkey existed under the GP key, we will need to refresh.
|
||||
// If this deleted subkey existed under the GP key, we will need to notify.
|
||||
db.isGPDirty = true
|
||||
} else if err != registry.ErrNotExist {
|
||||
db.logf("Error deleting NRPT rule key %q: %v", keyNameGP, err)
|
||||
@@ -193,8 +196,8 @@ func (db *nrptRuleDatabase) delRuleKeys(nrptRuleIDs []string) error {
|
||||
return registry.DeleteKey(registry.LOCAL_MACHINE, nrptBaseGP)
|
||||
}
|
||||
|
||||
// isPolicyConfigSubkeyEmpty returns true if and only if the nrptBaseGP exists
|
||||
// and does not contain any values or subkeys.
|
||||
// isPolicyConfigSubkeyEmpty returns whether the nrptBaseGP exists and does not
|
||||
// contain any values or subkeys.
|
||||
func isPolicyConfigSubkeyEmpty() (bool, error) {
|
||||
subKey, err := registry.OpenKey(registry.LOCAL_MACHINE, nrptBaseGP, registry.READ)
|
||||
if err != nil {
|
||||
@@ -210,7 +213,7 @@ func isPolicyConfigSubkeyEmpty() (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return (ki.ValueCount == 0 && ki.SubKeyCount == 0), nil
|
||||
return regKeyHasNoValues(ki) && ki.SubKeyCount == 0, nil
|
||||
}
|
||||
|
||||
func (db *nrptRuleDatabase) WriteSplitDNSConfig(servers []string, domains []dnsname.FQDN) error {
|
||||
@@ -277,26 +280,22 @@ func (db *nrptRuleDatabase) WriteSplitDNSConfig(servers []string, domains []dnsn
|
||||
return nil
|
||||
}
|
||||
|
||||
// Refresh notifies the Windows group policy engine when policies have changed.
|
||||
func (db *nrptRuleDatabase) Refresh() {
|
||||
// NotifyPolicyChanged notifies the Windows group policy engine when policies have changed.
|
||||
func (db *nrptRuleDatabase) NotifyPolicyChanged() {
|
||||
db.mu.Lock()
|
||||
defer db.mu.Unlock()
|
||||
|
||||
db.refreshLocked()
|
||||
db.notifyPolicyChangedLocked()
|
||||
}
|
||||
|
||||
func (db *nrptRuleDatabase) refreshLocked() {
|
||||
func (db *nrptRuleDatabase) notifyPolicyChangedLocked() {
|
||||
if !db.isGPDirty {
|
||||
return
|
||||
}
|
||||
|
||||
// Record that we are about to initiate a refresh.
|
||||
// (*nrptRuleDatabase).watchForGPChanges() checks this value to avoid false
|
||||
// positives.
|
||||
db.isGPRefreshPending.Store(true)
|
||||
|
||||
if err := gp.RefreshMachinePolicy(true); err != nil {
|
||||
db.logf("RefreshMachinePolicy failed: %v", err)
|
||||
db.logf("Calling NotifyMachinePolicyChange")
|
||||
if err := gp.NotifyMachinePolicyChange(); err != nil {
|
||||
db.logf("NotifyMachinePolicyChange failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -355,12 +354,7 @@ func writeNRPTValues(key registry.Key, servers string, doms []string) error {
|
||||
|
||||
func (db *nrptRuleDatabase) watchForGPChanges() {
|
||||
watchHandler := func() {
|
||||
// Do not invoke detectWriteAsGP when we ourselves were responsible for
|
||||
// initiating the group policy refresh.
|
||||
if db.isGPRefreshPending.CompareAndSwap(true, false) {
|
||||
return
|
||||
}
|
||||
db.logf("Computer group policies refreshed, reconfiguring NRPT rule database.")
|
||||
db.logf("Received machine group policy change notification. Reconfiguring NRPT rule database.")
|
||||
db.detectWriteAsGP()
|
||||
}
|
||||
|
||||
@@ -380,8 +374,8 @@ func (db *nrptRuleDatabase) watchForGPChanges() {
|
||||
// db.mu must already be locked.
|
||||
func (db *nrptRuleDatabase) updateGroupPoliciesLocked(writeAsGP bool) {
|
||||
// Since we're updating the group policy NRPT table, we need
|
||||
// to refresh once this updateGroupPoliciesLocked is done.
|
||||
defer db.refreshLocked()
|
||||
// to notify once this updateGroupPoliciesLocked is done.
|
||||
defer db.notifyPolicyChangedLocked()
|
||||
|
||||
for _, id := range db.ruleIDs {
|
||||
if writeAsGP {
|
||||
|
||||
Reference in New Issue
Block a user