diff --git a/ipn/local.go b/ipn/local.go index 8cfbb85b2..b096a5701 100644 --- a/ipn/local.go +++ b/ipn/local.go @@ -129,6 +129,11 @@ func (b *LocalBackend) linkChange(major bool, ifst *interfaces.State) { hadPAC := b.prevIfState.HasPAC() b.prevIfState = ifst + networkUp := ifst.AnyInterfaceUp() + if b.c != nil { + go b.c.SetPaused(b.state == Stopped || !networkUp) + } + // If the PAC-ness of the network changed, reconfig wireguard+route to // add/remove subnets. if hadPAC != ifst.HasPAC() { @@ -1197,6 +1202,7 @@ func (b *LocalBackend) enterState(newState State) { prefs := b.prefs notify := b.notify bc := b.c + networkUp := b.prevIfState.AnyInterfaceUp() b.mu.Unlock() if state == newState { @@ -1209,7 +1215,7 @@ func (b *LocalBackend) enterState(newState State) { } if bc != nil { - bc.SetPaused(newState == Stopped) + bc.SetPaused(newState == Stopped || !networkUp) } switch newState { diff --git a/net/interfaces/interfaces.go b/net/interfaces/interfaces.go index 2e84f845e..1bae8948d 100644 --- a/net/interfaces/interfaces.go +++ b/net/interfaces/interfaces.go @@ -161,10 +161,11 @@ type State struct { InterfaceUp map[string]bool // HaveV6Global is whether this machine has an IPv6 global address - // on some interface. + // on some non-Tailscale interface that's up. HaveV6Global bool - // HaveV4 is whether the machine has some non-localhost IPv4 address. + // HaveV4 is whether the machine has some non-localhost, + // non-link-local IPv4 address on a non-Tailscale interface that's up. HaveV4 bool // IsExpensive is whether the current network interface is @@ -174,6 +175,8 @@ type State struct { // DefaultRouteInterface is the interface name for the machine's default route. // It is not yet populated on all OSes. + // Its exact value should not be assumed to be a map key for + // the Interface maps above; it's only used for debugging. DefaultRouteInterface string // HTTPProxy is the HTTP proxy to use. @@ -244,20 +247,29 @@ func (s *State) Equal(s2 *State) bool { func (s *State) HasPAC() bool { return s != nil && s.PAC != "" } +// AnyInterfaceUp reports whether any interface seems like it has Internet access. +func (s *State) AnyInterfaceUp() bool { + return s != nil && (s.HaveV4 || s.HaveV6Global) +} + // RemoveTailscaleInterfaces modifes s to remove any interfaces that // are owned by this process. (TODO: make this true; currently it // makes the Linux-only assumption that the interface is named // /^tailscale/) func (s *State) RemoveTailscaleInterfaces() { for name := range s.InterfaceIPs { - if name == "Tailscale" || // as it is on Windows - strings.HasPrefix(name, "tailscale") { // TODO: use --tun flag value, etc; see TODO in method doc + if isTailscaleInterfaceName(name) { delete(s.InterfaceIPs, name) delete(s.InterfaceUp, name) } } } +func isTailscaleInterfaceName(name string) bool { + return name == "Tailscale" || // as it is on Windows + strings.HasPrefix(name, "tailscale") // TODO: use --tun flag value, etc; see TODO in method doc +} + // getPAC, if non-nil, returns the current PAC file URL. var getPAC func() string @@ -270,24 +282,29 @@ func GetState() (*State, error) { InterfaceUp: make(map[string]bool), } if err := ForeachInterfaceAddress(func(ni Interface, ip netaddr.IP) { + ifUp := ni.IsUp() s.InterfaceIPs[ni.Name] = append(s.InterfaceIPs[ni.Name], ip) - s.InterfaceUp[ni.Name] = ni.IsUp() - s.HaveV6Global = s.HaveV6Global || isGlobalV6(ip) - s.HaveV4 = s.HaveV4 || (ip.Is4() && !ip.IsLoopback()) + s.InterfaceUp[ni.Name] = ifUp + if ifUp && !ip.IsLoopback() && !ip.IsLinkLocalUnicast() && !isTailscaleInterfaceName(ni.Name) { + s.HaveV6Global = s.HaveV6Global || isGlobalV6(ip) + s.HaveV4 = s.HaveV4 || ip.Is4() + } }); err != nil { return nil, err } s.DefaultRouteInterface, _ = DefaultRouteInterface() - req, err := http.NewRequest("GET", LoginEndpointForProxyDetermination, nil) - if err != nil { - return nil, err - } - if u, err := tshttpproxy.ProxyFromEnvironment(req); err == nil && u != nil { - s.HTTPProxy = u.String() - } - if getPAC != nil { - s.PAC = getPAC() + if s.AnyInterfaceUp() { + req, err := http.NewRequest("GET", LoginEndpointForProxyDetermination, nil) + if err != nil { + return nil, err + } + if u, err := tshttpproxy.ProxyFromEnvironment(req); err == nil && u != nil { + s.HTTPProxy = u.String() + } + if getPAC != nil { + s.PAC = getPAC() + } } return s, nil diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go index fa4daf94b..156235ec4 100644 --- a/wgengine/magicsock/magicsock.go +++ b/wgengine/magicsock/magicsock.go @@ -208,6 +208,11 @@ type Conn struct { // necessarily have a netcheck.Report and don't want to skip // logging. noV4, noV6 syncs.AtomicBool + + // networkUp is whether the network is up (some interface is up + // with IPv4 or IPv6). It's used to suppress log spam and prevent + // new connection that'll fail. + networkUp syncs.AtomicBool } // derpRoute is a route entry for a public key, saying that a certain @@ -345,6 +350,7 @@ func newConn() *Conn { discoOfAddr: make(map[netaddr.IPPort]tailcfg.DiscoKey), } c.muCond = sync.NewCond(&c.mu) + c.networkUp.Set(true) // assume up until told otherwise return c } @@ -969,8 +975,15 @@ func (as *AddrSet) appendDests(dsts []netaddr.IPPort, b []byte) (_ []netaddr.IPP } var errNoDestinations = errors.New("magicsock: no destinations") +var errNetworkDown = errors.New("magicsock: network down") + +func (c *Conn) networkDown() bool { return !c.networkUp.Get() } func (c *Conn) Send(b []byte, ep conn.Endpoint) error { + if c.networkDown() { + return errNetworkDown + } + var as *AddrSet switch v := ep.(type) { default: @@ -1111,6 +1124,10 @@ func (c *Conn) derpWriteChanOfAddr(addr netaddr.IPPort, peer key.Public) chan<- } regionID := int(addr.Port) + if c.networkDown() { + return nil + } + c.mu.Lock() defer c.mu.Unlock() if !c.wantDerpLocked() || c.closed { @@ -1304,15 +1321,19 @@ func (c *Conn) runDerpReader(ctx context.Context, derpFakeAddr netaddr.IPPort, d for { msg, err := dc.Recv() - if err == derphttp.ErrClientClosed { - return - } if err != nil { // Forget that all these peers have routes. for peer := range peerPresent { delete(peerPresent, peer) c.removeDerpPeerRoute(peer, regionID, dc) } + if err == derphttp.ErrClientClosed { + return + } + if c.networkDown() { + c.logf("magicsock: derp.Recv(derp-%d): network down, closing", regionID) + return + } select { case <-ctx.Done(): return @@ -1691,7 +1712,9 @@ func (c *Conn) sendDiscoMessage(dst netaddr.IPPort, dstKey tailcfg.NodeKey, dstD } else if err == nil { // Can't send. (e.g. no IPv6 locally) } else { - c.logf("magicsock: disco: failed to send %T to %v: %v", m, dst, err) + if !c.networkDown() { + c.logf("magicsock: disco: failed to send %T to %v: %v", m, dst, err) + } } return sent, err } @@ -1956,6 +1979,21 @@ func (c *Conn) sharedDiscoKeyLocked(k tailcfg.DiscoKey) *[32]byte { return shared } +func (c *Conn) SetNetworkUp(up bool) { + c.mu.Lock() + defer c.mu.Unlock() + if c.networkUp.Get() == up { + return + } + + c.logf("magicsock: SetNetworkUp(%v)", up) + c.networkUp.Set(up) + + if !up { + c.closeAllDerpLocked("network-down") + } +} + // SetPrivateKey sets the connection's private key. // // This is only used to be able prove our identity when connecting to @@ -2282,6 +2320,10 @@ func maxIdleBeforeSTUNShutdown() time.Duration { } func (c *Conn) shouldDoPeriodicReSTUN() bool { + if c.networkDown() { + return false + } + c.mu.Lock() defer c.mu.Unlock() if len(c.peerSet) == 0 { diff --git a/wgengine/tsdns/neterr_other.go b/wgengine/tsdns/neterr_other.go index cedca2a94..d245d0c38 100644 --- a/wgengine/tsdns/neterr_other.go +++ b/wgengine/tsdns/neterr_other.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// +build !darwin +// +build !darwin,!windows package tsdns diff --git a/wgengine/tsdns/neterr_windows.go b/wgengine/tsdns/neterr_windows.go new file mode 100644 index 000000000..90f0db2ab --- /dev/null +++ b/wgengine/tsdns/neterr_windows.go @@ -0,0 +1,29 @@ +// 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 tsdns + +import ( + "net" + "os" + + "golang.org/x/sys/windows" +) + +func networkIsDown(err error) bool { + if oe, ok := err.(*net.OpError); ok && oe.Op == "write" { + if se, ok := oe.Err.(*os.SyscallError); ok { + if se.Syscall == "wsasendto" && se.Err == windows.WSAENETUNREACH { + return true + } + } + } + return false +} + +func networkIsUnreachable(err error) bool { + // TODO(bradfitz,josharian): something here? what is the + // difference between down and unreachable? Add comments. + return false +} diff --git a/wgengine/userspace.go b/wgengine/userspace.go index 9001267db..1048b791c 100644 --- a/wgengine/userspace.go +++ b/wgengine/userspace.go @@ -246,6 +246,7 @@ func newUserspaceEngineAdvanced(conf EngineConfig) (_ Engine, reterr error) { e.tundev.Close() return nil, fmt.Errorf("wgengine: %v", err) } + e.magicConn.SetNetworkUp(e.linkState.AnyInterfaceUp()) // flags==0 because logf is already nested in another logger. // The outer one can display the preferred log prefixes, etc. @@ -1139,12 +1140,17 @@ func (e *userspaceEngine) LinkChange(isExpensive bool) { cur.IsExpensive = isExpensive needRebind, linkChangeCallback := e.setLinkState(cur) - if needRebind { - e.logf("LinkChange: major, rebinding. New state: %+v", cur) + up := cur.AnyInterfaceUp() + if !up { + e.logf("LinkChange: all links down; pausing: %v", cur) + } else if needRebind { + e.logf("LinkChange: major, rebinding. New state: %v", cur) } else { e.logf("LinkChange: minor") } + e.magicConn.SetNetworkUp(up) + why := "link-change-minor" if needRebind { why = "link-change-major"