|
|
|
|
@ -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.
|
|
|
|
|
|