Update
This commit is contained in:
1 parent
3530afb854
commit
6903fcff27
2146 files changed
+112256
-190703
No files matched your search
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package magicsock
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package magicsock
|
||||
@@ -42,7 +42,7 @@ func (c *Conn) ServeHTTPDebug(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "<h2 id=derp><a href=#derp>#</a> DERP</h2><ul>")
|
||||
if c.derpMap != nil {
|
||||
type D struct {
|
||||
regionID int
|
||||
regionID tailcfg.DERPRegionID
|
||||
lastWrite time.Time
|
||||
createTime time.Time
|
||||
}
|
||||
@@ -108,8 +108,8 @@ func (c *Conn) ServeHTTPDebug(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
sort.Slice(ent, func(i, j int) bool { return ent[i].pub.Less(ent[j].pub) })
|
||||
|
||||
peers := map[key.NodePublic]tailcfg.NodeView{}
|
||||
for _, p := range c.peers.All() {
|
||||
peers := make(map[key.NodePublic]tailcfg.NodeView, len(c.peersByID))
|
||||
for _, p := range c.peersByID {
|
||||
peers[p.Key()] = p
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ios && !js
|
||||
@@ -62,7 +62,8 @@ var (
|
||||
//
|
||||
//lint:ignore U1000 used on Linux/Darwin only
|
||||
debugPMTUD = envknob.RegisterBool("TS_DEBUG_PMTUD")
|
||||
// debugNeverDirectUDP disables the use of direct UDP connections, forcing
|
||||
// debugNeverDirectUDP disables the use of direct UDP connections by
|
||||
// suppressing/dropping inbound/outbound [disco.Ping] messages, forcing
|
||||
// all peer communication over DERP or peer relay.
|
||||
debugNeverDirectUDP = envknob.RegisterBool("TS_DEBUG_NEVER_DIRECT_UDP")
|
||||
// Hey you! Adding a new debugknob? Make sure to stub it out in the
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ios || js
|
||||
|
||||
+105
-35
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package magicsock
|
||||
@@ -6,6 +6,7 @@ package magicsock
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net"
|
||||
@@ -44,12 +45,12 @@ const frameReceiveRecordRate = 5 * time.Second
|
||||
// current connection for that regionID is dc. (but dc should not be
|
||||
// used to write directly; it's owned by the read/write loops)
|
||||
type derpRoute struct {
|
||||
regionID int
|
||||
regionID tailcfg.DERPRegionID
|
||||
dc *derphttp.Client // don't use directly; see comment above
|
||||
}
|
||||
|
||||
// removeDerpPeerRoute removes a DERP route entry previously added by addDerpPeerRoute.
|
||||
func (c *Conn) removeDerpPeerRoute(peer key.NodePublic, regionID int, dc *derphttp.Client) {
|
||||
func (c *Conn) removeDerpPeerRoute(peer key.NodePublic, regionID tailcfg.DERPRegionID, dc *derphttp.Client) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
r2 := derpRoute{regionID, dc}
|
||||
@@ -61,7 +62,7 @@ func (c *Conn) removeDerpPeerRoute(peer key.NodePublic, regionID int, dc *derpht
|
||||
// addDerpPeerRoute adds a DERP route entry, noting that peer was seen
|
||||
// on DERP node derpID, at least on the connection identified by dc.
|
||||
// See issue 150 for details.
|
||||
func (c *Conn) addDerpPeerRoute(peer key.NodePublic, regionID int, dc *derphttp.Client) {
|
||||
func (c *Conn) addDerpPeerRoute(peer key.NodePublic, regionID tailcfg.DERPRegionID, dc *derphttp.Client) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
mak.Set(&c.derpRoute, peer, derpRoute{regionID, dc})
|
||||
@@ -78,7 +79,7 @@ func (c *Conn) addDerpPeerRoute(peer key.NodePublic, regionID int, dc *derphttp.
|
||||
// first connection.
|
||||
//
|
||||
// This can also help nodes from a slow or misbehaving control plane.
|
||||
func (c *Conn) fallbackDERPRegionForPeer(peer key.NodePublic) (regionID int) {
|
||||
func (c *Conn) fallbackDERPRegionForPeer(peer key.NodePublic) (regionID tailcfg.DERPRegionID) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if dr, ok := c.derpRoute[peer]; ok {
|
||||
@@ -100,7 +101,8 @@ type activeDerp struct {
|
||||
}
|
||||
|
||||
var (
|
||||
pickDERPFallbackForTests func() int
|
||||
pickDERPFallbackForTests func() tailcfg.DERPRegionID
|
||||
reSTUNHookForTests func(why string)
|
||||
)
|
||||
|
||||
// pickDERPFallback returns a non-zero but deterministic DERP node to
|
||||
@@ -109,7 +111,7 @@ var (
|
||||
// checks aren't working).
|
||||
//
|
||||
// c.mu must NOT be held.
|
||||
func (c *Conn) pickDERPFallback() int {
|
||||
func (c *Conn) pickDERPFallback() tailcfg.DERPRegionID {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
@@ -154,7 +156,7 @@ var checkControlHealthDuringNearestDERPInTests = false
|
||||
// region that it selected and set (via setNearestDERP).
|
||||
//
|
||||
// c.mu must NOT be held.
|
||||
func (c *Conn) maybeSetNearestDERP(report *netcheck.Report) (preferredDERP int) {
|
||||
func (c *Conn) maybeSetNearestDERP(report *netcheck.Report, force bool) (preferredDERP tailcfg.DERPRegionID) {
|
||||
// Don't change our PreferredDERP if we don't have a connection to
|
||||
// control; if we don't, then we can't inform peers about a DERP home
|
||||
// change, which breaks all connectivity. Even if this DERP region is
|
||||
@@ -168,7 +170,10 @@ func (c *Conn) maybeSetNearestDERP(report *netcheck.Report) (preferredDERP int)
|
||||
//
|
||||
// Despite the above behaviour, ensure that we set the nearest DERP if
|
||||
// we don't currently have one set; any DERP server is better than
|
||||
// none, even if not connected to control.
|
||||
// none, even if not connected to control. The exception here is if we have
|
||||
// a cached netmap with a previous DERP server. Retaining the previous DERP
|
||||
// makes it easier for other nodes to find each other when control is not
|
||||
// available.
|
||||
var connectedToControl bool
|
||||
if testenv.InTest() && !checkControlHealthDuringNearestDERPInTests {
|
||||
connectedToControl = true
|
||||
@@ -178,7 +183,7 @@ func (c *Conn) maybeSetNearestDERP(report *netcheck.Report) (preferredDERP int)
|
||||
c.mu.Lock()
|
||||
myDerp := c.myDerp
|
||||
c.mu.Unlock()
|
||||
if !connectedToControl {
|
||||
if !connectedToControl && !force {
|
||||
if myDerp != 0 {
|
||||
metricDERPHomeNoChangeNoControl.Add(1)
|
||||
return myDerp
|
||||
@@ -197,16 +202,33 @@ func (c *Conn) maybeSetNearestDERP(report *netcheck.Report) (preferredDERP int)
|
||||
}
|
||||
if preferredDERP != myDerp {
|
||||
c.logf(
|
||||
"magicsock: home DERP changing from derp-%d [%dms] to derp-%d [%dms]",
|
||||
c.myDerp, report.RegionLatency[myDerp].Milliseconds(), preferredDERP, report.RegionLatency[preferredDERP].Milliseconds())
|
||||
"magicsock: home DERP changing from derp-%d [%dms] to derp-%d [%dms] (forced=%t)",
|
||||
c.myDerp, report.RegionLatency[myDerp].Milliseconds(), preferredDERP, report.RegionLatency[preferredDERP].Milliseconds(), force)
|
||||
}
|
||||
if !c.setNearestDERP(preferredDERP) {
|
||||
preferredDERP = 0
|
||||
} else if preferredDERP != myDerp {
|
||||
c.homeDERPChangedPub.Publish(HomeDERPChanged{Old: myDerp, New: preferredDERP})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Conn) derpRegionCodeLocked(regionID int) string {
|
||||
// HomeDERPChanged is an event sent on the [eventbus.Bus] when a new home DERP
|
||||
// server has been selected. Its publisher is [magicsock.Coon]; its main
|
||||
// subscriber is [ipnlocal.LocalBackend] that updates the homeDERP used by the
|
||||
// netmap cache.
|
||||
// TODO(cmol): Move the subscriber to not inject into localBackend, but rather
|
||||
// into the netmap at the controlClient mapSession level once there is a stable
|
||||
// abstraction to use.
|
||||
type HomeDERPChanged struct {
|
||||
Old, New tailcfg.DERPRegionID
|
||||
}
|
||||
|
||||
func (c *Conn) ForceSetNearestDERP(regionID tailcfg.DERPRegionID) tailcfg.DERPRegionID {
|
||||
return c.maybeSetNearestDERP(&netcheck.Report{PreferredDERP: regionID}, true)
|
||||
}
|
||||
|
||||
func (c *Conn) derpRegionCodeLocked(regionID tailcfg.DERPRegionID) string {
|
||||
if c.derpMap == nil {
|
||||
return ""
|
||||
}
|
||||
@@ -219,14 +241,14 @@ func (c *Conn) derpRegionCodeLocked(regionID int) string {
|
||||
// setHomeDERPGaugeLocked updates the home DERP gauge metric.
|
||||
//
|
||||
// c.mu must be held.
|
||||
func (c *Conn) setHomeDERPGaugeLocked(derpNum int) {
|
||||
func (c *Conn) setHomeDERPGaugeLocked(derpNum tailcfg.DERPRegionID) {
|
||||
if c.homeDERPGauge != nil {
|
||||
c.homeDERPGauge.Set(float64(derpNum))
|
||||
}
|
||||
}
|
||||
|
||||
// c.mu must NOT be held.
|
||||
func (c *Conn) setNearestDERP(derpNum int) (wantDERP bool) {
|
||||
func (c *Conn) setNearestDERP(derpNum tailcfg.DERPRegionID) (wantDERP bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if !c.wantDerpLocked() {
|
||||
@@ -284,7 +306,7 @@ func (c *Conn) startDerpHomeConnectLocked() {
|
||||
// DERP region ID.
|
||||
//
|
||||
// c.mu may be held, but does not need to be.
|
||||
func (c *Conn) goDerpConnect(regionID int) {
|
||||
func (c *Conn) goDerpConnect(regionID tailcfg.DERPRegionID) {
|
||||
if regionID == 0 {
|
||||
return
|
||||
}
|
||||
@@ -314,7 +336,7 @@ const derpWriteQueueDepth = 32
|
||||
//
|
||||
// It returns nil if the network is down, the Conn is closed, or the regionID is
|
||||
// not known.
|
||||
func (c *Conn) derpWriteChanForRegion(regionID int, peer key.NodePublic) chan derpWriteRequest {
|
||||
func (c *Conn) derpWriteChanForRegion(regionID tailcfg.DERPRegionID, peer key.NodePublic) chan derpWriteRequest {
|
||||
if c.networkDown() {
|
||||
return nil
|
||||
}
|
||||
@@ -368,8 +390,8 @@ func (c *Conn) derpWriteChanForRegion(regionID int, peer key.NodePublic) chan de
|
||||
firstDerp := false
|
||||
if c.activeDerp == nil {
|
||||
firstDerp = true
|
||||
c.activeDerp = make(map[int]activeDerp)
|
||||
c.prevDerp = make(map[int]*syncs.WaitGroupChan)
|
||||
c.activeDerp = make(map[tailcfg.DERPRegionID]activeDerp)
|
||||
c.prevDerp = make(map[tailcfg.DERPRegionID]*syncs.WaitGroupChan)
|
||||
}
|
||||
|
||||
// Note that derphttp.NewRegionClient does not dial the server
|
||||
@@ -392,6 +414,10 @@ func (c *Conn) derpWriteChanForRegion(regionID int, peer key.NodePublic) chan de
|
||||
return derpMap.Regions[regionID]
|
||||
})
|
||||
dc.HealthTracker = c.health
|
||||
dc.AppName = c.derpAppName
|
||||
if c.extraRootCAs != nil {
|
||||
dc.TLSConfig = &tls.Config{RootCAs: c.extraRootCAs}
|
||||
}
|
||||
|
||||
dc.SetCanAckPings(true)
|
||||
dc.NotePreferred(c.myDerp == regionID)
|
||||
@@ -436,7 +462,14 @@ func (c *Conn) derpWriteChanForRegion(regionID int, peer key.NodePublic) chan de
|
||||
|
||||
go c.runDerpReader(ctx, regionID, dc, wg, startGate)
|
||||
go c.runDerpWriter(ctx, dc, ch, wg, startGate)
|
||||
go c.derpActiveFunc()
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-startGate:
|
||||
c.derpActiveFunc()
|
||||
}
|
||||
}()
|
||||
|
||||
return ad.writeCh
|
||||
}
|
||||
@@ -448,7 +481,7 @@ func (c *Conn) derpWriteChanForRegion(regionID int, peer key.NodePublic) chan de
|
||||
// If there's any change, it logs.
|
||||
//
|
||||
// c.mu must be held.
|
||||
func (c *Conn) setPeerLastDerpLocked(peer key.NodePublic, regionID, homeID int) {
|
||||
func (c *Conn) setPeerLastDerpLocked(peer key.NodePublic, regionID, homeID tailcfg.DERPRegionID) {
|
||||
if peer.IsZero() {
|
||||
return
|
||||
}
|
||||
@@ -484,7 +517,7 @@ func (c *Conn) setPeerLastDerpLocked(peer key.NodePublic, regionID, homeID int)
|
||||
// get at the packet contents they need to call copyBuf to copy it
|
||||
// out, which also releases the buffer.
|
||||
type derpReadResult struct {
|
||||
regionID int
|
||||
regionID tailcfg.DERPRegionID
|
||||
n int // length of data received
|
||||
src key.NodePublic
|
||||
// copyBuf is called to copy the data to dst. It returns how
|
||||
@@ -497,7 +530,7 @@ type derpReadResult struct {
|
||||
|
||||
// runDerpReader runs in a goroutine for the life of a DERP
|
||||
// connection, handling received packets.
|
||||
func (c *Conn) runDerpReader(ctx context.Context, regionID int, dc *derphttp.Client, wg *syncs.WaitGroupChan, startGate <-chan struct{}) {
|
||||
func (c *Conn) runDerpReader(ctx context.Context, regionID tailcfg.DERPRegionID, dc *derphttp.Client, wg *syncs.WaitGroupChan, startGate <-chan struct{}) {
|
||||
defer wg.Decr()
|
||||
defer dc.Close()
|
||||
|
||||
@@ -675,7 +708,7 @@ func (c *Conn) runDerpWriter(ctx context.Context, dc *derphttp.Client, ch <-chan
|
||||
}
|
||||
}
|
||||
|
||||
func (c *connBind) receiveDERP(buffs [][]byte, sizes []int, eps []conn.Endpoint) (int, error) {
|
||||
func (c *connBind) receiveDERP(slab []byte, packets []conn.ReceivedPacket) (int, error) {
|
||||
if s := c.Conn.health.ReceiveFuncStats(health.ReceiveDERP); s != nil {
|
||||
s.Enter()
|
||||
defer s.Exit()
|
||||
@@ -685,13 +718,14 @@ func (c *connBind) receiveDERP(buffs [][]byte, sizes []int, eps []conn.Endpoint)
|
||||
if c.isClosed() {
|
||||
break
|
||||
}
|
||||
n, ep := c.processDERPReadResult(dm, buffs[0])
|
||||
n, ep := c.processDERPReadResult(dm, slab)
|
||||
if n == 0 {
|
||||
// No data read occurred. Wait for another packet.
|
||||
continue
|
||||
}
|
||||
sizes[0] = n
|
||||
eps[0] = ep
|
||||
packets[0].Size = n
|
||||
packets[0].Offset = 0
|
||||
packets[0].Endpoint = ep
|
||||
return 1, nil
|
||||
}
|
||||
return 0, net.ErrClosed
|
||||
@@ -701,7 +735,7 @@ func (c *Conn) processDERPReadResult(dm derpReadResult, b []byte) (n int, ep *en
|
||||
if dm.copyBuf == nil {
|
||||
return 0, nil
|
||||
}
|
||||
var regionID int
|
||||
var regionID tailcfg.DERPRegionID
|
||||
n, regionID = dm.n, dm.regionID
|
||||
ncopy := dm.copyBuf(b)
|
||||
if ncopy != n {
|
||||
@@ -718,6 +752,10 @@ func (c *Conn) processDERPReadResult(dm derpReadResult, b []byte) (n int, ep *en
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if c.onDERPRecv != nil && c.onDERPRecv(regionID, dm.src, b[:n]) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var ok bool
|
||||
c.mu.Lock()
|
||||
ep, ok = c.peerMap.endpointForNodeKey(dm.src)
|
||||
@@ -738,6 +776,15 @@ func (c *Conn) processDERPReadResult(dm derpReadResult, b []byte) (n int, ep *en
|
||||
return n, ep
|
||||
}
|
||||
|
||||
// SendDERPPacketTo sends an arbitrary packet to the given node key via
|
||||
// the DERP relay for the given region. It creates the DERP connection
|
||||
// to the region if one doesn't already exist.
|
||||
func (c *Conn) SendDERPPacketTo(dstKey key.NodePublic, regionID tailcfg.DERPRegionID, pkt []byte) (sent bool, err error) {
|
||||
return c.sendAddr(
|
||||
netip.AddrPortFrom(tailcfg.DerpMagicIPAddr, uint16(regionID)),
|
||||
dstKey, pkt, false, false)
|
||||
}
|
||||
|
||||
// SetOnlyTCP443 set whether the magicsock connection is restricted
|
||||
// to only using TCP port 443 outbound. If true, no UDP is allowed,
|
||||
// no STUN checks are performend, etc.
|
||||
@@ -747,7 +794,24 @@ func (c *Conn) SetOnlyTCP443(v bool) {
|
||||
|
||||
// SetDERPMap controls which (if any) DERP servers are used.
|
||||
// A nil value means to disable DERP; it's disabled by default.
|
||||
//
|
||||
// SetDERPMap triggers a ReSTUN after updating the map. Callers that want to
|
||||
// set the map without triggering a ReSTUN should use [Conn.SetDERPMapWithoutReSTUN]
|
||||
// instead.
|
||||
func (c *Conn) SetDERPMap(dm *tailcfg.DERPMap) {
|
||||
c.setDERPMap(dm, true)
|
||||
}
|
||||
|
||||
// SetDERPMapWithoutReSTUN is like [Conn.SetDERPMap] but does not trigger a
|
||||
// ReSTUN after updating the map.
|
||||
//
|
||||
// It is used for setting the map from a cache, so the homeDERP can be set
|
||||
// from cache before any STUN happens.
|
||||
func (c *Conn) SetDERPMapWithoutReSTUN(dm *tailcfg.DERPMap) {
|
||||
c.setDERPMap(dm, false)
|
||||
}
|
||||
|
||||
func (c *Conn) setDERPMap(dm *tailcfg.DERPMap, doReStun bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
@@ -760,7 +824,7 @@ func (c *Conn) SetDERPMap(dm *tailcfg.DERPMap) {
|
||||
}
|
||||
dm = &tailcfg.DERPMap{
|
||||
OmitDefaultRegions: true,
|
||||
Regions: map[int]*tailcfg.DERPRegion{
|
||||
Regions: map[tailcfg.DERPRegionID]*tailcfg.DERPRegion{
|
||||
999: {
|
||||
RegionID: 999,
|
||||
Nodes: []*tailcfg.DERPNode{{
|
||||
@@ -804,8 +868,14 @@ func (c *Conn) SetDERPMap(dm *tailcfg.DERPMap) {
|
||||
}
|
||||
}
|
||||
|
||||
go c.ReSTUN("derp-map-update")
|
||||
if doReStun {
|
||||
if reSTUNHookForTests != nil {
|
||||
reSTUNHookForTests("derp-map-update")
|
||||
}
|
||||
go c.ReSTUN("derp-map-update")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) wantDerpLocked() bool { return c.derpMap != nil }
|
||||
|
||||
// c.mu must be held.
|
||||
@@ -871,7 +941,7 @@ func (c *Conn) maybeCloseDERPsOnRebind(okayLocalIPs []netip.Prefix) {
|
||||
// why is a reason for logging.
|
||||
//
|
||||
// c.mu must be held.
|
||||
func (c *Conn) closeOrReconnectDERPLocked(regionID int, why string) {
|
||||
func (c *Conn) closeOrReconnectDERPLocked(regionID tailcfg.DERPRegionID, why string) {
|
||||
c.closeDerpLocked(regionID, why)
|
||||
if !c.privateKey.IsZero() && c.myDerp == regionID {
|
||||
c.startDerpHomeConnectLocked()
|
||||
@@ -880,7 +950,7 @@ func (c *Conn) closeOrReconnectDERPLocked(regionID int, why string) {
|
||||
|
||||
// c.mu must be held.
|
||||
// It is the responsibility of the caller to call logActiveDerpLocked after any set of closes.
|
||||
func (c *Conn) closeDerpLocked(regionID int, why string) {
|
||||
func (c *Conn) closeDerpLocked(regionID tailcfg.DERPRegionID, why string) {
|
||||
if ad, ok := c.activeDerp[regionID]; ok {
|
||||
c.logf("magicsock: closing connection to derp-%v (%v), age %v", regionID, why, time.Since(ad.createTime).Round(time.Second))
|
||||
go ad.c.Close()
|
||||
@@ -898,14 +968,14 @@ func (c *Conn) logActiveDerpLocked() {
|
||||
return
|
||||
}
|
||||
buf.WriteString(":")
|
||||
c.foreachActiveDerpSortedLocked(func(node int, ad activeDerp) {
|
||||
c.foreachActiveDerpSortedLocked(func(node tailcfg.DERPRegionID, ad activeDerp) {
|
||||
fmt.Fprintf(buf, " derp-%d=cr%v,wr%v", node, simpleDur(now.Sub(ad.createTime)), simpleDur(now.Sub(*ad.lastWrite)))
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
// c.mu must be held.
|
||||
func (c *Conn) foreachActiveDerpSortedLocked(fn func(regionID int, ad activeDerp)) {
|
||||
func (c *Conn) foreachActiveDerpSortedLocked(fn func(regionID tailcfg.DERPRegionID, ad activeDerp)) {
|
||||
if len(c.activeDerp) < 2 {
|
||||
for id, ad := range c.activeDerp {
|
||||
fn(id, ad)
|
||||
@@ -971,7 +1041,7 @@ func (c *Conn) DERPs() int {
|
||||
return len(c.activeDerp)
|
||||
}
|
||||
|
||||
func (c *Conn) derpRegionCodeOfIDLocked(regionID int) string {
|
||||
func (c *Conn) derpRegionCodeOfIDLocked(regionID tailcfg.DERPRegionID) string {
|
||||
if c.derpMap == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package magicsock
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by "stringer -type=discoPingPurpose -trimprefix=ping"; DO NOT EDIT.
|
||||
|
||||
+304
-65
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package magicsock
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"golang.org/x/net/ipv4"
|
||||
"golang.org/x/net/ipv6"
|
||||
"tailscale.com/disco"
|
||||
"tailscale.com/feature/buildfeatures"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/net/stun"
|
||||
@@ -30,6 +31,7 @@ import (
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstime/mono"
|
||||
"tailscale.com/types/bools"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/mak"
|
||||
@@ -70,7 +72,12 @@ type endpoint struct {
|
||||
fakeWGAddr netip.AddrPort // the UDP address we tell wireguard-go we're using
|
||||
nodeAddr netip.Addr // the node's first tailscale address; used for logging & wireguard rate-limiting (Issue 6686)
|
||||
|
||||
disco atomic.Pointer[endpointDisco] // if the peer supports disco, the key and short string
|
||||
// disco is non-nil if the peer supports disco, and in that case holds
|
||||
// the peer's disco key and short string.
|
||||
// It is safe to load and read the contents of the value, but the caller
|
||||
// must not store directly into this field. Instead, use
|
||||
// [updateDiscoKey] and [updateTSMPDiscoKey].
|
||||
disco atomic.Pointer[endpointDisco]
|
||||
|
||||
// mu protects all following fields.
|
||||
mu syncs.Mutex // Lock ordering: Conn.mu, then endpoint.mu
|
||||
@@ -96,7 +103,7 @@ type endpoint struct {
|
||||
probeUDPLifetime *probeUDPLifetime // UDP path lifetime probing; nil if disabled
|
||||
|
||||
expired bool // whether the node has expired
|
||||
isWireguardOnly bool // whether the endpoint is WireGuard only
|
||||
isWireguardOnly bool // whether the endpoint is WireGuard only. Must not be changed after initializing the endpont.
|
||||
relayCapable bool // whether the node is capable of speaking via a [tailscale.com/net/udprelay.Server]
|
||||
}
|
||||
|
||||
@@ -132,6 +139,20 @@ func (de *endpoint) udpRelayEndpointReady(maybeBest addrQuality) {
|
||||
func (de *endpoint) setBestAddrLocked(v addrQuality) {
|
||||
if v.epAddr != de.bestAddr.epAddr {
|
||||
de.probeUDPLifetime.resetCycleEndpointLocked()
|
||||
|
||||
// Reaching here, if we are upgrading from an invalid (missing) address
|
||||
// to a valid one, record metrics:
|
||||
if !de.bestAddr.ap.IsValid() && v.ap.IsValid() {
|
||||
// If we are using data from a cached netmap, increment the counter for peers established.
|
||||
isCached := de.c.usingCachedNetmap.Load()
|
||||
if isCached {
|
||||
metricCachedPeerContactDirect.Add(1)
|
||||
}
|
||||
// Regardless whether the netmap is cached, record how long it has
|
||||
// been since the endpoint was initialized.
|
||||
de.c.logf("magicsock: new contact: peer=%s usec=%d cached=%v via=direct",
|
||||
de.publicKey.ShortString(), int64(mono.Since(de.c.initializedAt)/time.Microsecond), isCached)
|
||||
}
|
||||
}
|
||||
de.bestAddr = v
|
||||
}
|
||||
@@ -357,11 +378,64 @@ func (de *endpoint) setProbeUDPLifetimeConfigLocked(desired *ProbeUDPLifetimeCon
|
||||
p.resetCycleEndpointLocked()
|
||||
}
|
||||
|
||||
// endpointDisco is the current disco key and short string for an endpoint. This
|
||||
// structure is immutable.
|
||||
// endpointDisco is the current disco key and short string for an endpoint for
|
||||
// keys learned both from controlClient and via TSMP. Only one key is active at
|
||||
// a time for sending.
|
||||
//
|
||||
// This structure is immutable.
|
||||
type endpointDisco struct {
|
||||
key key.DiscoPublic // for discovery messages.
|
||||
short string // ShortString of discoKey.
|
||||
controlKey key.DiscoPublic // key learned via control for disco messages.
|
||||
tsmpKey key.DiscoPublic // key learned via TSMP for disco messages.
|
||||
controlShort string // ShortString of control learned key.
|
||||
tsmpShort string // ShortString of TSMP learned key.
|
||||
tsmpActive bool
|
||||
}
|
||||
|
||||
// key returns the disco key currently regarded as active or a zero key if
|
||||
// endpointDisco is nil.
|
||||
func (e *endpointDisco) key() key.DiscoPublic {
|
||||
if e == nil {
|
||||
return key.DiscoPublic{}
|
||||
}
|
||||
if e.tsmpActive {
|
||||
return e.tsmpKey
|
||||
}
|
||||
return e.controlKey
|
||||
}
|
||||
|
||||
func (e *endpointDisco) keyFromControl() key.DiscoPublic {
|
||||
if e == nil {
|
||||
return key.DiscoPublic{}
|
||||
}
|
||||
return e.controlKey
|
||||
}
|
||||
|
||||
func (e *endpointDisco) keyFromTSMP() key.DiscoPublic {
|
||||
if e == nil {
|
||||
return key.DiscoPublic{}
|
||||
}
|
||||
return e.tsmpKey
|
||||
}
|
||||
|
||||
// tsmpIsActive reports whether the TSMP learned key is currently the active
|
||||
// disco key. Safe to call on a nil receiver (returns false).
|
||||
func (e *endpointDisco) tsmpIsActive() bool {
|
||||
if e == nil {
|
||||
return false
|
||||
}
|
||||
return e.tsmpActive
|
||||
}
|
||||
|
||||
// shortString returns the ShortString of the key currently regarded as active
|
||||
// or an empty string if endpointDisco is nil.
|
||||
func (e *endpointDisco) shortString() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
if e.tsmpActive {
|
||||
return e.tsmpShort
|
||||
}
|
||||
return e.controlShort
|
||||
}
|
||||
|
||||
type sentPing struct {
|
||||
@@ -503,7 +577,9 @@ func (de *endpoint) initFakeUDPAddr() {
|
||||
// Conn.noteRecvActivity no more than once every 10s, returning true if it
|
||||
// was called, otherwise false.
|
||||
func (de *endpoint) noteRecvActivity(src epAddr, now mono.Time) bool {
|
||||
if de.isWireguardOnly {
|
||||
if !hasUDPTransport {
|
||||
// Nothing to track; src is always our DERP home.
|
||||
} else if de.isWireguardOnly {
|
||||
de.mu.Lock()
|
||||
de.bestAddr.ap = src.ap
|
||||
de.bestAddrAt = now
|
||||
@@ -524,22 +600,13 @@ func (de *endpoint) noteRecvActivity(src epAddr, now mono.Time) bool {
|
||||
elapsed := now.Sub(de.lastRecvWG.LoadAtomic())
|
||||
if elapsed > 10*time.Second {
|
||||
de.lastRecvWG.StoreAtomic(now)
|
||||
|
||||
if de.c.noteRecvActivity == nil {
|
||||
return false
|
||||
}
|
||||
de.c.noteRecvActivity(de.publicKey)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (de *endpoint) discoShort() string {
|
||||
var short string
|
||||
if d := de.disco.Load(); d != nil {
|
||||
short = d.short
|
||||
}
|
||||
return short
|
||||
return de.disco.Load().shortString()
|
||||
}
|
||||
|
||||
// String exists purely so wireguard-go internals can log.Printf("%v")
|
||||
@@ -572,7 +639,7 @@ func (de *endpoint) addrForSendLocked(now mono.Time) (udpAddr epAddr, derpAddr n
|
||||
return udpAddr, netip.AddrPort{}, false
|
||||
}
|
||||
|
||||
if de.isWireguardOnly {
|
||||
if hasUDPTransport && de.isWireguardOnly {
|
||||
// If the endpoint is wireguard-only, we don't have a DERP
|
||||
// address to send to, so we have to send to the UDP address.
|
||||
udpAddr, shouldPing := de.addrForWireGuardSendLocked(now)
|
||||
@@ -697,7 +764,7 @@ func (de *endpoint) maybeProbeUDPLifetimeLocked() (afterInactivityFor time.Durat
|
||||
// shuffling probing probability where the local node ends up with a large
|
||||
// key value lexicographically relative to the other nodes it tends to
|
||||
// communicate with. If de's disco key changes, the cycle will reset.
|
||||
if de.c.discoAtomic.Public().Compare(epDisco.key) >= 0 {
|
||||
if de.c.discoAtomic.Public().Compare(epDisco.key()) >= 0 {
|
||||
// lower disco pub key node probes higher
|
||||
return afterInactivityFor, false
|
||||
}
|
||||
@@ -826,6 +893,9 @@ func (de *endpoint) heartbeat() {
|
||||
if now.Sub(de.lastSendExt) > sessionActiveTimeout {
|
||||
// Session's idle. Stop heartbeating.
|
||||
de.c.dlogf("[v1] magicsock: disco: ending heartbeats for idle session to %v (%v)", de.publicKey.ShortString(), de.discoShort())
|
||||
if !buildfeatures.HasNATTraversal {
|
||||
return
|
||||
}
|
||||
if afterInactivityFor, ok := de.maybeProbeUDPLifetimeLocked(); ok {
|
||||
// This is the best place to best effort schedule a probe of UDP
|
||||
// path lifetime in the future as it loosely translates to "UDP path
|
||||
@@ -863,7 +933,7 @@ func (de *endpoint) heartbeat() {
|
||||
de.sendDiscoPingsLocked(now, true)
|
||||
}
|
||||
|
||||
if de.wantUDPRelayPathDiscoveryLocked(now) {
|
||||
if buildfeatures.HasNATTraversal && de.wantUDPRelayPathDiscoveryLocked(now) {
|
||||
de.discoverUDPRelayPathsLocked(now)
|
||||
}
|
||||
|
||||
@@ -891,7 +961,7 @@ func (de *endpoint) wantUDPRelayPathDiscoveryLocked(now mono.Time) bool {
|
||||
if runtime.GOOS == "js" {
|
||||
return false
|
||||
}
|
||||
if !de.c.hasPeerRelayServers.Load() {
|
||||
if !de.c.relayManager.hasPeerRelayServers.Load() {
|
||||
// Changes in this value between its access and a call to
|
||||
// [endpoint.discoverUDPRelayPathsLocked] are fine, we will eventually
|
||||
// do the "right" thing during future path discovery. The worst case is
|
||||
@@ -949,7 +1019,7 @@ func (de *endpoint) wantFullPingLocked(now mono.Time) bool {
|
||||
|
||||
func (de *endpoint) noteTxActivityExtTriggerLocked(now mono.Time) {
|
||||
de.lastSendExt = now
|
||||
if de.heartBeatTimer == nil && !de.heartbeatDisabled {
|
||||
if hasUDPTransport && de.heartBeatTimer == nil && !de.heartbeatDisabled {
|
||||
de.heartBeatTimer = time.AfterFunc(heartbeatInterval, de.heartbeat)
|
||||
}
|
||||
}
|
||||
@@ -1022,7 +1092,7 @@ func (de *endpoint) discoPing(res *ipnstate.PingResult, size int, cb func(*ipnst
|
||||
for ep := range de.endpointState {
|
||||
de.startDiscoPingLocked(epAddr{ap: ep}, now, pingCLI, size, resCB)
|
||||
}
|
||||
if de.wantUDPRelayPathDiscoveryLocked(now) {
|
||||
if buildfeatures.HasNATTraversal && de.wantUDPRelayPathDiscoveryLocked(now) {
|
||||
de.discoverUDPRelayPathsLocked(now)
|
||||
}
|
||||
}
|
||||
@@ -1044,13 +1114,15 @@ func (de *endpoint) send(buffs [][]byte, offset int) error {
|
||||
now := mono.Now()
|
||||
udpAddr, derpAddr, startWGPing := de.addrForSendLocked(now)
|
||||
|
||||
if de.isWireguardOnly {
|
||||
if !hasUDPTransport {
|
||||
// There are no UDP paths to discover; everything goes over DERP.
|
||||
} else if de.isWireguardOnly {
|
||||
if startWGPing {
|
||||
de.sendWireGuardOnlyPingsLocked(now)
|
||||
}
|
||||
} else if !udpAddr.isDirect() || now.After(de.trustBestAddrUntil) {
|
||||
de.sendDiscoPingsLocked(now, true)
|
||||
if de.wantUDPRelayPathDiscoveryLocked(now) {
|
||||
if buildfeatures.HasNATTraversal && de.wantUDPRelayPathDiscoveryLocked(now) {
|
||||
de.discoverUDPRelayPathsLocked(now)
|
||||
}
|
||||
}
|
||||
@@ -1070,7 +1142,7 @@ func (de *endpoint) send(buffs [][]byte, offset int) error {
|
||||
}
|
||||
}
|
||||
var err error
|
||||
if udpAddr.ap.IsValid() {
|
||||
if hasUDPTransport && udpAddr.ap.IsValid() {
|
||||
_, err = de.c.sendUDPBatch(udpAddr, buffs, offset)
|
||||
|
||||
// If the error is known to indicate that the endpoint is no longer
|
||||
@@ -1177,8 +1249,7 @@ func (de *endpoint) discoPingTimeout(txid stun.TxID) {
|
||||
return
|
||||
}
|
||||
bestUntrusted := mono.Now().After(de.trustBestAddrUntil)
|
||||
if sp.to == de.bestAddr.epAddr && sp.to.vni.IsSet() && bestUntrusted {
|
||||
// TODO(jwhited): consider applying this to direct UDP paths as well
|
||||
if sp.to == de.bestAddr.epAddr && bestUntrusted {
|
||||
de.clearBestAddrLocked()
|
||||
}
|
||||
if debugDisco() || !de.bestAddr.ap.IsValid() || bestUntrusted {
|
||||
@@ -1211,7 +1282,7 @@ func (de *endpoint) removeSentDiscoPingLocked(txid stun.TxID, sp sentPing, resul
|
||||
// Stop the timer for the case where sendPing failed to write to UDP.
|
||||
// In the case of a timer already having fired, this is a no-op:
|
||||
sp.timer.Stop()
|
||||
if sp.purpose == pingHeartbeatForUDPLifetime {
|
||||
if buildfeatures.HasNATTraversal && sp.purpose == pingHeartbeatForUDPLifetime {
|
||||
de.probeUDPLifetimeCliffDoneLocked(result, txid)
|
||||
}
|
||||
delete(de.sentPing, txid)
|
||||
@@ -1343,13 +1414,16 @@ func (de *endpoint) startDiscoPingLocked(ep epAddr, now mono.Time, purpose disco
|
||||
if purpose == pingHeartbeatForUDPLifetime && de.probeUDPLifetime != nil {
|
||||
de.probeUDPLifetime.lastTxID = txid
|
||||
}
|
||||
go de.sendDiscoPing(ep, epDisco.key, txid, s, logLevel)
|
||||
go de.sendDiscoPing(ep, epDisco.key(), txid, s, logLevel)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// sendDiscoPingsLocked starts pinging all of ep's endpoints.
|
||||
// sendDiscoPingsLocked starts pinging all of ep's direct endpoints.
|
||||
// Sibling of discoverUDPRelayPathsLocked for udprelay.
|
||||
func (de *endpoint) sendDiscoPingsLocked(now mono.Time, sendCallMeMaybe bool) {
|
||||
if debugNeverDirectUDP() {
|
||||
return // skip when direct UDP is disabled
|
||||
}
|
||||
de.lastFullPing = now
|
||||
var sentAny bool
|
||||
for ep, st := range de.endpointState {
|
||||
@@ -1374,7 +1448,7 @@ func (de *endpoint) sendDiscoPingsLocked(now mono.Time, sendCallMeMaybe bool) {
|
||||
de.startDiscoPingLocked(epAddr{ap: ep}, now, pingDiscovery, 0, nil)
|
||||
}
|
||||
derpAddr := de.derpAddr
|
||||
if sentAny && sendCallMeMaybe && derpAddr.IsValid() {
|
||||
if buildfeatures.HasNATTraversal && sentAny && sendCallMeMaybe && derpAddr.IsValid() {
|
||||
// Have our magicsock.Conn figure out its STUN endpoint (if
|
||||
// it doesn't know already) and then send a CallMeMaybe
|
||||
// message to our peer via DERP informing them that we've
|
||||
@@ -1388,7 +1462,7 @@ func (de *endpoint) sendDiscoPingsLocked(now mono.Time, sendCallMeMaybe bool) {
|
||||
// a WireGuard only endpoint and initiates an ICMP ping for useable
|
||||
// addresses.
|
||||
func (de *endpoint) sendWireGuardOnlyPingsLocked(now mono.Time) {
|
||||
if runtime.GOOS == "js" {
|
||||
if !hasUDPTransport {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1464,8 +1538,167 @@ func (de *endpoint) setLastPing(ipp netip.AddrPort, now mono.Time) {
|
||||
state.lastPing = now
|
||||
}
|
||||
|
||||
// updateDiscoKey replaces the controlClient learned disco key for de.
|
||||
// Update only the control-provided key, leaving any existing TSMP key as-is.
|
||||
// If the new control key is zero, switch back to using the TSMP key if it
|
||||
// exists; otherwise mark the new control key as preferred.
|
||||
// Should both keys be zero, nil out the saved key.
|
||||
// The loop here ensures another update did not occur during the interval
|
||||
// between load and store (based on pointer identity).
|
||||
//
|
||||
// It reports whether the active disco key changed. If it reports true, the
|
||||
// caller must call [changeActiveDIscoLocked] to reset the state of the
|
||||
// disco connection.
|
||||
func (de *endpoint) updateDiscoKey(key key.DiscoPublic) bool {
|
||||
epDisco := &endpointDisco{}
|
||||
for {
|
||||
old := de.disco.Load()
|
||||
// Nothing to update
|
||||
if old == nil && key.IsZero() {
|
||||
return false
|
||||
}
|
||||
// Control key did not change and control is already active
|
||||
if old.keyFromControl() == key && !old.tsmpIsActive() && !key.IsZero() {
|
||||
return false
|
||||
}
|
||||
if old != nil {
|
||||
epDisco.tsmpKey = old.tsmpKey
|
||||
epDisco.tsmpShort = old.tsmpShort
|
||||
epDisco.tsmpActive = old.tsmpActive || key.IsZero()
|
||||
}
|
||||
if !key.IsZero() {
|
||||
epDisco.controlKey = key
|
||||
epDisco.controlShort = key.ShortString()
|
||||
}
|
||||
// We have no key material, nil out key.
|
||||
if epDisco.controlKey.IsZero() && epDisco.tsmpKey.IsZero() {
|
||||
epDisco = nil
|
||||
}
|
||||
if de.disco.CompareAndSwap(old, epDisco) {
|
||||
de.logKeyChange(old, epDisco, "controlClientUpdate")
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updateTSMPDiscoKey replaces the TSMP learned disco key for de.
|
||||
// Update only the TSMP-provided key, leaving any existing control key as-is.
|
||||
// If the new TSMP key is zero, switch back to using the control key if it
|
||||
// exists; otherwise mark the new TSMP key as preferred.
|
||||
// Should both keys be zero, nil out the saved key.
|
||||
// The loop here ensures another update did not occur during the interval
|
||||
// between load and store (based on pointer identity).
|
||||
//
|
||||
// Returns wether the active was swapped to the TSMP key, or if the TSMP key
|
||||
// changed. If the key has changed, call [changedActiveDiscoLocked] to reset
|
||||
// state of the disco connection.
|
||||
func (de *endpoint) updateTSMPDiscoKey(key key.DiscoPublic) bool {
|
||||
epDisco := &endpointDisco{}
|
||||
for {
|
||||
old := de.disco.Load()
|
||||
// Nothing to update
|
||||
if old == nil && key.IsZero() {
|
||||
return false
|
||||
}
|
||||
// TSMP key did not change and TSMP is already active
|
||||
if old.keyFromTSMP() == key && old.tsmpIsActive() && !key.IsZero() {
|
||||
return false
|
||||
}
|
||||
if old != nil {
|
||||
epDisco.controlKey = old.controlKey
|
||||
epDisco.controlShort = old.controlShort
|
||||
epDisco.tsmpActive = !key.IsZero()
|
||||
}
|
||||
if !key.IsZero() {
|
||||
epDisco.tsmpKey = key
|
||||
epDisco.tsmpShort = key.ShortString()
|
||||
epDisco.tsmpActive = true
|
||||
}
|
||||
// We have no key material, nil out key.
|
||||
if epDisco.controlKey.IsZero() && epDisco.tsmpKey.IsZero() {
|
||||
epDisco = nil
|
||||
}
|
||||
|
||||
// At this point, we know the relevant parts have changed enough to make an
|
||||
// update.
|
||||
if de.disco.CompareAndSwap(old, epDisco) {
|
||||
de.logKeyChange(old, epDisco, "tsmpUpdate")
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (de *endpoint) logKeyChange(oldDisco, newDisco *endpointDisco, reason string) {
|
||||
de.c.logf("[v1] magicsock: disco: node %s changed key from %s to %s, tsmpActive=%t -> %t, reason=%s",
|
||||
de.publicKey.ShortString(), oldDisco.key(), newDisco.key(),
|
||||
oldDisco.tsmpIsActive(), newDisco.tsmpIsActive(), reason)
|
||||
}
|
||||
|
||||
// sawDiscoKeyTestHook is a test only hook to create a race on writing to
|
||||
// [disco]. It is never safe to use outside the contexts of tests.
|
||||
var sawDiscoKeyTestHook func()
|
||||
|
||||
// checkAndUpdateDiscoKey reports whether key is one of the known disco keys
|
||||
// for de. If so, and it's not already the active disco key for de, it is
|
||||
// made the active key. In either case, the resulting disco key bundle for
|
||||
// de is returned (it may be nil, if de has no disco keys).
|
||||
func (de *endpoint) checkAndUpdateDiscoKey(key key.DiscoPublic) (*endpointDisco, bool) {
|
||||
epDisco := &endpointDisco{}
|
||||
for {
|
||||
current := de.disco.Load()
|
||||
if current == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if !current.key().IsZero() && current.key() == key {
|
||||
return current, true
|
||||
}
|
||||
|
||||
inactiveKey := bools.IfElse(current.tsmpActive, current.keyFromControl(),
|
||||
current.keyFromTSMP())
|
||||
|
||||
// No reason to swap to a zero key or not the key we are seeing.
|
||||
if inactiveKey.IsZero() || inactiveKey != key {
|
||||
return current, false
|
||||
}
|
||||
|
||||
// Hook used for testing update conflict for the CompareAndSwap operation.
|
||||
// Only used in testing, not safe to use in production.
|
||||
if sawDiscoKeyTestHook != nil {
|
||||
sawDiscoKeyTestHook()
|
||||
}
|
||||
|
||||
// We have seen the inactive key, swap to that one as being active.
|
||||
epDisco.controlKey = current.controlKey
|
||||
epDisco.controlShort = current.controlShort
|
||||
epDisco.tsmpKey = current.tsmpKey
|
||||
epDisco.tsmpShort = current.tsmpShort
|
||||
epDisco.tsmpActive = !current.tsmpActive
|
||||
if de.disco.CompareAndSwap(current, epDisco) {
|
||||
de.mu.Lock()
|
||||
de.changedActiveDiscoLocked()
|
||||
de.mu.Unlock()
|
||||
de.logKeyChange(current, epDisco, "keySeen")
|
||||
return epDisco, true
|
||||
} else {
|
||||
// The struct changed under us, run the search again.
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// changedActiveDiscoLocked invalidates the current disco path for a re-probe,
|
||||
// but keeps the bestAddr so data keeps flowing until a new path is confirmed.
|
||||
//
|
||||
// de.mu must be held.
|
||||
func (de *endpoint) changedActiveDiscoLocked() {
|
||||
de.trustBestAddrUntil = 0
|
||||
de.invalidateDiscoPathLocked()
|
||||
}
|
||||
|
||||
// updateFromNode updates the endpoint based on a tailcfg.Node from a NetMap
|
||||
// update.
|
||||
// update. The node is assumed to originate from the control client from the
|
||||
// perspective of discoKey management.
|
||||
func (de *endpoint) updateFromNode(n tailcfg.NodeView, heartbeatDisabled bool, probeUDPLifetimeEnabled bool) {
|
||||
if !n.Valid() {
|
||||
panic("nil node when updating endpoint")
|
||||
@@ -1474,30 +1707,28 @@ func (de *endpoint) updateFromNode(n tailcfg.NodeView, heartbeatDisabled bool, p
|
||||
defer de.mu.Unlock()
|
||||
|
||||
de.heartbeatDisabled = heartbeatDisabled
|
||||
if probeUDPLifetimeEnabled {
|
||||
de.setProbeUDPLifetimeConfigLocked(defaultProbeUDPLifetimeConfig)
|
||||
} else {
|
||||
de.setProbeUDPLifetimeConfigLocked(nil)
|
||||
if buildfeatures.HasNATTraversal {
|
||||
if probeUDPLifetimeEnabled {
|
||||
de.setProbeUDPLifetimeConfigLocked(defaultProbeUDPLifetimeConfig)
|
||||
} else {
|
||||
de.setProbeUDPLifetimeConfigLocked(nil)
|
||||
}
|
||||
}
|
||||
de.expired = n.Expired()
|
||||
|
||||
epDisco := de.disco.Load()
|
||||
var discoKey key.DiscoPublic
|
||||
if epDisco != nil {
|
||||
discoKey = epDisco.key
|
||||
}
|
||||
discoKey := de.disco.Load().keyFromControl()
|
||||
|
||||
if discoKey != n.DiscoKey() {
|
||||
de.c.logf("[v1] magicsock: disco: node %s changed from %s to %s", de.publicKey.ShortString(), discoKey, n.DiscoKey())
|
||||
de.disco.Store(&endpointDisco{
|
||||
key: n.DiscoKey(),
|
||||
short: n.DiscoKey().ShortString(),
|
||||
})
|
||||
de.c.logf("[v1] magicsock: disco: node %s changed control learned key from %s to %s",
|
||||
de.publicKey.ShortString(), discoKey, n.DiscoKey())
|
||||
key := n.DiscoKey()
|
||||
if de.updateDiscoKey(key) {
|
||||
de.changedActiveDiscoLocked()
|
||||
}
|
||||
de.debugUpdates.Add(EndpointChange{
|
||||
When: time.Now(),
|
||||
What: "updateFromNode-resetLocked",
|
||||
What: "updateFromNode-disco-key-changed",
|
||||
})
|
||||
de.resetLocked()
|
||||
}
|
||||
if n.HomeDERP() == 0 {
|
||||
if de.derpAddr.IsValid() {
|
||||
@@ -1521,7 +1752,9 @@ func (de *endpoint) updateFromNode(n tailcfg.NodeView, heartbeatDisabled bool, p
|
||||
de.derpAddr = newDerp
|
||||
}
|
||||
|
||||
de.setEndpointsLocked(n.Endpoints())
|
||||
if hasUDPTransport {
|
||||
de.setEndpointsLocked(n.Endpoints())
|
||||
}
|
||||
|
||||
de.relayCapable = capVerIsRelayCapable(n.Cap())
|
||||
}
|
||||
@@ -1719,7 +1952,7 @@ func (de *endpoint) handlePongConnLocked(m *disco.Pong, di *discoInfo, src epAdd
|
||||
now := mono.Now()
|
||||
latency := now.Sub(sp.at)
|
||||
|
||||
if !isDerp && !src.vni.IsSet() {
|
||||
if hasUDPTransport && !isDerp && !src.vni.IsSet() {
|
||||
// Note: we check vni.isSet() as relay [epAddr]'s are not stored in
|
||||
// endpointState, they are either de.bestAddr or not.
|
||||
st, ok := de.endpointState[sp.to.ap]
|
||||
@@ -1756,18 +1989,14 @@ func (de *endpoint) handlePongConnLocked(m *disco.Pong, di *discoInfo, src epAdd
|
||||
|
||||
// Promote this pong response to our current best address if it's lower latency.
|
||||
// TODO(bradfitz): decide how latency vs. preference order affects decision
|
||||
if !isDerp {
|
||||
if hasUDPTransport && !isDerp {
|
||||
thisPong := addrQuality{
|
||||
epAddr: sp.to,
|
||||
latency: latency,
|
||||
wireMTU: pingSizeToPktLen(sp.size, sp.to),
|
||||
}
|
||||
// TODO(jwhited): consider checking de.trustBestAddrUntil as well. If
|
||||
// de.bestAddr is untrusted we may want to clear it, otherwise we could
|
||||
// get stuck with a forever untrusted bestAddr that blackholes, since
|
||||
// we don't clear direct UDP paths on disco ping timeout (see
|
||||
// discoPingTimeout).
|
||||
if betterAddr(thisPong, de.bestAddr) {
|
||||
bestUntrusted := now.After(de.trustBestAddrUntil)
|
||||
if betterAddr(thisPong, de.bestAddr) || bestUntrusted {
|
||||
de.c.logf("magicsock: disco: node %v %v now using %v mtu=%v tx=%x", de.publicKey.ShortString(), de.discoShort(), sp.to, thisPong.wireMTU, m.TxID[:6])
|
||||
de.debugUpdates.Add(EndpointChange{
|
||||
When: time.Now(),
|
||||
@@ -2004,7 +2233,7 @@ func (de *endpoint) populatePeerStatus(ps *ipnstate.PeerStatus) {
|
||||
de.mu.Lock()
|
||||
defer de.mu.Unlock()
|
||||
|
||||
ps.Relay = de.c.derpRegionCodeOfIDLocked(int(de.derpAddr.Port()))
|
||||
ps.Relay = de.c.derpRegionCodeOfIDLocked(tailcfg.DERPRegionID(de.derpAddr.Port()))
|
||||
|
||||
if de.lastSendExt.IsZero() {
|
||||
return
|
||||
@@ -2055,9 +2284,17 @@ func (de *endpoint) stopAndReset() {
|
||||
// DERP-only endpoint. It does not stop the endpoint's heartbeat
|
||||
// timer, if one is running.
|
||||
func (de *endpoint) resetLocked() {
|
||||
de.clearBestAddrLocked()
|
||||
de.invalidateDiscoPathLocked()
|
||||
}
|
||||
|
||||
// invalidateDiscoPathLocked discards in-flight disco/relay state that was
|
||||
// established against the peer's current disco identity, so that path
|
||||
// discovery re-runs from scratch.
|
||||
func (de *endpoint) invalidateDiscoPathLocked() {
|
||||
de.lastSendExt = 0
|
||||
de.lastFullPing = 0
|
||||
de.clearBestAddrLocked()
|
||||
de.lastUDPRelayPathDiscovery = 0
|
||||
for _, es := range de.endpointState {
|
||||
es.lastPing = 0
|
||||
}
|
||||
@@ -2067,7 +2304,9 @@ func (de *endpoint) resetLocked() {
|
||||
}
|
||||
}
|
||||
de.probeUDPLifetime.resetCycleEndpointLocked()
|
||||
de.c.relayManager.stopWork(de)
|
||||
if buildfeatures.HasNATTraversal && de.c != nil {
|
||||
de.c.relayManager.stopWork(de)
|
||||
}
|
||||
}
|
||||
|
||||
func (de *endpoint) numStopAndReset() int64 {
|
||||
@@ -2082,7 +2321,7 @@ func (de *endpoint) setDERPHome(regionID uint16) {
|
||||
de.mu.Lock()
|
||||
defer de.mu.Unlock()
|
||||
de.derpAddr = netip.AddrPortFrom(tailcfg.DerpMagicIPAddr, uint16(regionID))
|
||||
if de.c.hasPeerRelayServers.Load() {
|
||||
if buildfeatures.HasNATTraversal && de.c.relayManager.hasPeerRelayServers.Load() {
|
||||
de.c.relayManager.handleDERPHomeChange(de.publicKey, regionID)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !js && !wasm && !plan9
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build wasm || plan9
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package magicsock
|
||||
|
||||
+853
-506
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !linux || ts_omit_listenrawdisco
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && !ts_omit_listenrawdisco
|
||||
|
||||
+4
-2
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !plan9
|
||||
@@ -8,6 +8,8 @@ package magicsock
|
||||
import (
|
||||
"errors"
|
||||
"syscall"
|
||||
|
||||
"tailscale.com/net/neterror"
|
||||
)
|
||||
|
||||
// shouldRebind returns if the error is one that is known to be healed by a
|
||||
@@ -17,7 +19,7 @@ func shouldRebind(err error) (ok bool, reason string) {
|
||||
// EPIPE/ENOTCONN are common errors when a send fails due to a closed
|
||||
// socket. There is some platform and version inconsistency in which
|
||||
// error is returned, but the meaning is the same.
|
||||
case errors.Is(err, syscall.EPIPE), errors.Is(err, syscall.ENOTCONN):
|
||||
case neterror.IsClosedPipeError(err):
|
||||
return true, "broken-pipe"
|
||||
|
||||
// EPERM is typically caused by EDR software, and has been observed to be
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build plan9
|
||||
|
||||
+52
-8
@@ -1,9 +1,11 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package magicsock
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/util/set"
|
||||
@@ -48,6 +50,7 @@ type peerMap struct {
|
||||
// byEpAddr. That issue is being tracked in http://go/corp/29422.
|
||||
relayEpAddrByNodeKey map[key.NodePublic]epAddr
|
||||
|
||||
nodesMu sync.RWMutex // protects nodesOfDisco
|
||||
// nodesOfDisco contains the set of nodes that are using a
|
||||
// DiscoKey. Usually those sets will be just one node.
|
||||
nodesOfDisco map[key.DiscoPublic]set.Set[key.NodePublic]
|
||||
@@ -74,6 +77,9 @@ func (m *peerMap) nodeCount() int {
|
||||
// knownPeerDiscoKey reports whether there exists any peer with the disco key
|
||||
// dk.
|
||||
func (m *peerMap) knownPeerDiscoKey(dk key.DiscoPublic) bool {
|
||||
m.nodesMu.RLock()
|
||||
defer m.nodesMu.RUnlock()
|
||||
|
||||
_, ok := m.nodesOfDisco[dk]
|
||||
return ok
|
||||
}
|
||||
@@ -117,8 +123,11 @@ func (m *peerMap) forEachEndpoint(f func(ep *endpoint)) {
|
||||
|
||||
// forEachEndpointWithDiscoKey invokes f on every endpoint in m that has the
|
||||
// provided DiscoKey until f returns false or there are no endpoints left to
|
||||
// iterate.
|
||||
// iterate. f must call back into peerMap and mutate [nodesOfDisco].
|
||||
func (m *peerMap) forEachEndpointWithDiscoKey(dk key.DiscoPublic, f func(*endpoint) (keepGoing bool)) {
|
||||
m.nodesMu.RLock()
|
||||
defer m.nodesMu.RUnlock()
|
||||
|
||||
for nk := range m.nodesOfDisco[dk] {
|
||||
pi, ok := m.byNodeKey[nk]
|
||||
if !ok {
|
||||
@@ -138,7 +147,9 @@ func (m *peerMap) forEachEndpointWithDiscoKey(dk key.DiscoPublic, f func(*endpoi
|
||||
// upsertEndpoint stores endpoint in the peerInfo for
|
||||
// ep.publicKey, and updates indexes. m must already have a
|
||||
// tailcfg.Node for ep.publicKey.
|
||||
func (m *peerMap) upsertEndpoint(ep *endpoint, oldDiscoKey key.DiscoPublic) {
|
||||
func (m *peerMap) upsertEndpoint(ep *endpoint, oldDiscoKey key.DiscoPublic,
|
||||
fromTSMP bool,
|
||||
) {
|
||||
if ep.nodeID == 0 {
|
||||
panic("internal error: upsertEndpoint called with zero NodeID")
|
||||
}
|
||||
@@ -149,9 +160,14 @@ func (m *peerMap) upsertEndpoint(ep *endpoint, oldDiscoKey key.DiscoPublic) {
|
||||
}
|
||||
m.byNodeID[ep.nodeID] = pi
|
||||
|
||||
// Load key and make the comparison based on the source of the new key.
|
||||
epDisco := ep.disco.Load()
|
||||
if epDisco == nil || oldDiscoKey != epDisco.key {
|
||||
delete(m.nodesOfDisco[oldDiscoKey], ep.publicKey)
|
||||
epKey := epDisco.keyFromControl()
|
||||
if fromTSMP {
|
||||
epKey = epDisco.keyFromTSMP()
|
||||
}
|
||||
if epDisco == nil || oldDiscoKey != epKey {
|
||||
m.cleanFromNodesOfDisco(oldDiscoKey, ep.publicKey)
|
||||
}
|
||||
if ep.isWireguardOnly {
|
||||
// If the peer is a WireGuard only peer, add all of its endpoints.
|
||||
@@ -166,12 +182,22 @@ func (m *peerMap) upsertEndpoint(ep *endpoint, oldDiscoKey key.DiscoPublic) {
|
||||
}
|
||||
return
|
||||
}
|
||||
discoSet := m.nodesOfDisco[epDisco.key]
|
||||
|
||||
// There is no need to insert a new node key under a zero disco key as it
|
||||
// wastes space and will risk stale entries just sitting there in a long list
|
||||
// under the zero key.
|
||||
if epKey.IsZero() {
|
||||
return
|
||||
}
|
||||
|
||||
m.nodesMu.Lock()
|
||||
discoSet := m.nodesOfDisco[epKey]
|
||||
if discoSet == nil {
|
||||
discoSet = set.Set[key.NodePublic]{}
|
||||
m.nodesOfDisco[epDisco.key] = discoSet
|
||||
m.nodesOfDisco[epKey] = discoSet
|
||||
}
|
||||
discoSet.Add(ep.publicKey)
|
||||
m.nodesMu.Unlock()
|
||||
}
|
||||
|
||||
// setNodeKeyForEpAddr makes future peer lookups by addr return the
|
||||
@@ -214,8 +240,16 @@ func (m *peerMap) deleteEndpoint(ep *endpoint) {
|
||||
|
||||
pi := m.byNodeKey[ep.publicKey]
|
||||
if epDisco != nil {
|
||||
delete(m.nodesOfDisco[epDisco.key], ep.publicKey)
|
||||
for _, discoKey := range []key.DiscoPublic{
|
||||
epDisco.keyFromControl(), epDisco.keyFromTSMP(),
|
||||
} {
|
||||
if discoKey.IsZero() {
|
||||
continue
|
||||
}
|
||||
m.cleanFromNodesOfDisco(discoKey, ep.publicKey)
|
||||
}
|
||||
}
|
||||
|
||||
delete(m.byNodeKey, ep.publicKey)
|
||||
if was, ok := m.byNodeID[ep.nodeID]; ok && was.ep == ep {
|
||||
delete(m.byNodeID, ep.nodeID)
|
||||
@@ -230,3 +264,13 @@ func (m *peerMap) deleteEndpoint(ep *endpoint) {
|
||||
}
|
||||
delete(m.relayEpAddrByNodeKey, ep.publicKey)
|
||||
}
|
||||
|
||||
func (m *peerMap) cleanFromNodesOfDisco(disco key.DiscoPublic, node key.NodePublic) {
|
||||
m.nodesMu.Lock()
|
||||
defer m.nodesMu.Unlock()
|
||||
s := m.nodesOfDisco[disco]
|
||||
delete(s, node)
|
||||
if len(s) == 0 {
|
||||
delete(m.nodesOfDisco, disco)
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build (darwin && !ios) || (linux && !android)
|
||||
@@ -84,6 +84,9 @@ func (c *Conn) PeerMTUEnabled() bool {
|
||||
// If the PMTUD settings changed, it resets the endpoint state so that it will
|
||||
// re-probe path MTUs to this peer.
|
||||
func (c *Conn) UpdatePMTUD() {
|
||||
if !hasUDPTransport {
|
||||
return
|
||||
}
|
||||
if debugPMTUD() {
|
||||
df4, err4 := c.getDontFragment("udp4")
|
||||
df6, err6 := c.getDontFragment("udp6")
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build darwin && !ios
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && !android
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build (!linux && !darwin) || android || ios
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build (darwin && !ios) || (linux && !android)
|
||||
|
||||
+15
-12
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package magicsock
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/net/ipv6"
|
||||
"tailscale.com/control/controlknobs"
|
||||
"tailscale.com/net/batching"
|
||||
"tailscale.com/net/netaddr"
|
||||
"tailscale.com/net/packet"
|
||||
@@ -41,11 +41,13 @@ type RebindingUDPConn struct {
|
||||
// nettype.PacketConn to a batchingConn when appropriate. This upgrade is
|
||||
// intentionally pushed closest to where read/write ops occur in order to avoid
|
||||
// disrupting surrounding code that assumes nettype.PacketConn is a
|
||||
// *net.UDPConn.
|
||||
func (c *RebindingUDPConn) setConnLocked(p nettype.PacketConn, network string, batchSize int) {
|
||||
upc := batching.TryUpgradeToConn(p, network, batchSize)
|
||||
c.pconn = upc
|
||||
c.pconnAtomic.Store(&upc)
|
||||
// *net.UDPConn. knobs may be nil.
|
||||
func (c *RebindingUDPConn) setConnLocked(p nettype.PacketConn, network string, knobs *controlknobs.Knobs) {
|
||||
if hasUDPTransport {
|
||||
p = batching.TryUpgradeToConn(p, network, "magicsock_udp_rxq_overflows", knobs)
|
||||
}
|
||||
c.pconn = p
|
||||
c.pconnAtomic.Store(&p)
|
||||
c.port = uint16(c.localAddrLocked().Port)
|
||||
}
|
||||
|
||||
@@ -118,20 +120,21 @@ func (c *RebindingUDPConn) WriteWireGuardBatchTo(buffs [][]byte, addr epAddr, of
|
||||
|
||||
// ReadBatch is an alias for [batching.Conn.ReadBatch] with fallback to single
|
||||
// packet operations if c.pconn is not a [batching.Conn].
|
||||
func (c *RebindingUDPConn) ReadBatch(msgs []ipv6.Message, flags int) (int, error) {
|
||||
func (c *RebindingUDPConn) ReadBatch(slab []byte, batchingPackets []batching.ReceivedPacket) (int, error) {
|
||||
for {
|
||||
pconn := *c.pconnAtomic.Load()
|
||||
b, ok := pconn.(batching.Conn)
|
||||
if !ok {
|
||||
n, ap, err := c.readFromWithInitPconn(pconn, msgs[0].Buffers[0])
|
||||
n, ap, err := c.readFromWithInitPconn(pconn, slab)
|
||||
if err == nil {
|
||||
msgs[0].N = n
|
||||
msgs[0].Addr = net.UDPAddrFromAddrPort(netaddr.Unmap(ap))
|
||||
batchingPackets[0].Offset = 0
|
||||
batchingPackets[0].Size = n
|
||||
batchingPackets[0].Source = netaddr.Unmap(ap)
|
||||
return 1, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
n, err := b.ReadBatch(msgs, flags)
|
||||
n, err := b.ReadBatch(slab, batchingPackets)
|
||||
if err != nil && pconn != c.currentConn() {
|
||||
continue
|
||||
}
|
||||
|
||||
+87
-8
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package magicsock
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/disco"
|
||||
@@ -34,6 +35,14 @@ import (
|
||||
type relayManager struct {
|
||||
initOnce sync.Once
|
||||
|
||||
// hasPeerRelayServers is whether relayManager is configured with at
|
||||
// least one peer relay server via [relayManager.handleRelayServersSet]
|
||||
// (or per-peer variants). Exposed as an atomic so [endpoint] hot paths
|
||||
// can short-circuit when there are no relay servers without taking any
|
||||
// lock or entering the run loop. Written only from runLoop() via
|
||||
// [relayManager.publishHasServersRunLoop].
|
||||
hasPeerRelayServers atomic.Bool
|
||||
|
||||
// ===================================================================
|
||||
// The following fields are owned by a single goroutine, runLoop().
|
||||
serversByNodeKey map[key.NodePublic]candidatePeerRelay
|
||||
@@ -56,6 +65,8 @@ type relayManager struct {
|
||||
newServerEndpointCh chan newRelayServerEndpointEvent
|
||||
rxDiscoMsgCh chan relayDiscoMsgEvent
|
||||
serversCh chan set.Set[candidatePeerRelay]
|
||||
serverUpsertCh chan candidatePeerRelay
|
||||
serverRemoveCh chan key.NodePublic
|
||||
getServersCh chan chan set.Set[candidatePeerRelay]
|
||||
derpHomeChangeCh chan derpHomeChangeEvent
|
||||
|
||||
@@ -228,6 +239,16 @@ func (r *relayManager) runLoop() {
|
||||
if !r.hasActiveWorkRunLoop() {
|
||||
return
|
||||
}
|
||||
case upsert := <-r.serverUpsertCh:
|
||||
r.handleServerUpsertRunLoop(upsert)
|
||||
if !r.hasActiveWorkRunLoop() {
|
||||
return
|
||||
}
|
||||
case nk := <-r.serverRemoveCh:
|
||||
r.handleServerRemoveRunLoop(nk)
|
||||
if !r.hasActiveWorkRunLoop() {
|
||||
return
|
||||
}
|
||||
case getServersCh := <-r.getServersCh:
|
||||
r.handleGetServersRunLoop(getServersCh)
|
||||
if !r.hasActiveWorkRunLoop() {
|
||||
@@ -265,6 +286,34 @@ func (r *relayManager) handleServersUpdateRunLoop(update set.Set[candidatePeerRe
|
||||
for _, v := range update.Slice() {
|
||||
r.serversByNodeKey[v.nodeKey] = v
|
||||
}
|
||||
r.publishHasServersRunLoop()
|
||||
}
|
||||
|
||||
// handleServerUpsertRunLoop inserts or updates cp in serversByNodeKey. It is
|
||||
// the per-peer analog of [relayManager.handleServersUpdateRunLoop] used by
|
||||
// [Conn.UpsertPeer].
|
||||
func (r *relayManager) handleServerUpsertRunLoop(cp candidatePeerRelay) {
|
||||
r.serversByNodeKey[cp.nodeKey] = cp
|
||||
r.publishHasServersRunLoop()
|
||||
}
|
||||
|
||||
// handleServerRemoveRunLoop deletes nk from serversByNodeKey. It is a no-op
|
||||
// if nk isn't currently a known server. It is the per-peer analog of
|
||||
// [relayManager.handleServersUpdateRunLoop] used by [Conn.RemovePeer] and by
|
||||
// [Conn.UpsertPeer] when a peer is upserted with fields that make it no
|
||||
// longer a relay candidate.
|
||||
func (r *relayManager) handleServerRemoveRunLoop(nk key.NodePublic) {
|
||||
if _, ok := r.serversByNodeKey[nk]; !ok {
|
||||
return
|
||||
}
|
||||
delete(r.serversByNodeKey, nk)
|
||||
r.publishHasServersRunLoop()
|
||||
}
|
||||
|
||||
// publishHasServersRunLoop updates [relayManager.hasPeerRelayServers] to
|
||||
// reflect whether any relay servers are currently known.
|
||||
func (r *relayManager) publishHasServersRunLoop() {
|
||||
r.hasPeerRelayServers.Store(len(r.serversByNodeKey) > 0)
|
||||
}
|
||||
|
||||
type relayDiscoMsgEvent struct {
|
||||
@@ -330,6 +379,8 @@ func (r *relayManager) init() {
|
||||
r.newServerEndpointCh = make(chan newRelayServerEndpointEvent)
|
||||
r.rxDiscoMsgCh = make(chan relayDiscoMsgEvent)
|
||||
r.serversCh = make(chan set.Set[candidatePeerRelay])
|
||||
r.serverUpsertCh = make(chan candidatePeerRelay)
|
||||
r.serverRemoveCh = make(chan key.NodePublic)
|
||||
r.getServersCh = make(chan chan set.Set[candidatePeerRelay])
|
||||
r.derpHomeChangeCh = make(chan derpHomeChangeEvent)
|
||||
r.runLoopStoppedCh = make(chan struct{}, 1)
|
||||
@@ -436,6 +487,21 @@ func (r *relayManager) handleRelayServersSet(servers set.Set[candidatePeerRelay]
|
||||
relayManagerInputEvent(r, nil, &r.serversCh, servers)
|
||||
}
|
||||
|
||||
// handleRelayServerUpsert is the O(1) per-peer variant of
|
||||
// [relayManager.handleRelayServersSet]: it inserts or updates a single
|
||||
// relay server entry.
|
||||
func (r *relayManager) handleRelayServerUpsert(cp candidatePeerRelay) {
|
||||
relayManagerInputEvent(r, nil, &r.serverUpsertCh, cp)
|
||||
}
|
||||
|
||||
// handleRelayServerRemove is the O(1) per-peer variant of
|
||||
// [relayManager.handleRelayServersSet]: it removes a single relay server
|
||||
// entry by node key. It is a no-op if nk is not currently a known relay
|
||||
// server.
|
||||
func (r *relayManager) handleRelayServerRemove(nk key.NodePublic) {
|
||||
relayManagerInputEvent(r, nil, &r.serverRemoveCh, nk)
|
||||
}
|
||||
|
||||
// relayManagerInputEvent initializes [relayManager] if necessary, starts
|
||||
// relayManager.runLoop() if it is not running, and writes 'event' on 'eventCh'.
|
||||
//
|
||||
@@ -679,6 +745,17 @@ func (r *relayManager) handleHandshakeWorkDoneRunLoop(done relayEndpointHandshak
|
||||
}
|
||||
|
||||
func (r *relayManager) handleNewServerEndpointRunLoop(newServerEndpoint newRelayServerEndpointEvent) {
|
||||
if newServerEndpoint.se.ServerDisco.IsZero() {
|
||||
// A relay server always has a nonzero disco key, so this endpoint is
|
||||
// malformed or malicious. It may come from a [disco.CallMeMaybeVia]
|
||||
// with a zeroed ServerDisco, including one carrying an unknown
|
||||
// message version, which parses to a zero-valued message. Dropping it
|
||||
// here prevents a panic in [relayManager.ensureDiscoInfoFor], whose
|
||||
// DiscoPrivate.Shared call rejects zero keys.
|
||||
newServerEndpoint.wlb.ep.c.logf("magicsock: relayManager: ignoring relay server endpoint with zero ServerDisco")
|
||||
return
|
||||
}
|
||||
|
||||
// Check for duplicate work by server disco + VNI.
|
||||
sdv := serverDiscoVNI{newServerEndpoint.se.ServerDisco, newServerEndpoint.se.VNI}
|
||||
existingWork, ok := r.handshakeWorkByServerDiscoVNI[sdv]
|
||||
@@ -724,8 +801,10 @@ func (r *relayManager) handleNewServerEndpointRunLoop(newServerEndpoint newRelay
|
||||
go r.sendCallMeMaybeVia(newServerEndpoint.wlb.ep, newServerEndpoint.se)
|
||||
}
|
||||
|
||||
lastBestMatchingServer := newServerEndpoint.se.ServerDisco.Compare(newServerEndpoint.wlb.lastBest.relayServerDisco) == 0
|
||||
if lastBestMatchingServer && newServerEndpoint.wlb.lastBestIsTrusted {
|
||||
lastBestMatchingServerVNI := newServerEndpoint.wlb.lastBest.vni.IsSet() &&
|
||||
newServerEndpoint.se.ServerDisco.Compare(newServerEndpoint.wlb.lastBest.relayServerDisco) == 0 &&
|
||||
newServerEndpoint.se.VNI == newServerEndpoint.wlb.lastBest.vni.Get()
|
||||
if lastBestMatchingServerVNI && newServerEndpoint.wlb.lastBestIsTrusted {
|
||||
// This relay server endpoint is the same as [endpoint]'s bestAddr at
|
||||
// the time UDP relay path discovery was started, and it was also a
|
||||
// trusted path (see endpoint.trustBestAddrUntil), so return early.
|
||||
@@ -801,7 +880,7 @@ func (r *relayManager) sendCallMeMaybeVia(ep *endpoint, se udprelay.ServerEndpoi
|
||||
AddrPorts: se.AddrPorts,
|
||||
},
|
||||
}
|
||||
ep.c.sendDiscoMessage(epAddr{ap: derpAddr}, ep.publicKey, epDisco.key, callMeMaybeVia, discoVerboseLog)
|
||||
ep.c.sendDiscoMessage(epAddr{ap: derpAddr}, ep.publicKey, epDisco.key(), callMeMaybeVia, discoVerboseLog)
|
||||
}
|
||||
|
||||
func (r *relayManager) handshakeServerEndpoint(work *relayHandshakeWork) {
|
||||
@@ -824,7 +903,7 @@ func (r *relayManager) handshakeServerEndpoint(work *relayHandshakeWork) {
|
||||
common := disco.BindUDPRelayEndpointCommon{
|
||||
VNI: work.se.VNI,
|
||||
Generation: work.handshakeGen,
|
||||
RemoteKey: epDisco.key,
|
||||
RemoteKey: epDisco.key(),
|
||||
}
|
||||
|
||||
work.dlogf("[v1] magicsock: relayManager: starting handshake addrPorts=%v",
|
||||
@@ -878,7 +957,7 @@ func (r *relayManager) handshakeServerEndpoint(work *relayHandshakeWork) {
|
||||
answer.Challenge = *withAnswer
|
||||
ep.c.sendDiscoMessage(epAddr{ap: to, vni: vni}, key.NodePublic{}, work.se.ServerDisco, answer, discoVerboseLog)
|
||||
}
|
||||
ep.c.sendDiscoMessage(epAddr{ap: to, vni: vni}, key.NodePublic{}, epDisco.key, ping, discoVerboseLog)
|
||||
ep.c.sendDiscoMessage(epAddr{ap: to, vni: vni}, key.NodePublic{}, epDisco.key(), ping, discoVerboseLog)
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -886,7 +965,7 @@ func (r *relayManager) handshakeServerEndpoint(work *relayHandshakeWork) {
|
||||
if common.VNI != work.se.VNI {
|
||||
return errors.New("mismatching VNI")
|
||||
}
|
||||
if common.RemoteKey.Compare(epDisco.key) != 0 {
|
||||
if common.RemoteKey.Compare(epDisco.key()) != 0 {
|
||||
return errors.New("mismatching RemoteKey")
|
||||
}
|
||||
return nil
|
||||
@@ -1031,7 +1110,7 @@ func (r *relayManager) allocateAllServersRunLoop(wlb endpointWithLastBest) {
|
||||
if remoteDisco == nil {
|
||||
return
|
||||
}
|
||||
discoKeys := key.NewSortedPairOfDiscoPublic(wlb.ep.c.discoAtomic.Public(), remoteDisco.key)
|
||||
discoKeys := key.NewSortedPairOfDiscoPublic(wlb.ep.c.discoAtomic.Public(), remoteDisco.key())
|
||||
for _, v := range r.serversByNodeKey {
|
||||
byDiscoKeys, ok := r.allocWorkByDiscoKeysByServerNodeKey[v.nodeKey]
|
||||
if !ok {
|
||||
|
||||
Reference in new issue
Block a user