mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-18 02:48:40 +00:00
wgengine/router: dns: unify on *BSD, multimode on Linux, Magic DNS (#536)
Signed-off-by: Dmytro Shynkevych <dmytro@tailscale.com>
This commit is contained in:
parent
6e8f0860af
commit
30bbbe9467
1
go.mod
1
go.mod
@ -9,6 +9,7 @@ require (
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
||||
github.com/gliderlabs/ssh v0.2.2
|
||||
github.com/go-ole/go-ole v1.2.4
|
||||
github.com/godbus/dbus/v5 v5.0.3
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e
|
||||
github.com/google/go-cmp v0.4.0
|
||||
github.com/goreleaser/nfpm v1.1.10
|
||||
|
3
go.sum
3
go.sum
@ -30,6 +30,9 @@ github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
|
||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
|
||||
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
|
||||
github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
|
||||
github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME=
|
||||
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
|
@ -50,7 +50,10 @@ func getVal() []interface{} {
|
||||
},
|
||||
},
|
||||
&router.Config{
|
||||
DNS: []netaddr.IP{netaddr.IPv4(8, 8, 8, 8)},
|
||||
DNSConfig: router.DNSConfig{
|
||||
Nameservers: []netaddr.IP{netaddr.IPv4(8, 8, 8, 8)},
|
||||
Domains: []string{"tailscale.net"},
|
||||
},
|
||||
},
|
||||
map[string]string{
|
||||
"key1": "val1",
|
||||
|
@ -471,7 +471,7 @@ func (b *LocalBackend) updateDNSMap(netMap *controlclient.NetworkMap) {
|
||||
// Like PeerStatus.SimpleHostName()
|
||||
domain = strings.TrimSuffix(domain, ".local")
|
||||
domain = strings.TrimSuffix(domain, ".localdomain")
|
||||
domain = domain + ".tailscale.us"
|
||||
domain = domain + ".b.tailscale.net"
|
||||
domainToIP[domain] = netaddr.IPFrom16(peer.Addresses[0].IP.Addr)
|
||||
}
|
||||
b.e.SetDNSMap(tsdns.NewMap(domainToIP))
|
||||
@ -868,11 +868,13 @@ func routerConfig(cfg *wgcfg.Config, prefs *Prefs, dnsDomains []string) *router.
|
||||
|
||||
rs := &router.Config{
|
||||
LocalAddrs: wgCIDRToNetaddr(addrs),
|
||||
DNS: wgIPToNetaddr(cfg.DNS),
|
||||
DNSDomains: dnsDomains,
|
||||
SubnetRoutes: wgCIDRToNetaddr(prefs.AdvertiseRoutes),
|
||||
SNATSubnetRoutes: !prefs.NoSNAT,
|
||||
NetfilterMode: prefs.NetfilterMode,
|
||||
DNSConfig: router.DNSConfig{
|
||||
Nameservers: wgIPToNetaddr(cfg.DNS),
|
||||
Domains: dnsDomains,
|
||||
},
|
||||
}
|
||||
|
||||
for _, peer := range cfg.Peers {
|
||||
|
83
wgengine/router/dns.go
Normal file
83
wgengine/router/dns.go
Normal file
@ -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 "???"
|
||||
}
|
||||
}
|
151
wgengine/router/dns_direct.go
Normal file
151
wgengine/router/dns_direct.go
Normal file
@ -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
|
||||
}
|
209
wgengine/router/dns_networkmanager.go
Normal file
209
wgengine/router/dns_networkmanager.go
Normal file
@ -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)
|
||||
}
|
84
wgengine/router/dns_resolvconf.go
Normal file
84
wgengine/router/dns_resolvconf.go
Normal file
@ -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
|
||||
}
|
177
wgengine/router/dns_resolved.go
Normal file
177
wgengine/router/dns_resolved.go
Normal file
@ -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
|
||||
}
|
@ -262,7 +262,7 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) error {
|
||||
}
|
||||
}()
|
||||
|
||||
setDNSDomains(guid, cfg.DNSDomains)
|
||||
setDNSDomains(guid, cfg.Domains)
|
||||
|
||||
routes := []winipcfg.RouteData{}
|
||||
var firstGateway4 *net.IP
|
||||
@ -359,7 +359,7 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) error {
|
||||
}
|
||||
|
||||
var dnsIPs []net.IP
|
||||
for _, ip := range cfg.DNS {
|
||||
for _, ip := range cfg.Nameservers {
|
||||
dnsIPs = append(dnsIPs, ip.IPAddr().IP)
|
||||
}
|
||||
err = iface.SetDNS(dnsIPs)
|
||||
|
@ -32,6 +32,7 @@ type Router interface {
|
||||
// New returns a new Router for the current platform, using the
|
||||
// provided tun device.
|
||||
func New(logf logger.Logf, wgdev *device.Device, tundev tun.Device) (Router, error) {
|
||||
logf = logger.WithPrefix(logf, "router: ")
|
||||
return newUserspaceRouter(logf, wgdev, tundev)
|
||||
}
|
||||
|
||||
@ -39,7 +40,7 @@ func New(logf logger.Logf, wgdev *device.Device, tundev tun.Device) (Router, err
|
||||
// in case the Tailscale daemon terminated without closing the router.
|
||||
// No other state needs to be instantiated before this runs.
|
||||
func Cleanup(logf logger.Logf, interfaceName string) {
|
||||
// TODO(dmytro): implement this.
|
||||
cleanup(logf, interfaceName)
|
||||
}
|
||||
|
||||
// NetfilterMode is the firewall management mode to use when
|
||||
@ -69,10 +70,10 @@ func (m NetfilterMode) String() string {
|
||||
// the OS's network stack.
|
||||
type Config struct {
|
||||
LocalAddrs []netaddr.IPPrefix
|
||||
DNS []netaddr.IP
|
||||
DNSDomains []string
|
||||
Routes []netaddr.IPPrefix // routes to point into the Tailscale interface
|
||||
|
||||
DNSConfig
|
||||
|
||||
// Linux-only things below, ignored on other platforms.
|
||||
|
||||
SubnetRoutes []netaddr.IPPrefix // subnets being advertised to other Tailscale nodes
|
||||
|
@ -52,3 +52,13 @@ func (r *darwinRouter) Up() error {
|
||||
}
|
||||
return r.Router.Up()
|
||||
}
|
||||
|
||||
func upDNS(config DNSConfig, interfaceName string) error {
|
||||
// Handled by IPNExtension
|
||||
return nil
|
||||
}
|
||||
|
||||
func downDNS(interfaceName string) error {
|
||||
// Handled by IPNExtension
|
||||
return nil
|
||||
}
|
||||
|
@ -15,3 +15,7 @@ import (
|
||||
func newUserspaceRouter(logf logger.Logf, tunname string, dev *device.Device, tuntap tun.Device, netChanged func()) Router {
|
||||
return NewFakeRouter(logf, tunname, dev, tuntap, netChanged)
|
||||
}
|
||||
|
||||
func cleanup() error {
|
||||
return nil
|
||||
}
|
||||
|
@ -5,6 +5,8 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/tailscale/wireguard-go/device"
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
"tailscale.com/types/logger"
|
||||
@ -18,3 +20,35 @@ import (
|
||||
func newUserspaceRouter(logf logger.Logf, _ *device.Device, tundev tun.Device) (Router, error) {
|
||||
return newUserspaceBSDRouter(logf, nil, tundev)
|
||||
}
|
||||
|
||||
func upDNS(config DNSConfig, interfaceName string) error {
|
||||
if len(config.Nameservers) == 0 {
|
||||
return downDNS(interfaceName)
|
||||
}
|
||||
|
||||
if resolvconfIsActive() {
|
||||
if err := dnsResolvconfUp(config, interfaceName); err != nil {
|
||||
return fmt.Errorf("resolvconf: %w")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := dnsDirectUp(config); err != nil {
|
||||
return fmt.Errorf("direct: %w")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func downDNS(interfaceName string) error {
|
||||
if resolvconfIsActive() {
|
||||
if err := dnsResolvconfDown(interfaceName); err != nil {
|
||||
return fmt.Errorf("resolvconf: %w")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := dnsDirectDown(); err != nil {
|
||||
return fmt.Errorf("direct: %w")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -5,19 +5,14 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/go-iptables/iptables"
|
||||
"github.com/tailscale/wireguard-go/device"
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
@ -73,6 +68,9 @@ type linuxRouter struct {
|
||||
snatSubnetRoutes bool
|
||||
netfilterMode NetfilterMode
|
||||
|
||||
dnsMode dnsMode
|
||||
dnsConfig DNSConfig
|
||||
|
||||
ipt4 netfilterRunner
|
||||
cmd commandRunner
|
||||
}
|
||||
@ -119,10 +117,27 @@ func (r *linuxRouter) Up() error {
|
||||
return err
|
||||
}
|
||||
|
||||
switch {
|
||||
// TODO(dmytro): enable resolved when per-domain resolvers are desired.
|
||||
case resolvedIsActive():
|
||||
r.dnsMode = dnsDirect
|
||||
// r.dnsMode = dnsResolved
|
||||
case nmIsActive():
|
||||
r.dnsMode = dnsNetworkManager
|
||||
case resolvconfIsActive():
|
||||
r.dnsMode = dnsResolvconf
|
||||
default:
|
||||
r.dnsMode = dnsDirect
|
||||
}
|
||||
r.logf("dns mode: %v", r.dnsMode)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *linuxRouter) down() error {
|
||||
func (r *linuxRouter) Close() error {
|
||||
if err := r.downDNS(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.downInterface(); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -139,20 +154,6 @@ func (r *linuxRouter) down() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *linuxRouter) Close() error {
|
||||
var ret error
|
||||
if ret = r.restoreResolvConf(); ret != nil {
|
||||
r.logf("failed to restore system resolv.conf: %v", ret)
|
||||
}
|
||||
if err := r.down(); err != nil {
|
||||
if ret == nil {
|
||||
ret = err
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// Set implements the Router interface.
|
||||
func (r *linuxRouter) Set(cfg *Config) error {
|
||||
if cfg == nil {
|
||||
@ -189,12 +190,14 @@ func (r *linuxRouter) Set(cfg *Config) error {
|
||||
}
|
||||
r.snatSubnetRoutes = cfg.SNATSubnetRoutes
|
||||
|
||||
// TODO: this:
|
||||
if false {
|
||||
if err := r.replaceResolvConf(cfg.DNS, cfg.DNSDomains); err != nil {
|
||||
return fmt.Errorf("replacing resolv.conf failed: %w", err)
|
||||
if !r.dnsConfig.EquivalentTo(cfg.DNSConfig) {
|
||||
if err := r.upDNS(cfg.DNSConfig); err != nil {
|
||||
r.logf("dns up: %v", err)
|
||||
} else {
|
||||
r.dnsConfig = cfg.DNSConfig
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -315,102 +318,6 @@ func (r *linuxRouter) setNetfilterMode(mode NetfilterMode) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
tsConf = "/etc/resolv.tailscale.conf"
|
||||
backupConf = "/etc/resolv.pre-tailscale-backup.conf"
|
||||
resolvConf = "/etc/resolv.conf"
|
||||
)
|
||||
|
||||
func (r *linuxRouter) replaceResolvConf(servers []netaddr.IP, domains []string) error {
|
||||
if len(servers) == 0 {
|
||||
return r.restoreResolvConf()
|
||||
}
|
||||
|
||||
// First write the tsConf file.
|
||||
buf := new(bytes.Buffer)
|
||||
fmt.Fprintf(buf, "# resolv.conf(5) file generated by tailscale\n")
|
||||
fmt.Fprintf(buf, "# DO NOT EDIT THIS FILE BY HAND -- CHANGES WILL BE OVERWRITTEN\n\n")
|
||||
for _, ns := range servers {
|
||||
fmt.Fprintf(buf, "nameserver %s\n", ns)
|
||||
}
|
||||
if len(domains) > 0 {
|
||||
fmt.Fprintf(buf, "search "+strings.Join(domains, " ")+"\n")
|
||||
}
|
||||
f, err := ioutil.TempFile(filepath.Dir(tsConf), filepath.Base(tsConf)+".*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.Close()
|
||||
if err := atomicfile.WriteFile(f.Name(), buf.Bytes(), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
os.Chmod(f.Name(), 0644) // ioutil.TempFile creates the file with 0600
|
||||
if err := os.Rename(f.Name(), tsConf); 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 os.IsNotExist(err) {
|
||||
// No existing /etc/resolv.conf file to backup.
|
||||
// Nothing to do.
|
||||
return nil
|
||||
} else if err != nil {
|
||||
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 nil
|
||||
}
|
||||
|
||||
out, _ := exec.Command("service", "systemd-resolved", "restart").CombinedOutput()
|
||||
if len(out) > 0 {
|
||||
r.logf("service systemd-resolved restart: %s", out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *linuxRouter) restoreResolvConf() error {
|
||||
if _, err := os.Stat(backupConf); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil // no backup resolv.conf to restore
|
||||
}
|
||||
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) // best effort removal of tsConf file
|
||||
out, _ := exec.Command("service", "systemd-resolved", "restart").CombinedOutput()
|
||||
if len(out) > 0 {
|
||||
r.logf("service systemd-resolved restart: %s", out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// addAddress adds an IP/mask to the tunnel interface. Fails if the
|
||||
// address is already assigned to the interface, or if the addition
|
||||
// fails.
|
||||
@ -932,3 +839,69 @@ func normalizeCIDR(cidr netaddr.IPPrefix) string {
|
||||
nip := ncidr.IP.Mask(ncidr.Mask)
|
||||
return fmt.Sprintf("%s/%d", nip, cidr.Bits)
|
||||
}
|
||||
|
||||
// upDNS updates the system DNS configuration to the given one.
|
||||
func (r *linuxRouter) upDNS(config DNSConfig) error {
|
||||
if len(config.Nameservers) == 0 {
|
||||
return r.downDNS()
|
||||
}
|
||||
|
||||
switch r.dnsMode {
|
||||
case dnsResolved:
|
||||
if err := dnsResolvedUp(config); err != nil {
|
||||
return fmt.Errorf("resolved: %w", err)
|
||||
}
|
||||
case dnsResolvconf:
|
||||
if err := dnsResolvconfUp(config, r.tunname); err != nil {
|
||||
return fmt.Errorf("resolvconf: %w", err)
|
||||
}
|
||||
case dnsNetworkManager:
|
||||
if err := dnsNetworkManagerUp(config, r.tunname); err != nil {
|
||||
return fmt.Errorf("network manager: %w", err)
|
||||
}
|
||||
case dnsDirect:
|
||||
if err := dnsDirectUp(config); err != nil {
|
||||
return fmt.Errorf("direct: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// downDNS restores system DNS configuration to its state before upDNS.
|
||||
// It is idempotent (in particular, it does nothing if upDNS was never run).
|
||||
func (r *linuxRouter) downDNS() error {
|
||||
switch r.dnsMode {
|
||||
case dnsResolved:
|
||||
if err := dnsResolvedDown(); err != nil {
|
||||
return fmt.Errorf("resolved: %w", err)
|
||||
}
|
||||
case dnsResolvconf:
|
||||
if err := dnsResolvconfDown(r.tunname); err != nil {
|
||||
return fmt.Errorf("resolvconf: %w", err)
|
||||
}
|
||||
case dnsNetworkManager:
|
||||
if err := dnsNetworkManagerDown(r.tunname); err != nil {
|
||||
return fmt.Errorf("network manager: %w", err)
|
||||
}
|
||||
case dnsDirect:
|
||||
if err := dnsDirectDown(); err != nil {
|
||||
return fmt.Errorf("direct: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cleanup(logf logger.Logf, interfaceName string) {
|
||||
// Note: we need not do anything for dnsResolved,
|
||||
// as its settings are interface-bound and get cleaned up for us.
|
||||
switch {
|
||||
case resolvconfIsActive():
|
||||
if err := dnsResolvconfDown(interfaceName); err != nil {
|
||||
logf("down down: resolvconf: %v", err)
|
||||
}
|
||||
default:
|
||||
if err := dnsDirectDown(); err != nil {
|
||||
logf("dns down: direct: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,20 +5,14 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/tailscale/wireguard-go/device"
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
@ -31,6 +25,8 @@ type openbsdRouter struct {
|
||||
tunname string
|
||||
local netaddr.IPPrefix
|
||||
routes map[netaddr.IPPrefix]struct{}
|
||||
|
||||
dnsConfig DNSConfig
|
||||
}
|
||||
|
||||
func newUserspaceRouter(logf logger.Logf, _ *device.Device, tundev tun.Device) (Router, error) {
|
||||
@ -159,112 +155,28 @@ func (r *openbsdRouter) Set(cfg *Config) error {
|
||||
r.local = localAddr
|
||||
r.routes = newRoutes
|
||||
|
||||
if err := r.replaceResolvConf(cfg.DNS, cfg.DNSDomains); err != nil {
|
||||
errq = fmt.Errorf("replacing resolv.conf failed: %v", err)
|
||||
if !r.dnsConfig.EquivalentTo(cfg.DNSConfig) {
|
||||
if err := dnsDirectUp(cfg.DNSConfig); err != nil {
|
||||
errq = fmt.Errorf("dns up: direct: %v", err)
|
||||
} else {
|
||||
r.dnsConfig = cfg.DNSConfig
|
||||
}
|
||||
}
|
||||
|
||||
return errq
|
||||
}
|
||||
|
||||
func (r *openbsdRouter) Close() error {
|
||||
out, err := cmd("ifconfig", r.tunname, "down").CombinedOutput()
|
||||
cleanup(r.logf, r.tunname)
|
||||
return nil
|
||||
}
|
||||
|
||||
func cleanup(logf logger.Logf, interfaceName string) {
|
||||
if err := dnsDirectDown(); err != nil {
|
||||
logf("dns down: direct: %v", err)
|
||||
}
|
||||
out, err := cmd("ifconfig", interfaceName, "down").CombinedOutput()
|
||||
if err != nil {
|
||||
r.logf("running ifconfig failed: %v\n%s", err, out)
|
||||
logf("ifconfig down: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
if err := r.restoreResolvConf(); err != nil {
|
||||
r.logf("failed to restore system resolv.conf: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
tsConf = "/etc/resolv.tailscale.conf"
|
||||
backupConf = "/etc/resolv.pre-tailscale-backup.conf"
|
||||
resolvConf = "/etc/resolv.conf"
|
||||
)
|
||||
|
||||
func (r *openbsdRouter) replaceResolvConf(servers []netaddr.IP, domains []string) error {
|
||||
if len(servers) == 0 {
|
||||
return r.restoreResolvConf()
|
||||
}
|
||||
|
||||
// Write the tsConf file.
|
||||
buf := new(bytes.Buffer)
|
||||
fmt.Fprintf(buf, "# resolv.conf(5) file generated by tailscale\n")
|
||||
fmt.Fprintf(buf, "# DO NOT EDIT THIS FILE BY HAND -- CHANGES WILL BE OVERWRITTEN\n\n")
|
||||
for _, ns := range servers {
|
||||
fmt.Fprintf(buf, "nameserver %s\n", ns)
|
||||
}
|
||||
if len(domains) > 0 {
|
||||
fmt.Fprintf(buf, "search "+strings.Join(domains, " ")+"\n")
|
||||
}
|
||||
tf, err := ioutil.TempFile(filepath.Dir(tsConf), filepath.Base(tsConf)+".*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tempName := tf.Name()
|
||||
tf.Close()
|
||||
|
||||
if err := atomicfile.WriteFile(tempName, buf.Bytes(), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(tempName, tsConf); 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 os.IsNotExist(err) {
|
||||
// No existing /etc/resolv.conf file to backup.
|
||||
// Nothing to do.
|
||||
return nil
|
||||
} else if err != nil {
|
||||
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 nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *openbsdRouter) restoreResolvConf() error {
|
||||
if _, err := os.Stat(backupConf); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil // No backup resolv.conf to restore.
|
||||
}
|
||||
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) // Best effort removal.
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -23,6 +23,8 @@ type userspaceBSDRouter struct {
|
||||
tunname string
|
||||
local netaddr.IPPrefix
|
||||
routes map[netaddr.IPPrefix]struct{}
|
||||
|
||||
dnsConfig DNSConfig
|
||||
}
|
||||
|
||||
func newUserspaceBSDRouter(logf logger.Logf, _ *device.Device, tundev tun.Device) (Router, error) {
|
||||
@ -36,7 +38,7 @@ func newUserspaceBSDRouter(logf logger.Logf, _ *device.Device, tundev tun.Device
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *userspaceBSDRouter) cmd(args ...string) *exec.Cmd {
|
||||
func cmd(args ...string) *exec.Cmd {
|
||||
if len(args) == 0 {
|
||||
log.Fatalf("exec.Cmd(%#v) invalid; need argv[0]\n", args)
|
||||
}
|
||||
@ -45,7 +47,7 @@ func (r *userspaceBSDRouter) cmd(args ...string) *exec.Cmd {
|
||||
|
||||
func (r *userspaceBSDRouter) Up() error {
|
||||
ifup := []string{"ifconfig", r.tunname, "up"}
|
||||
if out, err := r.cmd(ifup...).CombinedOutput(); err != nil {
|
||||
if out, err := cmd(ifup...).CombinedOutput(); err != nil {
|
||||
r.logf("running ifconfig failed: %v\n%s", err, out)
|
||||
return err
|
||||
}
|
||||
@ -73,7 +75,7 @@ func (r *userspaceBSDRouter) Set(cfg *Config) error {
|
||||
if r.local != (netaddr.IPPrefix{}) {
|
||||
addrdel := []string{"ifconfig", r.tunname,
|
||||
"inet", r.local.String(), "-alias"}
|
||||
out, err := r.cmd(addrdel...).CombinedOutput()
|
||||
out, err := cmd(addrdel...).CombinedOutput()
|
||||
if err != nil {
|
||||
r.logf("addr del failed: %v: %v\n%s", addrdel, err, out)
|
||||
if errq == nil {
|
||||
@ -85,7 +87,7 @@ func (r *userspaceBSDRouter) Set(cfg *Config) error {
|
||||
// Add the interface.
|
||||
addradd := []string{"ifconfig", r.tunname,
|
||||
"inet", localAddr.String(), localAddr.IP.String()}
|
||||
out, err := r.cmd(addradd...).CombinedOutput()
|
||||
out, err := cmd(addradd...).CombinedOutput()
|
||||
if err != nil {
|
||||
r.logf("addr add failed: %v: %v\n%s", addradd, err, out)
|
||||
if errq == nil {
|
||||
@ -107,7 +109,7 @@ func (r *userspaceBSDRouter) Set(cfg *Config) error {
|
||||
routedel := []string{"route", "-q", "-n",
|
||||
"del", "-inet", nstr,
|
||||
"-iface", r.tunname}
|
||||
out, err := r.cmd(routedel...).CombinedOutput()
|
||||
out, err := cmd(routedel...).CombinedOutput()
|
||||
if err != nil {
|
||||
r.logf("route del failed: %v: %v\n%s", routedel, err, out)
|
||||
if errq == nil {
|
||||
@ -125,7 +127,7 @@ func (r *userspaceBSDRouter) Set(cfg *Config) error {
|
||||
routeadd := []string{"route", "-q", "-n",
|
||||
"add", "-inet", nstr,
|
||||
"-iface", r.tunname}
|
||||
out, err := r.cmd(routeadd...).CombinedOutput()
|
||||
out, err := cmd(routeadd...).CombinedOutput()
|
||||
if err != nil {
|
||||
r.logf("addr add failed: %v: %v\n%s", routeadd, err, out)
|
||||
if errq == nil {
|
||||
@ -139,18 +141,29 @@ func (r *userspaceBSDRouter) Set(cfg *Config) error {
|
||||
r.local = localAddr
|
||||
r.routes = newRoutes
|
||||
|
||||
if err := r.replaceResolvConf(cfg.DNS, cfg.DNSDomains); err != nil {
|
||||
errq = fmt.Errorf("replacing resolv.conf failed: %v", err)
|
||||
if !r.dnsConfig.EquivalentTo(cfg.DNSConfig) {
|
||||
if err := upDNS(cfg.DNSConfig, r.tunname); err != nil {
|
||||
errq = fmt.Errorf("dns up: %v", err)
|
||||
} else {
|
||||
r.dnsConfig = cfg.DNSConfig
|
||||
}
|
||||
}
|
||||
|
||||
return errq
|
||||
}
|
||||
|
||||
func (r *userspaceBSDRouter) Close() error {
|
||||
cleanup(r.logf, r.tunname)
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO(mbaillie): these are no-ops for now. They could re-use the Linux funcs
|
||||
// (sans systemd parts), but I note Linux DNS is disabled(?) so leaving for now.
|
||||
func (r *userspaceBSDRouter) replaceResolvConf(_ []netaddr.IP, _ []string) error { return nil }
|
||||
func (r *userspaceBSDRouter) restoreResolvConf() error { return nil }
|
||||
func cleanup(logf logger.Logf, interfaceName string) {
|
||||
if err := downDNS(interfaceName); err != nil {
|
||||
logf("dns down: %v", err)
|
||||
}
|
||||
|
||||
ifup := []string{"ifconfig", interfaceName, "down"}
|
||||
if out, err := cmd(ifup...).CombinedOutput(); err != nil {
|
||||
logf("ifconfig down: %v\n%s", err, out)
|
||||
}
|
||||
}
|
||||
|
@ -64,3 +64,7 @@ func (r *winRouter) Close() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cleanup(logf logger.Logf, interfaceName string) {
|
||||
// DNS is interface-bound, so nothing to do here.
|
||||
}
|
||||
|
@ -57,6 +57,9 @@ const (
|
||||
magicDNSPort = 53
|
||||
)
|
||||
|
||||
// magicDNSDomain is the parent domain for Tailscale nodes.
|
||||
const magicDNSDomain = "b.tailscale.net"
|
||||
|
||||
type userspaceEngine struct {
|
||||
logf logger.Logf
|
||||
reqCh chan struct{}
|
||||
@ -180,7 +183,7 @@ func newUserspaceEngineAdvanced(conf EngineConfig) (_ Engine, reterr error) {
|
||||
reqCh: make(chan struct{}, 1),
|
||||
waitCh: make(chan struct{}),
|
||||
tundev: tstun.WrapTUN(logf, conf.TUN),
|
||||
resolver: tsdns.NewResolver(logf, "tailscale.us"),
|
||||
resolver: tsdns.NewResolver(logf, magicDNSDomain),
|
||||
useTailscaleDNS: conf.UseTailscaleDNS,
|
||||
pingers: make(map[wgcfg.Key]*pinger),
|
||||
}
|
||||
@ -548,8 +551,7 @@ func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config)
|
||||
if !addr.IP.Is4() {
|
||||
continue
|
||||
}
|
||||
bs := addr.IP.As16()
|
||||
localAddrs[packet.NewIP(net.IP(bs[12:16]))] = true
|
||||
localAddrs[packet.IPFromNetaddr(addr.IP)] = true
|
||||
}
|
||||
e.localAddrs.Store(localAddrs)
|
||||
|
||||
@ -565,6 +567,13 @@ func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config)
|
||||
}
|
||||
e.mu.Unlock()
|
||||
|
||||
// If the only nameserver is quad 100 (Magic DNS), set up the resolver appropriately.
|
||||
if len(routerCfg.Nameservers) == 1 && routerCfg.Nameservers[0] == packet.IP(magicDNSIP).Netaddr() {
|
||||
// TODO(dmytro): plumb dnsReadConfig here instead of hardcoding this.
|
||||
e.resolver.SetNameservers([]string{"8.8.8.8:53"})
|
||||
routerCfg.Domains = append([]string{magicDNSDomain}, routerCfg.Domains...)
|
||||
}
|
||||
|
||||
engineChanged := updateSig(&e.lastEngineSig, cfg)
|
||||
routerChanged := updateSig(&e.lastRouterSig, routerCfg)
|
||||
if !engineChanged && !routerChanged {
|
||||
|
Loading…
x
Reference in New Issue
Block a user