|
|
@ -12,12 +12,19 @@ import (
|
|
|
|
"time"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"golang.org/x/sys/windows/registry"
|
|
|
|
"golang.org/x/sys/windows/registry"
|
|
|
|
|
|
|
|
"inet.af/netaddr"
|
|
|
|
"tailscale.com/types/logger"
|
|
|
|
"tailscale.com/types/logger"
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
const (
|
|
|
|
ipv4RegBase = `SYSTEM\CurrentControlSet\Services\Tcpip\Parameters`
|
|
|
|
ipv4RegBase = `SYSTEM\CurrentControlSet\Services\Tcpip\Parameters`
|
|
|
|
ipv6RegBase = `SYSTEM\CurrentControlSet\Services\Tcpip6\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}`
|
|
|
|
|
|
|
|
nrptOverrideDNS = 0x8 // bitmask value for "use the provided override DNS resolvers"
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
type windowsManager struct {
|
|
|
|
type windowsManager struct {
|
|
|
@ -26,10 +33,20 @@ type windowsManager struct {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func NewOSConfigurator(logf logger.Logf, interfaceName string) OSConfigurator {
|
|
|
|
func NewOSConfigurator(logf logger.Logf, interfaceName string) OSConfigurator {
|
|
|
|
return windowsManager{
|
|
|
|
ret := windowsManager{
|
|
|
|
logf: logf,
|
|
|
|
logf: logf,
|
|
|
|
guid: interfaceName,
|
|
|
|
guid: interfaceName,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
|
|
// rule, it may prevent us from reaching login.tailscale.com to
|
|
|
|
|
|
|
|
// boot up. The bootstrap resolver logic will save us, but it
|
|
|
|
|
|
|
|
// slows down start-up a bunch.
|
|
|
|
|
|
|
|
ret.delKey(nrptBase)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return ret
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// keyOpenTimeout is how long we wait for a registry key to
|
|
|
|
// keyOpenTimeout is how long we wait for a registry key to
|
|
|
@ -38,37 +55,86 @@ func NewOSConfigurator(logf logger.Logf, interfaceName string) OSConfigurator {
|
|
|
|
// can end up racing with that.
|
|
|
|
// can end up racing with that.
|
|
|
|
const keyOpenTimeout = 20 * time.Second
|
|
|
|
const keyOpenTimeout = 20 * time.Second
|
|
|
|
|
|
|
|
|
|
|
|
func setRegistryString(path, name, value string) error {
|
|
|
|
func (m windowsManager) openKey(path string) (registry.Key, error) {
|
|
|
|
key, err := openKeyWait(registry.LOCAL_MACHINE, path, registry.SET_VALUE, keyOpenTimeout)
|
|
|
|
key, err := openKeyWait(registry.LOCAL_MACHINE, path, registry.SET_VALUE, keyOpenTimeout)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("opening %s: %w", path, err)
|
|
|
|
return 0, fmt.Errorf("opening %s: %w", path, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
defer key.Close()
|
|
|
|
return key, nil
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
err = key.SetStringValue(name, value)
|
|
|
|
func (m windowsManager) ifPath(basePath string) string {
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Sprintf(`%s\Interfaces\%s`, basePath, m.guid)
|
|
|
|
return fmt.Errorf("setting %s[%s]: %w", path, name, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (m windowsManager) delKey(path string) error {
|
|
|
|
|
|
|
|
if err := registry.DeleteKey(registry.LOCAL_MACHINE, path); err != nil && err != registry.ErrNotExist {
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (m windowsManager) setNameservers(basePath string, nameservers []string) error {
|
|
|
|
func delValue(key registry.Key, name string) error {
|
|
|
|
path := fmt.Sprintf(`%s\Interfaces\%s`, basePath, m.guid)
|
|
|
|
if err := key.DeleteValue(name); err != nil && err != registry.ErrNotExist {
|
|
|
|
value := strings.Join(nameservers, ",")
|
|
|
|
return err
|
|
|
|
return setRegistryString(path, "NameServer", value)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (m windowsManager) setDomains(basePath string, domains []string) error {
|
|
|
|
// setSplitDNS configures an NRPT (Name Resolution Policy Table) rule
|
|
|
|
path := fmt.Sprintf(`%s\Interfaces\%s`, basePath, m.guid)
|
|
|
|
// to resolve queries for domains using resolvers, rather than the
|
|
|
|
value := strings.Join(domains, ",")
|
|
|
|
// system's "primary" resolver.
|
|
|
|
return setRegistryString(path, "SearchList", value)
|
|
|
|
//
|
|
|
|
|
|
|
|
// If no resolvers are provided, the Tailscale NRPT rule is deleted.
|
|
|
|
|
|
|
|
func (m windowsManager) setSplitDNS(resolvers []netaddr.IP, domains []string) error {
|
|
|
|
|
|
|
|
if len(resolvers) == 0 {
|
|
|
|
|
|
|
|
return m.delKey(nrptBase)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// CreateKey is actually open-or-create, which suits us fine.
|
|
|
|
|
|
|
|
key, _, err := registry.CreateKey(registry.LOCAL_MACHINE, nrptBase, registry.SET_VALUE)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
return fmt.Errorf("opening %s: %w", nrptBase, err)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
defer key.Close()
|
|
|
|
|
|
|
|
if err := key.SetDWordValue("Version", 1); err != nil {
|
|
|
|
|
|
|
|
return err
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := key.SetStringsValue("Name", doms); err != nil {
|
|
|
|
|
|
|
|
return err
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := key.SetStringValue("GenericDNSServers", strings.Join(servers, "; ")); err != nil {
|
|
|
|
|
|
|
|
return err
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := key.SetDWordValue("ConfigOptions", nrptOverrideDNS); err != nil {
|
|
|
|
|
|
|
|
return err
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (m windowsManager) SetDNS(config OSConfig) error {
|
|
|
|
// setPrimaryDNS sets the given resolvers and domains as the Tailscale
|
|
|
|
|
|
|
|
// interface's DNS configuration.
|
|
|
|
|
|
|
|
// If resolvers is non-empty, those resolvers become the system's
|
|
|
|
|
|
|
|
// "primary" resolvers.
|
|
|
|
|
|
|
|
// domains can be set without resolvers, which just contributes new
|
|
|
|
|
|
|
|
// paths to the global DNS search list.
|
|
|
|
|
|
|
|
func (m windowsManager) setPrimaryDNS(resolvers []netaddr.IP, domains []string) error {
|
|
|
|
var ipsv4 []string
|
|
|
|
var ipsv4 []string
|
|
|
|
var ipsv6 []string
|
|
|
|
var ipsv6 []string
|
|
|
|
|
|
|
|
|
|
|
|
for _, ip := range config.Nameservers {
|
|
|
|
for _, ip := range resolvers {
|
|
|
|
if ip.Is4() {
|
|
|
|
if ip.Is4() {
|
|
|
|
ipsv4 = append(ipsv4, ip.String())
|
|
|
|
ipsv4 = append(ipsv4, ip.String())
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
@ -76,19 +142,104 @@ func (m windowsManager) SetDNS(config OSConfig) error {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err := m.setNameservers(ipv4RegBase, ipsv4); err != nil {
|
|
|
|
key4, err := m.openKey(m.ifPath(ipv4RegBase))
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if err := m.setDomains(ipv4RegBase, config.Domains); err != nil {
|
|
|
|
defer key4.Close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if len(ipsv4) == 0 {
|
|
|
|
|
|
|
|
if err := delValue(key4, "NameServer"); err != nil {
|
|
|
|
|
|
|
|
return err
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} else if err := key4.SetStringValue("NameServer", strings.Join(ipsv4, ",")); err != nil {
|
|
|
|
return err
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err := m.setNameservers(ipv6RegBase, ipsv6); err != nil {
|
|
|
|
if len(domains) == 0 {
|
|
|
|
|
|
|
|
if err := delValue(key4, "SearchList"); err != nil {
|
|
|
|
|
|
|
|
return err
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} else if err := key4.SetStringValue("SearchList", strings.Join(domains, ",")); err != nil {
|
|
|
|
return err
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if err := m.setDomains(ipv6RegBase, config.Domains); err != nil {
|
|
|
|
|
|
|
|
|
|
|
|
key6, err := m.openKey(m.ifPath(ipv6RegBase))
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
defer key6.Close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if len(ipsv6) == 0 {
|
|
|
|
|
|
|
|
if err := delValue(key6, "NameServer"); err != nil {
|
|
|
|
|
|
|
|
return err
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} else if err := key6.SetStringValue("NameServer", strings.Join(ipsv6, ",")); err != nil {
|
|
|
|
|
|
|
|
return err
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if len(domains) == 0 {
|
|
|
|
|
|
|
|
if err := delValue(key6, "SearchList"); err != nil {
|
|
|
|
|
|
|
|
return err
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} else if err := key6.SetStringValue("SearchList", strings.Join(domains, ",")); err != nil {
|
|
|
|
|
|
|
|
return err
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Disable LLMNR on the Tailscale interface. We don't do
|
|
|
|
|
|
|
|
// multicast, and we certainly don't do LLMNR, so it's pointless
|
|
|
|
|
|
|
|
// to make Windows try it.
|
|
|
|
|
|
|
|
if err := key4.SetDWordValue("EnableMulticast", 0); err != nil {
|
|
|
|
|
|
|
|
return err
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := key6.SetDWordValue("EnableMulticast", 0); err != nil {
|
|
|
|
|
|
|
|
return err
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (m windowsManager) SetDNS(cfg OSConfig) error {
|
|
|
|
|
|
|
|
// We can configure Windows DNS in one of two ways:
|
|
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
// - In primary DNS mode, we set the NameServer and SearchList
|
|
|
|
|
|
|
|
// registry keys on our interface. Because our interface metric
|
|
|
|
|
|
|
|
// is very low, this turns us into the one and only "primary"
|
|
|
|
|
|
|
|
// resolver for the OS, i.e. all queries flow to the
|
|
|
|
|
|
|
|
// resolver(s) we specify.
|
|
|
|
|
|
|
|
// - In split DNS mode, we set the Domain registry key on our
|
|
|
|
|
|
|
|
// interface (which adds that domain to the global search list,
|
|
|
|
|
|
|
|
// but does not contribute other DNS configuration from the
|
|
|
|
|
|
|
|
// interface), and configure an NRPT (Name Resolution Policy
|
|
|
|
|
|
|
|
// Table) rule to route queries for our suffixes to the
|
|
|
|
|
|
|
|
// provided resolver.
|
|
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
// When switching modes, we delete all the configuration related
|
|
|
|
|
|
|
|
// to the other mode, so these two are an XOR.
|
|
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
// Windows actually supports much more advanced configurations as
|
|
|
|
|
|
|
|
// well, with arbitrary routing of hosts and suffixes to arbitrary
|
|
|
|
|
|
|
|
// resolvers. However, we use it in a "simple" split domain
|
|
|
|
|
|
|
|
// configuration only, routing one set of things to the "split"
|
|
|
|
|
|
|
|
// resolver and the rest to the primary.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if cfg.Primary {
|
|
|
|
|
|
|
|
if err := m.setSplitDNS(nil, nil); err != nil {
|
|
|
|
|
|
|
|
return err
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := m.setPrimaryDNS(cfg.Nameservers, cfg.Domains); err != nil {
|
|
|
|
|
|
|
|
return err
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
if err := m.setSplitDNS(cfg.Nameservers, cfg.Domains); err != nil {
|
|
|
|
|
|
|
|
return err
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// Still set search domains on the interface, since NRPT only
|
|
|
|
|
|
|
|
// handles query routing and not search domain expansion.
|
|
|
|
|
|
|
|
if err := m.setPrimaryDNS(nil, cfg.Domains); err != nil {
|
|
|
|
|
|
|
|
return err
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Force DNS re-registration in Active Directory. What we actually
|
|
|
|
// Force DNS re-registration in Active Directory. What we actually
|
|
|
|
// care about is that this command invokes the undocumented hidden
|
|
|
|
// care about is that this command invokes the undocumented hidden
|
|
|
@ -97,24 +248,41 @@ func (m windowsManager) SetDNS(config OSConfig) error {
|
|
|
|
// effect.
|
|
|
|
// effect.
|
|
|
|
//
|
|
|
|
//
|
|
|
|
// This command can take a few seconds to run, so run it async, best effort.
|
|
|
|
// This command can take a few seconds to run, so run it async, best effort.
|
|
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
// After re-registering DNS, also flush the DNS cache to clear out
|
|
|
|
|
|
|
|
// any cached split-horizon queries that are no longer the correct
|
|
|
|
|
|
|
|
// answer.
|
|
|
|
go func() {
|
|
|
|
go func() {
|
|
|
|
t0 := time.Now()
|
|
|
|
t0 := time.Now()
|
|
|
|
m.logf("running ipconfig /registerdns ...")
|
|
|
|
m.logf("running ipconfig /registerdns ...")
|
|
|
|
cmd := exec.Command("ipconfig", "/registerdns")
|
|
|
|
cmd := exec.Command("ipconfig", "/registerdns")
|
|
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
|
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
|
|
|
|
|
|
|
err := cmd.Run()
|
|
|
|
d := time.Since(t0).Round(time.Millisecond)
|
|
|
|
d := time.Since(t0).Round(time.Millisecond)
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
|
|
if err != nil {
|
|
|
|
m.logf("error running ipconfig /registerdns after %v: %v", d, err)
|
|
|
|
m.logf("error running ipconfig /registerdns after %v: %v", d, err)
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
m.logf("ran ipconfig /registerdns in %v", d)
|
|
|
|
m.logf("ran ipconfig /registerdns in %v", d)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
t0 = time.Now()
|
|
|
|
|
|
|
|
m.logf("running ipconfig /registerdns ...")
|
|
|
|
|
|
|
|
cmd = exec.Command("ipconfig", "/flushdns")
|
|
|
|
|
|
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
|
|
|
|
|
|
|
err = cmd.Run()
|
|
|
|
|
|
|
|
d = time.Since(t0).Round(time.Millisecond)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
m.logf("error running ipconfig /flushdns after %v: %v", d, err)
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
m.logf("ran ipconfig /flushdns in %v", d)
|
|
|
|
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (m windowsManager) SupportsSplitDNS() bool {
|
|
|
|
func (m windowsManager) SupportsSplitDNS() bool {
|
|
|
|
return false
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (m windowsManager) Close() error {
|
|
|
|
func (m windowsManager) Close() error {
|
|
|
|