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 similarity index 81% rename from wgengine/router/dns_direct.go rename to 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 similarity index 90% rename from wgengine/router/dns_networkmanager.go rename to 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 similarity index 75% rename from wgengine/router/dns_resolvconf.go rename to 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 similarity index 79% rename from wgengine/router/dns_resolved.go rename to 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 .. - // 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