net/dns: implement OS-level split DNS for Windows.
Part of #953. Signed-off-by: David Anderson <danderson@tailscale.com>
This commit is contained in:
parent
c16a926bf2
commit
3e915ac783
|
@ -12,12 +12,19 @@ import (
|
|||
"time"
|
||||
|
||||
"golang.org/x/sys/windows/registry"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
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}`
|
||||
nrptOverrideDNS = 0x8 // bitmask value for "use the provided override DNS resolvers"
|
||||
)
|
||||
|
||||
type windowsManager struct {
|
||||
|
@ -26,10 +33,20 @@ type windowsManager struct {
|
|||
}
|
||||
|
||||
func NewOSConfigurator(logf logger.Logf, interfaceName string) OSConfigurator {
|
||||
return windowsManager{
|
||||
ret := windowsManager{
|
||||
logf: logf,
|
||||
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
|
||||
|
@ -38,37 +55,86 @@ func NewOSConfigurator(logf logger.Logf, interfaceName string) OSConfigurator {
|
|||
// can end up racing with that.
|
||||
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)
|
||||
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)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting %s[%s]: %w", path, name, err)
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m windowsManager) setNameservers(basePath string, nameservers []string) error {
|
||||
path := fmt.Sprintf(`%s\Interfaces\%s`, basePath, m.guid)
|
||||
value := strings.Join(nameservers, ",")
|
||||
return setRegistryString(path, "NameServer", value)
|
||||
func delValue(key registry.Key, name string) error {
|
||||
if err := key.DeleteValue(name); err != nil && err != registry.ErrNotExist {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m windowsManager) setDomains(basePath string, domains []string) error {
|
||||
path := fmt.Sprintf(`%s\Interfaces\%s`, basePath, m.guid)
|
||||
value := strings.Join(domains, ",")
|
||||
return setRegistryString(path, "SearchList", value)
|
||||
// setSplitDNS configures an NRPT (Name Resolution Policy Table) rule
|
||||
// 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.
|
||||
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 ipsv6 []string
|
||||
|
||||
for _, ip := range config.Nameservers {
|
||||
for _, ip := range resolvers {
|
||||
if ip.Is4() {
|
||||
ipsv4 = append(ipsv4, ip.String())
|
||||
} 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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if err := m.setDomains(ipv6RegBase, config.Domains); err != nil {
|
||||
|
||||
key6, err := m.openKey(m.ifPath(ipv6RegBase))
|
||||
if err != nil {
|
||||
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
|
||||
// care about is that this command invokes the undocumented hidden
|
||||
|
@ -97,24 +248,41 @@ func (m windowsManager) SetDNS(config OSConfig) error {
|
|||
// effect.
|
||||
//
|
||||
// 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() {
|
||||
t0 := time.Now()
|
||||
m.logf("running ipconfig /registerdns ...")
|
||||
cmd := exec.Command("ipconfig", "/registerdns")
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
err := cmd.Run()
|
||||
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)
|
||||
} else {
|
||||
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
|
||||
}
|
||||
|
||||
func (m windowsManager) SupportsSplitDNS() bool {
|
||||
return false
|
||||
return true
|
||||
}
|
||||
|
||||
func (m windowsManager) Close() error {
|
||||
|
|
Loading…
Reference in New Issue