Browse Source

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 <[email protected]>
Aaron Klotz 3 years ago
parent
commit
b005b79236

+ 2 - 2
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

+ 54 - 54
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+

+ 111 - 17
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 {

+ 160 - 0
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
+}

+ 2 - 58
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
 	}

+ 137 - 0
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()
+}