@ -7,6 +7,8 @@ package dns
import (
"fmt"
"strings"
"sync"
"sync/atomic"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/registry"
@ -33,11 +35,25 @@ const (
// This is the name of the registry value we use to save Rule IDs under
// the Tailscale registry key.
nrptRuleIDValueName = ` NRPTRuleIDs `
// This is the name of the registry value the NRPT uses for storing a rule's version number.
nrptRuleVersionName = ` Version `
// This is the name of the registry value the NRPT uses for storing a rule's list of domains.
nrptRuleDomsName = ` Name `
// This is the name of the registry value the NRPT uses for storing a rule's list of DNS servers.
nrptRuleServersName = ` GenericDNSServers `
// This is the name of the registry value the NRPT uses for storing a rule's flags.
nrptRuleFlagsName = ` ConfigOptions `
)
var (
libUserenv = windows . NewLazySystemDLL ( "userenv.dll" )
procRefreshPolicyEx = libUserenv . NewProc ( "RefreshPolicyEx" )
procRegisterGPNotification = libUserenv . NewProc ( "RegisterGPNotification" )
procUnregisterGPNotification = libUserenv . NewProc ( "UnregisterGPNotification" )
)
const _RP_FORCE = 1 // Flag for RefreshPolicyEx
@ -46,16 +62,19 @@ const _RP_FORCE = 1 // Flag for RefreshPolicyEx
// Table (NRPT).
type nrptRuleDatabase struct {
logf logger . Logf
watcher * gpNotificationWatcher
isGPRefreshPending atomic . Value // of bool
mu sync . Mutex // protects the fields below
ruleIDs [ ] string
writeAsGP bool
isGPDirty bool
writeAsGP bool
}
func newNRPTRuleDatabase ( logf logger . Logf ) * nrptRuleDatabase {
ret := & nrptRuleDatabase { logf : logf }
ret . loadRuleSubkeyNames ( )
ret . ini tWriteAsGP( )
logf ( "nrptRuleDatabase using group policy: %v\n" , ret . writeAsGP )
ret . detec tWriteAsGP( )
ret . watchForGPChanges ( )
// 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
@ -75,14 +94,28 @@ func (db *nrptRuleDatabase) loadRuleSubkeyNames() {
db . ruleIDs = result
}
// ini tWriteAsGP determines which registry path should be used for writing
// detec tWriteAsGP determines which registry path should be used for writing
// NRPT rules. If there are rules in the GP path that don't belong to us, then
// we should use the GP path.
func ( db * nrptRuleDatabase ) initWriteAsGP ( ) {
// we should use the GP path. When detectWriteAsGP determines that the desired
// path has changed, it moves the NRPT policies as appropriate.
func ( db * nrptRuleDatabase ) detectWriteAsGP ( ) {
db . mu . Lock ( )
defer db . mu . Unlock ( )
writeAsGP := false
var err error
defer func ( ) {
if err != nil {
db . writeAsGP = false
return
}
prev := db . writeAsGP
db . writeAsGP = writeAsGP
db . logf ( "nrptRuleDatabase using group policy: %v, was %v\n" , writeAsGP , prev )
// When db.watcher == nil, prev != writeAsGP because we're initializing, not
// because anything has changed. We do not invoke db.movePolicies in that case.
if db . watcher != nil && prev != writeAsGP {
db . movePolicies ( writeAsGP )
}
} ( )
@ -101,14 +134,13 @@ func (db *nrptRuleDatabase) initWriteAsGP() {
// If the dnsKey contains any values, then we need to use the GP key.
if ki . ValueCount > 0 {
db. writeAsGP = true
writeAsGP = true
return
}
if ki . SubKeyCount == 0 {
// If dnsKey contains no values and no subkeys, then we definitely don't
// need to use the GP key.
db . writeAsGP = false
return
}
@ -139,11 +171,14 @@ func (db *nrptRuleDatabase) initWriteAsGP() {
// Any leftover rules do not belong to us. When group policy is being used
// by something else, we must also use the GP path.
db. writeAsGP = len ( gpSubkeyMap ) > 0
writeAsGP = len ( gpSubkeyMap ) > 0
}
// DelAllRuleKeys removes any and all NRPT rules that are owned by Tailscale.
func ( db * nrptRuleDatabase ) DelAllRuleKeys ( ) error {
db . mu . Lock ( )
defer db . mu . Unlock ( )
if err := db . delRuleKeys ( db . ruleIDs ) ; err != nil {
return err
}
@ -212,6 +247,9 @@ func isPolicyConfigSubkeyEmpty() (bool, error) {
}
func ( db * nrptRuleDatabase ) WriteSplitDNSConfig ( servers [ ] string , domains [ ] dnsname . FQDN ) error {
db . mu . Lock ( )
defer db . mu . Unlock ( )
// 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.
@ -224,6 +262,7 @@ func (db *nrptRuleDatabase) WriteSplitDNSConfig(servers []string, domains []dnsn
}
db . loadRuleSubkeyNames ( )
for len ( db . ruleIDs ) < domainRulesLen {
guid , err := windows . GenerateGUID ( )
if err != nil {
@ -280,9 +319,22 @@ func (db *nrptRuleDatabase) WriteSplitDNSConfig(servers []string, domains []dnsn
// Refresh notifies the Windows group policy engine when policies have changed.
func ( db * nrptRuleDatabase ) Refresh ( ) {
db . mu . Lock ( )
defer db . mu . Unlock ( )
db . refreshLocked ( )
}
func ( db * nrptRuleDatabase ) refreshLocked ( ) {
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 )
ok , _ , err := procRefreshPolicyEx . Call (
uintptr ( 1 ) , // Win32 TRUE: Refresh computer policy, not user policy.
uintptr ( _RP_FORCE ) ,
@ -291,6 +343,7 @@ func (db *nrptRuleDatabase) Refresh() {
db . logf ( "RefreshPolicyEx failed: %v" , err )
return
}
db . isGPDirty = false
}
@ -310,22 +363,256 @@ func (db *nrptRuleDatabase) writeNRPTRule(ruleID string, servers, doms []string)
return fmt . Errorf ( "opening %s: %w" , keyStr , err )
}
defer key . Close ( )
if err := key . SetDWordValue ( "Version" , 1 ) ; err != nil {
if err := writeNRPTValues ( key , strings . Join ( servers , "; " ) , doms ) ; err != nil {
return err
}
if err := key . SetStringsValue ( "Name" , doms ) ; err != nil {
db . isGPDirty = db . writeAsGP
return nil
}
func readNRPTValues ( key registry . Key ) ( servers string , doms [ ] string , err error ) {
doms , _ , err = key . GetStringsValue ( nrptRuleDomsName )
if err != nil {
return servers , doms , err
}
servers , _ , err = key . GetStringValue ( nrptRuleServersName )
return servers , doms , err
}
func writeNRPTValues ( key registry . Key , servers string , doms [ ] string ) error {
if err := key . SetDWordValue ( nrptRuleVersionName , 1 ) ; err != nil {
return err
}
if err := key . SetStringValue ( "GenericDNSServers" , strings . Join ( servers , "; " ) ) ; err != nil {
if err := key . SetStringsValue ( nrptRuleDomsName , doms ) ; err != nil {
return err
}
if err := key . SetDWordValue ( "ConfigOptions" , nrptOverrideDNS ) ; err != nil {
if err := key . SetStringValue ( nrptRuleServersName , servers ) ; err != nil {
return err
}
if db . writeAsGP {
return key . SetDWordValue ( nrptRuleFlagsName , nrptOverrideDNS )
}
func ( db * nrptRuleDatabase ) watchForGPChanges ( ) {
db . isGPRefreshPending . Store ( false )
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 . detectWriteAsGP ( )
}
watcher , err := newGPNotificationWatcher ( watchHandler )
if err != nil {
return
}
db . watcher = watcher
}
// movePolicies moves each NRPT rule depending on the value of writeAsGP.
// When writeAsGP is true, each NRPT rule is moved from the local NRPT table
// to the group policy NRPT table. When writeAsGP is false, the move is
// executed in the opposite direction. db.mu should already be locked.
func ( db * nrptRuleDatabase ) movePolicies ( writeAsGP bool ) {
// Since we're moving either in or out of the group policy NRPT table, we need
// to refresh once this movePolicies is done.
defer db . refreshLocked ( )
var fromBase string
var toBase string
if writeAsGP {
fromBase = nrptBaseLocal
toBase = nrptBaseGP
} else {
fromBase = nrptBaseGP
toBase = nrptBaseLocal
}
fromBase += ` \ `
toBase += ` \ `
for _ , id := range db . ruleIDs {
fromStr := fromBase + id
toStr := toBase + id
if err := executeMove ( fromStr , toStr ) ; err != nil {
db . logf ( "movePolicies: executeMove(\"%s\", \"%s\") failed with error %v" , fromStr , toStr , err )
return
}
db . isGPDirty = true
}
if writeAsGP {
return
}
// Now that we have moved our rules out of the group policy subkey, it should
// now be empty. Let's verify that.
isEmpty , err := isPolicyConfigSubkeyEmpty ( )
if err != nil {
db . logf ( "movePolicies: isPolicyConfigSubkeyEmpty error %v" , err )
return
}
if ! isEmpty {
db . logf ( "movePolicies: policy config subkey should be empty, but isn't!" )
return
}
// Delete the subkey itself. Group policy will continue to override local
// settings unless we do so.
if err := registry . DeleteKey ( registry . LOCAL_MACHINE , nrptBaseGP ) ; err != nil {
db . logf ( "movePolicies DeleteKey error %v" , err )
}
db . isGPDirty = true
}
func executeMove ( subKeyFrom , subKeyTo string ) error {
err := func ( ) error {
// Move the NRPT registry values from subKeyFrom to subKeyTo.
fromKey , err := registry . OpenKey ( registry . LOCAL_MACHINE , subKeyFrom , registry . QUERY_VALUE )
if err != nil {
return err
}
defer fromKey . Close ( )
toKey , _ , err := registry . CreateKey ( registry . LOCAL_MACHINE , subKeyTo , registry . WRITE )
if err != nil {
return err
}
defer toKey . Close ( )
servers , doms , err := readNRPTValues ( fromKey )
if err != nil {
return err
}
return writeNRPTValues ( toKey , servers , doms )
} ( )
if err != nil {
return err
}
// This is a move operation, so we must delete subKeyFrom.
return registry . DeleteKey ( registry . LOCAL_MACHINE , subKeyFrom )
}
func ( db * nrptRuleDatabase ) Close ( ) error {
if db . watcher == nil {
return nil
}
err := db . watcher . Close ( )
db . watcher = nil
return err
}
type gpNotificationWatcher struct {
gpWaitEvents [ 2 ] windows . Handle
handler func ( )
done chan struct { }
}
// newGPNotificationWatcher creates an instance of gpNotificationWatcher that
// invokes handler every time Windows notifies it of a group policy change.
func newGPNotificationWatcher ( handler func ( ) ) ( * gpNotificationWatcher , error ) {
var err error
// evtDone is signaled by (*gpNotificationWatcher).Close() to indicate that
// the doWatch goroutine should exit.
evtDone , err := windows . CreateEvent ( nil , 0 , 0 , nil )
if err != nil {
return nil , err
}
defer func ( ) {
if err != nil {
windows . CloseHandle ( evtDone )
}
} ( )
// evtChanged is registered with the Windows policy engine to become
// signalled any time group policy has been refreshed.
evtChanged , err := windows . CreateEvent ( nil , 0 , 0 , nil )
if err != nil {
return nil , err
}
defer func ( ) {
if err != nil {
windows . CloseHandle ( evtChanged )
}
} ( )
// Tell Windows to signal evtChanged whenever group policies are refreshed.
ok , _ , e := procRegisterGPNotification . Call (
uintptr ( evtChanged ) ,
uintptr ( 1 ) , // Win32 TRUE: We want to monitor computer policy changes, not user policy changes.
)
if ok == 0 {
err = e
return nil , err
}
result := & gpNotificationWatcher {
// Ordering of the event handles in gpWaitEvents is important:
// When calling windows.WaitForMultipleObjects and multiple objects are
// signalled simultaneously, it always returns the wait code for the
// lowest-indexed handle in its input array. evtDone is higher priority for
// us than evtChanged, so the former must be placed into the array ahead of
// the latter.
gpWaitEvents : [ 2 ] windows . Handle {
evtDone ,
evtChanged ,
} ,
handler : handler ,
done : make ( chan struct { } ) ,
}
go result . doWatch ( )
return result , nil
}
func ( w * gpNotificationWatcher ) doWatch ( ) {
// The wait code corresponding to the event that is signalled when a group
// policy change occurs.
const expectedWaitCode = windows . WAIT_OBJECT_0 + 1
for {
if waitCode , _ := windows . WaitForMultipleObjects ( w . gpWaitEvents [ : ] , false , windows . INFINITE ) ; waitCode != expectedWaitCode {
break
}
w . handler ( )
}
close ( w . done )
}
func ( w * gpNotificationWatcher ) Close ( ) error {
// Notify doWatch that we're done and it should exit.
if err := windows . SetEvent ( w . gpWaitEvents [ 0 ] ) ; err != nil {
return err
}
procUnregisterGPNotification . Call ( uintptr ( w . gpWaitEvents [ 1 ] ) )
// Wait for doWatch to complete.
<- w . done
// Now we may safely clean up all the things.
for i , evt := range w . gpWaitEvents {
windows . CloseHandle ( evt )
w . gpWaitEvents [ i ] = 0
}
w . handler = nil
return nil
}