// Copyright (c) 2020 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 ( "bufio" "bytes" "errors" "fmt" "net/netip" "os" "os/exec" "path/filepath" "sort" "strings" "syscall" "time" "golang.org/x/sys/windows" "golang.org/x/sys/windows/registry" "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" "tailscale.com/atomicfile" "tailscale.com/envknob" "tailscale.com/types/logger" "tailscale.com/util/dnsname" "tailscale.com/util/winutil" ) const ( versionKey = `SOFTWARE\Microsoft\Windows NT\CurrentVersion` ) var configureWSL = envknob.RegisterBool("TS_DEBUG_CONFIGURE_WSL") type windowsManager struct { logf logger.Logf guid string nrptDB *nrptRuleDatabase wslManager *wslManager } func NewOSConfigurator(logf logger.Logf, interfaceName string) (OSConfigurator, error) { ret := windowsManager{ logf: logf, guid: interfaceName, wslManager: newWSLManager(logf), } if isWindows10OrBetter() { ret.nrptDB = newNRPTRuleDatabase(logf) } // Log WSL status once at startup. if distros, err := wslDistros(); err != nil { logf("WSL: could not list distributions: %v", err) } else { logf("WSL: found %d distributions", len(distros)) } return ret, nil } func (m windowsManager) openInterfaceKey(pfx winutil.RegistryPathPrefix) (registry.Key, error) { path := pfx.WithSuffix(m.guid) key, err := winutil.OpenKeyWait(registry.LOCAL_MACHINE, path, registry.SET_VALUE) if err != nil { return 0, fmt.Errorf("opening %s: %w", path, err) } return key, nil } func delValue(key registry.Key, name string) error { if err := key.DeleteValue(name); err != nil && err != registry.ErrNotExist { return err } return nil } // 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 rules are deleted. func (m windowsManager) setSplitDNS(resolvers []netip.Addr, domains []dnsname.FQDN) error { if m.nrptDB == nil { if resolvers == nil { // Just a no-op in this case. return nil } return fmt.Errorf("Split DNS unsupported on this Windows version") } defer m.nrptDB.Refresh() if len(resolvers) == 0 { return m.nrptDB.DelAllRuleKeys() } servers := make([]string, 0, len(resolvers)) for _, resolver := range resolvers { servers = append(servers, resolver.String()) } return m.nrptDB.WriteSplitDNSConfig(servers, domains) } func setTailscaleHosts(prevHostsFile []byte, hosts []*HostEntry) ([]byte, error) { b := bytes.ReplaceAll(prevHostsFile, []byte("\r\n"), []byte("\n")) sc := bufio.NewScanner(bytes.NewReader(b)) const ( header = "# TailscaleHostsSectionStart" footer = "# TailscaleHostsSectionEnd" ) var comments = []string{ "# This section contains MagicDNS entries for Tailscale.", "# Do not edit this section manually.", } var out bytes.Buffer var inSection bool for sc.Scan() { line := strings.TrimSpace(sc.Text()) if line == header { inSection = true continue } if line == footer { inSection = false continue } if inSection { continue } fmt.Fprintln(&out, line) } if err := sc.Err(); err != nil { return nil, err } if len(hosts) > 0 { fmt.Fprintln(&out, header) for _, c := range comments { fmt.Fprintln(&out, c) } fmt.Fprintln(&out) for _, he := range hosts { fmt.Fprintf(&out, "%s %s\n", he.Addr, strings.Join(he.Hosts, " ")) } fmt.Fprintln(&out) fmt.Fprintln(&out, footer) } return bytes.ReplaceAll(out.Bytes(), []byte("\n"), []byte("\r\n")), nil } // setHosts sets the hosts file to contain the given host entries. func (m windowsManager) setHosts(hosts []*HostEntry) error { systemDir, err := windows.GetSystemDirectory() if err != nil { return err } hostsFile := filepath.Join(systemDir, "drivers", "etc", "hosts") b, err := os.ReadFile(hostsFile) if err != nil { return err } outB, err := setTailscaleHosts(b, hosts) if err != nil { return err } const fileMode = 0 // ignored on windows. return atomicfile.WriteFile(hostsFile, outB, fileMode) } // 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 []netip.Addr, domains []dnsname.FQDN) error { var ipsv4 []string var ipsv6 []string for _, ip := range resolvers { if ip.Is4() { ipsv4 = append(ipsv4, ip.String()) } else { ipsv6 = append(ipsv6, ip.String()) } } domStrs := make([]string, 0, len(domains)) for _, dom := range domains { domStrs = append(domStrs, dom.WithoutTrailingDot()) } key4, err := m.openInterfaceKey(winutil.IPv4TCPIPInterfacePrefix) 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(domStrs, ",")); err != nil { return err } key6, err := m.openInterfaceKey(winutil.IPv6TCPIPInterfacePrefix) 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(domStrs, ",")); 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. It is // being deprecated. 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. // Unconditionally disable dynamic DNS updates and NetBIOS on our // interfaces. if err := m.disableDynamicUpdates(); err != nil { m.logf("disableDynamicUpdates error: %v\n", err) } if err := m.disableNetBIOS(); err != nil { m.logf("disableNetBIOS error: %v\n", err) } if len(cfg.MatchDomains) == 0 { if err := m.setSplitDNS(nil, nil); err != nil { return err } if err := m.setHosts(nil); err != nil { return err } if err := m.setPrimaryDNS(cfg.Nameservers, cfg.SearchDomains); err != nil { return err } } else if m.nrptDB == nil { return errors.New("cannot set per-domain resolvers on Windows 7") } else { if err := m.setSplitDNS(cfg.Nameservers, cfg.MatchDomains); err != nil { return err } // Unset the resolver on the interface to ensure that we do not become // the primary resolver. Although this is what we want, at the moment // (2022-08-13) it causes single label resolutions from the OS resolver // to wait for a MDNS response from the Tailscale interface. // See #1659 and #5366 for more details. // // Still set search domains on the interface, since NRPT only handles // query routing and not search domain expansion. if err := m.setPrimaryDNS(nil, cfg.SearchDomains); err != nil { return err } // As we are not the primary resolver in this setup, we need to // explicitly set some single name hosts to ensure that we can resolve // them quickly and get around the 2.3s delay that otherwise occurs due // to multicast timeouts. if err := m.setHosts(cfg.Hosts); err != nil { return err } } // Force DNS re-registration in Active Directory. What we actually // care about is that this command invokes the undocumented hidden // function that forces Windows to notice that adapter settings // have changed, which makes the DNS settings actually take // 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 != 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 /flushdns ...") 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) } }() // On initial setup of WSL, the restart caused by --shutdown is slow, // so we do it out-of-line. if configureWSL() { go func() { if err := m.wslManager.SetDNS(cfg); err != nil { m.logf("WSL SetDNS: %v", err) // continue } else { m.logf("WSL SetDNS: success") } }() } return nil } func (m windowsManager) SupportsSplitDNS() bool { return m.nrptDB != nil } func (m windowsManager) Close() error { err := m.SetDNS(OSConfig{}) if m.nrptDB != nil { m.nrptDB.Close() } return err } // disableDynamicUpdates sets the appropriate registry values to prevent the // Windows DHCP client from sending dynamic DNS updates for our interface to // AD domain controllers. func (m windowsManager) disableDynamicUpdates() error { if err := m.setSingleDWORD(winutil.IPv4TCPIPInterfacePrefix, "DisableDynamicUpdate", 1); err != nil { return err } if err := m.setSingleDWORD(winutil.IPv6TCPIPInterfacePrefix, "DisableDynamicUpdate", 1); err != nil { return err } return nil } // setSingleDWORD opens the Registry Key in HKLM for the interface associated // with the windowsManager and sets the "keyPrefix\value" to data. func (m windowsManager) setSingleDWORD(prefix winutil.RegistryPathPrefix, value string, data uint32) error { k, err := m.openInterfaceKey(prefix) if err != nil { return err } defer k.Close() return k.SetDWordValue(value, data) } // disableNetBIOS sets the appropriate registry values to prevent Windows from // sending NetBIOS name resolution requests for our interface which we do not // handle nor want to. By leaving it enabled and not handling it we introduce // short-name resolution delays in certain conditions as Windows waits for // NetBIOS responses from our interface (#1659). // // Further, LLMNR and NetBIOS are being deprecated anyway in favor of MDNS. // https://techcommunity.microsoft.com/t5/networking-blog/aligning-on-mdns-ramping-down-netbios-name-resolution-and-llmnr/ba-p/3290816 func (m windowsManager) disableNetBIOS() error { return m.setSingleDWORD(winutil.NetBTInterfacePrefix, "NetbiosOptions", 2) } func (m windowsManager) GetBaseConfig() (OSConfig, error) { resolvers, err := m.getBasePrimaryResolver() if err != nil { return OSConfig{}, err } return OSConfig{ Nameservers: resolvers, // Don't return any search domains here, because even Windows // 7 correctly handles blending search domains from multiple // sources, and any search domains we add here will get tacked // onto the Tailscale config unnecessarily. }, nil } // getBasePrimaryResolver returns a guess of the non-Tailscale primary // resolver on the system. // It's used on Windows 7 to emulate split DNS by trying to figure out // what the "previous" primary resolver was. It might be wrong, or // incomplete. func (m windowsManager) getBasePrimaryResolver() (resolvers []netip.Addr, err error) { tsGUID, err := windows.GUIDFromString(m.guid) if err != nil { return nil, err } tsLUID, err := winipcfg.LUIDFromGUID(&tsGUID) if err != nil { return nil, err } ifrows, err := winipcfg.GetIPInterfaceTable(windows.AF_INET) if err == windows.ERROR_NOT_FOUND { // IPv4 seems disabled, try to get interface metrics from IPv6 instead. ifrows, err = winipcfg.GetIPInterfaceTable(windows.AF_INET6) } if err != nil { return nil, err } type candidate struct { id winipcfg.LUID metric uint32 } var candidates []candidate for _, row := range ifrows { if !row.Connected { continue } if row.InterfaceLUID == tsLUID { continue } candidates = append(candidates, candidate{row.InterfaceLUID, row.Metric}) } if len(candidates) == 0 { // No resolvers set outside of Tailscale. return nil, nil } sort.Slice(candidates, func(i, j int) bool { return candidates[i].metric < candidates[j].metric }) for _, candidate := range candidates { ips, err := candidate.id.DNS() if err != nil { return nil, err } ipLoop: for _, ip := range ips { ip = ip.Unmap() // Skip IPv6 site-local resolvers. These are an ancient // and obsolete IPv6 RFC, which Windows still faithfully // implements. The net result is that some low-metric // interfaces can "have" DNS resolvers, but they're just // site-local resolver IPs that don't go anywhere. So, we // skip the site-local resolvers in order to find the // first interface that has real DNS servers configured. for _, sl := range siteLocalResolvers { if ip.WithZone("") == sl { continue ipLoop } } resolvers = append(resolvers, ip) } if len(resolvers) > 0 { // Found some resolvers, we're done. break } } return resolvers, nil } var siteLocalResolvers = []netip.Addr{ netip.MustParseAddr("fec0:0:0:ffff::1"), netip.MustParseAddr("fec0:0:0:ffff::2"), netip.MustParseAddr("fec0:0:0:ffff::3"), } func isWindows10OrBetter() bool { key, err := registry.OpenKey(registry.LOCAL_MACHINE, versionKey, registry.READ) if err != nil { // Fail safe, assume old Windows. return false } // This key above only exists in Windows 10 and above. Its mere // presence is good enough. if _, _, err := key.GetIntegerValue("CurrentMajorVersionNumber"); err != nil { return false } return true }