mirror of
https://github.com/tailscale/tailscale.git
synced 2025-01-08 09:07:44 +00:00
net/dns: rename resolvconf.go to debian_resolvconf.go.
Signed-off-by: David Anderson <danderson@tailscale.com>
This commit is contained in:
parent
9831f1b183
commit
9a48bac8ad
171
net/dns/debian_resolvconf.go
Normal file
171
net/dns/debian_resolvconf.go
Normal file
@ -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
|
||||||
|
}
|
@ -9,12 +9,7 @@
|
|||||||
func NewOSConfigurator(logf logger.Logf, _ string) OSConfigurator {
|
func NewOSConfigurator(logf logger.Logf, _ string) OSConfigurator {
|
||||||
switch {
|
switch {
|
||||||
case isResolvconfActive():
|
case isResolvconfActive():
|
||||||
if resolvconfIsOpenresolv() {
|
|
||||||
return newOpenresolvManager()
|
|
||||||
} else {
|
|
||||||
// Debian resolvconf
|
|
||||||
return newResolvconfManager(logf)
|
return newResolvconfManager(logf)
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
return newDirectManager()
|
return newDirectManager()
|
||||||
}
|
}
|
||||||
|
@ -14,12 +14,7 @@ func NewOSConfigurator(logf logger.Logf, interfaceName string) OSConfigurator {
|
|||||||
// case isNMActive():
|
// case isNMActive():
|
||||||
// return newNMManager(interfaceName)
|
// return newNMManager(interfaceName)
|
||||||
case isResolvconfActive():
|
case isResolvconfActive():
|
||||||
if resolvconfIsOpenresolv() {
|
|
||||||
return newOpenresolvManager()
|
|
||||||
} else {
|
|
||||||
// Debian resolvconf
|
|
||||||
return newResolvconfManager(logf)
|
return newResolvconfManager(logf)
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
return newDirectManager()
|
return newDirectManager()
|
||||||
}
|
}
|
||||||
|
@ -11,18 +11,6 @@
|
|||||||
"strings"
|
"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
|
// openresolvManager manages DNS configuration using the openresolv
|
||||||
// implementation of the `resolvconf` program.
|
// implementation of the `resolvconf` program.
|
||||||
type openresolvManager struct{}
|
type openresolvManager struct{}
|
||||||
|
@ -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
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
@ -7,61 +7,16 @@
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
_ "embed"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"tailscale.com/atomicfile"
|
|
||||||
"tailscale.com/types/logger"
|
"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.
|
// isResolvconfActive indicates whether the system appears to be using resolvconf.
|
||||||
// If this is true, then directManager should be avoided:
|
// If this is true, then directManager should be avoided:
|
||||||
// resolvconf has exclusive ownership of /etc/resolv.conf.
|
// resolvconf has exclusive ownership of /etc/resolv.conf.
|
||||||
func isResolvconfActive() bool {
|
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")
|
_, err := exec.LookPath("resolvconf")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
@ -87,128 +42,18 @@ func isResolvconfActive() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolvconfManager manages DNS configuration using the Debian
|
func newResolvconfManager(logf logger.Logf) OSConfigurator {
|
||||||
// implementation of the `resolvconf` program, written by Thomas Hood.
|
_, err := exec.Command("resolvconf", "--version").CombinedOutput()
|
||||||
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("running %s: %s", cmd, out)
|
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)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
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
|
||||||
func (m *resolvconfManager) SupportsSplitDNS() bool {
|
// likely to misbehave.
|
||||||
return false
|
return newOpenresolvManager()
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user