ipn, wgengine, magicsock, tsdns: be quieter and less aggressive when offline

If no interfaces are up, calm down and stop spamming so much. It was
noticed as especially bad on Windows, but probably was bad
everywhere. I just have the best network conditions testing on a
Windows VM.

Updates #604
This commit is contained in:
Brad Fitzpatrick 2020-10-06 15:22:46 -07:00
parent 7616acd118
commit 6ee219a25d
6 changed files with 124 additions and 24 deletions

View File

@ -129,6 +129,11 @@ func (b *LocalBackend) linkChange(major bool, ifst *interfaces.State) {
hadPAC := b.prevIfState.HasPAC() hadPAC := b.prevIfState.HasPAC()
b.prevIfState = ifst 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 // If the PAC-ness of the network changed, reconfig wireguard+route to
// add/remove subnets. // add/remove subnets.
if hadPAC != ifst.HasPAC() { if hadPAC != ifst.HasPAC() {
@ -1197,6 +1202,7 @@ func (b *LocalBackend) enterState(newState State) {
prefs := b.prefs prefs := b.prefs
notify := b.notify notify := b.notify
bc := b.c bc := b.c
networkUp := b.prevIfState.AnyInterfaceUp()
b.mu.Unlock() b.mu.Unlock()
if state == newState { if state == newState {
@ -1209,7 +1215,7 @@ func (b *LocalBackend) enterState(newState State) {
} }
if bc != nil { if bc != nil {
bc.SetPaused(newState == Stopped) bc.SetPaused(newState == Stopped || !networkUp)
} }
switch newState { switch newState {

View File

@ -161,10 +161,11 @@ type State struct {
InterfaceUp map[string]bool InterfaceUp map[string]bool
// HaveV6Global is whether this machine has an IPv6 global address // HaveV6Global is whether this machine has an IPv6 global address
// on some interface. // on some non-Tailscale interface that's up.
HaveV6Global bool 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 HaveV4 bool
// IsExpensive is whether the current network interface is // 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. // DefaultRouteInterface is the interface name for the machine's default route.
// It is not yet populated on all OSes. // 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 DefaultRouteInterface string
// HTTPProxy is the HTTP proxy to use. // 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 != "" } 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 // RemoveTailscaleInterfaces modifes s to remove any interfaces that
// are owned by this process. (TODO: make this true; currently it // are owned by this process. (TODO: make this true; currently it
// makes the Linux-only assumption that the interface is named // makes the Linux-only assumption that the interface is named
// /^tailscale/) // /^tailscale/)
func (s *State) RemoveTailscaleInterfaces() { func (s *State) RemoveTailscaleInterfaces() {
for name := range s.InterfaceIPs { for name := range s.InterfaceIPs {
if name == "Tailscale" || // as it is on Windows if isTailscaleInterfaceName(name) {
strings.HasPrefix(name, "tailscale") { // TODO: use --tun flag value, etc; see TODO in method doc
delete(s.InterfaceIPs, name) delete(s.InterfaceIPs, name)
delete(s.InterfaceUp, 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. // getPAC, if non-nil, returns the current PAC file URL.
var getPAC func() string var getPAC func() string
@ -270,24 +282,29 @@ func GetState() (*State, error) {
InterfaceUp: make(map[string]bool), InterfaceUp: make(map[string]bool),
} }
if err := ForeachInterfaceAddress(func(ni Interface, ip netaddr.IP) { if err := ForeachInterfaceAddress(func(ni Interface, ip netaddr.IP) {
ifUp := ni.IsUp()
s.InterfaceIPs[ni.Name] = append(s.InterfaceIPs[ni.Name], ip) s.InterfaceIPs[ni.Name] = append(s.InterfaceIPs[ni.Name], ip)
s.InterfaceUp[ni.Name] = ni.IsUp() s.InterfaceUp[ni.Name] = ifUp
s.HaveV6Global = s.HaveV6Global || isGlobalV6(ip) if ifUp && !ip.IsLoopback() && !ip.IsLinkLocalUnicast() && !isTailscaleInterfaceName(ni.Name) {
s.HaveV4 = s.HaveV4 || (ip.Is4() && !ip.IsLoopback()) s.HaveV6Global = s.HaveV6Global || isGlobalV6(ip)
s.HaveV4 = s.HaveV4 || ip.Is4()
}
}); err != nil { }); err != nil {
return nil, err return nil, err
} }
s.DefaultRouteInterface, _ = DefaultRouteInterface() s.DefaultRouteInterface, _ = DefaultRouteInterface()
req, err := http.NewRequest("GET", LoginEndpointForProxyDetermination, nil) if s.AnyInterfaceUp() {
if err != nil { req, err := http.NewRequest("GET", LoginEndpointForProxyDetermination, nil)
return nil, err if err != nil {
} return nil, err
if u, err := tshttpproxy.ProxyFromEnvironment(req); err == nil && u != nil { }
s.HTTPProxy = u.String() if u, err := tshttpproxy.ProxyFromEnvironment(req); err == nil && u != nil {
} s.HTTPProxy = u.String()
if getPAC != nil { }
s.PAC = getPAC() if getPAC != nil {
s.PAC = getPAC()
}
} }
return s, nil return s, nil

View File

@ -208,6 +208,11 @@ type Conn struct {
// necessarily have a netcheck.Report and don't want to skip // necessarily have a netcheck.Report and don't want to skip
// logging. // logging.
noV4, noV6 syncs.AtomicBool 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 // 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), discoOfAddr: make(map[netaddr.IPPort]tailcfg.DiscoKey),
} }
c.muCond = sync.NewCond(&c.mu) c.muCond = sync.NewCond(&c.mu)
c.networkUp.Set(true) // assume up until told otherwise
return c 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 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 { func (c *Conn) Send(b []byte, ep conn.Endpoint) error {
if c.networkDown() {
return errNetworkDown
}
var as *AddrSet var as *AddrSet
switch v := ep.(type) { switch v := ep.(type) {
default: default:
@ -1111,6 +1124,10 @@ func (c *Conn) derpWriteChanOfAddr(addr netaddr.IPPort, peer key.Public) chan<-
} }
regionID := int(addr.Port) regionID := int(addr.Port)
if c.networkDown() {
return nil
}
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
if !c.wantDerpLocked() || c.closed { if !c.wantDerpLocked() || c.closed {
@ -1304,15 +1321,19 @@ func (c *Conn) runDerpReader(ctx context.Context, derpFakeAddr netaddr.IPPort, d
for { for {
msg, err := dc.Recv() msg, err := dc.Recv()
if err == derphttp.ErrClientClosed {
return
}
if err != nil { if err != nil {
// Forget that all these peers have routes. // Forget that all these peers have routes.
for peer := range peerPresent { for peer := range peerPresent {
delete(peerPresent, peer) delete(peerPresent, peer)
c.removeDerpPeerRoute(peer, regionID, dc) 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 { select {
case <-ctx.Done(): case <-ctx.Done():
return return
@ -1691,7 +1712,9 @@ func (c *Conn) sendDiscoMessage(dst netaddr.IPPort, dstKey tailcfg.NodeKey, dstD
} else if err == nil { } else if err == nil {
// Can't send. (e.g. no IPv6 locally) // Can't send. (e.g. no IPv6 locally)
} else { } 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 return sent, err
} }
@ -1956,6 +1979,21 @@ func (c *Conn) sharedDiscoKeyLocked(k tailcfg.DiscoKey) *[32]byte {
return shared 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. // SetPrivateKey sets the connection's private key.
// //
// This is only used to be able prove our identity when connecting to // 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 { func (c *Conn) shouldDoPeriodicReSTUN() bool {
if c.networkDown() {
return false
}
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
if len(c.peerSet) == 0 { if len(c.peerSet) == 0 {

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// +build !darwin // +build !darwin,!windows
package tsdns package tsdns

View File

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

View File

@ -246,6 +246,7 @@ func newUserspaceEngineAdvanced(conf EngineConfig) (_ Engine, reterr error) {
e.tundev.Close() e.tundev.Close()
return nil, fmt.Errorf("wgengine: %v", err) return nil, fmt.Errorf("wgengine: %v", err)
} }
e.magicConn.SetNetworkUp(e.linkState.AnyInterfaceUp())
// flags==0 because logf is already nested in another logger. // flags==0 because logf is already nested in another logger.
// The outer one can display the preferred log prefixes, etc. // The outer one can display the preferred log prefixes, etc.
@ -1139,12 +1140,17 @@ func (e *userspaceEngine) LinkChange(isExpensive bool) {
cur.IsExpensive = isExpensive cur.IsExpensive = isExpensive
needRebind, linkChangeCallback := e.setLinkState(cur) needRebind, linkChangeCallback := e.setLinkState(cur)
if needRebind { up := cur.AnyInterfaceUp()
e.logf("LinkChange: major, rebinding. New state: %+v", cur) if !up {
e.logf("LinkChange: all links down; pausing: %v", cur)
} else if needRebind {
e.logf("LinkChange: major, rebinding. New state: %v", cur)
} else { } else {
e.logf("LinkChange: minor") e.logf("LinkChange: minor")
} }
e.magicConn.SetNetworkUp(up)
why := "link-change-minor" why := "link-change-minor"
if needRebind { if needRebind {
why = "link-change-major" why = "link-change-major"