mirror of
https://github.com/tailscale/tailscale.git
synced 2024-12-04 23:45:34 +00:00
01ee638cca
os.IsNotExist doesn't unwrap errors. errors.Is does. The ioutil.ReadFile ones happened to be fine but I changed them so we're consistent with the rule: if the error comes from os, you can use os.IsNotExist, but from any other package, use errors.Is. (errors.Is always would also work, but not worth updating all the code) The motivation here was that we were logging about failure to migrate legacy relay node prefs file on startup, even though the code tried to avoid that. See golang/go#41122
189 lines
4.8 KiB
Go
189 lines
4.8 KiB
Go
// 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.
|
|
|
|
// +build linux freebsd openbsd
|
|
|
|
package dns
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"inet.af/netaddr"
|
|
"tailscale.com/atomicfile"
|
|
)
|
|
|
|
const (
|
|
tsConf = "/etc/resolv.tailscale.conf"
|
|
backupConf = "/etc/resolv.pre-tailscale-backup.conf"
|
|
resolvConf = "/etc/resolv.conf"
|
|
)
|
|
|
|
// writeResolvConf writes DNS configuration in resolv.conf format to the given writer.
|
|
func writeResolvConf(w io.Writer, servers []netaddr.IP, domains []string) {
|
|
io.WriteString(w, "# resolv.conf(5) file generated by tailscale\n")
|
|
io.WriteString(w, "# DO NOT EDIT THIS FILE BY HAND -- CHANGES WILL BE OVERWRITTEN\n\n")
|
|
for _, ns := range servers {
|
|
io.WriteString(w, "nameserver ")
|
|
io.WriteString(w, ns.String())
|
|
io.WriteString(w, "\n")
|
|
}
|
|
if len(domains) > 0 {
|
|
io.WriteString(w, "search")
|
|
for _, domain := range domains {
|
|
io.WriteString(w, " ")
|
|
io.WriteString(w, domain)
|
|
}
|
|
io.WriteString(w, "\n")
|
|
}
|
|
}
|
|
|
|
// readResolvConf reads DNS configuration from /etc/resolv.conf.
|
|
func readResolvConf() (Config, error) {
|
|
var config Config
|
|
|
|
f, err := os.Open("/etc/resolv.conf")
|
|
if err != nil {
|
|
return config, err
|
|
}
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
|
|
if strings.HasPrefix(line, "nameserver") {
|
|
nameserver := strings.TrimPrefix(line, "nameserver")
|
|
nameserver = strings.TrimSpace(nameserver)
|
|
ip, err := netaddr.ParseIP(nameserver)
|
|
if err != nil {
|
|
return config, err
|
|
}
|
|
config.Nameservers = append(config.Nameservers, ip)
|
|
continue
|
|
}
|
|
|
|
if strings.HasPrefix(line, "search") {
|
|
domain := strings.TrimPrefix(line, "search")
|
|
domain = strings.TrimSpace(domain)
|
|
config.Domains = append(config.Domains, domain)
|
|
continue
|
|
}
|
|
}
|
|
|
|
return config, nil
|
|
}
|
|
|
|
// isResolvedRunning reports whether systemd-resolved is running on the system,
|
|
// even if it is not managing the system DNS settings.
|
|
func isResolvedRunning() bool {
|
|
if runtime.GOOS != "linux" {
|
|
return false
|
|
}
|
|
|
|
// systemd-resolved is never installed without systemd.
|
|
_, err := exec.LookPath("systemctl")
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
// is-active exits with code 3 if the service is not active.
|
|
err = exec.Command("systemctl", "is-active", "systemd-resolved.service").Run()
|
|
|
|
return err == nil
|
|
}
|
|
|
|
// directManager is a managerImpl which replaces /etc/resolv.conf with a file
|
|
// generated from the given configuration, creating a backup of its old state.
|
|
//
|
|
// This way of configuring DNS is precarious, since it does not react
|
|
// to the disappearance of the Tailscale interface.
|
|
// The caller must call Down before program shutdown
|
|
// or as cleanup if the program terminates unexpectedly.
|
|
type directManager struct{}
|
|
|
|
func newDirectManager(mconfig ManagerConfig) managerImpl {
|
|
return directManager{}
|
|
}
|
|
|
|
// Up implements managerImpl.
|
|
func (m directManager) Up(config Config) error {
|
|
// Write the tsConf file.
|
|
buf := new(bytes.Buffer)
|
|
writeResolvConf(buf, config.Nameservers, config.Domains)
|
|
if err := atomicfile.WriteFile(tsConf, buf.Bytes(), 0644); err != nil {
|
|
return err
|
|
}
|
|
|
|
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
|
|
}
|
|
} else {
|
|
// Nothing to do, resolvConf already points to tsConf.
|
|
return nil
|
|
}
|
|
|
|
os.Remove(resolvConf)
|
|
if err := os.Symlink(tsConf, resolvConf); err != nil {
|
|
return err
|
|
}
|
|
|
|
if isResolvedRunning() {
|
|
exec.Command("systemctl", "restart", "systemd-resolved.service").Run() // Best-effort.
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Down implements managerImpl.
|
|
func (m directManager) Down() error {
|
|
if _, err := os.Stat(backupConf); err != nil {
|
|
// If the backup file does not exist, then Up never ran successfully.
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
if ln, err := os.Readlink(resolvConf); err != nil {
|
|
return err
|
|
} else if ln != tsConf {
|
|
return fmt.Errorf("resolv.conf is not a symlink to %s", tsConf)
|
|
}
|
|
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.
|
|
}
|
|
|
|
return nil
|
|
}
|