diff --git a/net/dns/direct.go b/net/dns/direct.go index 31613902a..c9081b16e 100644 --- a/net/dns/direct.go +++ b/net/dns/direct.go @@ -9,8 +9,6 @@ package dns import ( "bufio" "bytes" - "errors" - "fmt" "io" "io/ioutil" "os" @@ -23,7 +21,6 @@ import ( ) const ( - tsConf = "/etc/resolv.tailscale.conf" backupConf = "/etc/resolv.pre-tailscale-backup.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 readResolvConf() (OSConfig, error) { +func readResolvFile(path string) (OSConfig, error) { var config OSConfig - f, err := os.Open("/etc/resolv.conf") + f, err := os.Open(path) if err != nil { return config, err } @@ -82,6 +78,11 @@ func readResolvConf() (OSConfig, error) { 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, // even if it is not managing the system DNS settings. func isResolvedRunning() bool { @@ -114,46 +115,72 @@ func newDirectManager() directManager { return directManager{} } -func (m directManager) SetDNS(config OSConfig) error { - // Write the tsConf file. - buf := new(bytes.Buffer) - writeResolvConf(buf, config.Nameservers, config.SearchDomains) - if err := atomicfile.WriteFile(tsConf, buf.Bytes(), 0644); err != nil { - return err +// ownedByTailscale reports whether /etc/resolv.conf seems to be a +// tailscale-managed file. +func (m directManager) ownedByTailscale() (bool, error) { + st, err := os.Stat(resolvConf) + 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 +} - if linkPath, err := os.Readlink(resolvConf); err != nil { - // Remove any old backup that may exist. - os.Remove(backupConf) - - // Backup the existing /etc/resolv.conf file. - 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 +// 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 } - } else { - // Nothing to do, resolvConf already points to tsConf. + return err + } + + owned, err := m.ownedByTailscale() + if err != nil { + return err + } + if owned { return nil } - os.Remove(resolvConf) - if err := os.Symlink(tsConf, resolvConf); err != nil { + return os.Rename(resolvConf, backupConf) +} + +func (m directManager) SetDNS(config OSConfig) error { + if err := m.backupConfig(); err != nil { + 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() { - exec.Command("systemctl", "restart", "systemd-resolved.service").Run() // Best-effort. + exec.Command("systemctl", "restart", "systemd-resolved.service").Run() } return nil @@ -164,27 +191,53 @@ func (m directManager) SupportsSplitDNS() bool { } 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 { + // 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 the backup file does not exist, then Up never ran successfully. if os.IsNotExist(err) { + // No backup, nothing we can do. return nil } return err } - - if ln, err := os.Readlink(resolvConf); err != nil { + owned, err := m.ownedByTailscale() + if err != nil { 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 { return err } - os.Remove(tsConf) if isResolvedRunning() { exec.Command("systemctl", "restart", "systemd-resolved.service").Run() // Best-effort.