diff --git a/go.mod b/go.mod index 16397e375..e10e8e54e 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 10ccefa3c..f71e2480a 100644 --- a/go.sum +++ b/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= diff --git a/internal/deepprint/deepprint_test.go b/internal/deepprint/deepprint_test.go index 9c3f8dc6a..d13aff8e5 100644 --- a/internal/deepprint/deepprint_test.go +++ b/internal/deepprint/deepprint_test.go @@ -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", diff --git a/ipn/local.go b/ipn/local.go index bbc52be69..c2f852862 100644 --- a/ipn/local.go +++ b/ipn/local.go @@ -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 { diff --git a/wgengine/router/dns.go b/wgengine/router/dns.go new file mode 100644 index 000000000..56c3ad29d --- /dev/null +++ b/wgengine/router/dns.go @@ -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 "???" + } +} diff --git a/wgengine/router/dns_direct.go b/wgengine/router/dns_direct.go new file mode 100644 index 000000000..3418eea25 --- /dev/null +++ b/wgengine/router/dns_direct.go @@ -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 +} diff --git a/wgengine/router/dns_networkmanager.go b/wgengine/router/dns_networkmanager.go new file mode 100644 index 000000000..77a91b842 --- /dev/null +++ b/wgengine/router/dns_networkmanager.go @@ -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) +} diff --git a/wgengine/router/dns_resolvconf.go b/wgengine/router/dns_resolvconf.go new file mode 100644 index 000000000..204147ce9 --- /dev/null +++ b/wgengine/router/dns_resolvconf.go @@ -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 +} diff --git a/wgengine/router/dns_resolved.go b/wgengine/router/dns_resolved.go new file mode 100644 index 000000000..b37a4b87f --- /dev/null +++ b/wgengine/router/dns_resolved.go @@ -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 +} diff --git a/wgengine/router/ifconfig_windows.go b/wgengine/router/ifconfig_windows.go index 588381f1a..1c79cd8f2 100644 --- a/wgengine/router/ifconfig_windows.go +++ b/wgengine/router/ifconfig_windows.go @@ -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) diff --git a/wgengine/router/router.go b/wgengine/router/router.go index 34bdf6d7e..21926c5da 100644 --- a/wgengine/router/router.go +++ b/wgengine/router/router.go @@ -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 diff --git a/wgengine/router/router_darwin.go b/wgengine/router/router_darwin.go index 7dab1cf91..816cae53a 100644 --- a/wgengine/router/router_darwin.go +++ b/wgengine/router/router_darwin.go @@ -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 +} diff --git a/wgengine/router/router_default.go b/wgengine/router/router_default.go index c5440f8c9..db170fba0 100644 --- a/wgengine/router/router_default.go +++ b/wgengine/router/router_default.go @@ -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 +} diff --git a/wgengine/router/router_freebsd.go b/wgengine/router/router_freebsd.go index e7113223f..9fd8e1f41 100644 --- a/wgengine/router/router_freebsd.go +++ b/wgengine/router/router_freebsd.go @@ -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 +} diff --git a/wgengine/router/router_linux.go b/wgengine/router/router_linux.go index 7bb80a170..2a6154fd7 100644 --- a/wgengine/router/router_linux.go +++ b/wgengine/router/router_linux.go @@ -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) + } + } +} diff --git a/wgengine/router/router_openbsd.go b/wgengine/router/router_openbsd.go index dab71a5a8..574b2804a 100644 --- a/wgengine/router/router_openbsd.go +++ b/wgengine/router/router_openbsd.go @@ -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 } diff --git a/wgengine/router/router_userspace_bsd.go b/wgengine/router/router_userspace_bsd.go index 108b79161..5a8655f12 100644 --- a/wgengine/router/router_userspace_bsd.go +++ b/wgengine/router/router_userspace_bsd.go @@ -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) + } +} diff --git a/wgengine/router/router_windows.go b/wgengine/router/router_windows.go index a0e48cd9c..00f3e281c 100644 --- a/wgengine/router/router_windows.go +++ b/wgengine/router/router_windows.go @@ -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. +} diff --git a/wgengine/userspace.go b/wgengine/userspace.go index e6292240c..230ae2056 100644 --- a/wgengine/userspace.go +++ b/wgengine/userspace.go @@ -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 {