net/dns: implement OS-level split DNS for Windows.

Part of #953.

Signed-off-by: David Anderson <danderson@tailscale.com>
pull/1665/head
David Anderson 4 years ago committed by Dave Anderson
parent c16a926bf2
commit 3e915ac783

@ -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
}
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
}
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(ipv4RegBase, config.Domains); err != nil {
key6, err := m.openKey(m.ifPath(ipv6RegBase))
if err != nil {
return err return err
} }
defer key6.Close()
if err := m.setNameservers(ipv6RegBase, ipsv6); err != nil { 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 return err
} }
if err := m.setDomains(ipv6RegBase, config.Domains); err != nil {
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 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 {

Loading…
Cancel
Save