mirror of https://github.com/tailscale/tailscale/
wgengine/router: dns: unify on *BSD, multimode on Linux, Magic DNS (#536)
Signed-off-by: Dmytro Shynkevych <dmytro@tailscale.com>reviewable/pr553/r1
parent
6e8f0860af
commit
30bbbe9467
@ -0,0 +1,83 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"inet.af/netaddr"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DNSConfig is the subset of Config that contains DNS parameters.
|
||||||
|
type DNSConfig struct {
|
||||||
|
// Nameservers are the IP addresses of the nameservers to use.
|
||||||
|
Nameservers []netaddr.IP
|
||||||
|
// Domains are the search domains to use.
|
||||||
|
Domains []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// EquivalentTo determines whether its argument and receiver
|
||||||
|
// represent equivalent DNS configurations (then DNS reconfig is a no-op).
|
||||||
|
func (lhs DNSConfig) EquivalentTo(rhs DNSConfig) bool {
|
||||||
|
if len(lhs.Nameservers) != len(rhs.Nameservers) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(lhs.Domains) != len(rhs.Domains) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// With how we perform resolution order shouldn't matter,
|
||||||
|
// but it is unlikely that we will encounter different orders.
|
||||||
|
for i, server := range lhs.Nameservers {
|
||||||
|
if rhs.Nameservers[i] != server {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, domain := range lhs.Domains {
|
||||||
|
if rhs.Domains[i] != domain {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// dnsReconfigTimeout is the timeout for DNS reconfiguration.
|
||||||
|
//
|
||||||
|
// This is useful because certain conditions can cause indefinite hangs
|
||||||
|
// (such as improper dbus auth followed by contextless dbus.Object.Call).
|
||||||
|
// Such operations should be wrapped in a timeout context.
|
||||||
|
const dnsReconfigTimeout = time.Second
|
||||||
|
|
||||||
|
// dnsMode determines how DNS settings are managed.
|
||||||
|
type dnsMode uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
// dnsDirect indicates that /etc/resolv.conf is edited directly.
|
||||||
|
dnsDirect dnsMode = iota
|
||||||
|
// dnsResolvconf indicates that a resolvconf binary is used.
|
||||||
|
dnsResolvconf
|
||||||
|
// dnsNetworkManager indicates that the NetworkManaer DBus API is used.
|
||||||
|
dnsNetworkManager
|
||||||
|
// dnsResolved indicates that the systemd-resolved DBus API is used.
|
||||||
|
dnsResolved
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m dnsMode) String() string {
|
||||||
|
switch m {
|
||||||
|
case dnsDirect:
|
||||||
|
return "direct"
|
||||||
|
case dnsResolvconf:
|
||||||
|
return "resolvconf"
|
||||||
|
case dnsNetworkManager:
|
||||||
|
return "networkmanager"
|
||||||
|
case dnsResolved:
|
||||||
|
return "resolved"
|
||||||
|
default:
|
||||||
|
return "???"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,151 @@
|
|||||||
|
// 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 router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"inet.af/netaddr"
|
||||||
|
"tailscale.com/atomicfile"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tsConf = "/etc/resolv.tailscale.conf"
|
||||||
|
backupConf = "/etc/resolv.pre-tailscale-backup.conf"
|
||||||
|
resolvConf = "/etc/resolv.conf"
|
||||||
|
)
|
||||||
|
|
||||||
|
// dnsWriteConfig writes DNS configuration in resolv.conf format to the given writer.
|
||||||
|
func dnsWriteConfig(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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dnsReadConfig reads DNS configuration from /etc/resolv.conf.
|
||||||
|
func dnsReadConfig() (DNSConfig, error) {
|
||||||
|
var config DNSConfig
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// dnsDirectUp 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 dnsDirectDown before program shutdown
|
||||||
|
// and ensure that router.Cleanup is run if the program terminates unexpectedly.
|
||||||
|
func dnsDirectUp(config DNSConfig) error {
|
||||||
|
// Write the tsConf file.
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
dnsWriteConfig(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 && !os.IsNotExist(err) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// dnsDirectDown restores /etc/resolv.conf to its state before dnsDirectUp.
|
||||||
|
// It is idempotent and behaves correctly even if dnsDirectUp has never been run.
|
||||||
|
func dnsDirectDown() 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)
|
||||||
|
return nil
|
||||||
|
}
|
@ -0,0 +1,209 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/godbus/dbus/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type nmSettings map[string]map[string]dbus.Variant
|
||||||
|
|
||||||
|
// nmIsActive determines if NetworkManager is currently managing system DNS settings.
|
||||||
|
func nmIsActive() bool {
|
||||||
|
// This is somewhat tricky because NetworkManager supports a number
|
||||||
|
// of DNS configuration modes. In all cases, we expect it to be installed
|
||||||
|
// and /etc/resolv.conf to contain a mention of NetworkManager in the comments.
|
||||||
|
_, err := exec.LookPath("NetworkManager")
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open("/etc/resolv.conf")
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(f)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Bytes()
|
||||||
|
// Look for the word "NetworkManager" until comments end.
|
||||||
|
if len(line) > 0 && line[0] != '#' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if bytes.Contains(line, []byte("NetworkManager")) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// dnsNetworkManagerUp updates the DNS config for the Tailscale interface
|
||||||
|
// through the NetworkManager DBus API.
|
||||||
|
func dnsNetworkManagerUp(config DNSConfig, interfaceName string) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), dnsReconfigTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
conn, err := dbus.SystemBus()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connecting to system bus: %w", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
// This is how we get at the DNS settings:
|
||||||
|
// org.freedesktop.NetworkManager
|
||||||
|
// ⇩
|
||||||
|
// org.freedesktop.NetworkManager.Device
|
||||||
|
// (describes a network interface)
|
||||||
|
// ⇩
|
||||||
|
// org.freedesktop.NetworkManager.Connection.Active
|
||||||
|
// (active instance of a connection initialized from settings)
|
||||||
|
// ⇩
|
||||||
|
// org.freedesktop.NetworkManager.Connection
|
||||||
|
// (connection settings)
|
||||||
|
// contains {dns, dns-priority, dns-search}
|
||||||
|
//
|
||||||
|
// Ref: https://developer.gnome.org/NetworkManager/stable/settings-ipv4.html.
|
||||||
|
|
||||||
|
nm := conn.Object(
|
||||||
|
"org.freedesktop.NetworkManager",
|
||||||
|
dbus.ObjectPath("/org/freedesktop/NetworkManager"),
|
||||||
|
)
|
||||||
|
|
||||||
|
var devicePath dbus.ObjectPath
|
||||||
|
err = nm.CallWithContext(
|
||||||
|
ctx, "org.freedesktop.NetworkManager.GetDeviceByIpIface", 0,
|
||||||
|
interfaceName,
|
||||||
|
).Store(&devicePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("GetDeviceByIpIface: %w", err)
|
||||||
|
}
|
||||||
|
device := conn.Object("org.freedesktop.NetworkManager", devicePath)
|
||||||
|
|
||||||
|
var activeConnPath dbus.ObjectPath
|
||||||
|
err = device.CallWithContext(
|
||||||
|
ctx, "org.freedesktop.DBus.Properties.Get", 0,
|
||||||
|
"org.freedesktop.NetworkManager.Device", "ActiveConnection",
|
||||||
|
).Store(&activeConnPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting ActiveConnection: %w", err)
|
||||||
|
}
|
||||||
|
activeConn := conn.Object("org.freedesktop.NetworkManager", activeConnPath)
|
||||||
|
|
||||||
|
var connPath dbus.ObjectPath
|
||||||
|
err = activeConn.CallWithContext(
|
||||||
|
ctx, "org.freedesktop.DBus.Properties.Get", 0,
|
||||||
|
"org.freedesktop.NetworkManager.Connection.Active", "Connection",
|
||||||
|
).Store(&connPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting Connection: %w", err)
|
||||||
|
}
|
||||||
|
connection := conn.Object("org.freedesktop.NetworkManager", connPath)
|
||||||
|
|
||||||
|
// Note: strictly speaking, the following is not safe.
|
||||||
|
//
|
||||||
|
// It appears that the way to update connection settings
|
||||||
|
// in NetworkManager is to get an entire connection settings object,
|
||||||
|
// modify the fields we are interested in, then submit the modified object.
|
||||||
|
//
|
||||||
|
// This is unfortunate: if the network state changes in the meantime
|
||||||
|
// (most relevantly to us, if routes change), we will overwrite those changes.
|
||||||
|
//
|
||||||
|
// That said, fortunately, this should have no real effect, as Tailscale routes
|
||||||
|
// do not seem to show up in NetworkManager at all,
|
||||||
|
// so they are presumably immune from being tampered with.
|
||||||
|
|
||||||
|
var settings nmSettings
|
||||||
|
err = connection.CallWithContext(
|
||||||
|
ctx, "org.freedesktop.NetworkManager.Settings.Connection.GetSettings", 0,
|
||||||
|
).Store(&settings)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting Settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Frustratingly, NetworkManager represents IPv4 addresses as uint32s,
|
||||||
|
// although IPv6 addresses are represented as byte arrays.
|
||||||
|
// Perform the conversion here.
|
||||||
|
var (
|
||||||
|
dnsv4 []uint32
|
||||||
|
dnsv6 [][]byte
|
||||||
|
)
|
||||||
|
for _, ip := range config.Nameservers {
|
||||||
|
b := ip.As16()
|
||||||
|
if ip.Is4() {
|
||||||
|
dnsv4 = append(dnsv4, binary.BigEndian.Uint32(b[12:]))
|
||||||
|
} else {
|
||||||
|
dnsv6 = append(dnsv6, b[:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ipv4Map := settings["ipv4"]
|
||||||
|
ipv4Map["dns"] = dbus.MakeVariant(dnsv4)
|
||||||
|
ipv4Map["dns-search"] = dbus.MakeVariant(config.Domains)
|
||||||
|
// dns-priority = -1 ensures that we have priority
|
||||||
|
// over other interfaces, except those exploiting this same trick.
|
||||||
|
// Ref: https://bugs.launchpad.net/ubuntu/+source/network-manager/+bug/1211110/comments/92.
|
||||||
|
ipv4Map["dns-priority"] = dbus.MakeVariant(-1)
|
||||||
|
// In principle, we should not need set this to true,
|
||||||
|
// as our interface does not configure any automatic DNS settings (presumably via DHCP).
|
||||||
|
// All the same, better to be safe.
|
||||||
|
ipv4Map["ignore-auto-dns"] = dbus.MakeVariant(true)
|
||||||
|
|
||||||
|
ipv6Map := settings["ipv6"]
|
||||||
|
// This is a hack.
|
||||||
|
// Methods "disabled", "ignore", "link-local" (IPv6 default) prevent us from setting DNS.
|
||||||
|
// It seems that our only recourse is "manual" or "auto".
|
||||||
|
// "manual" requires addresses, so we use "auto", which will assign us a random IPv6 /64.
|
||||||
|
ipv6Map["method"] = dbus.MakeVariant("auto")
|
||||||
|
// Our IPv6 config is a fake, so it should never become the default route.
|
||||||
|
ipv6Map["never-default"] = dbus.MakeVariant(true)
|
||||||
|
// Moreover, we should ignore all autoconfigured routes (hopefully none), as they are bogus.
|
||||||
|
ipv6Map["ignore-auto-routes"] = dbus.MakeVariant(true)
|
||||||
|
|
||||||
|
// Finally, set the actual DNS config.
|
||||||
|
ipv6Map["dns"] = dbus.MakeVariant(dnsv6)
|
||||||
|
ipv6Map["dns-search"] = dbus.MakeVariant(config.Domains)
|
||||||
|
ipv6Map["dns-priority"] = dbus.MakeVariant(-1)
|
||||||
|
ipv6Map["ignore-auto-dns"] = dbus.MakeVariant(true)
|
||||||
|
|
||||||
|
// deprecatedProperties are the properties in interface settings
|
||||||
|
// that are deprecated by NetworkManager.
|
||||||
|
//
|
||||||
|
// In practice, this means that they are returned for reading,
|
||||||
|
// but submitting a settings object with them present fails
|
||||||
|
// with hard-to-diagnose errors. They must be removed.
|
||||||
|
deprecatedProperties := []string{
|
||||||
|
"addresses", "routes",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, property := range deprecatedProperties {
|
||||||
|
delete(ipv4Map, property)
|
||||||
|
delete(ipv6Map, property)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = connection.CallWithContext(
|
||||||
|
ctx, "org.freedesktop.NetworkManager.Settings.Connection.UpdateUnsaved", 0, settings,
|
||||||
|
).Store()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("setting Settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// dnsNetworkManagerDown undoes the changes made by dnsNetworkManagerUp.
|
||||||
|
func dnsNetworkManagerDown(interfaceName string) error {
|
||||||
|
return dnsNetworkManagerUp(DNSConfig{Nameservers: nil, Domains: nil}, interfaceName)
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// resolvconfIsActive indicates whether the system appears to be using resolvconf.
|
||||||
|
// If this is true, then dnsManualUp should be avoided:
|
||||||
|
// resolvconf has exclusive ownership of /etc/resolv.conf.
|
||||||
|
func resolvconfIsActive() bool {
|
||||||
|
// Sanity-check first: if there is no resolvconf binary, then this is fruitless.
|
||||||
|
//
|
||||||
|
// However, this binary may be a shim like the one systemd-resolved provides.
|
||||||
|
// Such a shim may not behave as expected: in particular, systemd-resolved
|
||||||
|
// does not seem to respect the exclusive mode -x, saying:
|
||||||
|
// -x Send DNS traffic preferably over this interface
|
||||||
|
// whereas e.g. openresolv sends DNS traffix _exclusively_ over that interface,
|
||||||
|
// or not at all (in case of another exclusive-mode request later in time).
|
||||||
|
//
|
||||||
|
// Moreover, resolvconf may be installed but unused, in which case we should
|
||||||
|
// not use it either, lest we clobber existing configuration.
|
||||||
|
//
|
||||||
|
// To handle all the above correctly, we scan the comments in /etc/resolv.conf
|
||||||
|
// to ensure that it was generated by a resolvconf implementation.
|
||||||
|
_, err := exec.LookPath("resolvconf")
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open("/etc/resolv.conf")
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(f)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Bytes()
|
||||||
|
// Look for the word "resolvconf" until comments end.
|
||||||
|
if len(line) > 0 && line[0] != '#' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if bytes.Contains(line, []byte("resolvconf")) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// dnsResolvconfUp invokes the resolvconf binary to associate
|
||||||
|
// the given DNS configuration the Tailscale interface.
|
||||||
|
func dnsResolvconfUp(config DNSConfig, interfaceName string) error {
|
||||||
|
stdin := new(bytes.Buffer)
|
||||||
|
dnsWriteConfig(stdin, config.Nameservers, config.Domains) // dns_direct.go
|
||||||
|
|
||||||
|
cmd := exec.Command("resolvconf", "-m", "0", "-x", "-a", interfaceName+".inet")
|
||||||
|
cmd.Stdin = stdin
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("running %s: %s", cmd, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// dnsResolvconfDown undoes the action of dnsResolvconfUp.
|
||||||
|
func dnsResolvconfDown(interfaceName string) error {
|
||||||
|
cmd := exec.Command("resolvconf", "-f", "-d", interfaceName+".inet")
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("running %s: %s", cmd, out)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@ -0,0 +1,177 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/godbus/dbus/v5"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
"inet.af/netaddr"
|
||||||
|
"tailscale.com/net/interfaces"
|
||||||
|
)
|
||||||
|
|
||||||
|
// resolvedListenAddr is the listen address of the resolved stub resolver.
|
||||||
|
//
|
||||||
|
// We only consider resolved to be the system resolver if the stub resolver is;
|
||||||
|
// that is, if this address is the sole nameserver in /etc/resolved.conf.
|
||||||
|
// In other cases, resolved may still be managing the system DNS configuration directly.
|
||||||
|
// Then the nameserver list will be a concatenation of those for all
|
||||||
|
// the interfaces that register their interest in being a default resolver with
|
||||||
|
// SetLinkDomains([]{{"~.", true}, ...})
|
||||||
|
// which includes at least the interface with the default route, i.e. not us.
|
||||||
|
// This does not work for us: there is a possibility of getting NXDOMAIN
|
||||||
|
// from the other nameservers before we are asked or get a chance to respond.
|
||||||
|
// We consider this case as lacking resolved support and fall through to dnsDirect.
|
||||||
|
//
|
||||||
|
// While it may seem that we need to read a config option to get at this,
|
||||||
|
// this address is, in fact, hard-coded into resolved.
|
||||||
|
var resolvedListenAddr = netaddr.IPv4(127, 0, 0, 53)
|
||||||
|
|
||||||
|
var errNotReady = errors.New("interface not ready")
|
||||||
|
|
||||||
|
type resolvedLinkNameserver struct {
|
||||||
|
Family int32
|
||||||
|
Address []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type resolvedLinkDomain struct {
|
||||||
|
Domain string
|
||||||
|
RoutingOnly bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolvedIsActive determines if resolved is currently managing system DNS settings.
|
||||||
|
func resolvedIsActive() bool {
|
||||||
|
// 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").Run()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := dnsReadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// The sole nameserver must be the systemd-resolved stub.
|
||||||
|
if len(config.Nameservers) == 1 && config.Nameservers[0] == resolvedListenAddr {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// dnsResolvedUp sets the DNS parameters for the Tailscale interface
|
||||||
|
// to given nameservers and search domains using the resolved DBus API.
|
||||||
|
func dnsResolvedUp(config DNSConfig) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), dnsReconfigTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
conn, err := dbus.SystemBus()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connecting to system bus: %w", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
resolved := conn.Object(
|
||||||
|
"org.freedesktop.resolve1",
|
||||||
|
dbus.ObjectPath("/org/freedesktop/resolve1"),
|
||||||
|
)
|
||||||
|
|
||||||
|
_, iface, err := interfaces.Tailscale()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting interface index: %w", err)
|
||||||
|
}
|
||||||
|
if iface == nil {
|
||||||
|
return errNotReady
|
||||||
|
}
|
||||||
|
|
||||||
|
var linkNameservers = make([]resolvedLinkNameserver, len(config.Nameservers))
|
||||||
|
for i, server := range config.Nameservers {
|
||||||
|
ip := server.As16()
|
||||||
|
if server.Is4() {
|
||||||
|
linkNameservers[i] = resolvedLinkNameserver{
|
||||||
|
Family: unix.AF_INET,
|
||||||
|
Address: ip[12:],
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
linkNameservers[i] = resolvedLinkNameserver{
|
||||||
|
Family: unix.AF_INET6,
|
||||||
|
Address: ip[:],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = resolved.CallWithContext(
|
||||||
|
ctx, "org.freedesktop.resolve1.Manager.SetLinkDNS", 0,
|
||||||
|
iface.Index, linkNameservers,
|
||||||
|
).Store()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("SetLinkDNS: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var linkDomains = make([]resolvedLinkDomain, len(config.Domains))
|
||||||
|
for i, domain := range config.Domains {
|
||||||
|
linkDomains[i] = resolvedLinkDomain{
|
||||||
|
Domain: domain,
|
||||||
|
RoutingOnly: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = resolved.CallWithContext(
|
||||||
|
ctx, "org.freedesktop.resolve1.Manager.SetLinkDomains", 0,
|
||||||
|
iface.Index, linkDomains,
|
||||||
|
).Store()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("SetLinkDomains: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// dnsResolvedDown undoes the changes made by dnsResolvedUp.
|
||||||
|
func dnsResolvedDown() error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), dnsReconfigTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
conn, err := dbus.SystemBus()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connecting to system bus: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved := conn.Object(
|
||||||
|
"org.freedesktop.resolve1",
|
||||||
|
dbus.ObjectPath("/org/freedesktop/resolve1"),
|
||||||
|
)
|
||||||
|
|
||||||
|
_, iface, err := interfaces.Tailscale()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting interface index: %w", err)
|
||||||
|
}
|
||||||
|
if iface == nil {
|
||||||
|
return errNotReady
|
||||||
|
}
|
||||||
|
|
||||||
|
err = resolved.CallWithContext(
|
||||||
|
ctx, "org.freedesktop.resolve1.Manager.RevertLink", 0,
|
||||||
|
iface.Index,
|
||||||
|
).Store()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("RevertLink: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in New Issue