From 9a48bac8ad2e658738b748775b69d640fd830a08 Mon Sep 17 00:00:00 2001 From: David Anderson Date: Sat, 10 Apr 2021 23:31:00 -0700 Subject: [PATCH] net/dns: rename resolvconf.go to debian_resolvconf.go. Signed-off-by: David Anderson --- net/dns/debian_resolvconf.go | 171 +++++++++++++++++++++++++++++++++ net/dns/manager_freebsd.go | 7 +- net/dns/manager_linux.go | 7 +- net/dns/openresolv.go | 12 --- net/dns/resolvconf.go | 179 +++-------------------------------- 5 files changed, 185 insertions(+), 191 deletions(-) create mode 100644 net/dns/debian_resolvconf.go diff --git a/net/dns/debian_resolvconf.go b/net/dns/debian_resolvconf.go new file mode 100644 index 000000000..f0c440547 --- /dev/null +++ b/net/dns/debian_resolvconf.go @@ -0,0 +1,171 @@ +// 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") + +// 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 newDebianResolvconfManager(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 +} diff --git a/net/dns/manager_freebsd.go b/net/dns/manager_freebsd.go index 2901b347d..1485c6728 100644 --- a/net/dns/manager_freebsd.go +++ b/net/dns/manager_freebsd.go @@ -9,12 +9,7 @@ import "tailscale.com/types/logger" func NewOSConfigurator(logf logger.Logf, _ string) OSConfigurator { switch { case isResolvconfActive(): - if resolvconfIsOpenresolv() { - return newOpenresolvManager() - } else { - // Debian resolvconf - return newResolvconfManager(logf) - } + return newResolvconfManager(logf) default: return newDirectManager() } diff --git a/net/dns/manager_linux.go b/net/dns/manager_linux.go index fbe4b119e..727b24e30 100644 --- a/net/dns/manager_linux.go +++ b/net/dns/manager_linux.go @@ -14,12 +14,7 @@ func NewOSConfigurator(logf logger.Logf, interfaceName string) OSConfigurator { // case isNMActive(): // return newNMManager(interfaceName) case isResolvconfActive(): - if resolvconfIsOpenresolv() { - return newOpenresolvManager() - } else { - // Debian resolvconf - return newResolvconfManager(logf) - } + return newResolvconfManager(logf) default: return newDirectManager() } diff --git a/net/dns/openresolv.go b/net/dns/openresolv.go index fc24afc43..183e8d240 100644 --- a/net/dns/openresolv.go +++ b/net/dns/openresolv.go @@ -11,18 +11,6 @@ import ( "strings" ) -// resolvconfIsOpenresolv reports whether the `resolvconf` binary on -// the system is the openresolv implementation. -func resolvconfIsOpenresolv() bool { - bs, err := exec.Command("resolvconf", "--version").CombinedOutput() - if err != nil { - // Either resolvconf isn't installed, or it's not openresolv. - return false - } - - return bytes.Contains(bs, []byte("openresolv ")) -} - // openresolvManager manages DNS configuration using the openresolv // implementation of the `resolvconf` program. type openresolvManager struct{} diff --git a/net/dns/resolvconf.go b/net/dns/resolvconf.go index b93ce98e5..73e7d947f 100644 --- a/net/dns/resolvconf.go +++ b/net/dns/resolvconf.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. +// Copyright (c) 2021 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. @@ -7,61 +7,16 @@ 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 @@ -87,128 +42,18 @@ func isResolvconfActive() bool { 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() +func newResolvconfManager(logf logger.Logf) OSConfigurator { + _, err := exec.Command("resolvconf", "--version").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 + if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 99 { + // Debian resolvconf doesn't understand --version, and + // exits with a specific error code. + return newDebianResolvconfManager(logf) } - 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 + // If --version works, or we got some surprising error while + // probing, use openresolv. It's the more common implementation, + // so in cases where we can't figure things out, it's the least + // likely to misbehave. + return newOpenresolvManager() }