// 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" "os" "os/exec" "path/filepath" "tailscale.com/atomicfile" "tailscale.com/types/logger" ) //go:embed resolvconf-workaround.sh var legacyResolvconfScript []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 scriptInstalled bool // libc update script has been installed } func newResolvconfManager(logf logger.Logf) *resolvconfManager { return &resolvconfManager{ logf: logf, } } 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, legacyResolvconfScript, 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) { return OSConfig{}, ErrGetBaseConfigNotSupported } 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 }