// 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" _ "embed" "fmt" "io/ioutil" "os" "os/exec" "path/filepath" "tailscale.com/atomicfile" "tailscale.com/types/logger" ) //go:embed resolvconf-workaround.sh var workaroundScript []byte // resolvconfConfigName is the name of the config submitted to // resolvconf. // The name starts with 'tun' in order to match the hardcoded // interface order in debian resolvconf, which will place this // configuration ahead of regular network links. In theory, this // doesn't matter because we then fix things up to ensure our config // is the only one in use, but in case that fails, this will make our // configuration slightly preferred. // The 'inet' suffix has no specific meaning, but conventionally // resolvconf implementations encourage adding a suffix roughly // indicating where the config came from, and "inet" is the "none of // the above" value (rather than, say, "ppp" or "dhcp"). const resolvconfConfigName = "tun-tailscale.inet" // resolvconfLibcHookPath is the directory containing libc update // scripts, which are run by Debian resolvconf when /etc/resolv.conf // has been updated. const resolvconfLibcHookPath = "/etc/resolvconf/update-libc.d" // resolvconfHookPath is the name of the libc hook script we install // to force Tailscale's DNS config to take effect. var resolvconfHookPath = filepath.Join(resolvconfLibcHookPath, "tailscale") // isResolvconfActive indicates whether the system appears to be using resolvconf. // If this is true, then directManager should be avoided: // resolvconf has exclusive ownership of /etc/resolv.conf. func isResolvconfActive() bool { // Sanity-check first: if there is no resolvconf binary, then this is fruitless. // // However, this binary may be a shim like the one systemd-resolved provides. // Such a shim may not behave as expected: in particular, systemd-resolved // does not seem to respect the exclusive mode -x, saying: // -x Send DNS traffic preferably over this interface // whereas e.g. openresolv sends DNS traffix _exclusively_ over that interface, // or not at all (in case of another exclusive-mode request later in time). // // Moreover, resolvconf may be installed but unused, in which case we should // not use it either, lest we clobber existing configuration. // // To handle all the above correctly, we scan the comments in /etc/resolv.conf // to ensure that it was generated by a resolvconf implementation. _, err := exec.LookPath("resolvconf") if err != nil { return false } f, err := os.Open("/etc/resolv.conf") if err != nil { return false } defer f.Close() scanner := bufio.NewScanner(f) for scanner.Scan() { line := scanner.Bytes() // Look for the word "resolvconf" until comments end. if len(line) > 0 && line[0] != '#' { return false } if bytes.Contains(line, []byte("resolvconf")) { return true } } return false } // resolvconfManager manages DNS configuration using the Debian // implementation of the `resolvconf` program, written by Thomas Hood. type resolvconfManager struct { logf logger.Logf listRecordsPath string interfacesDir string scriptInstalled bool // libc update script has been installed } func newResolvconfManager(logf logger.Logf) *resolvconfManager { ret := &resolvconfManager{ logf: logf, listRecordsPath: "/lib/resolvconf/list-records", interfacesDir: "/etc/resolvconf/run/interface", // panic fallback if nothing seems to work } if _, err := os.Stat(ret.listRecordsPath); os.IsNotExist(err) { // This might be a Debian system from before the big /usr // merge, try /usr instead. ret.listRecordsPath = "/usr" + ret.listRecordsPath } // The runtime directory is currently (2020-04) canonically // /etc/resolvconf/run, but the manpage is making noise about // switching to /run/resolvconf and dropping the /etc path. So, // let's probe the possible directories and use the first one // that works. for _, path := range []string{ "/etc/resolvconf/run/interface", "/run/resolvconf/interface", "/var/run/resolvconf/interface", } { if _, err := os.Stat(path); err == nil { ret.interfacesDir = path break } } if ret.interfacesDir == "" { // None of the paths seem to work, use the canonical location // that the current manpage says to use. ret.interfacesDir = "/etc/resolvconf/run/interfaces" } return ret } func (m *resolvconfManager) SetDNS(config OSConfig) error { if !m.scriptInstalled { m.logf("injecting resolvconf workaround script") if err := os.MkdirAll(resolvconfLibcHookPath, 0755); err != nil { return err } if err := atomicfile.WriteFile(resolvconfHookPath, workaroundScript, 0755); err != nil { return err } m.scriptInstalled = true } stdin := new(bytes.Buffer) writeResolvConf(stdin, config.Nameservers, config.SearchDomains) // dns_direct.go // This resolvconf implementation doesn't support exclusive mode // or interface priorities, so it will end up blending our // configuration with other sources. However, this will get fixed // up by the script we injected above. cmd := exec.Command("resolvconf", "-a", resolvconfConfigName) cmd.Stdin = stdin out, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("running %s: %s", cmd, out) } return nil } func (m *resolvconfManager) SupportsSplitDNS() bool { return false } func (m *resolvconfManager) GetBaseConfig() (OSConfig, error) { var bs bytes.Buffer cmd := exec.Command(m.listRecordsPath) // list-records assumes it's being run with CWD set to the // interfaces runtime dir, and returns nonsense otherwise. cmd.Dir = m.interfacesDir cmd.Stdout = &bs if err := cmd.Run(); err != nil { return OSConfig{}, err } var conf bytes.Buffer sc := bufio.NewScanner(&bs) for sc.Scan() { if sc.Text() == resolvconfConfigName { continue } bs, err := ioutil.ReadFile(filepath.Join(m.interfacesDir, sc.Text())) if err != nil { if os.IsNotExist(err) { // Probably raced with a deletion, that's okay. continue } return OSConfig{}, err } conf.Write(bs) conf.WriteByte('\n') } return readResolv(&conf) } func (m *resolvconfManager) Close() error { cmd := exec.Command("resolvconf", "-d", resolvconfConfigName) out, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("running %s: %s", cmd, out) } if m.scriptInstalled { m.logf("removing resolvconf workaround script") os.Remove(resolvconfHookPath) // Best-effort } return nil }