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:
Aaron Klotz
2026-07-10 13:53:26 -06:00
parent a68be19739
commit 2b62cb54a7
7 changed files with 102 additions and 58 deletions
+23 -25
View File
@@ -85,23 +85,23 @@ func TestHostFileChanged(t *testing.T) {
}
func TestManagerWindowsLocal(t *testing.T) {
if !isWindows10OrBetter() || !winutil.IsCurrentProcessElevated() {
t.Skipf("test requires running as elevated user on Windows 10+")
if !winutil.IsCurrentProcessLocalSystem() {
t.Skipf("test requires running as LocalSystem on Windows 10+")
}
runTest(t, true)
}
func TestManagerWindowsGP(t *testing.T) {
if !isWindows10OrBetter() || !winutil.IsCurrentProcessElevated() {
t.Skipf("test requires running as elevated user on Windows 10+")
if !winutil.IsCurrentProcessLocalSystem() {
t.Skipf("test requires running as LocalSystem on Windows 10+")
}
checkGPNotificationsWork(t)
// Make sure group policy is refreshed before this test exits but after we've
// Make sure group policy is current before this test exits but after we've
// cleaned everything else up.
defer gp.RefreshMachinePolicy(true)
defer gp.NotifyMachinePolicyChange()
err := createFakeGPKey()
if err != nil {
@@ -113,8 +113,8 @@ func TestManagerWindowsGP(t *testing.T) {
}
func TestManagerWindowsGPCopy(t *testing.T) {
if !isWindows10OrBetter() || !winutil.IsCurrentProcessElevated() {
t.Skipf("test requires running as elevated user on Windows 10+")
if !winutil.IsCurrentProcessLocalSystem() {
t.Skipf("test requires running as LocalSystem on Windows 10+")
}
checkGPNotificationsWork(t)
@@ -179,9 +179,9 @@ func TestManagerWindowsGPCopy(t *testing.T) {
t.Fatalf("regWatcher.watch: %v\n", err)
}
err = gp.RefreshMachinePolicy(true)
err = gp.NotifyMachinePolicyChange()
if err != nil {
t.Fatalf("testDoRefresh: %v\n", err)
t.Fatalf("NotifyMachinePolicyChange: %v\n", err)
}
err = regWatcher.wait()
@@ -203,9 +203,9 @@ func TestManagerWindowsGPCopy(t *testing.T) {
t.Fatalf("regWatcher.watch: %v\n", err)
}
err = gp.RefreshMachinePolicy(true)
err = gp.NotifyMachinePolicyChange()
if err != nil {
t.Fatalf("testDoRefresh: %v\n", err)
t.Fatalf("NotifyMachinePolicyChange: %v\n", err)
}
err = regWatcher.wait()
@@ -236,13 +236,13 @@ func checkGPNotificationsWork(t *testing.T) {
}
defer trk.Close()
err = gp.RefreshMachinePolicy(true)
err = gp.NotifyMachinePolicyChange()
if err != nil {
t.Fatalf("RefreshPolicyEx error: %v\n", err)
t.Fatalf("NotifyMachinePolicyChange error: %v\n", err)
}
timeout := uint32(10000) // Milliseconds
if !trk.DidRefreshTimeout(timeout) {
if !trk.DidGroupPolicyChangeTimeout(timeout) {
t.Skipf("GP notifications are not working on this machine\n")
}
}
@@ -338,8 +338,8 @@ func runTest(t *testing.T, isLocal bool) {
}
validateRegistry(t, regBaseValidate, caseDomains)
ensureNoRulesInSubkey(t, regBaseEnsure)
if !isLocal && !trk.DidRefresh(true) {
t.Fatalf("DidRefresh false, want true\n")
if !isLocal && !trk.DidGroupPolicyChange(true) {
t.Fatalf("DidGroupPolicyChange false, want true\n")
}
}
@@ -347,10 +347,6 @@ func runTest(t *testing.T, isLocal bool) {
runCase(n)
}
if isLocal && trk.DidRefresh(false) {
t.Errorf("DidRefresh true, want false\n")
}
t.Logf("Test case: nil resolver\n")
err = mgr.setSplitDNS(nil, domains)
if err != nil {
@@ -573,7 +569,7 @@ var (
)
// gpNotificationTracker registers with the Windows policy engine and receives
// notifications when policy refreshes occur.
// notifications when policy change notifications occur.
type gpNotificationTracker struct {
event windows.Handle
}
@@ -602,7 +598,7 @@ func newGPNotificationTracker() (*gpNotificationTracker, error) {
return &gpNotificationTracker{evt}, nil
}
func (trk *gpNotificationTracker) DidRefresh(isExpected bool) bool {
func (trk *gpNotificationTracker) DidGroupPolicyChange(isExpected bool) bool {
// If we're not expecting a refresh event, then we need to use a timeout.
timeout := uint32(1000) // 1 second (in milliseconds)
if isExpected {
@@ -610,10 +606,10 @@ func (trk *gpNotificationTracker) DidRefresh(isExpected bool) bool {
timeout = windows.INFINITE
}
return trk.DidRefreshTimeout(timeout)
return trk.DidGroupPolicyChangeTimeout(timeout)
}
func (trk *gpNotificationTracker) DidRefreshTimeout(timeout uint32) bool {
func (trk *gpNotificationTracker) DidGroupPolicyChangeTimeout(timeout uint32) bool {
waitCode, _ := windows.WaitForSingleObject(trk.event, timeout)
return waitCode == windows.WAIT_OBJECT_0
}
@@ -625,6 +621,8 @@ func (trk *gpNotificationTracker) Close() error {
return nil
}
const dnsBaseGP = `SOFTWARE\Policies\Microsoft\Windows NT\DNSClient`
type regKeyWatcher struct {
keyGP registry.Key
evtGP windows.Handle