From 58760f7b82f7e44763a118e831ed90de94933e25 Mon Sep 17 00:00:00 2001 From: David Anderson Date: Sat, 10 Apr 2021 18:55:05 -0700 Subject: [PATCH] net/dns: split resolvconfManager into a debian and an openresolv manager. Signed-off-by: David Anderson --- net/dns/manager_freebsd.go | 7 +++- net/dns/manager_linux.go | 7 +++- net/dns/openresolv.go | 61 +++++++++++++++++++++++++++++ net/dns/resolvconf.go | 80 +++++++------------------------------- 4 files changed, 86 insertions(+), 69 deletions(-) create mode 100644 net/dns/openresolv.go diff --git a/net/dns/manager_freebsd.go b/net/dns/manager_freebsd.go index 1485c6728..2901b347d 100644 --- a/net/dns/manager_freebsd.go +++ b/net/dns/manager_freebsd.go @@ -9,7 +9,12 @@ import "tailscale.com/types/logger" func NewOSConfigurator(logf logger.Logf, _ string) OSConfigurator { switch { case isResolvconfActive(): - return newResolvconfManager(logf) + if resolvconfIsOpenresolv() { + return newOpenresolvManager() + } else { + // Debian resolvconf + return newResolvconfManager(logf) + } default: return newDirectManager() } diff --git a/net/dns/manager_linux.go b/net/dns/manager_linux.go index 727b24e30..fbe4b119e 100644 --- a/net/dns/manager_linux.go +++ b/net/dns/manager_linux.go @@ -14,7 +14,12 @@ func NewOSConfigurator(logf logger.Logf, interfaceName string) OSConfigurator { // case isNMActive(): // return newNMManager(interfaceName) case isResolvconfActive(): - return newResolvconfManager(logf) + if resolvconfIsOpenresolv() { + return newOpenresolvManager() + } else { + // Debian resolvconf + return newResolvconfManager(logf) + } default: return newDirectManager() } diff --git a/net/dns/openresolv.go b/net/dns/openresolv.go new file mode 100644 index 000000000..1d7083ec9 --- /dev/null +++ b/net/dns/openresolv.go @@ -0,0 +1,61 @@ +// 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. + +package dns + +import ( + "bytes" + "fmt" + "os/exec" +) + +// 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{} + +func newOpenresolvManager() openresolvManager { + return openresolvManager{} +} + +func (m openresolvManager) SetDNS(config OSConfig) error { + var stdin bytes.Buffer + writeResolvConf(&stdin, config.Nameservers, config.SearchDomains) + + cmd := exec.Command("resolvconf", "-m", "0", "-x", "-a", "tailscale") + cmd.Stdin = &stdin + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("running %s: %s", cmd, out) + } + return nil +} + +func (m openresolvManager) SupportsSplitDNS() bool { + return false +} + +func (m openresolvManager) GetBaseConfig() (OSConfig, error) { + return OSConfig{}, ErrGetBaseConfigNotSupported +} + +func (m openresolvManager) Close() error { + cmd := exec.Command("resolvconf", "-f", "-d", "tailscale") + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("running %s: %s", cmd, out) + } + return nil +} diff --git a/net/dns/resolvconf.go b/net/dns/resolvconf.go index 750c10c22..4f0b7d43c 100644 --- a/net/dns/resolvconf.go +++ b/net/dns/resolvconf.go @@ -86,62 +86,21 @@ func isResolvconfActive() bool { return false } -// resolvconfImpl enumerates supported implementations of the resolvconf CLI. -type resolvconfImpl uint8 - -const ( - // resolvconfOpenresolv is the implementation packaged as "openresolv" on Ubuntu. - // It supports exclusive mode and interface metrics. - resolvconfOpenresolv resolvconfImpl = iota - // resolvconfLegacy is the implementation by Thomas Hood packaged as "resolvconf" on Ubuntu. - // It does not support exclusive mode or interface metrics. - resolvconfLegacy -) - -func (impl resolvconfImpl) String() string { - switch impl { - case resolvconfOpenresolv: - return "openresolv" - case resolvconfLegacy: - return "legacy" - default: - return "unknown" - } -} - -// getResolvconfImpl returns the implementation of resolvconf that appears to be in use. -func getResolvconfImpl() resolvconfImpl { - err := exec.Command("resolvconf", "-v").Run() - if err != nil { - if exitErr, ok := err.(*exec.ExitError); ok { - // Thomas Hood's resolvconf has a minimal flag set - // and exits with code 99 when passed an unknown flag. - if exitErr.ExitCode() == 99 { - return resolvconfLegacy - } - } - } - return resolvconfOpenresolv -} - +// resolvconfManager manages DNS configuration using the Debian +// implementation of the `resolvconf` program, written by Thomas Hood. type resolvconfManager struct { - logf logger.Logf - impl resolvconfImpl - workaroundApplied bool // libc update script has been installed. + logf logger.Logf + scriptInstalled bool // libc update script has been installed } func newResolvconfManager(logf logger.Logf) *resolvconfManager { - impl := getResolvconfImpl() - logf("resolvconf implementation is %s", impl) - return &resolvconfManager{ logf: logf, - impl: impl, } } func (m *resolvconfManager) SetDNS(config OSConfig) error { - if m.impl == resolvconfLegacy && !m.workaroundApplied { + if !m.scriptInstalled { m.logf("injecting resolvconf workaround script") if err := os.MkdirAll(resolvconfLibcHookPath, 0755); err != nil { return err @@ -149,22 +108,17 @@ func (m *resolvconfManager) SetDNS(config OSConfig) error { if err := atomicfile.WriteFile(resolvconfHookPath, legacyResolvconfScript, 0755); err != nil { return err } - m.workaroundApplied = true + m.scriptInstalled = true } stdin := new(bytes.Buffer) writeResolvConf(stdin, config.Nameservers, config.SearchDomains) // dns_direct.go - var cmd *exec.Cmd - switch m.impl { - case resolvconfOpenresolv: - // Request maximal priority (metric 0) and exclusive mode. - cmd = exec.Command("resolvconf", "-m", "0", "-x", "-a", resolvconfConfigName) - case resolvconfLegacy: - // This does not quite give us the desired behavior (queries leak), - // but there is nothing else we can do without messing with other interfaces' settings. - cmd = exec.Command("resolvconf", "-a", resolvconfConfigName) - } + // 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 { @@ -183,21 +137,13 @@ func (m *resolvconfManager) GetBaseConfig() (OSConfig, error) { } func (m *resolvconfManager) Close() error { - var cmd *exec.Cmd - switch m.impl { - case resolvconfOpenresolv: - cmd = exec.Command("resolvconf", "-f", "-d", resolvconfConfigName) - case resolvconfLegacy: - // resolvconfLegacy lacks the -f flag. - // Instead, it succeeds even when the config does not exist. - cmd = exec.Command("resolvconf", "-d", resolvconfConfigName) - } + cmd := exec.Command("resolvconf", "-d", resolvconfConfigName) out, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("running %s: %s", cmd, out) } - if m.workaroundApplied { + if m.scriptInstalled { m.logf("removing resolvconf workaround script") os.Remove(resolvconfHookPath) // Best-effort }