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>
92 lines
3.3 KiB
Go
92 lines
3.3 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
// Package gp contains [Group Policy]-related functions and types.
|
|
//
|
|
// [Group Policy]: https://web.archive.org/web/20240630210707/https://learn.microsoft.com/en-us/previous-versions/windows/desktop/policy/group-policy-start-page
|
|
package gp
|
|
|
|
import (
|
|
"fmt"
|
|
"runtime"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
// Scope is a user or machine policy scope.
|
|
type Scope int
|
|
|
|
const (
|
|
// MachinePolicy indicates a machine policy.
|
|
// Registry-based machine policies reside in HKEY_LOCAL_MACHINE.
|
|
MachinePolicy Scope = iota
|
|
// UserPolicy indicates a user policy.
|
|
// Registry-based user policies reside in HKEY_CURRENT_USER of the corresponding user.
|
|
UserPolicy
|
|
)
|
|
|
|
// _RP_FORCE causes RefreshPolicyEx to reapply policy even if no policy change was detected.
|
|
// See [RP_FORCE] for details.
|
|
//
|
|
// [RP_FORCE]: https://web.archive.org/save/https://learn.microsoft.com/en-us/windows/win32/api/userenv/nf-userenv-refreshpolicyex
|
|
const _RP_FORCE = 0x1
|
|
|
|
// RefreshUserPolicy triggers a machine policy refresh, but does not wait for it to complete.
|
|
// When the force parameter is true, it causes the Group Policy to reapply policy even
|
|
// if no policy change was detected.
|
|
func RefreshMachinePolicy(force bool) error {
|
|
return refreshPolicyEx(true, toRefreshPolicyFlags(force))
|
|
}
|
|
|
|
// RefreshUserPolicy triggers a user policy refresh, but does not wait for it to complete.
|
|
// When the force parameter is true, it causes the Group Policy to reapply policy even
|
|
// if no policy change was detected.
|
|
//
|
|
// The token indicates user whose policy should be refreshed.
|
|
// If specified, the token must be either a primary token with TOKEN_QUERY and TOKEN_DUPLICATE
|
|
// access, or an impersonation token with TOKEN_QUERY and TOKEN_IMPERSONATE access,
|
|
// and the specified user must be logged in interactively.
|
|
//
|
|
// Otherwise, a zero token value indicates the current user. It should not
|
|
// be used by services or other applications running under system identities.
|
|
//
|
|
// The function fails with windows.ERROR_ACCESS_DENIED if the user represented by the token
|
|
// is not logged in interactively at the time of the call.
|
|
func RefreshUserPolicy(token windows.Token, force bool) error {
|
|
if token != 0 {
|
|
// Impersonate the user whose policy we need to refresh.
|
|
runtime.LockOSThread()
|
|
defer runtime.UnlockOSThread()
|
|
if err := impersonateLoggedOnUser(token); err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
if err := windows.RevertToSelf(); err != nil {
|
|
// RevertToSelf errors are non-recoverable.
|
|
panic(fmt.Errorf("could not revert impersonation: %w", err))
|
|
}
|
|
}()
|
|
}
|
|
|
|
return refreshPolicyEx(true, toRefreshPolicyFlags(force))
|
|
}
|
|
|
|
func toRefreshPolicyFlags(force bool) uint32 {
|
|
if force {
|
|
return _RP_FORCE
|
|
}
|
|
return 0
|
|
}
|
|
|
|
var mgmtProductTailscale = unsafe.SliceData([]uint16{'T', 'a', 'i', 'l', 's', 'c', 'a', 'l', 'e', 0})
|
|
|
|
// NotifyMachinePolicyChange sends a machine-scoped group policy change
|
|
// notification which Windows broadcasts to group policy change subscribers.
|
|
// The caller must be running as LocalSystem.
|
|
func NotifyMachinePolicyChange() error {
|
|
// GenerateGPNotification is quasi-documented. Note that its implementation
|
|
// contains an access check ensuring that the calling user is LocalSystem!
|
|
return generateGPNotification(true, mgmtProductTailscale, 0)
|
|
}
|