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:
parent
2685260ba1
commit
e638a4d86b
|
@ -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.
|
||||||
|
|
Loading…
Reference in New Issue