summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--cmd/tailscaled/tailscaled.go11
-rw-r--r--cmd/tailscaled/tailscaled.service2
-rw-r--r--control/controlclient/direct.go26
-rw-r--r--control/controlclient/netmap.go10
-rw-r--r--internal/deepprint/deepprint_test.go3
-rw-r--r--ipn/local.go129
-rw-r--r--logpolicy/logpolicy.go25
-rw-r--r--net/interfaces/interfaces_darwin.go8
-rw-r--r--net/tsaddr/tsaddr.go22
-rw-r--r--tailcfg/tailcfg.go25
-rw-r--r--version/cmdname.go26
-rw-r--r--wgengine/filter/filter.go40
-rw-r--r--wgengine/filter/filter_test.go56
-rw-r--r--wgengine/magicsock/magicsock.go38
-rw-r--r--wgengine/magicsock/magicsock_test.go3
-rw-r--r--wgengine/packet/packet.go2
-rw-r--r--wgengine/packet/packet_test.go2
-rw-r--r--wgengine/router/dns.go74
-rw-r--r--wgengine/router/dns/config.go75
-rw-r--r--wgengine/router/dns/direct.go (renamed from wgengine/router/dns_direct.go)36
-rw-r--r--wgengine/router/dns/manager.go94
-rw-r--r--wgengine/router/dns/manager_default.go14
-rw-r--r--wgengine/router/dns/manager_freebsd.go14
-rw-r--r--wgengine/router/dns/manager_linux.go27
-rw-r--r--wgengine/router/dns/manager_openbsd.go9
-rw-r--r--wgengine/router/dns/manager_windows.go83
-rw-r--r--wgengine/router/dns/nm.go (renamed from wgengine/router/dns_networkmanager.go)32
-rw-r--r--wgengine/router/dns/noop.go17
-rw-r--r--wgengine/router/dns/resolvconf.go (renamed from wgengine/router/dns_resolvconf.go)62
-rw-r--r--wgengine/router/dns/resolved.go (renamed from wgengine/router/dns_resolved.go)44
-rw-r--r--wgengine/router/ifconfig_windows.go35
-rw-r--r--wgengine/router/router.go12
-rw-r--r--wgengine/router/router_darwin.go53
-rw-r--r--wgengine/router/router_darwin_support.go23
-rw-r--r--wgengine/router/router_default.go4
-rw-r--r--wgengine/router/router_freebsd.go39
-rw-r--r--wgengine/router/router_linux.go100
-rw-r--r--wgengine/router/router_openbsd.go29
-rw-r--r--wgengine/router/router_userspace_bsd.go34
-rw-r--r--wgengine/router/router_windows.go24
-rw-r--r--wgengine/tsdns/tsdns.go3
-rw-r--r--wgengine/userspace.go109
42 files changed, 911 insertions, 563 deletions
diff --git a/cmd/tailscaled/tailscaled.go b/cmd/tailscaled/tailscaled.go
index 90c44a492..a9226c0ee 100644
--- a/cmd/tailscaled/tailscaled.go
+++ b/cmd/tailscaled/tailscaled.go
@@ -149,11 +149,16 @@ func run() error {
ctx, cancel := context.WithCancel(context.Background())
// Exit gracefully by cancelling the ipnserver context in most common cases:
// interrupted from the TTY or killed by a service manager.
+ interrupt := make(chan os.Signal, 1)
+ signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
+ // SIGPIPE sometimes gets generated when CLIs disconnect from
+ // tailscaled. The default action is to terminate the process, we
+ // want to keep running.
+ signal.Ignore(syscall.SIGPIPE)
go func() {
- interrupt := make(chan os.Signal, 1)
- signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
select {
- case <-interrupt:
+ case s := <-interrupt:
+ logf("tailscaled got signal %v; shutting down", s)
cancel()
case <-ctx.Done():
// continue
diff --git a/cmd/tailscaled/tailscaled.service b/cmd/tailscaled/tailscaled.service
index 48fef12d5..7d61d656e 100644
--- a/cmd/tailscaled/tailscaled.service
+++ b/cmd/tailscaled/tailscaled.service
@@ -3,8 +3,6 @@ Description=Tailscale node agent
Documentation=https://tailscale.com/kb/
Wants=network-pre.target
After=network-pre.target
-StartLimitIntervalSec=0
-StartLimitBurst=0
[Service]
EnvironmentFile=/etc/default/tailscaled
diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go
index a541f00bd..e5d69e373 100644
--- a/control/controlclient/direct.go
+++ b/control/controlclient/direct.go
@@ -30,6 +30,7 @@ import (
"github.com/tailscale/wireguard-go/wgcfg"
"golang.org/x/crypto/nacl/box"
"golang.org/x/oauth2"
+ "inet.af/netaddr"
"tailscale.com/log/logheap"
"tailscale.com/net/netns"
"tailscale.com/net/tlsdial"
@@ -638,8 +639,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
Domain: resp.Domain,
Roles: resp.Roles,
- DNS: resp.DNS,
- DNSDomains: resp.SearchPaths,
+ DNS: resp.DNSConfig,
Hostinfo: resp.Node.Hostinfo,
PacketFilter: c.parsePacketFilter(resp.PacketFilter),
DERPMap: lastDERPMap,
@@ -653,6 +653,15 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
} else {
nm.MachineStatus = tailcfg.MachineUnauthorized
}
+ if len(resp.DNS) > 0 {
+ nm.DNS.Nameservers = wgIPToNetaddr(resp.DNS)
+ }
+ if len(resp.SearchPaths) > 0 {
+ nm.DNS.Domains = resp.SearchPaths
+ }
+ if Debug.ProxyDNS {
+ nm.DNS.Proxied = true
+ }
// Printing the netmap can be extremely verbose, but is very
// handy for debugging. Let's limit how often we do it.
@@ -792,12 +801,24 @@ func loadServerKey(ctx context.Context, httpc *http.Client, serverURL string) (w
return key, nil
}
+func wgIPToNetaddr(ips []wgcfg.IP) (ret []netaddr.IP) {
+ for _, ip := range ips {
+ nip, ok := netaddr.FromStdIP(ip.IP())
+ if !ok {
+ panic(fmt.Sprintf("conversion of %s from wgcfg to netaddr IP failed", ip))
+ }
+ ret = append(ret, nip.Unmap())
+ }
+ return ret
+}
+
// Debug contains temporary internal-only debug knobs.
// They're unexported to not draw attention to them.
var Debug = initDebug()
type debug struct {
NetMap bool
+ ProxyDNS bool
OnlyDisco bool
Disco bool
ForceDisco bool // ask control server to not filter out our disco key
@@ -806,6 +827,7 @@ type debug struct {
func initDebug() debug {
d := debug{
NetMap: envBool("TS_DEBUG_NETMAP"),
+ ProxyDNS: envBool("TS_DEBUG_PROXY_DNS"),
OnlyDisco: os.Getenv("TS_DEBUG_USE_DISCO") == "only",
ForceDisco: os.Getenv("TS_DEBUG_USE_DISCO") == "only" || envBool("TS_DEBUG_USE_DISCO"),
}
diff --git a/control/controlclient/netmap.go b/control/controlclient/netmap.go
index 872954030..1ef0bb12f 100644
--- a/control/controlclient/netmap.go
+++ b/control/controlclient/netmap.go
@@ -32,8 +32,7 @@ type NetworkMap struct {
LocalPort uint16 // used for debugging
MachineStatus tailcfg.MachineStatus
Peers []*tailcfg.Node // sorted by Node.ID
- DNS []wgcfg.IP
- DNSDomains []string
+ DNS tailcfg.DNSConfig
Hostinfo tailcfg.Hostinfo
PacketFilter filter.Matches
@@ -219,8 +218,8 @@ const (
// TODO(bradfitz): UAPI seems to only be used by the old confnode and
// pingnode; delete this when those are deleted/rewritten?
-func (nm *NetworkMap) UAPI(flags WGConfigFlags, dnsOverride []wgcfg.IP) string {
- wgcfg, err := nm.WGCfg(log.Printf, flags, dnsOverride)
+func (nm *NetworkMap) UAPI(flags WGConfigFlags) string {
+ wgcfg, err := nm.WGCfg(log.Printf, flags)
if err != nil {
log.Fatalf("WGCfg() failed unexpectedly: %v", err)
}
@@ -237,13 +236,12 @@ func (nm *NetworkMap) UAPI(flags WGConfigFlags, dnsOverride []wgcfg.IP) string {
const EndpointDiscoSuffix = ".disco.tailscale:12345"
// WGCfg returns the NetworkMaps's Wireguard configuration.
-func (nm *NetworkMap) WGCfg(logf logger.Logf, flags WGConfigFlags, dnsOverride []wgcfg.IP) (*wgcfg.Config, error) {
+func (nm *NetworkMap) WGCfg(logf logger.Logf, flags WGConfigFlags) (*wgcfg.Config, error) {
cfg := &wgcfg.Config{
Name: "tailscale",
PrivateKey: nm.PrivateKey,
Addresses: nm.Addresses,
ListenPort: nm.LocalPort,
- DNS: append([]wgcfg.IP(nil), dnsOverride...),
Peers: make([]wgcfg.Peer, 0, len(nm.Peers)),
}
diff --git a/internal/deepprint/deepprint_test.go b/internal/deepprint/deepprint_test.go
index d13aff8e5..d88541f67 100644
--- a/internal/deepprint/deepprint_test.go
+++ b/internal/deepprint/deepprint_test.go
@@ -11,6 +11,7 @@ import (
"github.com/tailscale/wireguard-go/wgcfg"
"inet.af/netaddr"
"tailscale.com/wgengine/router"
+ "tailscale.com/wgengine/router/dns"
)
func TestDeepPrint(t *testing.T) {
@@ -50,7 +51,7 @@ func getVal() []interface{} {
},
},
&router.Config{
- DNSConfig: router.DNSConfig{
+ DNS: dns.Config{
Nameservers: []netaddr.IP{netaddr.IPv4(8, 8, 8, 8)},
Domains: []string{"tailscale.net"},
},
diff --git a/ipn/local.go b/ipn/local.go
index d7b806e89..9f1829dce 100644
--- a/ipn/local.go
+++ b/ipn/local.go
@@ -19,6 +19,7 @@ import (
"tailscale.com/internal/deepprint"
"tailscale.com/ipn/ipnstate"
"tailscale.com/ipn/policy"
+ "tailscale.com/net/tsaddr"
"tailscale.com/portlist"
"tailscale.com/tailcfg"
"tailscale.com/types/empty"
@@ -28,6 +29,7 @@ import (
"tailscale.com/wgengine"
"tailscale.com/wgengine/filter"
"tailscale.com/wgengine/router"
+ "tailscale.com/wgengine/router/dns"
"tailscale.com/wgengine/tsdns"
)
@@ -209,7 +211,7 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
interact := b.interact
if st.Persist != nil {
- if b.prefs.Persist.Equals(st.Persist) {
+ if !b.prefs.Persist.Equals(st.Persist) {
prefsChanged = true
b.prefs.Persist = st.Persist.Clone()
}
@@ -437,38 +439,47 @@ func (b *LocalBackend) Start(opts Options) error {
// updateFilter updates the packet filter in wgengine based on the
// given netMap and user preferences.
func (b *LocalBackend) updateFilter(netMap *controlclient.NetworkMap, prefs *Prefs) {
- if netMap == nil {
- // Not configured yet, block everything
- b.logf("netmap packet filter: (not ready yet)")
- b.e.SetFilter(filter.NewAllowNone(b.logf))
- return
+ // NOTE(danderson): keep change detection as the first thing in
+ // this function. Don't try to optimize by returning early, more
+ // likely than not you'll just end up breaking the change
+ // detection and end up with the wrong filter installed. This is
+ // quite hard to debug, so save yourself the trouble.
+ var (
+ haveNetmap = netMap != nil
+ addrs []wgcfg.CIDR
+ packetFilter filter.Matches
+ advRoutes []wgcfg.CIDR
+ shieldsUp = prefs == nil || prefs.ShieldsUp // Be conservative when not ready
+ )
+ if haveNetmap {
+ addrs = netMap.Addresses
+ packetFilter = netMap.PacketFilter
}
- packetFilter := netMap.PacketFilter
-
- var advRoutes []wgcfg.CIDR
if prefs != nil {
advRoutes = prefs.AdvertiseRoutes
}
- // Be conservative while not ready.
- shieldsUp := prefs == nil || prefs.ShieldsUp
- changed := deepprint.UpdateHash(&b.filterHash, packetFilter, advRoutes, shieldsUp)
+ changed := deepprint.UpdateHash(&b.filterHash, haveNetmap, addrs, packetFilter, advRoutes, shieldsUp)
if !changed {
return
}
+ if !haveNetmap {
+ b.logf("netmap packet filter: (not ready yet)")
+ b.e.SetFilter(filter.NewAllowNone(b.logf))
+ return
+ }
+
localNets := wgCIDRsToFilter(netMap.Addresses, advRoutes)
if shieldsUp {
- // Shields up, block everything
b.logf("netmap packet filter: (shields up)")
var prevFilter *filter.Filter // don't reuse old filter state
b.e.SetFilter(filter.New(filter.Matches{}, localNets, prevFilter, b.logf))
- return
+ } else {
+ b.logf("netmap packet filter: %v", packetFilter)
+ b.e.SetFilter(filter.New(packetFilter, localNets, b.e.GetFilter(), b.logf))
}
-
- b.logf("netmap packet filter: %v", packetFilter)
- b.e.SetFilter(filter.New(packetFilter, localNets, b.e.GetFilter(), b.logf))
}
// dnsCIDRsEqual determines whether two CIDR lists are equal
@@ -911,28 +922,71 @@ func (b *LocalBackend) authReconfig() {
flags |= controlclient.AllowSingleHosts
}
- dns := nm.DNS
- dom := nm.DNSDomains
- if !uc.CorpDNS {
- dns = []wgcfg.IP{}
- dom = []string{}
- }
- cfg, err := nm.WGCfg(b.logf, flags, dns)
+ cfg, err := nm.WGCfg(b.logf, flags)
if err != nil {
b.logf("wgcfg: %v", err)
return
}
- err = b.e.Reconfig(cfg, routerConfig(cfg, uc, dom))
+ rcfg := routerConfig(cfg, uc)
+
+ // If CorpDNS is false, rcfg.DNS remains the zero value.
+ if uc.CorpDNS {
+ domains := nm.DNS.Domains
+ proxied := nm.DNS.Proxied
+ if proxied {
+ if len(nm.DNS.Nameservers) == 0 {
+ b.logf("[unexpected] dns proxied but no nameservers")
+ proxied = false
+ } else {
+ domains = append(domains, domainsForProxying(nm)...)
+ }
+ }
+ rcfg.DNS = dns.Config{
+ Nameservers: nm.DNS.Nameservers,
+ Domains: domains,
+ PerDomain: nm.DNS.PerDomain,
+ Proxied: proxied,
+ }
+ }
+
+ err = b.e.Reconfig(cfg, rcfg)
if err == wgengine.ErrNoChanges {
return
}
b.logf("authReconfig: ra=%v dns=%v 0x%02x: %v", uc.RouteAll, uc.CorpDNS, flags, err)
}
-// routerConfig produces a router.Config from a wireguard config,
-// IPN prefs, and the dnsDomains pulled from control's network map.
-func routerConfig(cfg *wgcfg.Config, prefs *Prefs, dnsDomains []string) *router.Config {
+// domainsForProxying produces a list of search domains for proxied DNS.
+func domainsForProxying(nm *controlclient.NetworkMap) []string {
+ var domains []string
+ if idx := strings.IndexByte(nm.Name, '.'); idx != -1 {
+ domains = append(domains, nm.Name[idx+1:])
+ }
+ for _, peer := range nm.Peers {
+ idx := strings.IndexByte(peer.Name, '.')
+ if idx == -1 {
+ continue
+ }
+ domain := peer.Name[idx+1:]
+ seen := false
+ // In theory this makes the function O(n^2) worst case,
+ // but in practice we expect domains to contain very few elements
+ // (only one until invitations are introduced).
+ for _, seenDomain := range domains {
+ if domain == seenDomain {
+ seen = true
+ }
+ }
+ if !seen {
+ domains = append(domains, domain)
+ }
+ }
+ return domains
+}
+
+// routerConfig produces a router.Config from a wireguard config and IPN prefs.
+func routerConfig(cfg *wgcfg.Config, prefs *Prefs) *router.Config {
var addrs []wgcfg.CIDR
for _, addr := range cfg.Addresses {
addrs = append(addrs, wgcfg.CIDR{
@@ -946,20 +1000,14 @@ func routerConfig(cfg *wgcfg.Config, prefs *Prefs, dnsDomains []string) *router.
SubnetRoutes: wgCIDRToNetaddr(prefs.AdvertiseRoutes),
SNATSubnetRoutes: !prefs.NoSNAT,
NetfilterMode: prefs.NetfilterMode,
- DNSConfig: router.DNSConfig{
- Nameservers: wgIPToNetaddr(cfg.DNS),
- Domains: dnsDomains,
- },
}
for _, peer := range cfg.Peers {
rs.Routes = append(rs.Routes, wgCIDRToNetaddr(peer.AllowedIPs)...)
}
- // The Tailscale DNS IP.
- // TODO(dmytro): make this configurable.
rs.Routes = append(rs.Routes, netaddr.IPPrefix{
- IP: netaddr.IPv4(100, 100, 100, 100),
+ IP: tsaddr.TailscaleServiceIP(),
Bits: 32,
})
@@ -983,17 +1031,6 @@ func wgCIDRsToFilter(cidrLists ...[]wgcfg.CIDR) (ret []filter.Net) {
return ret
}
-func wgIPToNetaddr(ips []wgcfg.IP) (ret []netaddr.IP) {
- for _, ip := range ips {
- nip, ok := netaddr.FromStdIP(ip.IP())
- if !ok {
- panic(fmt.Sprintf("conversion of %s from wgcfg to netaddr IP failed", ip))
- }
- ret = append(ret, nip.Unmap())
- }
- return ret
-}
-
func wgCIDRToNetaddr(cidrs []wgcfg.CIDR) (ret []netaddr.IPPrefix) {
for _, cidr := range cidrs {
ncidr, ok := netaddr.FromStdIPNet(cidr.IPNet())
diff --git a/logpolicy/logpolicy.go b/logpolicy/logpolicy.go
index 90d9e864f..7708ebfbb 100644
--- a/logpolicy/logpolicy.go
+++ b/logpolicy/logpolicy.go
@@ -149,6 +149,14 @@ func runningUnderSystemd() bool {
// moved from whereever it does exist, into dir. Leftover logs state
// in / and $CACHE_DIRECTORY is deleted.
func tryFixLogStateLocation(dir, cmdname string) {
+ switch runtime.GOOS {
+ case "linux", "freebsd", "openbsd":
+ // These are the OSes where we might have written stuff into
+ // root. Others use different logic to find the logs storage
+ // dir.
+ default:
+ return
+ }
if cmdname == "" {
log.Printf("[unexpected] no cmdname given to tryFixLogStateLocation, please file a bug at https://github.com/tailscale/tailscale")
return
@@ -163,14 +171,6 @@ func tryFixLogStateLocation(dir, cmdname string) {
// Only root could have written log configs to weird places.
return
}
- switch runtime.GOOS {
- case "linux", "freebsd", "openbsd":
- // These are the OSes where we might have written stuff into
- // root. Others use different logic to find the logs storage
- // dir.
- default:
- return
- }
// We stored logs in 2 incorrect places: either /, or CACHE_DIR
// (aka /var/cache/tailscale). We want to move files into the
@@ -305,11 +305,10 @@ func New(collection string) *Policy {
dir := logsDir()
- if runtime.GOOS != "windows" { // version.CmdName call was blowing some Windows stack limit via goversion DLL loading
- tryFixLogStateLocation(dir, version.CmdName())
- }
+ cmdName := version.CmdName()
+ tryFixLogStateLocation(dir, cmdName)
- cfgPath := filepath.Join(dir, fmt.Sprintf("%s.log.conf", version.CmdName()))
+ cfgPath := filepath.Join(dir, fmt.Sprintf("%s.log.conf", cmdName))
var oldc *Config
data, err := ioutil.ReadFile(cfgPath)
if err != nil {
@@ -359,7 +358,7 @@ func New(collection string) *Policy {
HTTPC: &http.Client{Transport: newLogtailTransport(logtail.DefaultHost)},
}
- filchBuf, filchErr := filch.New(filepath.Join(dir, version.CmdName()), filch.Options{})
+ filchBuf, filchErr := filch.New(filepath.Join(dir, cmdName), filch.Options{})
if filchBuf != nil {
c.Buffer = filchBuf
}
diff --git a/net/interfaces/interfaces_darwin.go b/net/interfaces/interfaces_darwin.go
index 75bd1700c..0b9870a74 100644
--- a/net/interfaces/interfaces_darwin.go
+++ b/net/interfaces/interfaces_darwin.go
@@ -10,6 +10,7 @@ import (
"go4.org/mem"
"inet.af/netaddr"
"tailscale.com/util/lineread"
+ "tailscale.com/version"
)
func init() {
@@ -32,6 +33,13 @@ default link#14 UCSI utun2
*/
func likelyHomeRouterIPDarwin() (ret netaddr.IP, ok bool) {
+ if version.IsMobile() {
+ // Don't try to do subprocesses on iOS. Ends up with log spam like:
+ // kernel: "Sandbox: IPNExtension(86580) deny(1) process-fork"
+ // TODO(bradfitz): let our iOS app register a func with this package
+ // and have it call into C/Swift to get the routing table.
+ return ret, false
+ }
cmd := exec.Command("/usr/sbin/netstat", "-r", "-n", "-f", "inet")
stdout, err := cmd.StdoutPipe()
if err != nil {
diff --git a/net/tsaddr/tsaddr.go b/net/tsaddr/tsaddr.go
index 3b929e8fe..8a4bbef03 100644
--- a/net/tsaddr/tsaddr.go
+++ b/net/tsaddr/tsaddr.go
@@ -32,6 +32,15 @@ func CGNATRange() netaddr.IPPrefix {
var cgnatRange oncePrefix
+// TailscaleServiceIP returns the listen address of services
+// provided by Tailscale itself such as the Magic DNS proxy.
+func TailscaleServiceIP() netaddr.IP {
+ serviceIP.Do(func() { mustIP(&serviceIP.v, "100.100.100.100") })
+ return serviceIP.v
+}
+
+var serviceIP onceIP
+
// IsTailscaleIP reports whether ip is an IP address in a range that
// Tailscale assigns from.
func IsTailscaleIP(ip netaddr.IP) bool {
@@ -50,3 +59,16 @@ type oncePrefix struct {
sync.Once
v netaddr.IPPrefix
}
+
+func mustIP(v *netaddr.IP, ip string) {
+ var err error
+ *v, err = netaddr.ParseIP(ip)
+ if err != nil {
+ panic(err)
+ }
+}
+
+type onceIP struct {
+ sync.Once
+ v netaddr.IP
+}
diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go
index e9ca209c2..bb138ff27 100644
--- a/tailcfg/tailcfg.go
+++ b/tailcfg/tailcfg.go
@@ -17,6 +17,7 @@ import (
"github.com/tailscale/wireguard-go/wgcfg"
"go4.org/mem"
"golang.org/x/oauth2"
+ "inet.af/netaddr"
"tailscale.com/types/key"
"tailscale.com/types/opt"
"tailscale.com/types/structs"
@@ -492,15 +493,31 @@ var FilterAllowAll = []FilterRule{
},
}
+// DNSConfig is the DNS configuration.
+type DNSConfig struct {
+ Nameservers []netaddr.IP `json:",omitempty"`
+ Domains []string `json:",omitempty"`
+ PerDomain bool
+ Proxied bool
+}
+
type MapResponse struct {
KeepAlive bool // if set, all other fields are ignored
// Networking
- Node *Node
- Peers []*Node
- DNS []wgcfg.IP
+ Node *Node
+ Peers []*Node
+ DERPMap *DERPMap
+
+ // DNS is the same as DNSConfig.Nameservers.
+ //
+ // TODO(dmytro): should be sent in DNSConfig.Nameservers once clients have updated.
+ DNS []wgcfg.IP
+ // SearchPaths are the same as DNSConfig.Domains.
+ //
+ // TODO(dmytro): should be sent in DNSConfig.Domains once clients have updated.
SearchPaths []string
- DERPMap *DERPMap
+ DNSConfig DNSConfig
// ACLs
Domain string
diff --git a/version/cmdname.go b/version/cmdname.go
index 4e5280aff..a7899ed9f 100644
--- a/version/cmdname.go
+++ b/version/cmdname.go
@@ -9,6 +9,7 @@ package version
import (
"os"
"path"
+ "path/filepath"
"strings"
"rsc.io/goversion/version"
@@ -22,22 +23,27 @@ func CmdName() string {
if err != nil {
return "cmd"
}
+
+ // fallbackName, the lowercase basename of the executable, is what we return if
+ // we can't find the Go module metadata embedded in the file.
+ fallbackName := filepath.Base(strings.TrimSuffix(strings.ToLower(e), ".exe"))
+
var ret string
v, err := version.ReadExe(e)
if err != nil {
- ret = strings.TrimSuffix(strings.ToLower(e), ".exe")
- } else {
- // v is like:
- // "path\ttailscale.com/cmd/tailscale\nmod\ttailscale.com\t(devel)\t\ndep\tgithub.com/apenwarr/fixconsole\tv0.0.0-20191012055117-5a9f6489cc29\th1:muXWUcay7DDy1/hEQWrYlBy+g0EuwT70sBHg65SeUc4=\ndep\tgithub....
- for _, line := range strings.Split(v.ModuleInfo, "\n") {
- if strings.HasPrefix(line, "path\t") {
- ret = path.Base(strings.TrimPrefix(line, "path\t"))
- break
- }
+ return fallbackName
+ }
+ // v is like:
+ // "path\ttailscale.com/cmd/tailscale\nmod\ttailscale.com\t(devel)\t\ndep\tgithub.com/apenwarr/fixconsole\tv0.0.0-20191012055117-5a9f6489cc29\th1:muXWUcay7DDy1/hEQWrYlBy+g0EuwT70sBHg65SeUc4=\ndep\tgithub....
+ for _, line := range strings.Split(v.ModuleInfo, "\n") {
+ if strings.HasPrefix(line, "path\t") {
+ goPkg := strings.TrimPrefix(line, "path\t") // like "tailscale.com/cmd/tailscale"
+ ret = path.Base(goPkg) // goPkg is always forward slashes; use path, not filepath
+ break
}
}
if ret == "" {
- return "cmd"
+ return fallbackName
}
return ret
}
diff --git a/wgengine/filter/filter.go b/wgengine/filter/filter.go
index 60c48d198..d9d721baf 100644
--- a/wgengine/filter/filter.go
+++ b/wgengine/filter/filter.go
@@ -6,6 +6,7 @@
package filter
import (
+ "fmt"
"sync"
"time"
@@ -139,7 +140,7 @@ var dropBucket = rate.NewLimiter(rate.Every(5*time.Second), 10)
func (f *Filter) logRateLimit(runflags RunFlags, q *packet.ParsedPacket, dir direction, r Response, why string) {
var verdict string
- if r == Drop && f.omitDropLogging(q, dir) {
+ if r == Drop && omitDropLogging(q, dir) {
return
}
@@ -266,6 +267,17 @@ const (
out
)
+func (d direction) String() string {
+ switch d {
+ case in:
+ return "in"
+ case out:
+ return "out"
+ default:
+ return fmt.Sprintf("[??dir=%d]", int(d))
+ }
+}
+
func (f *Filter) pre(q *packet.ParsedPacket, rf RunFlags, dir direction) Response {
if len(q.Buffer()) == 0 {
// wireguard keepalive packet, always permit.
@@ -295,24 +307,34 @@ func (f *Filter) pre(q *packet.ParsedPacket, rf RunFlags, dir direction) Respons
return noVerdict
}
-// ipv6AllRoutersLinkLocal is ff02::2 (All link-local routers).
-const ipv6AllRoutersLinkLocal = "\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02"
+const (
+ // ipv6AllRoutersLinkLocal is ff02::2 (All link-local routers)
+ ipv6AllRoutersLinkLocal = "\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02"
+ // ipv6AllMLDv2CapableRouters is ff02::16 (All MLDv2-capable routers)
+ ipv6AllMLDv2CapableRouters = "\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x16"
+)
// omitDropLogging reports whether packet p, which has already been
// deemded a packet to Drop, should bypass the [rate-limited] logging.
// We don't want to log scary & spammy reject warnings for packets that
// are totally normal, like IPv6 route announcements.
-func (f *Filter) omitDropLogging(p *packet.ParsedPacket, dir direction) bool {
+func omitDropLogging(p *packet.ParsedPacket, dir direction) bool {
+ b := p.Buffer()
switch dir {
case out:
switch p.IPVersion {
case 4:
- // Omit logging about outgoing IGMP queries being dropped.
- if p.IPProto == packet.IGMP {
+ // ParsedPacket.Decode zeros out ParsedPacket.IPProtocol for protocols
+ // it doesn't know about, so parse it out ourselves if needed.
+ ipProto := p.IPProto
+ if ipProto == 0 && len(b) > 8 {
+ ipProto = packet.IPProto(b[9])
+ }
+ // Omit logging about outgoing IGMP.
+ if ipProto == packet.IGMP {
return true
}
case 6:
- b := p.Buffer()
if len(b) < 40 {
return false
}
@@ -324,6 +346,10 @@ func (f *Filter) omitDropLogging(p *packet.ParsedPacket, dir direction) bool {
return true
}
}
+ if string(dst) == ipv6AllMLDv2CapableRouters {
+ return true
+ }
+ panic(fmt.Sprintf("Got proto=%2x; src=%x dst=%x", int(p.IPProto), src, dst))
}
}
return false
diff --git a/wgengine/filter/filter_test.go b/wgengine/filter/filter_test.go
index 16cd5f676..5f8c64f67 100644
--- a/wgengine/filter/filter_test.go
+++ b/wgengine/filter/filter_test.go
@@ -6,7 +6,9 @@ package filter
import (
"encoding/binary"
+ "encoding/hex"
"encoding/json"
+ "strings"
"testing"
"tailscale.com/types/logger"
@@ -298,3 +300,57 @@ func rawdefault(proto packet.IPProto, trimLength int) []byte {
port := uint16(53)
return rawpacket(proto, ip, ip, port, port, trimLength)
}
+
+func parseHexPkt(t *testing.T, h string) *packet.ParsedPacket {
+ t.Helper()
+ b, err := hex.DecodeString(strings.ReplaceAll(h, " ", ""))
+ if err != nil {
+ t.Fatalf("failed to read hex %q: %v", h, err)
+ }
+ p := new(packet.ParsedPacket)
+ p.Decode(b)
+ return p
+}
+
+func TestOmitDropLogging(t *testing.T) {
+ tests := []struct {
+ name string
+ pkt *packet.ParsedPacket
+ dir direction
+ want bool
+ }{
+ {
+ name: "v4_tcp_out",
+ pkt: &packet.ParsedPacket{IPVersion: 4, IPProto: packet.TCP},
+ dir: out,
+ want: false,
+ },
+ {
+ name: "v6_icmp_out", // as seen on Linux
+ pkt: parseHexPkt(t, "60 00 00 00 00 00 3a 00 fe800000000000000000000000000000 ff020000000000000000000000000002"),
+ dir: out,
+ want: true,
+ },
+ {
+ name: "v6_to_MLDv2_capable_routers", // as seen on Windows
+ pkt: parseHexPkt(t, "60 00 00 00 00 24 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ff 02 00 00 00 00 00 00 00 00 00 00 00 00 00 16 3a 00 05 02 00 00 01 00 8f 00 6e 80 00 00 00 01 04 00 00 00 ff 02 00 00 00 00 00 00 00 00 00 00 00 00 00 0c"),
+ dir: out,
+ want: true,
+ },
+ {
+ name: "v4_igmp_out", // on Windows, from https://github.com/tailscale/tailscale/issues/618
+ pkt: parseHexPkt(t, "46 00 00 30 37 3a 00 00 01 02 10 0e a9 fe 53 6b e0 00 00 16 94 04 00 00 22 00 14 05 00 00 00 02 04 00 00 00 e0 00 00 fb 04 00 00 00 e0 00 00 fc"),
+ dir: out,
+ want: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := omitDropLogging(tt.pkt, tt.dir)
+ if got != tt.want {
+ t.Errorf("got %v; want %v\npacket: %#v\n%s", got, tt.want, tt.pkt, packet.Hexdump(tt.pkt.Buffer()))
+ }
+ })
+ }
+}
diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go
index 1aeb97b56..587f4f72d 100644
--- a/wgengine/magicsock/magicsock.go
+++ b/wgengine/magicsock/magicsock.go
@@ -1531,7 +1531,7 @@ func (c *Conn) sendDiscoMessage(dst netaddr.IPPort, dstKey tailcfg.NodeKey, dstD
c.mu.Lock()
if c.closed {
c.mu.Unlock()
- return false, errClosed
+ return false, errConnClosed
}
var nonce [disco.NonceLen]byte
if _, err := crand.Read(nonce[:]); err != nil {
@@ -1587,6 +1587,10 @@ func (c *Conn) handleDiscoMessage(msg []byte, src netaddr.IPPort) bool {
if debugDisco {
c.logf("magicsock: disco: got disco-looking frame from %v", sender.ShortString())
}
+ if c.privateKey.IsZero() {
+ // Ignore disco messages when we're stopped.
+ return false
+ }
if c.discoPrivate.IsZero() {
if debugDisco {
c.logf("magicsock: disco: ignoring disco-looking frame, no local key")
@@ -1836,6 +1840,12 @@ func (c *Conn) SetPrivateKey(privateKey wgcfg.PrivateKey) error {
c.goDerpConnect(c.myDerp)
}
+ if newKey.IsZero() {
+ for _, de := range c.endpointOfDisco {
+ de.stopAndReset()
+ }
+ }
+
return nil
}
@@ -1937,7 +1947,6 @@ func (c *Conn) SetNetworkMap(nm *controlclient.NetworkMap) {
c.nodeOfDisco[n.DiscoKey] = n
if old, ok := c.discoOfNode[n.Key]; ok && old != n.DiscoKey {
c.logf("magicsock: node %s changed discovery key from %x to %x", n.Key.ShortString(), old[:8], n.DiscoKey[:8])
- // TODO: reset AddrSet states, reset wireguard session key, etc.
}
c.discoOfNode[n.Key] = n.DiscoKey
}
@@ -1950,7 +1959,7 @@ func (c *Conn) SetNetworkMap(nm *controlclient.NetworkMap) {
// Clean c.endpointOfDisco for discovery keys that are no longer present.
for dk, de := range c.endpointOfDisco {
if _, ok := c.nodeOfDisco[dk]; !ok {
- de.cleanup()
+ de.stopAndReset()
delete(c.endpointOfDisco, dk)
delete(c.sharedDiscoKey, dk)
}
@@ -2068,7 +2077,7 @@ func (c *Conn) Close() error {
defer c.mu.Unlock()
for _, ep := range c.endpointOfDisco {
- ep.cleanup()
+ ep.stopAndReset()
}
c.closed = true
@@ -3438,14 +3447,27 @@ func (de *discoEndpoint) populatePeerStatus(ps *ipnstate.PeerStatus) {
}
}
-// cleanup is called when a discovery endpoint is no longer present in the NetworkMap.
-// This is where we can do cleanup such as closing goroutines or canceling timers.
-func (de *discoEndpoint) cleanup() {
+// stopAndReset stops timers associated with de and resets its state back to zero.
+// It's called when a discovery endpoint is no longer present in the NetworkMap,
+// or when magicsock is transition from running to stopped state (via SetPrivateKey(zero))
+func (de *discoEndpoint) stopAndReset() {
de.mu.Lock()
defer de.mu.Unlock()
de.c.logf("magicsock: doing cleanup for discovery key %x", de.discoKey[:])
+ // Zero these fields so if the user re-starts the network, the discovery
+ // state isn't a mix of before & after two sessions.
+ de.lastSend = time.Time{}
+ de.lastFullPing = time.Time{}
+ de.bestAddr = netaddr.IPPort{}
+ de.bestAddrLatency = 0
+ de.bestAddrAt = time.Time{}
+ de.trustBestAddrUntil = time.Time{}
+ for _, es := range de.endpointState {
+ es.lastPing = time.Time{}
+ }
+
for txid, sp := range de.sentPing {
de.removeSentPingLocked(txid, sp)
}
@@ -3497,5 +3519,3 @@ type ippCacheKey struct {
// derpStr replaces DERP IPs in s with "derp-".
func derpStr(s string) string { return strings.ReplaceAll(s, "127.3.3.40:", "derp-") }
-
-var errClosed = errors.New("conn is closed")
diff --git a/wgengine/magicsock/magicsock_test.go b/wgengine/magicsock/magicsock_test.go
index 4ac4b1e9d..01ca63335 100644
--- a/wgengine/magicsock/magicsock_test.go
+++ b/wgengine/magicsock/magicsock_test.go
@@ -277,7 +277,7 @@ func meshStacks(logf logger.Logf, ms []*magicStack) (cleanup func()) {
peerSet[key.Public(peer.Key)] = struct{}{}
}
m.conn.UpdatePeers(peerSet)
- wg, err := netmap.WGCfg(logf, controlclient.AllowSingleHosts, nil)
+ wg, err := netmap.WGCfg(logf, controlclient.AllowSingleHosts)
if err != nil {
// We're too far from the *testing.T to be graceful,
// blow up. Shouldn't happen anyway.
@@ -1262,6 +1262,7 @@ func initAddrSet(as *AddrSet) {
func TestDiscoMessage(t *testing.T) {
c := newConn()
c.logf = t.Logf
+ c.privateKey = key.NewPrivate()
peer1Pub := c.DiscoPublicKey()
peer1Priv := c.discoPrivate
diff --git a/wgengine/packet/packet.go b/wgengine/packet/packet.go
index 75bf0bad1..a82e84ab1 100644
--- a/wgengine/packet/packet.go
+++ b/wgengine/packet/packet.go
@@ -57,7 +57,7 @@ type NextHeader uint8
func (p *ParsedPacket) String() string {
if p.IPVersion == 6 {
- return "IPv6{???}"
+ return fmt.Sprintf("IPv6{Proto=%d}", p.IPProto)
}
switch p.IPProto {
case Unknown:
diff --git a/wgengine/packet/packet_test.go b/wgengine/packet/packet_test.go
index 7669a2e90..8b286da51 100644
--- a/wgengine/packet/packet_test.go
+++ b/wgengine/packet/packet_test.go
@@ -200,7 +200,7 @@ func TestParsedPacket(t *testing.T) {
{"tcp", tcpPacketDecode, "TCP{1.2.3.4:123 > 5.6.7.8:567}"},
{"icmp", icmpRequestDecode, "ICMP{1.2.3.4:0 > 5.6.7.8:0}"},
{"unknown", unknownPacketDecode, "Unknown{???}"},
- {"ipv6", ipv6PacketDecode, "IPv6{???}"},
+ {"ipv6", ipv6PacketDecode, "IPv6{Proto=58}"},
}
for _, tt := range tests {
diff --git a/wgengine/router/dns.go b/wgengine/router/dns.go
deleted file mode 100644
index f3a2d146b..000000000
--- a/wgengine/router/dns.go
+++ /dev/null
@@ -1,74 +0,0 @@
-// 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 (
- "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
-}
-
-// 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/config.go b/wgengine/router/dns/config.go
new file mode 100644
index 000000000..fec5d8ffa
--- /dev/null
+++ b/wgengine/router/dns/config.go
@@ -0,0 +1,75 @@
+// 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 dns
+
+import (
+ "inet.af/netaddr"
+
+ "tailscale.com/types/logger"
+)
+
+// Config is the set of parameters that uniquely determine
+// the state to which a manager should bring system DNS settings.
+type Config struct {
+ // Nameservers are the IP addresses of the nameservers to use.
+ Nameservers []netaddr.IP
+ // Domains are the search domains to use.
+ Domains []string
+ // PerDomain indicates whether it is preferred to use Nameservers
+ // only for DNS queries for subdomains of Domains.
+ // Note that Nameservers may still be applied to all queries
+ // if the manager does not support per-domain settings.
+ PerDomain bool
+ // Proxied indicates whether DNS requests are proxied through a tsdns.Resolver.
+ Proxied bool
+}
+
+// Equal determines whether its argument and receiver
+// represent equivalent DNS configurations (then DNS reconfig is a no-op).
+func (lhs Config) Equal(rhs Config) bool {
+ if lhs.Proxied != rhs.Proxied || lhs.PerDomain != rhs.PerDomain {
+ return false
+ }
+
+ 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
+ }
+ }
+
+ // The order of domains, on the other hand, is significant.
+ for i, domain := range lhs.Domains {
+ if rhs.Domains[i] != domain {
+ return false
+ }
+ }
+
+ return true
+}
+
+// ManagerConfig is the set of parameters from which
+// a manager implementation is chosen and initialized.
+type ManagerConfig struct {
+ // logf is the logger for the manager to use.
+ Logf logger.Logf
+ // InterfaceNAme is the name of the interface with which DNS settings should be associated.
+ InterfaceName string
+ // Cleanup indicates that the manager is created for cleanup only.
+ // A no-op manager will be instantiated if the system needs no cleanup.
+ Cleanup bool
+ // PerDomain indicates that a manager capable of per-domain configuration is preferred.
+ // Certain managers are per-domain only; they will not be considered if this is false.
+ PerDomain bool
+}
diff --git a/wgengine/router/dns_direct.go b/wgengine/router/dns/direct.go
index 90615153d..62814c5f6 100644
--- a/wgengine/router/dns_direct.go
+++ b/wgengine/router/dns/direct.go
@@ -4,7 +4,7 @@
// +build linux freebsd openbsd
-package router
+package dns
import (
"bufio"
@@ -27,8 +27,8 @@ const (
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) {
+// writeResolvConf writes DNS configuration in resolv.conf format to the given writer.
+func writeResolvConf(w io.Writer, servers []netaddr.IP, domains []string) {
io.WriteString(w, "# resolv.conf(5) file generated by tailscale\n")
io.WriteString(w, "# DO NOT EDIT THIS FILE BY HAND -- CHANGES WILL BE OVERWRITTEN\n\n")
for _, ns := range servers {
@@ -46,9 +46,9 @@ func dnsWriteConfig(w io.Writer, servers []netaddr.IP, domains []string) {
}
}
-// dnsReadConfig reads DNS configuration from /etc/resolv.conf.
-func dnsReadConfig() (DNSConfig, error) {
- var config DNSConfig
+// readResolvConf reads DNS configuration from /etc/resolv.conf.
+func readResolvConf() (Config, error) {
+ var config Config
f, err := os.Open("/etc/resolv.conf")
if err != nil {
@@ -100,17 +100,24 @@ func isResolvedRunning() bool {
return err == nil
}
-// dnsDirectUp replaces /etc/resolv.conf with a file generated
-// from the given configuration, creating a backup of its old state.
+// directManager is a managerImpl which replaces /etc/resolv.conf with a file
+// generated from the given configuration, creating a backup of its old state.
//
// This way of configuring DNS is precarious, since it does not react
// to the disappearance of the Tailscale interface.
-// The caller must call dnsDirectDown before program shutdown
-// and ensure that router.Cleanup is run if the program terminates unexpectedly.
-func dnsDirectUp(config DNSConfig) error {
+// The caller must call Down before program shutdown
+// or as cleanup if the program terminates unexpectedly.
+type directManager struct{}
+
+func newDirectManager(mconfig ManagerConfig) managerImpl {
+ return directManager{}
+}
+
+// Up implements managerImpl.
+func (m directManager) Up(config Config) error {
// Write the tsConf file.
buf := new(bytes.Buffer)
- dnsWriteConfig(buf, config.Nameservers, config.Domains)
+ writeResolvConf(buf, config.Nameservers, config.Domains)
if err := atomicfile.WriteFile(tsConf, buf.Bytes(), 0644); err != nil {
return err
}
@@ -152,9 +159,8 @@ func dnsDirectUp(config DNSConfig) error {
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 {
+// Down implements managerImpl.
+func (m directManager) Down() error {
if _, err := os.Stat(backupConf); err != nil {
// If the backup file does not exist, then Up never ran successfully.
if os.IsNotExist(err) {
diff --git a/wgengine/router/dns/manager.go b/wgengine/router/dns/manager.go
new file mode 100644
index 000000000..c8cc82bdd
--- /dev/null
+++ b/wgengine/router/dns/manager.go
@@ -0,0 +1,94 @@
+// 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 dns
+
+import (
+ "time"
+
+ "tailscale.com/types/logger"
+)
+
+// reconfigTimeout is the time interval within which Manager.{Up,Down} should complete.
+//
+// This is particularly 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 reconfigTimeout = time.Second
+
+type managerImpl interface {
+ // Up updates system DNS settings to match the given configuration.
+ Up(Config) error
+ // Down undoes the effects of Up.
+ // It is idempotent and performs no action if Up has never been called.
+ Down() error
+}
+
+// Manager manages system DNS settings.
+type Manager struct {
+ logf logger.Logf
+
+ impl managerImpl
+
+ config Config
+ mconfig ManagerConfig
+}
+
+// NewManagers created a new manager from the given config.
+func NewManager(mconfig ManagerConfig) *Manager {
+ mconfig.Logf = logger.WithPrefix(mconfig.Logf, "dns: ")
+ m := &Manager{
+ logf: mconfig.Logf,
+ impl: newManager(mconfig),
+
+ config: Config{PerDomain: mconfig.PerDomain},
+ mconfig: mconfig,
+ }
+
+ m.logf("using %T", m.impl)
+ return m
+}
+
+func (m *Manager) Set(config Config) error {
+ if config.Equal(m.config) {
+ return nil
+ }
+
+ m.logf("Set: %+v", config)
+
+ if len(config.Nameservers) == 0 {
+ err := m.impl.Down()
+ // If we save the config, we will not retry next time. Only do this on success.
+ if err == nil {
+ m.config = config
+ }
+ return err
+ }
+
+ // Switching to and from per-domain mode may require a change of manager.
+ if config.PerDomain != m.config.PerDomain {
+ if err := m.impl.Down(); err != nil {
+ return err
+ }
+ m.mconfig.PerDomain = config.PerDomain
+ m.impl = newManager(m.mconfig)
+ m.logf("switched to %T", m.impl)
+ }
+
+ err := m.impl.Up(config)
+ // If we save the config, we will not retry next time. Only do this on success.
+ if err == nil {
+ m.config = config
+ }
+
+ return err
+}
+
+func (m *Manager) Up() error {
+ return m.impl.Up(m.config)
+}
+
+func (m *Manager) Down() error {
+ return m.impl.Down()
+}
diff --git a/wgengine/router/dns/manager_default.go b/wgengine/router/dns/manager_default.go
new file mode 100644
index 000000000..04c8bb811
--- /dev/null
+++ b/wgengine/router/dns/manager_default.go
@@ -0,0 +1,14 @@
+// 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,!windows
+
+package dns
+
+func newManager(mconfig ManagerConfig) managerImpl {
+ // TODO(dmytro): on darwin, we should use a macOS-specific method such as scutil.
+ // This is currently not implemented. Editing /etc/resolv.conf does not work,
+ // as most applications use the system resolver, which disregards it.
+ return newNoopManager(mconfig)
+}
diff --git a/wgengine/router/dns/manager_freebsd.go b/wgengine/router/dns/manager_freebsd.go
new file mode 100644
index 000000000..232635f7e
--- /dev/null
+++ b/wgengine/router/dns/manager_freebsd.go
@@ -0,0 +1,14 @@
+// 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 dns
+
+func newManager(mconfig ManagerConfig) managerImpl {
+ switch {
+ case isResolvconfActive():
+ return newResolvconfManager(mconfig)
+ default:
+ return newDirectManager(mconfig)
+ }
+}
diff --git a/wgengine/router/dns/manager_linux.go b/wgengine/router/dns/manager_linux.go
new file mode 100644
index 000000000..f53aed7d3
--- /dev/null
+++ b/wgengine/router/dns/manager_linux.go
@@ -0,0 +1,27 @@
+// 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 dns
+
+func newManager(mconfig ManagerConfig) managerImpl {
+ switch {
+ // systemd-resolved should only activate per-domain.
+ case isResolvedActive() && mconfig.PerDomain:
+ if mconfig.Cleanup {
+ return newNoopManager(mconfig)
+ } else {
+ return newResolvedManager(mconfig)
+ }
+ case isNMActive():
+ if mconfig.Cleanup {
+ return newNoopManager(mconfig)
+ } else {
+ return newNMManager(mconfig)
+ }
+ case isResolvconfActive():
+ return newResolvconfManager(mconfig)
+ default:
+ return newDirectManager(mconfig)
+ }
+}
diff --git a/wgengine/router/dns/manager_openbsd.go b/wgengine/router/dns/manager_openbsd.go
new file mode 100644
index 000000000..228e3cca5
--- /dev/null
+++ b/wgengine/router/dns/manager_openbsd.go
@@ -0,0 +1,9 @@
+// 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 dns
+
+func newManager(mconfig ManagerConfig) managerImpl {
+ return newDirectManager(mconfig)
+}
diff --git a/wgengine/router/dns/manager_windows.go b/wgengine/router/dns/manager_windows.go
new file mode 100644
index 000000000..4196d1f70
--- /dev/null
+++ b/wgengine/router/dns/manager_windows.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 dns
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/tailscale/wireguard-go/tun"
+ "golang.org/x/sys/windows/registry"
+ "tailscale.com/types/logger"
+)
+
+type windowsManager struct {
+ logf logger.Logf
+ guid string
+}
+
+func newManager(mconfig ManagerConfig) managerImpl {
+ return windowsManager{
+ logf: mconfig.Logf,
+ guid: tun.WintunGUID,
+ }
+}
+
+func setRegistry(path, nameservers, domains string) error {
+ key, err := registry.OpenKey(registry.LOCAL_MACHINE, path, registry.READ|registry.SET_VALUE)
+ if err != nil {
+ return fmt.Errorf("opening %s: %w", path, err)
+ }
+ defer key.Close()
+
+ err = key.SetStringValue("NameServer", nameservers)
+ if err != nil {
+ return fmt.Errorf("setting %s/NameServer: %w", path, err)
+ }
+
+ err = key.SetStringValue("Domain", domains)
+ if err != nil {
+ return fmt.Errorf("setting %s/Domain: %w", path, err)
+ }
+
+ return nil
+}
+
+func (m windowsManager) Up(config Config) error {
+ var ipsv4 []string
+ var ipsv6 []string
+ for _, ip := range config.Nameservers {
+ if ip.Is4() {
+ ipsv4 = append(ipsv4, ip.String())
+ } else {
+ ipsv6 = append(ipsv6, ip.String())
+ }
+ }
+ nsv4 := strings.Join(ipsv4, ",")
+ nsv6 := strings.Join(ipsv6, ",")
+
+ var domains string
+ if len(config.Domains) > 0 {
+ if len(config.Domains) > 1 {
+ m.logf("only a single search domain is supported")
+ }
+ domains = config.Domains[0]
+ }
+
+ v4Path := `SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\` + m.guid
+ if err := setRegistry(v4Path, nsv4, domains); err != nil {
+ return err
+ }
+ v6Path := `SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters\Interfaces\` + m.guid
+ if err := setRegistry(v6Path, nsv6, domains); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (m windowsManager) Down() error {
+ return m.Up(Config{Nameservers: nil, Domains: nil})
+}
diff --git a/wgengine/router/dns_networkmanager.go b/wgengine/router/dns/nm.go
index bf192e87c..bf80abb6c 100644
--- a/wgengine/router/dns_networkmanager.go
+++ b/wgengine/router/dns/nm.go
@@ -4,7 +4,7 @@
// +build linux
-package router
+package dns
import (
"bufio"
@@ -20,8 +20,8 @@ import (
type nmConnectionSettings map[string]map[string]dbus.Variant
-// nmIsActive determines if NetworkManager is currently managing system DNS settings.
-func nmIsActive() bool {
+// isNMActive determines if NetworkManager is currently managing system DNS settings.
+func isNMActive() 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.
@@ -50,10 +50,20 @@ func nmIsActive() bool {
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)
+// nmManager uses the NetworkManager DBus API.
+type nmManager struct {
+ interfaceName string
+}
+
+func newNMManager(mconfig ManagerConfig) managerImpl {
+ return nmManager{
+ interfaceName: mconfig.InterfaceName,
+ }
+}
+
+// Up implements managerImpl.
+func (m nmManager) Up(config Config) error {
+ ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout)
defer cancel()
// conn is a shared connection whose lifecycle is managed by the dbus package.
@@ -90,7 +100,7 @@ func dnsNetworkManagerUp(config DNSConfig, interfaceName string) error {
var devicePath dbus.ObjectPath
err = nm.CallWithContext(
ctx, "org.freedesktop.NetworkManager.GetDeviceByIpIface", 0,
- interfaceName,
+ m.interfaceName,
).Store(&devicePath)
if err != nil {
return fmt.Errorf("getDeviceByIpIface: %w", err)
@@ -189,7 +199,7 @@ func dnsNetworkManagerUp(config DNSConfig, interfaceName string) error {
return nil
}
-// dnsNetworkManagerDown undoes the changes made by dnsNetworkManagerUp.
-func dnsNetworkManagerDown(interfaceName string) error {
- return dnsNetworkManagerUp(DNSConfig{Nameservers: nil, Domains: nil}, interfaceName)
+// Down implements managerImpl.
+func (m nmManager) Down() error {
+ return m.Up(Config{Nameservers: nil, Domains: nil})
}
diff --git a/wgengine/router/dns/noop.go b/wgengine/router/dns/noop.go
new file mode 100644
index 000000000..35c07a232
--- /dev/null
+++ b/wgengine/router/dns/noop.go
@@ -0,0 +1,17 @@
+// 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 dns
+
+type noopManager struct{}
+
+// Up implements managerImpl.
+func (m noopManager) Up(Config) error { return nil }
+
+// Down implements managerImpl.
+func (m noopManager) Down() error { return nil }
+
+func newNoopManager(mconfig ManagerConfig) managerImpl {
+ return noopManager{}
+}
diff --git a/wgengine/router/dns_resolvconf.go b/wgengine/router/dns/resolvconf.go
index b0795edd6..8bf97ee88 100644
--- a/wgengine/router/dns_resolvconf.go
+++ b/wgengine/router/dns/resolvconf.go
@@ -4,7 +4,7 @@
// +build linux freebsd
-package router
+package dns
import (
"bufio"
@@ -14,10 +14,10 @@ import (
"os/exec"
)
-// resolvconfIsActive indicates whether the system appears to be using resolvconf.
-// If this is true, then dnsManualUp should be avoided:
+// isResolvconfActive indicates whether the system appears to be using resolvconf.
+// If this is true, then directManager should be avoided:
// resolvconf has exclusive ownership of /etc/resolv.conf.
-func resolvconfIsActive() bool {
+func isResolvconfActive() 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.
@@ -57,21 +57,31 @@ func resolvconfIsActive() bool {
return false
}
-// resolvconfImplementation enumerates supported implementations of the resolvconf CLI.
-type resolvconfImplementation uint8
+// resolvconfImpl enumerates supported implementations of the resolvconf CLI.
+type resolvconfImpl uint8
const (
// resolvconfOpenresolv is the implementation packaged as "openresolv" on Ubuntu.
// It supports exclusive mode and interface metrics.
- resolvconfOpenresolv resolvconfImplementation = iota
+ resolvconfOpenresolv resolvconfImpl = iota
// resolvconfLegacy is the implementation by Thomas Hood packaged as "resolvconf" on Ubuntu.
// It does not support exclusive mode or interface metrics.
resolvconfLegacy
)
-// getResolvconfImplementation returns the implementation of resolvconf
-// that appears to be in use.
-func getResolvconfImplementation() resolvconfImplementation {
+func (impl resolvconfImpl) String() string {
+ switch impl {
+ case resolvconfOpenresolv:
+ return "openresolv"
+ case resolvconfLegacy:
+ return "legacy"
+ default:
+ return "unknown"
+ }
+}
+
+// getResolvconfImpl returns the implementation of resolvconf that appears to be in use.
+func getResolvconfImpl() resolvconfImpl {
err := exec.Command("resolvconf", "-v").Run()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
@@ -85,21 +95,31 @@ func getResolvconfImplementation() resolvconfImplementation {
return resolvconfOpenresolv
}
+type resolvconfManager struct {
+ impl resolvconfImpl
+}
+
+func newResolvconfManager(mconfig ManagerConfig) managerImpl {
+ impl := getResolvconfImpl()
+ mconfig.Logf("resolvconf implementation is %s", impl)
+
+ return resolvconfManager{
+ impl: impl,
+ }
+}
+
// resolvconfConfigName is the name of the config submitted to resolvconf.
// It has this form to match the "tun*" rule in interface-order
// when running resolvconfLegacy, hopefully placing our config first.
const resolvconfConfigName = "tun-tailscale.inet"
-// dnsResolvconfUp invokes the resolvconf binary to associate
-// the given DNS configuration the Tailscale interface.
-func dnsResolvconfUp(config DNSConfig, interfaceName string) error {
- implementation := getResolvconfImplementation()
-
+// Up implements managerImpl.
+func (m resolvconfManager) Up(config Config) error {
stdin := new(bytes.Buffer)
- dnsWriteConfig(stdin, config.Nameservers, config.Domains) // dns_direct.go
+ writeResolvConf(stdin, config.Nameservers, config.Domains) // dns_direct.go
var cmd *exec.Cmd
- switch implementation {
+ switch m.impl {
case resolvconfOpenresolv:
// Request maximal priority (metric 0) and exclusive mode.
cmd = exec.Command("resolvconf", "-m", "0", "-x", "-a", resolvconfConfigName)
@@ -117,12 +137,10 @@ func dnsResolvconfUp(config DNSConfig, interfaceName string) error {
return nil
}
-// dnsResolvconfDown undoes the action of dnsResolvconfUp.
-func dnsResolvconfDown(interfaceName string) error {
- implementation := getResolvconfImplementation()
-
+// Down implements managerImpl.
+func (m resolvconfManager) Down() error {
var cmd *exec.Cmd
- switch implementation {
+ switch m.impl {
case resolvconfOpenresolv:
cmd = exec.Command("resolvconf", "-f", "-d", resolvconfConfigName)
case resolvconfLegacy:
diff --git a/wgengine/router/dns_resolved.go b/wgengine/router/dns/resolved.go
index 511955217..9d8c40d90 100644
--- a/wgengine/router/dns_resolved.go
+++ b/wgengine/router/dns/resolved.go
@@ -4,14 +4,13 @@
// +build linux
-package router
+package dns
import (
"context"
"errors"
"fmt"
"os/exec"
- "time"
"github.com/godbus/dbus/v5"
"golang.org/x/sys/unix"
@@ -23,7 +22,7 @@ import (
//
// 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.
+// In other cases, resolved may 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}, ...})
@@ -36,13 +35,6 @@ import (
// this address is, in fact, hard-coded into resolved.
var resolvedListenAddr = netaddr.IPv4(127, 0, 0, 53)
-// 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
-
var errNotReady = errors.New("interface not ready")
type resolvedLinkNameserver struct {
@@ -55,8 +47,8 @@ type resolvedLinkDomain struct {
RoutingOnly bool
}
-// resolvedIsActive determines if resolved is currently managing system DNS settings.
-func resolvedIsActive() bool {
+// isResolvedActive determines if resolved is currently managing system DNS settings.
+func isResolvedActive() bool {
// systemd-resolved is never installed without systemd.
_, err := exec.LookPath("systemctl")
if err != nil {
@@ -69,7 +61,7 @@ func resolvedIsActive() bool {
return false
}
- config, err := dnsReadConfig()
+ config, err := readResolvConf()
if err != nil {
return false
}
@@ -82,10 +74,16 @@ func resolvedIsActive() bool {
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)
+// resolvedManager uses the systemd-resolved DBus API.
+type resolvedManager struct{}
+
+func newResolvedManager(mconfig ManagerConfig) managerImpl {
+ return resolvedManager{}
+}
+
+// Up implements managerImpl.
+func (m resolvedManager) Up(config Config) error {
+ ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout)
defer cancel()
// conn is a shared connection whose lifecycle is managed by the dbus package.
@@ -100,6 +98,8 @@ func dnsResolvedUp(config DNSConfig) error {
dbus.ObjectPath("/org/freedesktop/resolve1"),
)
+ // In principle, we could persist this in the manager struct
+ // if we knew that interface indices are persistent. This does not seem to be the case.
_, iface, err := interfaces.Tailscale()
if err != nil {
return fmt.Errorf("getting interface index: %w", err)
@@ -129,7 +129,7 @@ func dnsResolvedUp(config DNSConfig) error {
iface.Index, linkNameservers,
).Store()
if err != nil {
- return fmt.Errorf("SetLinkDNS: %w", err)
+ return fmt.Errorf("setLinkDNS: %w", err)
}
var linkDomains = make([]resolvedLinkDomain, len(config.Domains))
@@ -145,15 +145,15 @@ func dnsResolvedUp(config DNSConfig) error {
iface.Index, linkDomains,
).Store()
if err != nil {
- return fmt.Errorf("SetLinkDomains: %w", err)
+ 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)
+// Down implements managerImpl.
+func (m resolvedManager) Down() error {
+ ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout)
defer cancel()
// conn is a shared connection whose lifecycle is managed by the dbus package.
diff --git a/wgengine/router/ifconfig_windows.go b/wgengine/router/ifconfig_windows.go
index 1c79cd8f2..410e4facb 100644
--- a/wgengine/router/ifconfig_windows.go
+++ b/wgengine/router/ifconfig_windows.go
@@ -21,7 +21,6 @@ import (
"github.com/tailscale/wireguard-go/device"
"github.com/tailscale/wireguard-go/tun"
"golang.org/x/sys/windows"
- "golang.org/x/sys/windows/registry"
"tailscale.com/wgengine/winnet"
)
@@ -157,28 +156,6 @@ func monitorDefaultRoutes(device *device.Device, autoMTU bool, tun *tun.NativeTu
return cb, nil
}
-func setDNSDomains(g windows.GUID, dnsDomains []string) {
- gs := g.String()
- log.Printf("setDNSDomains(%v) guid=%v\n", dnsDomains, gs)
- p := `SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\` + gs
- key, err := registry.OpenKey(registry.LOCAL_MACHINE, p, registry.READ|registry.SET_VALUE)
- if err != nil {
- log.Printf("setDNSDomains(%v): open: %v\n", p, err)
- return
- }
- defer key.Close()
-
- // Windows only supports a single per-interface DNS domain.
- dom := ""
- if len(dnsDomains) > 0 {
- dom = dnsDomains[0]
- }
- err = key.SetStringValue("Domain", dom)
- if err != nil {
- log.Printf("setDNSDomains(%v): SetStringValue: %v\n", p, err)
- }
-}
-
func setFirewall(ifcGUID *windows.GUID) (bool, error) {
c := ole.Connection{}
err := c.Initialize()
@@ -262,8 +239,6 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) error {
}
}()
- setDNSDomains(guid, cfg.Domains)
-
routes := []winipcfg.RouteData{}
var firstGateway4 *net.IP
var firstGateway6 *net.IP
@@ -358,16 +333,6 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) error {
errAcc = err
}
- var dnsIPs []net.IP
- for _, ip := range cfg.Nameservers {
- dnsIPs = append(dnsIPs, ip.IPAddr().IP)
- }
- err = iface.SetDNS(dnsIPs)
- if err != nil && errAcc == nil {
- log.Printf("setdns: %v\n", err)
- errAcc = err
- }
-
ipif, err := iface.GetIpInterface(winipcfg.AF_INET)
if err != nil {
log.Printf("getipif: %v\n", err)
diff --git a/wgengine/router/router.go b/wgengine/router/router.go
index 21926c5da..c65a0b806 100644
--- a/wgengine/router/router.go
+++ b/wgengine/router/router.go
@@ -11,6 +11,7 @@ import (
"github.com/tailscale/wireguard-go/tun"
"inet.af/netaddr"
"tailscale.com/types/logger"
+ "tailscale.com/wgengine/router/dns"
)
// Router is responsible for managing the system network stack.
@@ -40,6 +41,15 @@ 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) {
+ mconfig := dns.ManagerConfig{
+ Logf: logf,
+ InterfaceName: interfaceName,
+ Cleanup: true,
+ }
+ dns := dns.NewManager(mconfig)
+ if err := dns.Down(); err != nil {
+ logf("dns down: %v", err)
+ }
cleanup(logf, interfaceName)
}
@@ -72,7 +82,7 @@ type Config struct {
LocalAddrs []netaddr.IPPrefix
Routes []netaddr.IPPrefix // routes to point into the Tailscale interface
- DNSConfig
+ DNS dns.Config
// Linux-only things below, ignored on other platforms.
diff --git a/wgengine/router/router_darwin.go b/wgengine/router/router_darwin.go
index 816cae53a..26b689355 100644
--- a/wgengine/router/router_darwin.go
+++ b/wgengine/router/router_darwin.go
@@ -10,55 +10,10 @@ import (
"tailscale.com/types/logger"
)
-type darwinRouter struct {
- logf logger.Logf
- tunname string
- Router
+func newUserspaceRouter(logf logger.Logf, wgdev *device.Device, tundev tun.Device) (Router, error) {
+ return newUserspaceBSDRouter(logf, wgdev, tundev)
}
-func newUserspaceRouter(logf logger.Logf, _ *device.Device, tundev tun.Device) (Router, error) {
- tunname, err := tundev.Name()
- if err != nil {
- return nil, err
- }
-
- userspaceRouter, err := newUserspaceBSDRouter(logf, nil, tundev)
- if err != nil {
- return nil, err
- }
-
- return &darwinRouter{
- logf: logf,
- tunname: tunname,
- Router: userspaceRouter,
- }, nil
-}
-
-func (r *darwinRouter) Set(cfg *Config) error {
- if cfg == nil {
- cfg = &shutdownConfig
- }
-
- if SetRoutesFunc != nil {
- return SetRoutesFunc(cfg)
- }
-
- return r.Router.Set(cfg)
-}
-
-func (r *darwinRouter) Up() error {
- if SetRoutesFunc != nil {
- return nil // bringing up the tunnel is handled externally
- }
- 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
+func cleanup(logger.Logf, string) {
+ // Nothing to do.
}
diff --git a/wgengine/router/router_darwin_support.go b/wgengine/router/router_darwin_support.go
deleted file mode 100644
index 506a6ba17..000000000
--- a/wgengine/router/router_darwin_support.go
+++ /dev/null
@@ -1,23 +0,0 @@
-// 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
-
-// SetRoutesFunc applies the given router settings to the OS network
-// stack. cfg is guaranteed to be non-nil.
-//
-// This is logically part of the router_darwin.go implementation, and
-// should not be used on other platforms.
-//
-// The code to reconfigure the network stack on MacOS and iOS is in
-// the non-open `ipn-go-bridge` package, which bridges between the Go
-// and Swift pieces of the application. The ipn-go-bridge sets
-// SetRoutesFunc at startup.
-//
-// So why isn't this in router_darwin.go? Because in the non-oss
-// repository, we build ipn-go-bridge when developing on Linux as well
-// as MacOS, so that we don't have to wait until the Mac CI to
-// discover that we broke it. So this one definition needs to exist in
-// both the darwin and linux builds. Hence this file and build tag.
-var SetRoutesFunc func(cfg *Config) error
diff --git a/wgengine/router/router_default.go b/wgengine/router/router_default.go
index db170fba0..4dda1ec29 100644
--- a/wgengine/router/router_default.go
+++ b/wgengine/router/router_default.go
@@ -16,6 +16,6 @@ func newUserspaceRouter(logf logger.Logf, tunname string, dev *device.Device, tu
return NewFakeRouter(logf, tunname, dev, tuntap, netChanged)
}
-func cleanup() error {
- return nil
+func cleanup(logf logger.Logf, interfaceName string) {
+ // Nothing to do here.
}
diff --git a/wgengine/router/router_freebsd.go b/wgengine/router/router_freebsd.go
index 9fd8e1f41..e56e3f82d 100644
--- a/wgengine/router/router_freebsd.go
+++ b/wgengine/router/router_freebsd.go
@@ -5,8 +5,6 @@
package router
import (
- "fmt"
-
"github.com/tailscale/wireguard-go/device"
"github.com/tailscale/wireguard-go/tun"
"tailscale.com/types/logger"
@@ -21,34 +19,13 @@ func newUserspaceRouter(logf logger.Logf, _ *device.Device, tundev tun.Device) (
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")
+func cleanup(logf logger.Logf, interfaceName string) {
+ // If the interface was left behind, ifconfig down will not remove it.
+ // In fact, this will leave a system in a tainted state where starting tailscaled
+ // will result in "interface tailscale0 already exists"
+ // until the defunct interface is ifconfig-destroyed.
+ ifup := []string{"ifconfig", interfaceName, "destroy"}
+ if out, err := cmd(ifup...).CombinedOutput(); err != nil {
+ logf("ifconfig destroy: %v\n%s", err, out)
}
- return nil
}
diff --git a/wgengine/router/router_linux.go b/wgengine/router/router_linux.go
index 44ef9b3d3..ea28c3450 100644
--- a/wgengine/router/router_linux.go
+++ b/wgengine/router/router_linux.go
@@ -15,6 +15,7 @@ import (
"inet.af/netaddr"
"tailscale.com/net/tsaddr"
"tailscale.com/types/logger"
+ "tailscale.com/wgengine/router/dns"
)
// The following bits are added to packet marks for Tailscale use.
@@ -84,8 +85,7 @@ type linuxRouter struct {
snatSubnetRoutes bool
netfilterMode NetfilterMode
- dnsMode dnsMode
- dnsConfig DNSConfig
+ dns *dns.Manager
ipt4 netfilterRunner
cmd commandRunner
@@ -109,6 +109,11 @@ func newUserspaceRouterAdvanced(logf logger.Logf, tunname string, netfilter netf
_, err := exec.Command("ip", "rule").Output()
ipRuleAvailable := (err == nil)
+ mconfig := dns.ManagerConfig{
+ Logf: logf,
+ InterfaceName: tunname,
+ }
+
return &linuxRouter{
logf: logf,
ipRuleAvailable: ipRuleAvailable,
@@ -116,6 +121,7 @@ func newUserspaceRouterAdvanced(logf logger.Logf, tunname string, netfilter netf
netfilterMode: NetfilterOff,
ipt4: netfilter,
cmd: cmd,
+ dns: dns.NewManager(mconfig),
}, nil
}
@@ -133,26 +139,12 @@ 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) Close() error {
- if err := r.downDNS(); err != nil {
- return err
+ if err := r.dns.Down(); err != nil {
+ return fmt.Errorf("dns down: %v", err)
}
if err := r.downInterface(); err != nil {
return err
@@ -206,12 +198,8 @@ func (r *linuxRouter) Set(cfg *Config) error {
}
r.snatSubnetRoutes = cfg.SNATSubnetRoutes
- 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
- }
+ if err := r.dns.Set(cfg.DNS); err != nil {
+ return fmt.Errorf("dns set: %v", err)
}
return nil
@@ -855,68 +843,6 @@ func normalizeCIDR(cidr netaddr.IPPrefix) string {
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)
- }
- }
+ // TODO(dmytro): clean up iptables.
}
diff --git a/wgengine/router/router_openbsd.go b/wgengine/router/router_openbsd.go
index 574b2804a..334f89a29 100644
--- a/wgengine/router/router_openbsd.go
+++ b/wgengine/router/router_openbsd.go
@@ -14,6 +14,7 @@ import (
"github.com/tailscale/wireguard-go/tun"
"inet.af/netaddr"
"tailscale.com/types/logger"
+ "tailscale.com/wgengine/router/dns"
)
// For now this router only supports the WireGuard userspace implementation.
@@ -26,7 +27,7 @@ type openbsdRouter struct {
local netaddr.IPPrefix
routes map[netaddr.IPPrefix]struct{}
- dnsConfig DNSConfig
+ dns *dns.Manager
}
func newUserspaceRouter(logf logger.Logf, _ *device.Device, tundev tun.Device) (Router, error) {
@@ -34,9 +35,16 @@ func newUserspaceRouter(logf logger.Logf, _ *device.Device, tundev tun.Device) (
if err != nil {
return nil, err
}
+
+ mconfig := dns.ManagerConfig{
+ Logf: logf,
+ InterfaceName: tunname,
+ }
+
return &openbsdRouter{
logf: logf,
tunname: tunname,
+ dns: dns.NewManager(mconfig),
}, nil
}
@@ -62,7 +70,10 @@ func (r *openbsdRouter) Set(cfg *Config) error {
}
// TODO: support configuring multiple local addrs on interface.
- if len(cfg.LocalAddrs) != 1 {
+ if len(cfg.LocalAddrs) == 0 {
+ return nil
+ }
+ if len(cfg.LocalAddrs) > 1 {
return errors.New("freebsd doesn't support setting multiple local addrs yet")
}
localAddr := cfg.LocalAddrs[0]
@@ -155,26 +166,22 @@ func (r *openbsdRouter) Set(cfg *Config) error {
r.local = localAddr
r.routes = newRoutes
- 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
- }
+ if err := r.dns.Set(cfg.DNS); err != nil {
+ errq = fmt.Errorf("dns set: %v", err)
}
return errq
}
func (r *openbsdRouter) Close() error {
+ if err := r.dns.Down(); err != nil {
+ return fmt.Errorf("dns down: %v", err)
+ }
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 {
logf("ifconfig down: %v\n%s", err, out)
diff --git a/wgengine/router/router_userspace_bsd.go b/wgengine/router/router_userspace_bsd.go
index 7c2aa1b88..dc75a381b 100644
--- a/wgengine/router/router_userspace_bsd.go
+++ b/wgengine/router/router_userspace_bsd.go
@@ -16,6 +16,7 @@ import (
"github.com/tailscale/wireguard-go/tun"
"inet.af/netaddr"
"tailscale.com/types/logger"
+ "tailscale.com/wgengine/router/dns"
)
type userspaceBSDRouter struct {
@@ -24,7 +25,7 @@ type userspaceBSDRouter struct {
local netaddr.IPPrefix
routes map[netaddr.IPPrefix]struct{}
- dnsConfig DNSConfig
+ dns *dns.Manager
}
func newUserspaceBSDRouter(logf logger.Logf, _ *device.Device, tundev tun.Device) (Router, error) {
@@ -32,9 +33,16 @@ func newUserspaceBSDRouter(logf logger.Logf, _ *device.Device, tundev tun.Device
if err != nil {
return nil, err
}
+
+ mconfig := dns.ManagerConfig{
+ Logf: logf,
+ InterfaceName: tunname,
+ }
+
return &userspaceBSDRouter{
logf: logf,
tunname: tunname,
+ dns: dns.NewManager(mconfig),
}, nil
}
@@ -141,35 +149,17 @@ func (r *userspaceBSDRouter) Set(cfg *Config) error {
r.local = localAddr
r.routes = newRoutes
- 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
- }
+ if err := r.dns.Set(cfg.DNS); err != nil {
+ errq = fmt.Errorf("dns set: %v", err)
}
return errq
}
func (r *userspaceBSDRouter) Close() error {
- if err := downDNS(r.tunname); err != nil {
+ if err := r.dns.Down(); err != nil {
r.logf("dns down: %v", err)
}
// No interface cleanup is necessary during normal shutdown.
return nil
}
-
-func cleanup(logf logger.Logf, interfaceName string) {
- if err := downDNS(interfaceName); err != nil {
- logf("dns down: %v", err)
- }
- // If the interface was left behind, ifconfig down will not remove it.
- // In fact, this will leave a system in a tainted state where starting tailscaled
- // will result in "interface tailscale0 already exists"
- // until the defunct interface is ifconfig-destroyed.
- ifup := []string{"ifconfig", interfaceName, "destroy"}
- if out, err := cmd(ifup...).CombinedOutput(); err != nil {
- logf("ifconfig destroy: %v\n%s", err, out)
- }
-}
diff --git a/wgengine/router/router_windows.go b/wgengine/router/router_windows.go
index 00f3e281c..60652297c 100644
--- a/wgengine/router/router_windows.go
+++ b/wgengine/router/router_windows.go
@@ -5,12 +5,14 @@
package router
import (
+ "fmt"
"log"
winipcfg "github.com/tailscale/winipcfg-go"
"github.com/tailscale/wireguard-go/device"
"github.com/tailscale/wireguard-go/tun"
"tailscale.com/types/logger"
+ "tailscale.com/wgengine/router/dns"
)
type winRouter struct {
@@ -19,6 +21,7 @@ type winRouter struct {
nativeTun *tun.NativeTun
wgdev *device.Device
routeChangeCallback *winipcfg.RouteChangeCallback
+ dns *dns.Manager
}
func newUserspaceRouter(logf logger.Logf, wgdev *device.Device, tundev tun.Device) (Router, error) {
@@ -26,11 +29,20 @@ func newUserspaceRouter(logf logger.Logf, wgdev *device.Device, tundev tun.Devic
if err != nil {
return nil, err
}
+
+ nativeTun := tundev.(*tun.NativeTun)
+ guid := nativeTun.GUID().String()
+ mconfig := dns.ManagerConfig{
+ Logf: logf,
+ InterfaceName: guid,
+ }
+
return &winRouter{
logf: logf,
wgdev: wgdev,
tunname: tunname,
- nativeTun: tundev.(*tun.NativeTun),
+ nativeTun: nativeTun,
+ dns: dns.NewManager(mconfig),
}, nil
}
@@ -55,10 +67,18 @@ func (r *winRouter) Set(cfg *Config) error {
r.logf("ConfigureInterface: %v\n", err)
return err
}
+
+ if err := r.dns.Set(cfg.DNS); err != nil {
+ return fmt.Errorf("dns set: %w", err)
+ }
+
return nil
}
func (r *winRouter) Close() error {
+ if err := r.dns.Down(); err != nil {
+ return fmt.Errorf("dns down: %w", err)
+ }
if r.routeChangeCallback != nil {
r.routeChangeCallback.Unregister()
}
@@ -66,5 +86,5 @@ func (r *winRouter) Close() error {
}
func cleanup(logf logger.Logf, interfaceName string) {
- // DNS is interface-bound, so nothing to do here.
+ // Nothing to do here.
}
diff --git a/wgengine/tsdns/tsdns.go b/wgengine/tsdns/tsdns.go
index d875aad02..d2aa08109 100644
--- a/wgengine/tsdns/tsdns.go
+++ b/wgengine/tsdns/tsdns.go
@@ -40,6 +40,7 @@ var ErrClosed = errors.New("closed")
var (
errAllFailed = errors.New("all upstream nameservers failed")
errFullQueue = errors.New("request queue full")
+ errNoNameservers = errors.New("no upstream nameservers set")
errMapNotSet = errors.New("domain map not set")
errNotImplemented = errors.New("query type not implemented")
errNotQuery = errors.New("not a DNS query")
@@ -257,7 +258,7 @@ func (r *Resolver) delegate(query []byte) ([]byte, error) {
r.mu.RUnlock()
if len(nameservers) == 0 {
- return nil, errAllFailed
+ return nil, errNoNameservers
}
ctx, cancel := context.WithTimeout(context.Background(), delegateTimeout)
diff --git a/wgengine/userspace.go b/wgengine/userspace.go
index 544cc2ae2..c7bef0ad7 100644
--- a/wgengine/userspace.go
+++ b/wgengine/userspace.go
@@ -13,6 +13,7 @@ import (
"fmt"
"io"
"log"
+ "net"
"os"
"os/exec"
"runtime"
@@ -31,6 +32,7 @@ import (
"tailscale.com/internal/deepprint"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/interfaces"
+ "tailscale.com/net/tsaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/logger"
@@ -69,19 +71,28 @@ const (
// (This includes peers that have never been idle, which
// effectively have infinite idleness)
lazyPeerIdleThreshold = 5 * time.Minute
+
+ // packetSendTimeUpdateFrequency controls how often we record
+ // the time that we wrote a packet to an IP address.
+ packetSendTimeUpdateFrequency = 10 * time.Second
+
+ // packetSendRecheckWireguardThreshold controls how long we can go
+ // between packet sends to an IP before checking to see
+ // whether this IP address needs to be added back to the
+ // Wireguard peer oconfig.
+ packetSendRecheckWireguardThreshold = 1 * time.Minute
)
type userspaceEngine struct {
- logf logger.Logf
- reqCh chan struct{}
- waitCh chan struct{} // chan is closed when first Close call completes; contrast with closing bool
- tundev *tstun.TUN
- wgdev *device.Device
- router router.Router
- resolver *tsdns.Resolver
- useTailscaleDNS bool
- magicConn *magicsock.Conn
- linkMon *monitor.Mon
+ logf logger.Logf
+ reqCh chan struct{}
+ waitCh chan struct{} // chan is closed when first Close call completes; contrast with closing bool
+ tundev *tstun.TUN
+ wgdev *device.Device
+ router router.Router
+ resolver *tsdns.Resolver
+ magicConn *magicsock.Conn
+ linkMon *monitor.Mon
// localAddrs is the set of IP addresses assigned to the local
// tunnel interface. It's used to reflect local packets
@@ -121,13 +132,9 @@ type EngineConfig struct {
RouterGen RouterGen
// ListenPort is the port on which the engine will listen.
ListenPort uint16
- // EchoRespondToAll determines whether ICMP Echo requests incoming from Tailscale peers
- // will be intercepted and responded to, regardless of the source host.
- EchoRespondToAll bool
- // UseTailscaleDNS determines whether DNS requests for names of the form <mynode>.<mydomain>.<root>
- // directed to the designated Taislcale DNS address (see wgengine/tsdns)
- // will be intercepted and resolved by a tsdns.Resolver.
- UseTailscaleDNS bool
+ // Fake determines whether this engine is running in fake mode,
+ // which disables such features as DNS configuration and unrestricted ICMP Echo responses.
+ Fake bool
}
type Loggify struct {
@@ -142,11 +149,11 @@ func (l *Loggify) Write(b []byte) (int, error) {
func NewFakeUserspaceEngine(logf logger.Logf, listenPort uint16) (Engine, error) {
logf("Starting userspace wireguard engine (FAKE tuntap device).")
conf := EngineConfig{
- Logf: logf,
- TUN: tstun.NewFakeTUN(),
- RouterGen: router.NewFake,
- ListenPort: listenPort,
- EchoRespondToAll: true,
+ Logf: logf,
+ TUN: tstun.NewFakeTUN(),
+ RouterGen: router.NewFake,
+ ListenPort: listenPort,
+ Fake: true,
}
return NewUserspaceEngineAdvanced(conf)
}
@@ -173,8 +180,6 @@ func NewUserspaceEngine(logf logger.Logf, tunname string, listenPort uint16) (En
TUN: tun,
RouterGen: router.New,
ListenPort: listenPort,
- // TODO(dmytro): plumb this down.
- UseTailscaleDNS: true,
}
e, err := NewUserspaceEngineAdvanced(conf)
@@ -194,19 +199,18 @@ func newUserspaceEngineAdvanced(conf EngineConfig) (_ Engine, reterr error) {
logf := conf.Logf
e := &userspaceEngine{
- logf: logf,
- reqCh: make(chan struct{}, 1),
- waitCh: make(chan struct{}),
- tundev: tstun.WrapTUN(logf, conf.TUN),
- resolver: tsdns.NewResolver(logf, magicDNSDomain),
- useTailscaleDNS: conf.UseTailscaleDNS,
- pingers: make(map[wgcfg.Key]*pinger),
+ logf: logf,
+ reqCh: make(chan struct{}, 1),
+ waitCh: make(chan struct{}),
+ tundev: tstun.WrapTUN(logf, conf.TUN),
+ resolver: tsdns.NewResolver(logf, magicDNSDomain),
+ pingers: make(map[wgcfg.Key]*pinger),
}
e.localAddrs.Store(map[packet.IP]bool{})
e.linkState, _ = getLinkState()
// Respond to all pings only in fake mode.
- if conf.EchoRespondToAll {
+ if conf.Fake {
e.tundev.PostFilterIn = echoRespondToAll
}
e.tundev.PreFilterOut = e.handleLocalPackets
@@ -366,11 +370,9 @@ func echoRespondToAll(p *packet.ParsedPacket, t *tstun.TUN) filter.Response {
// tailscaled directly. Other packets are allowed to proceed into the
// main ACL filter.
func (e *userspaceEngine) handleLocalPackets(p *packet.ParsedPacket, t *tstun.TUN) filter.Response {
- if e.useTailscaleDNS {
- if verdict := e.handleDNS(p, t); verdict == filter.Drop {
- // local DNS handled the packet.
- return filter.Drop
- }
+ if verdict := e.handleDNS(p, t); verdict == filter.Drop {
+ // local DNS handled the packet.
+ return filter.Drop
}
if runtime.GOOS == "darwin" && e.isLocalAddr(p.DstIP) {
@@ -763,12 +765,19 @@ func (e *userspaceEngine) updateActivityMapsLocked(trackDisco []tailcfg.DiscoKey
if fn == nil {
// This is the func that gets run on every outgoing packet for tracked IPs:
fn = func() {
- now, old := time.Now().Unix(), atomic.LoadInt64(timePtr)
- if old > now-10 {
- return
+ now := time.Now().Unix()
+ old := atomic.LoadInt64(timePtr)
+
+ // How long's it been since we last sent a packet?
+ // For our first packet, old is Unix epoch time 0 (1970).
+ elapsedSec := now - old
+
+ if elapsedSec >= int64(packetSendTimeUpdateFrequency/time.Second) {
+ atomic.StoreInt64(timePtr, now)
}
- atomic.StoreInt64(timePtr, now)
- if old == 0 || (now-old) <= 60 {
+ // On a big jump, assume we might no longer be in the wireguard
+ // config and go check.
+ if elapsedSec >= int64(packetSendRecheckWireguardThreshold/time.Second) {
e.wgLock.Lock()
defer e.wgLock.Unlock()
e.maybeReconfigWireguardLocked()
@@ -807,13 +816,6 @@ 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 := deepprint.UpdateHash(&e.lastEngineSigFull, cfg)
routerChanged := deepprint.UpdateHash(&e.lastRouterSig, routerCfg)
if !engineChanged && !routerChanged {
@@ -835,6 +837,15 @@ func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config)
}
if routerChanged {
+ if routerCfg.DNS.Proxied {
+ ips := routerCfg.DNS.Nameservers
+ nameservers := make([]string, len(ips))
+ for i, ip := range ips {
+ nameservers[i] = net.JoinHostPort(ip.String(), "53")
+ }
+ e.resolver.SetNameservers(nameservers)
+ routerCfg.DNS.Nameservers = []netaddr.IP{tsaddr.TailscaleServiceIP()}
+ }
e.logf("wgengine: Reconfig: configuring router")
if err := e.router.Set(routerCfg); err != nil {
return err