wgengine/router: dns: unify on *BSD, multimode on Linux, Magic DNS (#536)

Signed-off-by: Dmytro Shynkevych <dmytro@tailscale.com>
reviewable/pr553/r1
Dmytro Shynkevych 4 years ago committed by GitHub
parent 6e8f0860af
commit 30bbbe9467
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

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

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

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

@ -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()
if err != nil {
r.logf("running ifconfig failed: %v\n%s", err, out)
}
if err := r.restoreResolvConf(); err != nil {
r.logf("failed to restore system resolv.conf: %v", err)
}
cleanup(r.logf, r.tunname)
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()
func cleanup(logf logger.Logf, interfaceName string) {
if err := dnsDirectDown(); err != nil {
logf("dns down: direct: %v", err)
}
// 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)+".*")
out, err := cmd("ifconfig", interfaceName, "down").CombinedOutput()
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
logf("ifconfig down: %v\n%s", err, out)
}
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…
Cancel
Save