From 1003c0a3c89d90c61d3b8f891e39091b48dc3d13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Fri, 17 Oct 2025 10:55:05 +0200 Subject: [PATCH 1/4] Refresh policies after configuring NRPT under group policy key --- lib/vnet/osconfig_windows.go | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/lib/vnet/osconfig_windows.go b/lib/vnet/osconfig_windows.go index b480d2d7a5a67..0de66462b44e5 100644 --- a/lib/vnet/osconfig_windows.go +++ b/lib/vnet/osconfig_windows.go @@ -25,9 +25,15 @@ import ( "strings" "github.com/gravitational/trace" + "golang.org/x/sys/windows" "golang.org/x/sys/windows/registry" ) +var ( + userenv = windows.NewLazySystemDLL("userenv.dll") + procRefreshPolicyEx = userenv.NewProc("RefreshPolicyEx") +) + // platformOSConfigState holds state about which addresses and routes have // already been configured in the OS. Experimentally, IP routing seems to be // flaky/broken on Windows when the same routes are repeatedly configured, as we @@ -175,8 +181,17 @@ func configureDNS(ctx context.Context, zones, nameservers []string) error { } nrptRegKey = groupPolicyNRPTParentKey + `\` + vnetNRPTKeyID - return trace.Wrap(configureDNSAtNRPTKey(ctx, nrptRegKey, zones, nameservers), - "configuring DNS NRPT at group policy path %s", nrptRegKey) + if err := configureDNSAtNRPTKey(ctx, nrptRegKey, zones, nameservers); err != nil { + return trace.Wrap(err, "configuring DNS NRPT at group policy path %s", nrptRegKey) + } + // In some cases, rules under groupPolicyKey don't seem to be picked up by the DNS client service + // until the computer refreshes its policies. [1] A force refresh here ensures they're picked up + // immediately. See also https://github.com/gravitational/teleport/issues/60468. + // 1: https://github.com/tailscale/tailscale/issues/4607#issuecomment-1130586168 + if err := forceRefreshComputerPolicies(); err != nil { + return trace.Wrap(err, "refreshing computer policies") + } + return nil } func configureDNSAtNRPTKey(ctx context.Context, nrptRegKey string, zones, nameservers []string) (err error) { @@ -262,3 +277,20 @@ func deleteRegistryKey(key string) error { keyHandle.Close() return trace.Wrap(deleteErr, "failed to delete DNS registry key %s", key) } + +// https://learn.microsoft.com/en-us/windows/win32/api/userenv/nf-userenv-refreshpolicyex +func forceRefreshComputerPolicies() error { + // rp_force is a flag for RefreshPolicyEx which makes it reapply all policies even if no policy + // change was detected. + const rp_force = 1 + + retVal, _, err := procRefreshPolicyEx.Call( + // Refresh computer policies. + uintptr(1), + uintptr(rp_force), + ) + if retVal == 0 { + return trace.Wrap(err) + } + return nil +} From ad1a3893d59b3ca8fb81044c83045f81d9ef9c09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Mon, 1 Dec 2025 11:23:06 +0100 Subject: [PATCH 2/4] Configure DNS only when zones or nameservers change --- lib/vnet/osconfig_windows.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/vnet/osconfig_windows.go b/lib/vnet/osconfig_windows.go index 0de66462b44e5..036958de5be56 100644 --- a/lib/vnet/osconfig_windows.go +++ b/lib/vnet/osconfig_windows.go @@ -49,6 +49,11 @@ type platformOSConfigState struct { configuredRanges []string ifaceIndex string + + // configuredDNSZones caches DNS zones so DNS is reconfigured when they change. + configuredDNSZones []string + // configuredDNSAddrs caches DNS addresses so DNS is reconfigured when they change. + configuredDNSAddrs []string } func (p *platformOSConfigState) getIfaceIndex() (string, error) { @@ -118,8 +123,15 @@ func platformConfigureOS(ctx context.Context, cfg *osConfig, state *platformOSCo state.configuredV6Address = true } - if err := configureDNS(ctx, cfg.dnsZones, cfg.dnsAddrs); err != nil { - return trace.Wrap(err, "configuring DNS") + // Configure DNS only if the DNS zones or addresses have changed. This typically happens when the + // user logs in or out of a cluster. Otherwise configureDNS would refresh all computer policies + // every 10 seconds when platformConfigureOS is called. + if !slices.Equal(cfg.dnsZones, state.configuredDNSZones) || !slices.Equal(cfg.dnsAddrs, state.configuredDNSAddrs) { + if err := configureDNS(ctx, cfg.dnsZones, cfg.dnsAddrs); err != nil { + return trace.Wrap(err, "configuring DNS") + } + state.configuredDNSZones = cfg.dnsZones + state.configuredDNSAddrs = cfg.dnsAddrs } return nil @@ -199,7 +211,7 @@ func configureDNSAtNRPTKey(ctx context.Context, nrptRegKey string, zones, namese // Can't handle any zones if there are no nameservers. zones = nil } - log.InfoContext(ctx, "Configuring DNS.", "zones", zones, "nameservers", nameservers) + log.InfoContext(ctx, "Configuring DNS.", "reg_key", nrptRegKey, "zones", zones, "nameservers", nameservers) if len(zones) == 0 { // Either we have no zones we want to handle (the user is not From 5f0948a1f09c77632fa2df21743549bd6d85a45b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Mon, 1 Dec 2025 12:08:26 +0100 Subject: [PATCH 3/4] Take group policy key existence into account as well --- lib/vnet/osconfig_windows.go | 39 ++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/lib/vnet/osconfig_windows.go b/lib/vnet/osconfig_windows.go index 036958de5be56..c3749edd28512 100644 --- a/lib/vnet/osconfig_windows.go +++ b/lib/vnet/osconfig_windows.go @@ -54,6 +54,9 @@ type platformOSConfigState struct { configuredDNSZones []string // configuredDNSAddrs caches DNS addresses so DNS is reconfigured when they change. configuredDNSAddrs []string + // configuredGroupPolicyKey caches existence of the group policy key so DNS is reconfigured when + // the key is created or removed. + configuredGroupPolicyKey bool } func (p *platformOSConfigState) getIfaceIndex() (string, error) { @@ -126,12 +129,19 @@ func platformConfigureOS(ctx context.Context, cfg *osConfig, state *platformOSCo // Configure DNS only if the DNS zones or addresses have changed. This typically happens when the // user logs in or out of a cluster. Otherwise configureDNS would refresh all computer policies // every 10 seconds when platformConfigureOS is called. - if !slices.Equal(cfg.dnsZones, state.configuredDNSZones) || !slices.Equal(cfg.dnsAddrs, state.configuredDNSAddrs) { - if err := configureDNS(ctx, cfg.dnsZones, cfg.dnsAddrs); err != nil { + doesGroupPolicyKeyExist, err := doesKeyPathExist(registry.LOCAL_MACHINE, groupPolicyNRPTParentKey) + if err != nil { + return trace.Wrap(err, "checking existence of group policy NRPT registry key %s", groupPolicyNRPTParentKey) + } + if !slices.Equal(cfg.dnsZones, state.configuredDNSZones) || + !slices.Equal(cfg.dnsAddrs, state.configuredDNSAddrs) || + doesGroupPolicyKeyExist != state.configuredGroupPolicyKey { + if err := configureDNS(ctx, cfg.dnsZones, cfg.dnsAddrs, doesGroupPolicyKeyExist); err != nil { return trace.Wrap(err, "configuring DNS") } state.configuredDNSZones = cfg.dnsZones state.configuredDNSAddrs = cfg.dnsAddrs + state.configuredGroupPolicyKey = doesGroupPolicyKeyExist } return nil @@ -166,7 +176,7 @@ const ( vnetNRPTKeyID = `{ad074e9a-bd1b-447e-9108-14e545bf11a5}` ) -func configureDNS(ctx context.Context, zones, nameservers []string) error { +func configureDNS(ctx context.Context, zones, nameservers []string, doesGroupPolicyKeyExist bool) error { // Always configure NRPT rules under the local system NRPT registry key. // This is harmless/innefective if groupPolicyNRPTParentKey exists, but // always writing the rules here means they will be effective if @@ -180,17 +190,10 @@ func configureDNS(ctx context.Context, zones, nameservers []string) error { // systemNRPTParentKey will be ignored and rules under // groupPolicyNRPTParentKey take precendence, so VNet needs to write rules // under this key as well. - groupPolicyKey, err := registry.OpenKey(registry.LOCAL_MACHINE, groupPolicyNRPTParentKey, registry.READ) - if err != nil { - if !errors.Is(err, registry.ErrNotExist) { - return trace.Wrap(err, "opening group policy NRPT registry key %s", groupPolicyNRPTParentKey) - } + if !doesGroupPolicyKeyExist { // The group policy parent key doesn't exist, no need to write under it. return nil } - if err := groupPolicyKey.Close(); err != nil { - return trace.Wrap(err, "closing registry key %s", groupPolicyNRPTParentKey) - } nrptRegKey = groupPolicyNRPTParentKey + `\` + vnetNRPTKeyID if err := configureDNSAtNRPTKey(ctx, nrptRegKey, zones, nameservers); err != nil { @@ -206,6 +209,20 @@ func configureDNS(ctx context.Context, zones, nameservers []string) error { return nil } +func doesKeyPathExist(k registry.Key, path string) (bool, error) { + key, err := registry.OpenKey(k, path, registry.READ) + if err != nil { + if !errors.Is(err, registry.ErrNotExist) { + return false, trace.Wrap(err, "opening registry key %s", path) + } + return false, nil + } + if err := key.Close(); err != nil { + return true, trace.Wrap(err, "closing registry key %s", path) + } + return true, nil +} + func configureDNSAtNRPTKey(ctx context.Context, nrptRegKey string, zones, nameservers []string) (err error) { if len(nameservers) == 0 { // Can't handle any zones if there are no nameservers. From 64bd0618a1f38956da95b2e53d53c8cfab6dbc29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Mon, 8 Dec 2025 11:29:32 +0100 Subject: [PATCH 4/4] Add const for first arg of RefreshPolicyEx --- lib/vnet/osconfig_windows.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/vnet/osconfig_windows.go b/lib/vnet/osconfig_windows.go index c3749edd28512..27a387b89a788 100644 --- a/lib/vnet/osconfig_windows.go +++ b/lib/vnet/osconfig_windows.go @@ -309,14 +309,16 @@ func deleteRegistryKey(key string) error { // https://learn.microsoft.com/en-us/windows/win32/api/userenv/nf-userenv-refreshpolicyex func forceRefreshComputerPolicies() error { - // rp_force is a flag for RefreshPolicyEx which makes it reapply all policies even if no policy - // change was detected. - const rp_force = 1 + // refreshComputerPolicies corresponds to the first argument of RefreshPolicyEx which specifies + // whether to refresh computer or user policies. + const refreshComputerPolicies = 1 + // rpForce corresponds to the RP_FORCE flag for RefreshPolicyEx which makes it reapply all + // policies even if no policy change was detected. + const rpForce = 1 retVal, _, err := procRefreshPolicyEx.Call( - // Refresh computer policies. - uintptr(1), - uintptr(rp_force), + uintptr(refreshComputerPolicies), + uintptr(rpForce), ) if retVal == 0 { return trace.Wrap(err)