net/dns: make directManager support split DNS, and work in sandboxes.

Fixes #1495, #683.

Signed-off-by: David Anderson <danderson@tailscale.com>
This commit is contained in:
David Anderson 2021-04-09 02:52:21 -07:00
parent 2685260ba1
commit e638a4d86b
1 changed files with 96 additions and 43 deletions

View File

@ -9,8 +9,6 @@ package dns
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"errors"
"fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"os" "os"
@ -23,7 +21,6 @@ import (
) )
const ( const (
tsConf = "/etc/resolv.tailscale.conf"
backupConf = "/etc/resolv.pre-tailscale-backup.conf" backupConf = "/etc/resolv.pre-tailscale-backup.conf"
resolvConf = "/etc/resolv.conf" resolvConf = "/etc/resolv.conf"
) )
@ -47,11 +44,10 @@ func writeResolvConf(w io.Writer, servers []netaddr.IP, domains []string) {
} }
} }
// readResolvConf reads DNS configuration from /etc/resolv.conf. func readResolvFile(path string) (OSConfig, error) {
func readResolvConf() (OSConfig, error) {
var config OSConfig var config OSConfig
f, err := os.Open("/etc/resolv.conf") f, err := os.Open(path)
if err != nil { if err != nil {
return config, err return config, err
} }
@ -82,6 +78,11 @@ func readResolvConf() (OSConfig, error) {
return config, nil return config, nil
} }
// readResolvConf reads DNS configuration from /etc/resolv.conf.
func readResolvConf() (OSConfig, error) {
return readResolvFile(resolvConf)
}
// isResolvedRunning reports whether systemd-resolved is running on the system, // isResolvedRunning reports whether systemd-resolved is running on the system,
// even if it is not managing the system DNS settings. // even if it is not managing the system DNS settings.
func isResolvedRunning() bool { func isResolvedRunning() bool {
@ -114,46 +115,72 @@ func newDirectManager() directManager {
return directManager{} return directManager{}
} }
func (m directManager) SetDNS(config OSConfig) error { // ownedByTailscale reports whether /etc/resolv.conf seems to be a
// Write the tsConf file. // tailscale-managed file.
buf := new(bytes.Buffer) func (m directManager) ownedByTailscale() (bool, error) {
writeResolvConf(buf, config.Nameservers, config.SearchDomains) st, err := os.Stat(resolvConf)
if err := atomicfile.WriteFile(tsConf, buf.Bytes(), 0644); err != nil { if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
if !st.Mode().IsRegular() {
return false, nil
}
bs, err := ioutil.ReadFile(resolvConf)
if err != nil {
return false, err
}
if bytes.Contains(bs, []byte("generated by tailscale")) {
return true, nil
}
return false, nil
}
// backupConfig creates or updates a backup of /etc/resolv.conf, if
// resolv.conf does not currently contain a Tailscale-managed config.
func (m directManager) backupConfig() error {
if _, err := os.Stat(resolvConf); err != nil {
if os.IsNotExist(err) {
// No resolv.conf, nothing to back up. Also get rid of any
// existing backup file, to avoid restoring something old.
os.Remove(backupConf)
return nil
}
return err return err
} }
if linkPath, err := os.Readlink(resolvConf); err != nil { owned, err := m.ownedByTailscale()
// Remove any old backup that may exist. if err != nil {
os.Remove(backupConf) return err
}
// Backup the existing /etc/resolv.conf file. if owned {
contents, err := ioutil.ReadFile(resolvConf)
// If the original did not exist, still back up an empty file.
// The presence of a backup file is the way we know that Up ran.
if err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
if err := atomicfile.WriteFile(backupConf, contents, 0644); err != nil {
return err
}
} else if linkPath != tsConf {
// Backup the existing symlink.
os.Remove(backupConf)
if err := os.Symlink(linkPath, backupConf); err != nil {
return err
}
} else {
// Nothing to do, resolvConf already points to tsConf.
return nil return nil
} }
os.Remove(resolvConf) return os.Rename(resolvConf, backupConf)
if err := os.Symlink(tsConf, resolvConf); err != nil { }
func (m directManager) SetDNS(config OSConfig) error {
if err := m.backupConfig(); err != nil {
return err return err
} }
buf := new(bytes.Buffer)
writeResolvConf(buf, config.Nameservers, config.SearchDomains)
if err := atomicfile.WriteFile(resolvConf, buf.Bytes(), 0644); err != nil {
return err
}
// We might have taken over a configuration managed by resolved,
// in which case it will notice this on restart and gracefully
// start using our configuration. This shouldn't happen because we
// try to manage DNS through resolved when it's around, but as a
// best-effort fallback if we messed up the detection, try to
// restart resolved to make the system configuration consistent.
if isResolvedRunning() { if isResolvedRunning() {
exec.Command("systemctl", "restart", "systemd-resolved.service").Run() // Best-effort. exec.Command("systemctl", "restart", "systemd-resolved.service").Run()
} }
return nil return nil
@ -164,27 +191,53 @@ func (m directManager) SupportsSplitDNS() bool {
} }
func (m directManager) GetBaseConfig() (OSConfig, error) { func (m directManager) GetBaseConfig() (OSConfig, error) {
return OSConfig{}, ErrGetBaseConfigNotSupported owned, err := m.ownedByTailscale()
if err != nil {
return OSConfig{}, err
}
fileToRead := resolvConf
if owned {
fileToRead = backupConf
}
return readResolvFile(fileToRead)
} }
func (m directManager) Close() error { func (m directManager) Close() error {
// We used to keep a file for the tailscale config and symlinked
// to it, but then we stopped because /etc/resolv.conf being a
// symlink to surprising places breaks snaps and other sandboxing
// things. Clean it up if it's still there.
os.Remove("/etc/resolv.tailscale.conf")
if _, err := os.Stat(backupConf); err != nil { if _, err := os.Stat(backupConf); err != nil {
// If the backup file does not exist, then Up never ran successfully.
if os.IsNotExist(err) { if os.IsNotExist(err) {
// No backup, nothing we can do.
return nil return nil
} }
return err return err
} }
owned, err := m.ownedByTailscale()
if ln, err := os.Readlink(resolvConf); err != nil { if err != nil {
return err return err
} else if ln != tsConf {
return fmt.Errorf("resolv.conf is not a symlink to %s", tsConf)
} }
_, err = os.Stat(resolvConf)
if err != nil && !os.IsNotExist(err) {
return err
}
resolvConfExists := !os.IsNotExist(err)
if resolvConfExists && !owned {
// There's already a non-tailscale config in place, get rid of
// our backup.
os.Remove(backupConf)
return nil
}
// We own resolv.conf, and a backup exists.
if err := os.Rename(backupConf, resolvConf); err != nil { if err := os.Rename(backupConf, resolvConf); err != nil {
return err return err
} }
os.Remove(tsConf)
if isResolvedRunning() { if isResolvedRunning() {
exec.Command("systemctl", "restart", "systemd-resolved.service").Run() // Best-effort. exec.Command("systemctl", "restart", "systemd-resolved.service").Run() // Best-effort.