diff --git a/go.mod b/go.mod index d080d13..4b3206a 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/rs/zerolog v1.30.0 github.com/spf13/viper v1.16.0 golang.org/x/oauth2 v0.11.0 - tailscale.com v1.48.1 + tailscale.com v1.48.2 ) require ( diff --git a/go.sum b/go.sum index 64d75da..d1d17c9 100644 --- a/go.sum +++ b/go.sum @@ -730,5 +730,5 @@ rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= software.sslmate.com/src/go-pkcs12 v0.2.0 h1:nlFkj7bTysH6VkC4fGphtjXRbezREPgrHuJG20hBGPE= software.sslmate.com/src/go-pkcs12 v0.2.0/go.mod h1:23rNcYsMabIc1otwLpTkCCPwUq6kQsTyowttG/as0kQ= -tailscale.com v1.48.1 h1:xHpPXMiCdibpC8UuXiKZpKIWjoRB+TC4CtVLDupu5wA= -tailscale.com v1.48.1/go.mod h1:RWW4emjviEEAIqr6P6bbZZGXr19BdAdtwtUVfW9SBvU= +tailscale.com v1.48.2 h1:bLAzGvkFMih+QCRy//WSemmD7KKXIo0m8P+o1Fp/YjA= +tailscale.com v1.48.2/go.mod h1:RWW4emjviEEAIqr6P6bbZZGXr19BdAdtwtUVfW9SBvU= diff --git a/vendor/modules.txt b/vendor/modules.txt index f8572ca..2d35404 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -552,7 +552,7 @@ nhooyr.io/websocket/internal/bpool nhooyr.io/websocket/internal/errd nhooyr.io/websocket/internal/wsjs nhooyr.io/websocket/internal/xsync -# tailscale.com v1.48.1 +# tailscale.com v1.48.2 ## explicit; go 1.21 tailscale.com tailscale.com/atomicfile diff --git a/vendor/tailscale.com/VERSION.txt b/vendor/tailscale.com/VERSION.txt index 5525f03..4b12da2 100644 --- a/vendor/tailscale.com/VERSION.txt +++ b/vendor/tailscale.com/VERSION.txt @@ -1 +1 @@ -1.48.1 +1.48.2 diff --git a/vendor/tailscale.com/wgengine/magicsock/derp.go b/vendor/tailscale.com/wgengine/magicsock/derp.go index 23ae90a..076507c 100644 --- a/vendor/tailscale.com/wgengine/magicsock/derp.go +++ b/vendor/tailscale.com/wgengine/magicsock/derp.go @@ -665,7 +665,7 @@ func (c *Conn) processDERPReadResult(dm derpReadResult, b []byte) (n int, ep *en return 0, nil } - ep.noteRecvActivity() + ep.noteRecvActivity(ipp) if stats := c.stats.Load(); stats != nil { stats.UpdateRxPhysical(ep.nodeAddr, ipp, dm.n) } diff --git a/vendor/tailscale.com/wgengine/magicsock/endpoint.go b/vendor/tailscale.com/wgengine/magicsock/endpoint.go index 6c1cc0c..d78366f 100644 --- a/vendor/tailscale.com/wgengine/magicsock/endpoint.go +++ b/vendor/tailscale.com/wgengine/magicsock/endpoint.go @@ -61,7 +61,7 @@ type endpoint struct { heartBeatTimer *time.Timer // nil when idle lastSend mono.Time // last time there was outgoing packets sent to this peer (from wireguard-go) - lastFullPing mono.Time // last time we pinged all disco endpoints + lastFullPing mono.Time // last time we pinged all disco or wireguard only endpoints derpAddr netip.AddrPort // fallback/bootstrap path, if non-zero (non-zero for well-behaved clients) bestAddr addrLatency // best non-DERP path; zero if none @@ -132,6 +132,14 @@ type endpointState struct { index int16 // index in nodecfg.Node.Endpoints; meaningless if lastGotPing non-zero } +// clear removes all derived / probed state from an endpointState. +func (s *endpointState) clear() { + *s = endpointState{ + index: s.index, + lastGotPing: s.lastGotPing, + } +} + // pongHistoryCount is how many pongReply values we keep per endpointState const pongHistoryCount = 64 @@ -220,14 +228,26 @@ func (de *endpoint) initFakeUDPAddr() { // noteRecvActivity records receive activity on de, and invokes // Conn.noteRecvActivity no more than once every 10s. -func (de *endpoint) noteRecvActivity() { - if de.c.noteRecvActivity == nil { - return - } +func (de *endpoint) noteRecvActivity(ipp netip.AddrPort) { now := mono.Now() + + // TODO(raggi): this probably applies relatively equally well to disco + // managed endpoints, but that would be a less conservative change. + if de.isWireguardOnly { + de.mu.Lock() + de.bestAddr.AddrPort = ipp + de.bestAddrAt = now + de.trustBestAddrUntil = now.Add(5 * time.Second) + de.mu.Unlock() + } + elapsed := now.Sub(de.lastRecv.LoadAtomic()) if elapsed > 10*time.Second { de.lastRecv.StoreAtomic(now) + + if de.c.noteRecvActivity == nil { + return + } de.c.noteRecvActivity(de.publicKey) } } @@ -289,11 +309,23 @@ func (de *endpoint) addrForSendLocked(now mono.Time) (udpAddr, derpAddr netip.Ad // // de.mu must be held. func (de *endpoint) addrForWireGuardSendLocked(now mono.Time) (udpAddr netip.AddrPort, shouldPing bool) { + if len(de.endpointState) == 0 { + de.c.logf("magicsock: addrForSendWireguardLocked: [unexpected] no candidates available for endpoint") + return udpAddr, false + } + // lowestLatency is a high duration initially, so we // can be sure we're going to have a duration lower than this // for the first latency retrieved. lowestLatency := time.Hour + var oldestPing mono.Time for ipp, state := range de.endpointState { + if oldestPing.IsZero() { + oldestPing = state.lastPing + } else if state.lastPing.Before(oldestPing) { + oldestPing = state.lastPing + } + if latency, ok := state.latencyLocked(); ok { if latency < lowestLatency || latency == lowestLatency && ipp.Addr().Is6() { // If we have the same latency,IPv6 is prioritized. @@ -304,35 +336,25 @@ func (de *endpoint) addrForWireGuardSendLocked(now mono.Time) (udpAddr netip.Add } } } + needPing := len(de.endpointState) > 1 && now.Sub(oldestPing) > wireguardPingInterval - if udpAddr.IsValid() { - // Set trustBestAddrUntil to an hour, so we will - // continue to use this address for a long period of time. - de.bestAddr.AddrPort = udpAddr - de.trustBestAddrUntil = now.Add(1 * time.Hour) - return udpAddr, false - } + if !udpAddr.IsValid() { + candidates := maps.Keys(de.endpointState) - candidates := maps.Keys(de.endpointState) - if len(candidates) == 0 { - de.c.logf("magicsock: addrForSendWireguardLocked: [unexpected] no candidates available for endpoint") - return udpAddr, false + // Randomly select an address to use until we retrieve latency information + // and give it a short trustBestAddrUntil time so we avoid flapping between + // addresses while waiting on latency information to be populated. + udpAddr = candidates[rand.Intn(len(candidates))] } - // Randomly select an address to use until we retrieve latency information - // and give it a short trustBestAddrUntil time so we avoid flapping between - // addresses while waiting on latency information to be populated. - udpAddr = candidates[rand.Intn(len(candidates))] de.bestAddr.AddrPort = udpAddr - if len(candidates) == 1 { - // if we only have one address that we can send data too, - // we should trust it for a longer period of time. - de.trustBestAddrUntil = now.Add(1 * time.Hour) - } else { - de.trustBestAddrUntil = now.Add(15 * time.Second) - } - - return udpAddr, len(candidates) > 1 + // Only extend trustBestAddrUntil by one second to avoid packet + // reordering and/or CPU usage from random selection during the first + // second. We should receive a response due to a WireGuard handshake in + // less than one second in good cases, in which case this will be then + // extended to 15 seconds. + de.trustBestAddrUntil = now.Add(time.Second) + return udpAddr, needPing } // heartbeat is called every heartbeatInterval to keep the best UDP path alive, @@ -467,6 +489,14 @@ func (de *endpoint) send(buffs [][]byte) error { var err error if udpAddr.IsValid() { _, err = de.c.sendUDPBatch(udpAddr, buffs) + + // If the error is known to indicate that the endpoint is no longer + // usable, clear the endpoint statistics so that the next send will + // re-evaluate the best endpoint. + if err != nil && isBadEndpointErr(err) { + de.noteBadEndpoint(udpAddr) + } + // TODO(raggi): needs updating for accuracy, as in error conditions we may have partial sends. if stats := de.c.stats.Load(); err == nil && stats != nil { var txBytes int @@ -858,6 +888,30 @@ func (de *endpoint) addCandidateEndpoint(ep netip.AddrPort, forRxPingTxID stun.T return false } +// clearBestAddrLocked clears the bestAddr and related fields such that future +// packets will re-evaluate the best address to send to next. +// +// de.mu must be held. +func (de *endpoint) clearBestAddrLocked() { + de.bestAddr = addrLatency{} + de.bestAddrAt = 0 + de.trustBestAddrUntil = 0 +} + +// noteBadEndpoint marks ipp as a bad endpoint that would need to be +// re-evaluated before future use, this should be called for example if a send +// to ipp fails due to a host unreachable error or similar. +func (de *endpoint) noteBadEndpoint(ipp netip.AddrPort) { + de.mu.Lock() + defer de.mu.Unlock() + + de.clearBestAddrLocked() + + if st, ok := de.endpointState[ipp]; ok { + st.clear() + } +} + // noteConnectivityChange is called when connectivity changes enough // that we should question our earlier assumptions about which paths // work. @@ -865,7 +919,11 @@ func (de *endpoint) noteConnectivityChange() { de.mu.Lock() defer de.mu.Unlock() - de.trustBestAddrUntil = 0 + de.clearBestAddrLocked() + + for k := range de.endpointState { + de.endpointState[k].clear() + } } // handlePongConnLocked handles a Pong message (a reply to an earlier ping). @@ -1142,9 +1200,7 @@ func (de *endpoint) stopAndReset() { func (de *endpoint) resetLocked() { de.lastSend = 0 de.lastFullPing = 0 - de.bestAddr = addrLatency{} - de.bestAddrAt = 0 - de.trustBestAddrUntil = 0 + de.clearBestAddrLocked() for _, es := range de.endpointState { es.lastPing = 0 } diff --git a/vendor/tailscale.com/wgengine/magicsock/endpoint_default.go b/vendor/tailscale.com/wgengine/magicsock/endpoint_default.go new file mode 100644 index 0000000..fc30378 --- /dev/null +++ b/vendor/tailscale.com/wgengine/magicsock/endpoint_default.go @@ -0,0 +1,23 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +//go:build !js && !wasm +// +build !js,!wasm + +package magicsock + +import ( + "errors" + "syscall" +) + +// errHOSTUNREACH wraps unix.EHOSTUNREACH in an interface type to pass to +// errors.Is while avoiding an allocation per call. +var errHOSTUNREACH error = syscall.EHOSTUNREACH + +// isBadEndpointErr checks if err is one which is known to report that an +// endpoint can no longer be sent to. It is not exhaustive, and for unknown +// errors always reports false. +func isBadEndpointErr(err error) bool { + return errors.Is(err, errHOSTUNREACH) +} diff --git a/vendor/tailscale.com/wgengine/magicsock/endpoint_js.go b/vendor/tailscale.com/wgengine/magicsock/endpoint_js.go new file mode 100644 index 0000000..005571e --- /dev/null +++ b/vendor/tailscale.com/wgengine/magicsock/endpoint_js.go @@ -0,0 +1,14 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +//go:build js || wasm +// +build js wasm + +package magicsock + +// isBadEndpointErr checks if err is one which is known to report that an +// endpoint can no longer be sent to. It is not exhaustive, but covers known +// cases. +func isBadEndpointErr(err error) bool { + return false +} diff --git a/vendor/tailscale.com/wgengine/magicsock/magicsock.go b/vendor/tailscale.com/wgengine/magicsock/magicsock.go index 33f2015..8fdca4c 100644 --- a/vendor/tailscale.com/wgengine/magicsock/magicsock.go +++ b/vendor/tailscale.com/wgengine/magicsock/magicsock.go @@ -1188,7 +1188,7 @@ func (c *Conn) receiveIP(b []byte, ipp netip.AddrPort, cache *ippEndpointCache) cache.gen = de.numStopAndReset() ep = de } - ep.noteRecvActivity() + ep.noteRecvActivity(ipp) if stats := c.stats.Load(); stats != nil { stats.UpdateRxPhysical(ep.nodeAddr, ipp, len(b)) } @@ -2607,6 +2607,11 @@ var ( // resetting the counter, as the first pings likely didn't through // the firewall) discoPingInterval = 5 * time.Second + + // wireguardPingInterval is the minimum time between pings to an endpoint. + // Pings are only sent if we have not observed bidirectional traffic with an + // endpoint in at least this duration. + wireguardPingInterval = 5 * time.Second ) // indexSentinelDeleted is the temporary value that endpointState.index takes while