From b005b79236cc5f4b659aa8d90809cd16f3cb6122 Mon Sep 17 00:00:00 2001 From: Aaron Klotz Date: Wed, 25 May 2022 15:51:54 -0600 Subject: [PATCH] net/dns, paths, util/winutil: change net/dns/windowsManager NRPT management to support more than 50 domains. AFAICT this isn't documented on MSDN, but based on the issue referenced below, NRPT rules are not working when a rule specifies > 50 domains. This patch modifies our NRPT rule generator to split the list of domains into chunks as necessary, and write a separate rule for each chunk. For compatibility reasons, we continue to use the hard-coded rule ID, but as additional rules are required, we generate new GUIDs. Those GUIDs are stored under the Tailscale registry path so that we know which rules are ours. I made some changes to winutils to add additional helper functions in support of both the code and its test: I added additional registry accessors, and also moved some token accessors from paths to util/winutil. Fixes https://github.com/tailscale/coral/issues/63 Signed-off-by: Aaron Klotz --- cmd/tailscale/depaware.txt | 4 +- cmd/tailscaled/depaware.txt | 108 ++++++++++----------- net/dns/manager_windows.go | 128 +++++++++++++++++++++---- net/dns/manager_windows_test.go | 160 ++++++++++++++++++++++++++++++++ paths/paths_windows.go | 60 +----------- util/winutil/winutil_windows.go | 137 +++++++++++++++++++++++++++ 6 files changed, 466 insertions(+), 131 deletions(-) create mode 100644 net/dns/manager_windows_test.go diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index bcc0ccdb7..b6863ac7f 100644 --- a/cmd/tailscale/depaware.txt +++ b/cmd/tailscale/depaware.txt @@ -63,7 +63,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep tailscale.com/net/tlsdial from tailscale.com/derp/derphttp tailscale.com/net/tsaddr from tailscale.com/net/interfaces+ 💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+ - 💣 tailscale.com/paths from tailscale.com/cmd/tailscale/cli+ + tailscale.com/paths from tailscale.com/cmd/tailscale/cli+ tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli+ tailscale.com/syncs from tailscale.com/net/interfaces+ tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+ @@ -88,7 +88,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep W tailscale.com/util/endian from tailscale.com/net/netns tailscale.com/util/groupmember from tailscale.com/cmd/tailscale/cli tailscale.com/util/lineread from tailscale.com/net/interfaces+ - W tailscale.com/util/winutil from tailscale.com/hostinfo + W 💣 tailscale.com/util/winutil from tailscale.com/hostinfo+ tailscale.com/version from tailscale.com/cmd/tailscale/cli+ tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+ tailscale.com/wgengine/filter from tailscale.com/types/netmap diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 9baa40f34..00521fcc9 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -113,7 +113,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de L 💣 github.com/vishvananda/netlink/nl from github.com/tailscale/netlink L github.com/vishvananda/netns from github.com/tailscale/netlink+ 💣 go4.org/intern from inet.af/netaddr - 💣 go4.org/mem from tailscale.com/client/tailscale+ + 💣 go4.org/mem from tailscale.com/control/controlbase+ go4.org/unsafe/assume-no-moving-gc from go4.org/intern W 💣 golang.zx2c4.com/wintun from golang.zx2c4.com/wireguard/tun 💣 golang.zx2c4.com/wireguard/conn from golang.zx2c4.com/wireguard/device+ @@ -140,9 +140,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de 💣 gvisor.dev/gvisor/pkg/state from gvisor.dev/gvisor/pkg/atomicbitops+ gvisor.dev/gvisor/pkg/state/wire from gvisor.dev/gvisor/pkg/state 💣 gvisor.dev/gvisor/pkg/sync from gvisor.dev/gvisor/pkg/linewriter+ - gvisor.dev/gvisor/pkg/tcpip from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+ + gvisor.dev/gvisor/pkg/tcpip from gvisor.dev/gvisor/pkg/tcpip/header+ gvisor.dev/gvisor/pkg/tcpip/adapters/gonet from tailscale.com/wgengine/netstack - 💣 gvisor.dev/gvisor/pkg/tcpip/buffer from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+ + 💣 gvisor.dev/gvisor/pkg/tcpip/buffer from gvisor.dev/gvisor/pkg/tcpip/header+ gvisor.dev/gvisor/pkg/tcpip/hash/jenkins from gvisor.dev/gvisor/pkg/tcpip/stack+ gvisor.dev/gvisor/pkg/tcpip/header from gvisor.dev/gvisor/pkg/tcpip/header/parse+ gvisor.dev/gvisor/pkg/tcpip/header/parse from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+ @@ -155,18 +155,18 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de gvisor.dev/gvisor/pkg/tcpip/network/ipv6 from tailscale.com/wgengine/netstack gvisor.dev/gvisor/pkg/tcpip/ports from gvisor.dev/gvisor/pkg/tcpip/stack+ gvisor.dev/gvisor/pkg/tcpip/seqnum from gvisor.dev/gvisor/pkg/tcpip/header+ - 💣 gvisor.dev/gvisor/pkg/tcpip/stack from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+ - gvisor.dev/gvisor/pkg/tcpip/transport from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+ + 💣 gvisor.dev/gvisor/pkg/tcpip/stack from gvisor.dev/gvisor/pkg/tcpip/header/parse+ + gvisor.dev/gvisor/pkg/tcpip/transport from gvisor.dev/gvisor/pkg/tcpip/transport/internal/network+ gvisor.dev/gvisor/pkg/tcpip/transport/icmp from tailscale.com/wgengine/netstack - gvisor.dev/gvisor/pkg/tcpip/transport/internal/network from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+ + gvisor.dev/gvisor/pkg/tcpip/transport/internal/network from gvisor.dev/gvisor/pkg/tcpip/transport/raw+ gvisor.dev/gvisor/pkg/tcpip/transport/internal/noop from gvisor.dev/gvisor/pkg/tcpip/transport/raw gvisor.dev/gvisor/pkg/tcpip/transport/packet from gvisor.dev/gvisor/pkg/tcpip/transport/raw - gvisor.dev/gvisor/pkg/tcpip/transport/raw from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+ + gvisor.dev/gvisor/pkg/tcpip/transport/raw from gvisor.dev/gvisor/pkg/tcpip/transport/udp+ 💣 gvisor.dev/gvisor/pkg/tcpip/transport/tcp from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+ gvisor.dev/gvisor/pkg/tcpip/transport/tcpconntrack from gvisor.dev/gvisor/pkg/tcpip/stack - gvisor.dev/gvisor/pkg/tcpip/transport/udp from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+ + gvisor.dev/gvisor/pkg/tcpip/transport/udp from tailscale.com/net/tstun+ gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context+ - inet.af/netaddr from inet.af/wf+ + inet.af/netaddr from tailscale.com/control/controlclient+ inet.af/peercred from tailscale.com/ipn/ipnserver W 💣 inet.af/wf from tailscale.com/wf L nhooyr.io/websocket from tailscale.com/derp/derphttp+ @@ -176,23 +176,23 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/atomicfile from tailscale.com/ipn+ LD tailscale.com/chirp from tailscale.com/cmd/tailscaled tailscale.com/client/tailscale from tailscale.com/derp - tailscale.com/client/tailscale/apitype from tailscale.com/client/tailscale+ - tailscale.com/cmd/tailscaled/childproc from tailscale.com/cmd/tailscaled+ + tailscale.com/client/tailscale/apitype from tailscale.com/ipn/ipnlocal+ + tailscale.com/cmd/tailscaled/childproc from tailscale.com/ssh/tailssh+ tailscale.com/control/controlbase from tailscale.com/control/controlclient+ - tailscale.com/control/controlclient from tailscale.com/cmd/tailscaled+ + tailscale.com/control/controlclient from tailscale.com/ipn/ipnlocal+ tailscale.com/control/controlhttp from tailscale.com/control/controlclient tailscale.com/control/controlknobs from tailscale.com/control/controlclient+ tailscale.com/derp from tailscale.com/derp/derphttp+ - tailscale.com/derp/derphttp from tailscale.com/cmd/tailscaled+ + tailscale.com/derp/derphttp from tailscale.com/net/netcheck+ L tailscale.com/derp/wsconn from tailscale.com/derp/derphttp tailscale.com/disco from tailscale.com/derp+ - tailscale.com/envknob from tailscale.com/cmd/tailscaled+ + tailscale.com/envknob from tailscale.com/control/controlclient+ tailscale.com/health from tailscale.com/control/controlclient+ tailscale.com/hostinfo from tailscale.com/control/controlclient+ - tailscale.com/ipn from tailscale.com/client/tailscale+ - tailscale.com/ipn/ipnlocal from tailscale.com/ipn/ipnserver+ + tailscale.com/ipn from tailscale.com/ipn/ipnlocal+ + tailscale.com/ipn/ipnlocal from tailscale.com/ssh/tailssh+ tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled - tailscale.com/ipn/ipnstate from tailscale.com/client/tailscale+ + tailscale.com/ipn/ipnstate from tailscale.com/control/controlclient+ tailscale.com/ipn/localapi from tailscale.com/ipn/ipnserver tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal tailscale.com/ipn/store from tailscale.com/cmd/tailscaled @@ -203,41 +203,41 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/log/filelogger from tailscale.com/logpolicy tailscale.com/log/logheap from tailscale.com/control/controlclient tailscale.com/logpolicy from tailscale.com/cmd/tailscaled+ - tailscale.com/logtail from tailscale.com/cmd/tailscaled+ - tailscale.com/logtail/backoff from tailscale.com/cmd/tailscaled+ + tailscale.com/logtail from tailscale.com/control/controlclient+ + tailscale.com/logtail/backoff from tailscale.com/control/controlclient+ tailscale.com/logtail/filch from tailscale.com/logpolicy 💣 tailscale.com/metrics from tailscale.com/derp+ - tailscale.com/net/dns from tailscale.com/cmd/tailscaled+ + tailscale.com/net/dns from tailscale.com/ipn/ipnlocal+ tailscale.com/net/dns/publicdns from tailscale.com/net/dns/resolver tailscale.com/net/dns/resolvconffile from tailscale.com/net/dns+ tailscale.com/net/dns/resolver from tailscale.com/ipn/ipnlocal+ tailscale.com/net/dnscache from tailscale.com/control/controlclient+ tailscale.com/net/dnsfallback from tailscale.com/control/controlclient+ tailscale.com/net/flowtrack from tailscale.com/net/packet+ - 💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscaled+ + 💣 tailscale.com/net/interfaces from tailscale.com/control/controlclient+ tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock tailscale.com/net/neterror from tailscale.com/net/dns/resolver+ - tailscale.com/net/netknob from tailscale.com/logpolicy+ - tailscale.com/net/netns from tailscale.com/cmd/tailscaled+ + tailscale.com/net/netknob from tailscale.com/net/netns+ + tailscale.com/net/netns from tailscale.com/derp/derphttp+ 💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnserver tailscale.com/net/netutil from tailscale.com/ipn/ipnlocal+ tailscale.com/net/packet from tailscale.com/net/tstun+ - tailscale.com/net/portmapper from tailscale.com/cmd/tailscaled+ + tailscale.com/net/portmapper from tailscale.com/net/netcheck+ tailscale.com/net/proxymux from tailscale.com/cmd/tailscaled tailscale.com/net/socks5 from tailscale.com/cmd/tailscaled tailscale.com/net/stun from tailscale.com/net/netcheck+ tailscale.com/net/tlsdial from tailscale.com/control/controlclient+ tailscale.com/net/tsaddr from tailscale.com/ipn+ - tailscale.com/net/tsdial from tailscale.com/cmd/tailscaled+ - 💣 tailscale.com/net/tshttpproxy from tailscale.com/cmd/tailscaled+ - tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+ - 💣 tailscale.com/paths from tailscale.com/client/tailscale+ + tailscale.com/net/tsdial from tailscale.com/control/controlclient+ + 💣 tailscale.com/net/tshttpproxy from tailscale.com/control/controlclient+ + tailscale.com/net/tstun from tailscale.com/net/dns+ + tailscale.com/paths from tailscale.com/ipn/ipnlocal+ tailscale.com/portlist from tailscale.com/ipn/ipnlocal tailscale.com/safesocket from tailscale.com/client/tailscale+ tailscale.com/smallzstd from tailscale.com/ipn/ipnserver+ LD 💣 tailscale.com/ssh/tailssh from tailscale.com/cmd/tailscaled tailscale.com/syncs from tailscale.com/control/controlknobs+ - tailscale.com/tailcfg from tailscale.com/client/tailscale+ + tailscale.com/tailcfg from tailscale.com/client/tailscale/apitype+ LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh W tailscale.com/tsconst from tailscale.com/net/interfaces tailscale.com/tstime from tailscale.com/wgengine/magicsock @@ -248,8 +248,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/types/empty from tailscale.com/control/controlclient+ tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled tailscale.com/types/ipproto from tailscale.com/net/flowtrack+ - tailscale.com/types/key from tailscale.com/cmd/tailscaled+ - tailscale.com/types/logger from tailscale.com/cmd/tailscaled+ + tailscale.com/types/key from tailscale.com/control/controlbase+ + tailscale.com/types/logger from tailscale.com/control/controlclient+ tailscale.com/types/netmap from tailscale.com/control/controlclient+ tailscale.com/types/nettype from tailscale.com/wgengine/magicsock tailscale.com/types/opt from tailscale.com/control/controlclient+ @@ -258,7 +258,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/types/preftype from tailscale.com/ipn+ tailscale.com/types/structs from tailscale.com/control/controlclient+ tailscale.com/types/views from tailscale.com/ipn/ipnlocal+ - tailscale.com/util/clientmetric from tailscale.com/cmd/tailscaled+ + tailscale.com/util/clientmetric from tailscale.com/control/controlclient+ LW tailscale.com/util/cmpver from tailscale.com/net/dns+ 💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+ tailscale.com/util/dnsname from tailscale.com/hostinfo+ @@ -266,23 +266,23 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/util/groupmember from tailscale.com/ipn/ipnserver tailscale.com/util/lineread from tailscale.com/hostinfo+ tailscale.com/util/mak from tailscale.com/control/controlclient+ - tailscale.com/util/multierr from tailscale.com/cmd/tailscaled+ + tailscale.com/util/multierr from tailscale.com/control/controlclient+ tailscale.com/util/netconv from tailscale.com/wgengine/magicsock - tailscale.com/util/osshare from tailscale.com/cmd/tailscaled+ + tailscale.com/util/osshare from tailscale.com/ipn/ipnlocal+ tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver tailscale.com/util/racebuild from tailscale.com/logpolicy tailscale.com/util/systemd from tailscale.com/control/controlclient+ tailscale.com/util/uniq from tailscale.com/wgengine/magicsock - tailscale.com/util/winutil from tailscale.com/cmd/tailscaled+ - tailscale.com/version from tailscale.com/cmd/tailscaled+ - tailscale.com/version/distro from tailscale.com/cmd/tailscaled+ + 💣 tailscale.com/util/winutil from tailscale.com/cmd/tailscaled+ + tailscale.com/version from tailscale.com/derp+ + tailscale.com/version/distro from tailscale.com/hostinfo+ W tailscale.com/wf from tailscale.com/cmd/tailscaled - tailscale.com/wgengine from tailscale.com/cmd/tailscaled+ + tailscale.com/wgengine from tailscale.com/ipn/ipnlocal+ tailscale.com/wgengine/filter from tailscale.com/control/controlclient+ tailscale.com/wgengine/magicsock from tailscale.com/ipn/ipnlocal+ - tailscale.com/wgengine/monitor from tailscale.com/cmd/tailscaled+ + tailscale.com/wgengine/monitor from tailscale.com/control/controlclient+ tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled - tailscale.com/wgengine/router from tailscale.com/cmd/tailscaled+ + tailscale.com/wgengine/router from tailscale.com/ipn/ipnlocal+ tailscale.com/wgengine/wgcfg from tailscale.com/ipn/ipnlocal+ tailscale.com/wgengine/wgcfg/nmcfg from tailscale.com/ipn/ipnlocal tailscale.com/wgengine/wglog from tailscale.com/wgengine @@ -320,7 +320,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+ LD golang.org/x/sys/unix from github.com/insomniacslk/dhcp/interfaces+ W golang.org/x/sys/windows from github.com/go-ole/go-ole+ - W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+ + W golang.org/x/sys/windows/registry from golang.org/x/sys/windows/svc/eventlog+ W golang.org/x/sys/windows/svc from golang.org/x/sys/windows/svc/mgr+ W golang.org/x/sys/windows/svc/eventlog from tailscale.com/cmd/tailscaled W golang.org/x/sys/windows/svc/mgr from tailscale.com/cmd/tailscaled @@ -354,10 +354,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de crypto/sha256 from crypto/tls+ crypto/sha512 from crypto/ecdsa+ crypto/subtle from crypto/aes+ - crypto/tls from github.com/aws/aws-sdk-go-v2/aws/transport/http+ + crypto/tls from github.com/tcnksm/go-httpstat+ crypto/x509 from crypto/tls+ crypto/x509/pkix from crypto/x509+ - embed from crypto/elliptic+ + embed from tailscale.com+ encoding from encoding/json+ encoding/asn1 from crypto/x509+ encoding/base64 from encoding/json+ @@ -365,19 +365,19 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de encoding/hex from crypto/x509+ encoding/json from expvar+ encoding/pem from crypto/tls+ - encoding/xml from github.com/aws/aws-sdk-go-v2/aws/protocol/xml+ + encoding/xml from github.com/tailscale/goupnp+ errors from bufio+ expvar from tailscale.com/derp+ - flag from tailscale.com/cmd/tailscaled+ + flag from tailscale.com/control/controlclient+ fmt from compress/flate+ hash from crypto+ hash/crc32 from compress/gzip+ - hash/fnv from gvisor.dev/gvisor/pkg/tcpip/network/ipv6+ + hash/fnv from tailscale.com/wgengine/magicsock+ hash/maphash from go4.org/mem - html from net/http/pprof+ + html from tailscale.com/ipn/ipnlocal+ io from bufio+ io/fs from crypto/rand+ - io/ioutil from github.com/aws/aws-sdk-go-v2/aws/protocol/query+ + io/ioutil from github.com/godbus/dbus/v5+ log from expvar+ LD log/syslog from tailscale.com/ssh/tailssh math from compress/flate+ @@ -394,19 +394,19 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de net/http/internal from net/http+ net/http/pprof from tailscale.com/cmd/tailscaled+ net/netip from golang.zx2c4.com/wireguard/conn+ - net/textproto from github.com/aws/aws-sdk-go-v2/aws/signer/v4+ + net/textproto from golang.org/x/net/http/httpguts+ net/url from crypto/x509+ os from crypto/rand+ - os/exec from github.com/aws/aws-sdk-go-v2/credentials/processcreds+ + os/exec from github.com/coreos/go-iptables/iptables+ os/signal from tailscale.com/cmd/tailscaled+ os/user from github.com/godbus/dbus/v5+ - path from github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds+ + path from github.com/godbus/dbus/v5+ path/filepath from crypto/x509+ reflect from crypto/x509+ - regexp from github.com/aws/aws-sdk-go-v2/internal/endpoints/v2+ + regexp from github.com/coreos/go-iptables/iptables+ regexp/syntax from regexp - runtime/debug from github.com/klauspost/compress/zstd+ - runtime/pprof from net/http/pprof+ + runtime/debug from golang.org/x/sync/singleflight+ + runtime/pprof from tailscale.com/log/logheap+ runtime/trace from net/http/pprof sort from compress/flate+ strconv from compress/flate+ diff --git a/net/dns/manager_windows.go b/net/dns/manager_windows.go index 7b60e6771..2d4857f19 100644 --- a/net/dns/manager_windows.go +++ b/net/dns/manager_windows.go @@ -20,18 +20,27 @@ import ( "tailscale.com/envknob" "tailscale.com/types/logger" "tailscale.com/util/dnsname" + "tailscale.com/util/winutil" ) const ( ipv4RegBase = `SYSTEM\CurrentControlSet\Services\Tcpip\Parameters` ipv6RegBase = `SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters` - // the GUID is randomly generated. At present, Tailscale installs - // zero or one NRPT rules, so hardcoding a single GUID everywhere - // is fine. - nrptBase = `SYSTEM\CurrentControlSet\services\Dnscache\Parameters\DnsPolicyConfig\{5abe529b-675b-4486-8459-25a634dacc23}` + nrptBase = `SYSTEM\CurrentControlSet\Services\Dnscache\Parameters\DnsPolicyConfig\` nrptOverrideDNS = 0x8 // bitmask value for "use the provided override DNS resolvers" + // This is the legacy rule ID that previous versions used when we supported + // only a single rule. Now that we support multiple rules are required, we + // generate their GUIDs and store them under the Tailscale registry key. + nrptSingleRuleID = `{5abe529b-675b-4486-8459-25a634dacc23}` + // Apparently NRPT rules cannot handle > 50 domains. + nrptMaxDomainsPerRule = 50 + + // This is the name of the registry value we use to save Rule IDs under + // the Tailscale registry key. + nrptRuleIDValueName = `NRPTRuleIDs` + versionKey = `SOFTWARE\Microsoft\Windows NT\CurrentVersion` ) @@ -44,6 +53,15 @@ type windowsManager struct { wslManager *wslManager } +func loadRuleSubkeyNames() []string { + result := winutil.GetRegStrings(nrptRuleIDValueName, nil) + if result == nil { + // Use the legacy rule ID if none are specified in our registry key + result = []string{nrptSingleRuleID} + } + return result +} + func NewOSConfigurator(logf logger.Logf, interfaceName string) (OSConfigurator, error) { ret := windowsManager{ logf: logf, @@ -59,7 +77,7 @@ func NewOSConfigurator(logf logger.Logf, interfaceName string) (OSConfigurator, // boot up. The bootstrap resolver logic will save us, but it // slows down start-up a bunch. if ret.nrptWorks { - ret.delKey(nrptBase) + ret.delAllRuleKeys() } // Log WSL status once at startup. @@ -90,10 +108,26 @@ func (m windowsManager) ifPath(basePath string) string { return fmt.Sprintf(`%s\Interfaces\%s`, basePath, m.guid) } -func (m windowsManager) delKey(path string) error { - if err := registry.DeleteKey(registry.LOCAL_MACHINE, path); err != nil && err != registry.ErrNotExist { +func (m windowsManager) delAllRuleKeys() error { + nrptRuleIDs := loadRuleSubkeyNames() + if err := m.delRuleKeys(nrptRuleIDs); err != nil { return err } + if err := winutil.DeleteRegValue(nrptRuleIDValueName); err != nil { + m.logf("Error deleting registry value %q: %v", nrptRuleIDValueName, err) + return err + } + return nil +} + +func (m windowsManager) delRuleKeys(nrptRuleIDs []string) error { + for _, rid := range nrptRuleIDs { + keyName := nrptBase + rid + if err := registry.DeleteKey(registry.LOCAL_MACHINE, keyName); err != nil && err != registry.ErrNotExist { + m.logf("Error deleting NRPT rule key %q: %v", keyName, err) + return err + } + } return nil } @@ -104,31 +138,91 @@ func delValue(key registry.Key, name string) error { return nil } -// setSplitDNS configures an NRPT (Name Resolution Policy Table) rule +// setSplitDNS configures one or more NRPT (Name Resolution Policy Table) rules // to resolve queries for domains using resolvers, rather than the // system's "primary" resolver. // -// If no resolvers are provided, the Tailscale NRPT rule is deleted. +// If no resolvers are provided, the Tailscale NRPT rules are deleted. func (m windowsManager) setSplitDNS(resolvers []netaddr.IP, domains []dnsname.FQDN) error { if len(resolvers) == 0 { - return m.delKey(nrptBase) + return m.delAllRuleKeys() } servers := make([]string, 0, len(resolvers)) for _, resolver := range resolvers { servers = append(servers, resolver.String()) } - doms := make([]string, 0, len(domains)) - for _, domain := range domains { - // NRPT rules must have a leading dot, which is not usual for - // DNS search paths. - doms = append(doms, "."+domain.WithoutTrailingDot()) + + // 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. + dq := len(domains) / nrptMaxDomainsPerRule + dr := len(domains) % nrptMaxDomainsPerRule + + domainRulesLen := dq + if dr > 0 { + domainRulesLen++ + } + + nrptRuleIDs := loadRuleSubkeyNames() + for len(nrptRuleIDs) < domainRulesLen { + guid, err := windows.GenerateGUID() + if err != nil { + return err + } + nrptRuleIDs = append(nrptRuleIDs, guid.String()) + } + + // Remove any surplus rules that are no longer needed. + ruleIDsToRemove := nrptRuleIDs[domainRulesLen:] + m.delRuleKeys(ruleIDsToRemove) + + // We need to save the list of rule IDs to our Tailscale registry key so that + // we know which rules are ours during subsequent modifications to NRPT rules. + ruleIDsToWrite := nrptRuleIDs[:domainRulesLen] + if len(ruleIDsToWrite) > 0 { + if err := winutil.SetRegStrings(nrptRuleIDValueName, ruleIDsToWrite); err != nil { + return err + } + } else { + if err := winutil.DeleteRegValue(nrptRuleIDValueName); err != nil { + return err + } + } + + doms := make([]string, 0, nrptMaxDomainsPerRule) + for i := 0; i < domainRulesLen; i++ { + // Each iteration consumes nrptMaxDomainsPerRule domains... + curLen := nrptMaxDomainsPerRule + // Except for the final iteration: when we have a remainder, use that instead. + if i == domainRulesLen-1 && dr > 0 { + curLen = dr + } + + // Obtain the slice of domains to consume within the current iteration. + start := i * nrptMaxDomainsPerRule + end := start + curLen + for _, domain := range domains[start:end] { + // NRPT rules must have a leading dot, which is not usual for + // DNS search paths. + doms = append(doms, "."+domain.WithoutTrailingDot()) + } + + if err := writeNRPTRule(nrptRuleIDs[i], doms, servers); err != nil { + return err + } + + doms = doms[:0] } + return nil +} + +func writeNRPTRule(ruleID string, doms, servers []string) error { // CreateKey is actually open-or-create, which suits us fine. - key, _, err := registry.CreateKey(registry.LOCAL_MACHINE, nrptBase, registry.SET_VALUE) + key, _, err := registry.CreateKey(registry.LOCAL_MACHINE, nrptBase+ruleID, registry.SET_VALUE) if err != nil { - return fmt.Errorf("opening %s: %w", nrptBase, err) + return fmt.Errorf("opening %s: %w", nrptBase+ruleID, err) } defer key.Close() if err := key.SetDWordValue("Version", 1); err != nil { diff --git a/net/dns/manager_windows_test.go b/net/dns/manager_windows_test.go new file mode 100644 index 000000000..820785a4b --- /dev/null +++ b/net/dns/manager_windows_test.go @@ -0,0 +1,160 @@ +// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package dns + +import ( + "math/rand" + "strings" + "testing" + "time" + + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/registry" + "inet.af/netaddr" + "tailscale.com/util/dnsname" + "tailscale.com/util/winutil" +) + +func TestManagerWindows(t *testing.T) { + if !winutil.IsCurrentProcessElevated() { + t.Skipf("test requires running as elevated user") + } + + logf := func(format string, args ...any) { + t.Logf(format, args...) + } + + fakeInterface, err := windows.GenerateGUID() + if err != nil { + t.Fatalf("windows.GenerateGUID: %v\n", err) + } + + cfg, err := NewOSConfigurator(logf, fakeInterface.String()) + if err != nil { + t.Fatalf("NewOSConfigurator: %v\n", err) + } + mgr := cfg.(windowsManager) + + // Upon initialization of cfg, we should not have any NRPT rules + ensureNoRules(t) + + resolvers := []netaddr.IP{netaddr.MustParseIP("1.1.1.1")} + + domains := make([]dnsname.FQDN, 0, 2*nrptMaxDomainsPerRule+1) + + r := rand.New(rand.NewSource(time.Now().UnixNano())) + const charset = "abcdefghijklmnopqrstuvwxyz" + + // Just generate a bunch of random subdomains + for len(domains) < cap(domains) { + l := r.Intn(19) + 1 + b := make([]byte, l) + for i, _ := range b { + b[i] = charset[r.Intn(len(charset))] + } + d := string(b) + ".example.com" + fqdn, err := dnsname.ToFQDN(d) + if err != nil { + t.Fatalf("dnsname.ToFQDN: %v\n", err) + } + domains = append(domains, fqdn) + } + + cases := []int{ + 1, + 50, + 51, + 100, + 101, + 100, + 50, + 1, + 51, + } + + for _, n := range cases { + t.Logf("Test case: %d domains\n", n) + caseDomains := domains[:n] + err := mgr.setSplitDNS(resolvers, caseDomains) + if err != nil { + t.Fatalf("setSplitDNS: %v\n", err) + } + validateRegistry(t, caseDomains) + } + + t.Logf("Test case: nil resolver\n") + err = mgr.setSplitDNS(nil, domains) + if err != nil { + t.Fatalf("setSplitDNS: %v\n", err) + } + ensureNoRules(t) +} + +func ensureNoRules(t *testing.T) { + ruleIDs := winutil.GetRegStrings(nrptRuleIDValueName, nil) + if ruleIDs != nil { + t.Errorf("%s: %v, want nil\n", nrptRuleIDValueName, ruleIDs) + } + + legacyKeyPath := nrptBase + nrptSingleRuleID + key, err := registry.OpenKey(registry.LOCAL_MACHINE, legacyKeyPath, registry.READ) + if err == nil { + key.Close() + } + if err != registry.ErrNotExist { + t.Errorf("%s: %q, want %q\n", legacyKeyPath, err, registry.ErrNotExist) + } +} + +func validateRegistry(t *testing.T, domains []dnsname.FQDN) { + q := len(domains) / nrptMaxDomainsPerRule + r := len(domains) % nrptMaxDomainsPerRule + numRules := q + if r > 0 { + numRules++ + } + + ruleIDs := winutil.GetRegStrings(nrptRuleIDValueName, nil) + if ruleIDs == nil { + ruleIDs = []string{nrptSingleRuleID} + } else if len(ruleIDs) != numRules { + t.Errorf("%s for %d domains: %d, want %d\n", nrptRuleIDValueName, len(domains), len(ruleIDs), numRules) + } + + for i, ruleID := range ruleIDs { + savedDomains, err := getSavedDomainsForRule(ruleID) + if err != nil { + t.Fatalf("getSavedDomainsForRule(%q): %v\n", ruleID, err) + } + + start := i * nrptMaxDomainsPerRule + end := start + nrptMaxDomainsPerRule + if i == len(ruleIDs)-1 && r > 0 { + end = start + r + } + + checkDomains := domains[start:end] + if len(checkDomains) != len(savedDomains) { + t.Errorf("len(checkDomains) != len(savedDomains): %d, want %d\n", len(savedDomains), len(checkDomains)) + } + for j, cd := range checkDomains { + sd := strings.TrimPrefix(savedDomains[j], ".") + if string(cd.WithoutTrailingDot()) != sd { + t.Errorf("checkDomain differs savedDomain: %s, want %s\n", sd, cd.WithoutTrailingDot()) + } + } + } +} + +func getSavedDomainsForRule(ruleID string) ([]string, error) { + keyPath := nrptBase + ruleID + key, err := registry.OpenKey(registry.LOCAL_MACHINE, keyPath, registry.READ) + if err != nil { + return nil, err + } + defer key.Close() + result, _, err := key.GetStringsValue("Name") + return result, err +} diff --git a/paths/paths_windows.go b/paths/paths_windows.go index 19f0b9298..f74be0200 100644 --- a/paths/paths_windows.go +++ b/paths/paths_windows.go @@ -8,67 +8,11 @@ import ( "os" "path/filepath" "strings" - "unsafe" "golang.org/x/sys/windows" + "tailscale.com/util/winutil" ) -func getTokenInfo(token windows.Token, infoClass uint32) ([]byte, error) { - var desiredLen uint32 - err := windows.GetTokenInformation(token, infoClass, nil, 0, &desiredLen) - if err != nil && err != windows.ERROR_INSUFFICIENT_BUFFER { - return nil, err - } - - buf := make([]byte, desiredLen) - actualLen := desiredLen - err = windows.GetTokenInformation(token, infoClass, &buf[0], desiredLen, &actualLen) - return buf, err -} - -func getTokenUserInfo(token windows.Token) (*windows.Tokenuser, error) { - buf, err := getTokenInfo(token, windows.TokenUser) - if err != nil { - return nil, err - } - - return (*windows.Tokenuser)(unsafe.Pointer(&buf[0])), nil -} - -func getTokenPrimaryGroupInfo(token windows.Token) (*windows.Tokenprimarygroup, error) { - buf, err := getTokenInfo(token, windows.TokenPrimaryGroup) - if err != nil { - return nil, err - } - - return (*windows.Tokenprimarygroup)(unsafe.Pointer(&buf[0])), nil -} - -type userSids struct { - User *windows.SID - PrimaryGroup *windows.SID -} - -func getCurrentUserSids() (*userSids, error) { - token, err := windows.OpenCurrentProcessToken() - if err != nil { - return nil, err - } - defer token.Close() - - userInfo, err := getTokenUserInfo(token) - if err != nil { - return nil, err - } - - primaryGroup, err := getTokenPrimaryGroupInfo(token) - if err != nil { - return nil, err - } - - return &userSids{userInfo.User.Sid, primaryGroup.PrimaryGroup}, nil -} - // ensureStateDirPerms applies a restrictive ACL to the directory specified by dirPath. // It sets the following security attributes on the directory: // Owner: The user for the current process; @@ -93,7 +37,7 @@ func ensureStateDirPerms(dirPath string) error { } // We need the info for our current user as SIDs - sids, err := getCurrentUserSids() + sids, err := winutil.GetCurrentUserSIDs() if err != nil { return err } diff --git a/util/winutil/winutil_windows.go b/util/winutil/winutil_windows.go index d52694ec8..08f212225 100644 --- a/util/winutil/winutil_windows.go +++ b/util/winutil/winutil_windows.go @@ -11,6 +11,7 @@ import ( "os/exec" "runtime" "syscall" + "unsafe" "golang.org/x/sys/windows" "golang.org/x/sys/windows/registry" @@ -94,6 +95,70 @@ func getRegStringInternal(subKey, name string) (string, error) { return val, nil } +// GetRegStrings looks up a registry value in the local machine path, or returns +// the given default if it can't. +func GetRegStrings(name string, defval []string) []string { + s, err := getRegStringsInternal(regBase, name) + if err != nil { + return defval + } + return s +} + +func getRegStringsInternal(subKey, name string) ([]string, error) { + key, err := registry.OpenKey(registry.LOCAL_MACHINE, subKey, registry.READ) + if err != nil { + log.Printf("registry.OpenKey(%v): %v", subKey, err) + return nil, err + } + defer key.Close() + + val, _, err := key.GetStringsValue(name) + if err != nil { + if err != registry.ErrNotExist { + log.Printf("registry.GetStringValue(%v): %v", name, err) + } + return nil, err + } + return val, nil +} + +// SetRegStrings sets a MULTI_SZ value in the in the local machine path +// to the strings specified by values. +func SetRegStrings(name string, values []string) error { + return setRegStringsInternal(regBase, name, values) +} + +func setRegStringsInternal(subKey, name string, values []string) error { + key, _, err := registry.CreateKey(registry.LOCAL_MACHINE, subKey, registry.SET_VALUE) + if err != nil { + log.Printf("registry.CreateKey(%v): %v", subKey, err) + } + defer key.Close() + + return key.SetStringsValue(name, values) +} + +// DeleteRegValue removes a registry value in the local machine path. +func DeleteRegValue(name string) error { + return deleteRegValueInternal(regBase, name) +} + +func deleteRegValueInternal(subKey, name string) error { + key, err := registry.OpenKey(registry.LOCAL_MACHINE, subKey, registry.SET_VALUE) + if err != nil { + log.Printf("registry.OpenKey(%v): %v", subKey, err) + return err + } + defer key.Close() + + err = key.DeleteValue(name) + if err == registry.ErrNotExist { + err = nil + } + return err +} + func getRegIntegerInternal(subKey, name string) (uint64, error) { key, err := registry.OpenKey(registry.LOCAL_MACHINE, subKey, registry.READ) if err != nil { @@ -254,3 +319,75 @@ func StartProcessAsCurrentGUIUser(exePath string, extraEnv []string) error { func CreateAppMutex(name string) (windows.Handle, error) { return windows.CreateMutex(nil, false, windows.StringToUTF16Ptr(name)) } + +func getTokenInfo(token windows.Token, infoClass uint32) ([]byte, error) { + var desiredLen uint32 + err := windows.GetTokenInformation(token, infoClass, nil, 0, &desiredLen) + if err != nil && err != windows.ERROR_INSUFFICIENT_BUFFER { + return nil, err + } + + buf := make([]byte, desiredLen) + actualLen := desiredLen + err = windows.GetTokenInformation(token, infoClass, &buf[0], desiredLen, &actualLen) + return buf, err +} + +func getTokenUserInfo(token windows.Token) (*windows.Tokenuser, error) { + buf, err := getTokenInfo(token, windows.TokenUser) + if err != nil { + return nil, err + } + + return (*windows.Tokenuser)(unsafe.Pointer(&buf[0])), nil +} + +func getTokenPrimaryGroupInfo(token windows.Token) (*windows.Tokenprimarygroup, error) { + buf, err := getTokenInfo(token, windows.TokenPrimaryGroup) + if err != nil { + return nil, err + } + + return (*windows.Tokenprimarygroup)(unsafe.Pointer(&buf[0])), nil +} + +// UserSIDs contains the SIDs for a Windows NT token object's associated user +// as well as its primary group. +type UserSIDs struct { + User *windows.SID + PrimaryGroup *windows.SID +} + +// GetCurrentUserSIDs returns a UserSIDs struct containing SIDs for the +// current process' user and primary group. +func GetCurrentUserSIDs() (*UserSIDs, error) { + token, err := windows.OpenCurrentProcessToken() + if err != nil { + return nil, err + } + defer token.Close() + + userInfo, err := getTokenUserInfo(token) + if err != nil { + return nil, err + } + + primaryGroup, err := getTokenPrimaryGroupInfo(token) + if err != nil { + return nil, err + } + + return &UserSIDs{userInfo.User.Sid, primaryGroup.PrimaryGroup}, nil +} + +// IsCurrentProcessElevated returns true when the current process is +// running with an elevated token, implying Administrator access. +func IsCurrentProcessElevated() bool { + token, err := windows.OpenCurrentProcessToken() + if err != nil { + return false + } + defer token.Close() + + return token.IsElevated() +}