From 9863bf253a7beab92434a84e6b58de642aaaf151 Mon Sep 17 00:00:00 2001 From: JadenJSJ Date: Wed, 22 Jul 2026 15:12:44 +0000 Subject: [PATCH 1/2] magicsock: add per-peer connection order --- tsnet/tsnet.go | 18 +++ wgengine/magicsock/connection_order.go | 143 ++++++++++++++++++++ wgengine/magicsock/connection_order_test.go | 106 +++++++++++++++ wgengine/magicsock/derp.go | 11 ++ wgengine/magicsock/endpoint.go | 127 ++++++++++++++++- wgengine/magicsock/magicsock.go | 6 + wgengine/magicsock/relaymanager.go | 80 ++++++++--- 7 files changed, 463 insertions(+), 28 deletions(-) create mode 100644 wgengine/magicsock/connection_order.go create mode 100644 wgengine/magicsock/connection_order_test.go diff --git a/tsnet/tsnet.go b/tsnet/tsnet.go index 4bee804f7..9182167d1 100644 --- a/tsnet/tsnet.go +++ b/tsnet/tsnet.go @@ -200,6 +200,7 @@ import ( "github.com/metacubex/tailscale/util/set" "github.com/metacubex/tailscale/util/testenv" "github.com/metacubex/tailscale/wgengine" + "github.com/metacubex/tailscale/wgengine/magicsock" "github.com/metacubex/tailscale/wgengine/netstack" ) @@ -316,6 +317,18 @@ type Server struct { // infrastructure hostnames such as control and DERP. LookupHook dnscache.LookupHookFunc + // ConnectionOrder optionally sets ordered direct, peer-relay, and DERP + // paths for individual Tailscale peers. See [magicsock.ConnectionOrder]. + // + // This is an extension provided by the metacubex Tailscale fork. Leave it + // empty to retain standard Tailscale path selection. + ConnectionOrder []magicsock.ConnectionOrder + + // RelayPreferences is the former name of ConnectionOrder. + // + // Deprecated: use ConnectionOrder. + RelayPreferences []magicsock.RelayPreference + // AdvertiseTags specifies tags that should be applied to this node, for // purposes of ACL enforcement. These can be referenced from the ACL policy // document. Note that advertising a tag on the client doesn't guarantee @@ -930,6 +943,11 @@ func (s *Server) start() (reterr error) { } closePool.add(s.dialer) sys.Set(eng) + connectionOrder := s.ConnectionOrder + if len(connectionOrder) == 0 { + connectionOrder = s.RelayPreferences + } + sys.MagicSock.Get().SetConnectionOrder(connectionOrder) sys.HealthTracker.Get().SetMetricsRegistry(sys.UserMetricsRegistry()) // TODO(oxtoacart): do we need to support Taildrive on tsnet, and if so, how? diff --git a/wgengine/magicsock/connection_order.go b/wgengine/magicsock/connection_order.go new file mode 100644 index 000000000..96069679b --- /dev/null +++ b/wgengine/magicsock/connection_order.go @@ -0,0 +1,143 @@ +// Copyright (c) Tailscale Inc & contributors +// SPDX-License-Identifier: BSD-3-Clause + +package magicsock + +import ( + "net/netip" + "strings" + + "github.com/metacubex/tailscale/tailcfg" +) + +// ConnectionOrder configures the ordered paths to use for one peer. +// +// Target is a Tailscale IP address of the peer being contacted. Paths is an +// ordered list whose entries are "DIRECT", a Tailscale IP address of an +// eligible peer relay server, or a DERP region code (for example, "TYO"). +// Entries that are not present in the current network map are ignored. When +// Paths is empty, normal Tailscale path selection is used. +// +// Only explicitly listed data paths are used. Peer relay allocation begins +// for the listed relay servers, and the first ready path in the list wins. +// DERP failures are retried after a short backoff. +type ConnectionOrder struct { + Target netip.Addr + Paths []string +} + +// RelayPreference is the former name of ConnectionOrder. +// +// Deprecated: use ConnectionOrder. +type RelayPreference = ConnectionOrder + +type relayPreferenceSet struct { + byTarget map[netip.Addr][]string +} + +type relayPreferenceForEndpoint struct { + enabled bool + directRank int + peerRelayRanks map[netip.Addr]int + derpFallbacks []preferredDERP +} + +type preferredDERP struct { + addr netip.AddrPort + rank int +} + +func nodePrimaryTailscaleIP(n tailcfg.NodeView) netip.Addr { + var result netip.Addr + n.Addresses().All()(func(_ int, prefix netip.Prefix) bool { + if prefix.IsSingleIP() && prefix.Addr().IsValid() { + result = prefix.Addr() + return false + } + return true + }) + return result +} + +// SetConnectionOrder sets per-peer connection orders. It is safe to call +// before or after the connection is started; subsequent netmap updates apply +// the latest orders to their endpoints. +func (c *Conn) SetConnectionOrder(orders []ConnectionOrder) { + set := &relayPreferenceSet{byTarget: make(map[netip.Addr][]string, len(orders))} + for _, order := range orders { + if !order.Target.IsValid() || len(order.Paths) == 0 { + continue + } + paths := make([]string, 0, len(order.Paths)) + for _, path := range order.Paths { + if path = strings.TrimSpace(path); path != "" { + paths = append(paths, path) + } + } + if len(paths) != 0 { + set.byTarget[order.Target] = paths + } + } + c.relayPreferenceSet.Store(set) +} + +// SetRelayPreferences is the former name of SetConnectionOrder. +// +// Deprecated: use SetConnectionOrder. +func (c *Conn) SetRelayPreferences(preferences []RelayPreference) { + c.SetConnectionOrder(preferences) +} + +func (c *Conn) relayPreferenceForNode(n tailcfg.NodeView) relayPreferenceForEndpoint { + set := c.relayPreferenceSet.Load() + if set == nil || len(set.byTarget) == 0 { + return relayPreferenceForEndpoint{} + } + + var paths []string + n.Addresses().All()(func(_ int, prefix netip.Prefix) bool { + if paths = set.byTarget[prefix.Addr()]; len(paths) != 0 { + return false + } + return true + }) + if len(paths) == 0 { + return relayPreferenceForEndpoint{} + } + + preference := relayPreferenceForEndpoint{ + directRank: -1, + peerRelayRanks: make(map[netip.Addr]int), + } + dm := c.derpMapAtomic.Load() + for rank, path := range paths { + if strings.EqualFold(path, "DIRECT") { + if preference.directRank == -1 { + preference.directRank = rank + preference.enabled = true + } + continue + } + if ip, err := netip.ParseAddr(path); err == nil { + if _, exists := preference.peerRelayRanks[ip]; !exists { + preference.peerRelayRanks[ip] = rank + preference.enabled = true + } + continue + } + if dm == nil { + continue + } + for regionID, region := range dm.Regions { + if strings.EqualFold(path, region.RegionCode) { + preference.derpFallbacks = append(preference.derpFallbacks, preferredDERP{ + addr: netip.AddrPortFrom(tailcfg.DerpMagicIPAddr, uint16(regionID)), + rank: rank, + }) + preference.enabled = true + break + } + } + } + return preference +} diff --git a/wgengine/magicsock/connection_order_test.go b/wgengine/magicsock/connection_order_test.go new file mode 100644 index 000000000..38dc1e7c4 --- /dev/null +++ b/wgengine/magicsock/connection_order_test.go @@ -0,0 +1,106 @@ +// Copyright (c) Tailscale Inc & contributors +// SPDX-License-Identifier: BSD-3-Clause + +package magicsock + +import ( + "net/netip" + "testing" + "time" + + "github.com/metacubex/tailscale/tailcfg" + "github.com/metacubex/tailscale/tstime/mono" + "github.com/metacubex/tailscale/types/logger" +) + +func TestConnectionOrderForNode(t *testing.T) { + target := netip.MustParseAddr("100.120.147.123") + firstRelay := netip.MustParseAddr("100.91.245.79") + lastRelay := netip.MustParseAddr("100.67.42.33") + c := newConn(logger.Discard) + c.derpMapAtomic.Store(&tailcfg.DERPMap{Regions: map[int]*tailcfg.DERPRegion{ + 1: {RegionID: 1, RegionCode: "TYO"}, + 2: {RegionID: 2, RegionCode: "SIN"}, + 3: {RegionID: 3, RegionCode: "FRA"}, + }}) + c.SetConnectionOrder([]ConnectionOrder{{ + Target: target, + Paths: []string{firstRelay.String(), "TYO", "SIN", "FRA", lastRelay.String()}, + }}) + + node := (&tailcfg.Node{Addresses: []netip.Prefix{netip.PrefixFrom(target, target.BitLen())}}).View() + got := c.relayPreferenceForNode(node) + if !got.enabled { + t.Fatal("preference was not enabled") + } + if got.peerRelayRanks[firstRelay] != 0 || got.peerRelayRanks[lastRelay] != 4 { + t.Fatalf("peer relay ordering = %#v, want first=0 and last=4", got.peerRelayRanks) + } + wantDERP := []netip.AddrPort{ + netip.AddrPortFrom(tailcfg.DerpMagicIPAddr, 1), + netip.AddrPortFrom(tailcfg.DerpMagicIPAddr, 2), + netip.AddrPortFrom(tailcfg.DerpMagicIPAddr, 3), + } + if len(got.derpFallbacks) != len(wantDERP) { + t.Fatalf("DERP fallback count = %d, want %d", len(got.derpFallbacks), len(wantDERP)) + } + for i, want := range wantDERP { + if got.derpFallbacks[i].addr != want { + t.Errorf("DERP fallback[%d] = %v, want %v", i, got.derpFallbacks[i].addr, want) + } + } + + now := mono.Now() + ep := &endpoint{relayPreference: got} + _, derpAddr, _ := ep.addrForSendLocked(now) + if derpAddr != wantDERP[0] { + t.Fatalf("initial relay path = %v, want %v", derpAddr, wantDERP[0]) + } + ep.failedPreferredDERP = map[int]mono.Time{1: now} + _, derpAddr, _ = ep.addrForSendLocked(now) + if derpAddr != wantDERP[1] { + t.Fatalf("relay path after TYO failure = %v, want %v", derpAddr, wantDERP[1]) + } + ep.failedPreferredDERP[1] = now.Add(-preferredDERPFailureBackoff) + _, derpAddr, _ = ep.addrForSendLocked(now) + if derpAddr != wantDERP[0] { + t.Fatalf("relay path after failure backoff = %v, want %v", derpAddr, wantDERP[0]) + } +} + +func TestConnectionOrderDirect(t *testing.T) { + target := netip.MustParseAddr("100.120.147.123") + relay := netip.MustParseAddr("100.91.245.79") + c := newConn(logger.Discard) + c.derpMapAtomic.Store(&tailcfg.DERPMap{Regions: map[int]*tailcfg.DERPRegion{ + 1: {RegionID: 1, RegionCode: "TYO"}, + }}) + c.SetConnectionOrder([]ConnectionOrder{{ + Target: target, + Paths: []string{"TYO", relay.String(), "DIRECT"}, + }}) + + node := (&tailcfg.Node{Addresses: []netip.Prefix{netip.PrefixFrom(target, target.BitLen())}}).View() + order := c.relayPreferenceForNode(node) + if order.directRank != 2 { + t.Fatalf("direct rank = %d, want 2", order.directRank) + } + + now := mono.Now() + directAddr := epAddr{ap: netip.MustParseAddrPort("192.0.2.1:1234")} + ep := &endpoint{ + relayPreference: order, + bestAddr: addrQuality{epAddr: directAddr}, + trustBestAddrUntil: now.Add(time.Minute), + } + udpAddr, derpAddr, _ := ep.addrForSendLocked(now) + if udpAddr.ap.IsValid() || derpAddr.Port() != 1 { + t.Fatalf("path with TYO before direct = (%v, %v), want DERP-1", udpAddr, derpAddr) + } + + ep.failedPreferredDERP = map[int]mono.Time{0: now} + udpAddr, derpAddr, _ = ep.addrForSendLocked(now) + if udpAddr != directAddr || derpAddr.IsValid() { + t.Fatalf("path after TYO failure = (%v, %v), want direct %v", udpAddr, derpAddr, directAddr) + } +} diff --git a/wgengine/magicsock/derp.go b/wgengine/magicsock/derp.go index d5b4bbc47..31919ef21 100644 --- a/wgengine/magicsock/derp.go +++ b/wgengine/magicsock/derp.go @@ -650,6 +650,7 @@ func (c *Conn) runDerpReader(ctx context.Context, regionID int, dc *derphttp.Cli c.health.SetDERPRegionHealth(regionID, m.Problem) continue case derp.PeerGoneMessage: + c.notePreferredDERPFailure(key.NodePublic(m.Peer), regionID) switch m.Reason { case derp.PeerGoneReasonDisconnected: // Do nothing. @@ -766,6 +767,7 @@ func (c *Conn) processDERPReadResult(dm derpReadResult, b []byte) (n int, ep *en } ep.noteRecvActivity(srcAddr, mono.Now()) + ep.notePreferredDERPReachable(regionID) if update := c.connCounter.Load(); update != nil { update(0, netip.AddrPortFrom(ep.nodeAddr, 0), srcAddr.ap, 1, dm.n, true) } @@ -775,6 +777,15 @@ func (c *Conn) processDERPReadResult(dm derpReadResult, b []byte) (n int, ep *en return n, ep } +func (c *Conn) notePreferredDERPFailure(peer key.NodePublic, regionID int) { + c.mu.Lock() + ep, ok := c.peerMap.endpointForNodeKey(peer) + c.mu.Unlock() + if ok { + ep.notePreferredDERPFailure(regionID) + } +} + // 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. diff --git a/wgengine/magicsock/endpoint.go b/wgengine/magicsock/endpoint.go index 89e58f079..16e5e56cb 100644 --- a/wgengine/magicsock/endpoint.go +++ b/wgengine/magicsock/endpoint.go @@ -87,6 +87,8 @@ type endpoint struct { lastUDPRelayPathDiscovery mono.Time // last time we ran UDP relay path discovery lastDiscoKeyAdvertisement mono.Time // last time we sent a TSMPDiscoAdvertisement or not to this endpoint derpAddr netip.AddrPort // fallback/bootstrap path, if non-zero (non-zero for well-behaved clients) + relayPreference relayPreferenceForEndpoint + failedPreferredDERP map[int]mono.Time bestAddr addrQuality // best non-DERP path; zero if none; mutate via setBestAddrLocked() bestAddrAt mono.Time // time best address re-confirmed @@ -116,6 +118,21 @@ func (de *endpoint) udpRelayEndpointReady(maybeBest addrQuality) { curBestAddrTrusted := now.Before(de.trustBestAddrUntil) sameRelayServer := de.bestAddr.vni.IsSet() && maybeBest.relayServerDisco.Compare(de.bestAddr.relayServerDisco) == 0 + if de.relayPreference.enabled { + if !maybeBest.preferredRelay { + return + } + currentRank, currentOrdered := de.bestConnectionOrderRankLocked(now) + if !curBestAddrTrusted || !currentOrdered || + maybeBest.relayPreferenceRank < currentRank || + de.bestAddr.relayServerDisco.Compare(maybeBest.relayServerDisco) == 0 { + de.c.logf("magicsock: connection order: node %v now using %v", de.publicKey.ShortString(), maybeBest.epAddr) + de.setBestAddrLocked(maybeBest) + de.trustBestAddrUntil = now.Add(trustUDPAddrDuration) + } + return + } + if !curBestAddrTrusted || sameRelayServer || betterAddr(maybeBest, de.bestAddr) { @@ -135,6 +152,59 @@ func (de *endpoint) udpRelayEndpointReady(maybeBest addrQuality) { } } +func (de *endpoint) bestConnectionOrderRankLocked(now mono.Time) (rank int, ok bool) { + if now.After(de.trustBestAddrUntil) { + return 0, false + } + switch { + case de.bestAddr.epAddr.isDirect() && de.relayPreference.directRank >= 0: + return de.relayPreference.directRank, true + case de.bestAddr.vni.IsSet() && de.bestAddr.preferredRelay: + return de.bestAddr.relayPreferenceRank, true + default: + return 0, false + } +} + +const preferredDERPFailureBackoff = 30 * time.Second + +func (de *endpoint) preferredDERPFailedLocked(rank int, now mono.Time) bool { + failedAt, failed := de.failedPreferredDERP[rank] + if !failed { + return false + } + if now.Sub(failedAt) >= preferredDERPFailureBackoff { + delete(de.failedPreferredDERP, rank) + return false + } + return true +} + +func (de *endpoint) notePreferredDERPFailure(regionID int) { + de.mu.Lock() + defer de.mu.Unlock() + for _, derp := range de.relayPreference.derpFallbacks { + if int(derp.addr.Port()) == regionID { + if de.failedPreferredDERP == nil { + de.failedPreferredDERP = make(map[int]mono.Time) + } + de.failedPreferredDERP[derp.rank] = mono.Now() + return + } + } +} + +func (de *endpoint) notePreferredDERPReachable(regionID int) { + de.mu.Lock() + defer de.mu.Unlock() + for _, derp := range de.relayPreference.derpFallbacks { + if int(derp.addr.Port()) == regionID { + delete(de.failedPreferredDERP, derp.rank) + return + } + } +} + func (de *endpoint) setBestAddrLocked(v addrQuality) { if v.epAddr != de.bestAddr.epAddr { de.probeUDPLifetime.resetCycleEndpointLocked() @@ -575,6 +645,21 @@ func (de *endpoint) DstToBytes() []byte { return packIPPort(de.fakeWGAddr) } // TODO(val): Rewrite the addrFor*Locked() variations to share code. func (de *endpoint) addrForSendLocked(now mono.Time) (udpAddr epAddr, derpAddr netip.AddrPort, sendWGPing bool) { udpAddr = de.bestAddr.epAddr + if de.relayPreference.enabled { + peerRelayRank := math.MaxInt + if rank, ok := de.bestConnectionOrderRankLocked(now); ok { + peerRelayRank = rank + } + for _, derp := range de.relayPreference.derpFallbacks { + if !de.preferredDERPFailedLocked(derp.rank, now) && derp.rank < peerRelayRank { + return epAddr{}, derp.addr, false + } + } + if peerRelayRank != math.MaxInt { + return udpAddr, netip.AddrPort{}, false + } + return epAddr{}, netip.AddrPort{}, false + } if udpAddr.ap.IsValid() && !now.After(de.trustBestAddrUntil) { return udpAddr, netip.AddrPort{}, false @@ -890,7 +975,7 @@ func (de *endpoint) discoverUDPRelayPathsLocked(now mono.Time) { de.lastUDPRelayPathDiscovery = now lastBest := de.bestAddr lastBestIsTrusted := mono.Now().Before(de.trustBestAddrUntil) - de.c.relayManager.startUDPRelayPathDiscoveryFor(de, lastBest, lastBestIsTrusted) + de.c.relayManager.startUDPRelayPathDiscoveryFor(de, lastBest, lastBestIsTrusted, de.relayPreference) } // wantUDPRelayPathDiscoveryLocked reports whether we should kick off UDP relay @@ -1051,6 +1136,15 @@ func (de *endpoint) send(buffs [][]byte, offset int) error { now := mono.Now() udpAddr, derpAddr, startWGPing := de.addrForSendLocked(now) + derpRank := -1 + if de.relayPreference.enabled { + for _, preferred := range de.relayPreference.derpFallbacks { + if preferred.addr == derpAddr { + derpRank = preferred.rank + break + } + } + } if de.isWireguardOnly { if startWGPing { @@ -1124,11 +1218,14 @@ func (de *endpoint) send(buffs [][]byte, offset int) error { buff = buff[offset:] const isDisco = false const isGeneveEncap = false - ok, _ := de.c.sendAddr(derpAddr, de.publicKey, buff, isDisco, isGeneveEncap) + ok, sendErr := de.c.sendAddr(derpAddr, de.publicKey, buff, isDisco, isGeneveEncap) txBytes += len(buff) if !ok { allOk = false } + if err == nil { + err = sendErr + } } if update := de.c.connCounter.Load(); update != nil { @@ -1137,6 +1234,9 @@ func (de *endpoint) send(buffs [][]byte, offset int) error { if allOk { return nil } + if derpRank >= 0 { + de.notePreferredDERPFailure(int(derpAddr.Port())) + } } return err } @@ -1537,6 +1637,8 @@ func (de *endpoint) updateFromNode(n tailcfg.NodeView, heartbeatDisabled bool, p } de.derpAddr = newDerp } + de.relayPreference = de.c.relayPreferenceForNode(n) + de.failedPreferredDERP = nil de.setEndpointsLocked(n.Endpoints()) @@ -1781,7 +1883,18 @@ func (de *endpoint) handlePongConnLocked(m *disco.Pong, di *discoInfo, src epAdd wireMTU: pingSizeToPktLen(sp.size, sp.to), } bestUntrusted := now.After(de.trustBestAddrUntil) - if betterAddr(thisPong, de.bestAddr) || bestUntrusted { + useThisPong := betterAddr(thisPong, de.bestAddr) || bestUntrusted + if de.relayPreference.enabled { + if de.relayPreference.directRank < 0 { + useThisPong = false + } else if currentRank, currentOrdered := de.bestConnectionOrderRankLocked(now); !currentOrdered { + useThisPong = true + } else { + useThisPong = de.bestAddr.epAddr == thisPong.epAddr || + de.relayPreference.directRank < currentRank + } + } + if useThisPong { 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(), @@ -1830,9 +1943,11 @@ func (e epAddr) String() string { // is associated, a round-trip latency measurement, and path mtu. type addrQuality struct { epAddr - relayServerDisco key.DiscoPublic // only relevant if epAddr.vni.isSet(), otherwise zero value - latency time.Duration - wireMTU tstun.WireMTU + relayServerDisco key.DiscoPublic // only relevant if epAddr.vni.isSet(), otherwise zero value + preferredRelay bool // whether this path came from an explicit connection order + relayPreferenceRank int // only relevant when preferredRelay is true + latency time.Duration + wireMTU tstun.WireMTU } func (a addrQuality) String() string { diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go index 4356f5579..463306142 100644 --- a/wgengine/magicsock/magicsock.go +++ b/wgengine/magicsock/magicsock.go @@ -256,6 +256,9 @@ type Conn struct { // sync.Mutex. For use with NewRegionClient's callback, to avoid // lock ordering deadlocks. See issue 3726 and mu field docs. derpMapAtomic atomic.Pointer[tailcfg.DERPMap] + // relayPreferenceSet contains optional per-peer connection orders. It is + // immutable after publication. + relayPreferenceSet atomic.Pointer[relayPreferenceSet] lastNetCheckReport atomic.Pointer[netcheck.Report] @@ -2937,6 +2940,7 @@ func (c *Conn) updateRelayServersSet(filt *filter.Filter, self tailcfg.NodeView, nodeKey: maybeCandidate.Key(), discoKey: maybeCandidate.DiscoKey(), derpHomeRegionID: uint16(maybeCandidate.HomeDERP()), + tailscaleIP: nodePrimaryTailscaleIP(maybeCandidate), }) } // [relayManager]'s run loop updates [relayManager.hasPeerRelayServers] @@ -2986,6 +2990,7 @@ type candidatePeerRelay struct { nodeKey key.NodePublic discoKey key.DiscoPublic derpHomeRegionID uint16 + tailscaleIP netip.Addr } func (c *candidatePeerRelay) isValid() bool { @@ -3337,6 +3342,7 @@ func (c *Conn) relayCandidateLocked(p tailcfg.NodeView) (ok bool, cp candidatePe nodeKey: p.Key(), discoKey: p.DiscoKey(), derpHomeRegionID: uint16(p.HomeDERP()), + tailscaleIP: nodePrimaryTailscaleIP(p), } } diff --git a/wgengine/magicsock/relaymanager.go b/wgengine/magicsock/relaymanager.go index 021a427fa..4e4010da7 100644 --- a/wgengine/magicsock/relaymanager.go +++ b/wgengine/magicsock/relaymanager.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "net/netip" + "sort" "sync" "sync/atomic" "time" @@ -89,9 +90,11 @@ type serverDiscoVNI struct { // relayHandshakeWork serves to track in-progress relay handshake work for a // [udprelay.ServerEndpoint]. This structure is immutable once initialized. type relayHandshakeWork struct { - wlb endpointWithLastBest - se udprelay.ServerEndpoint - server candidatePeerRelay + wlb endpointWithLastBest + se udprelay.ServerEndpoint + server candidatePeerRelay + preferenceRank int + preferred bool handshakeGen uint32 @@ -132,9 +135,11 @@ func (r *relayHandshakeWork) dlogf(format string, args ...any) { // [disco.CallMeMaybeVia] reception. This structure is immutable once // initialized. type newRelayServerEndpointEvent struct { - wlb endpointWithLastBest - se udprelay.ServerEndpoint - server candidatePeerRelay // zero value if learned via [disco.CallMeMaybeVia] + wlb endpointWithLastBest + se udprelay.ServerEndpoint + server candidatePeerRelay // zero value if learned via [disco.CallMeMaybeVia] + preferenceRank int // -1 if not from an explicit preference + preferred bool } // relayEndpointAllocWorkDoneEvent indicates relay server endpoint allocation @@ -332,6 +337,8 @@ type relayEndpointAllocWork struct { wlb endpointWithLastBest discoKeys key.SortedPairOfDiscoPublic candidatePeerRelay candidatePeerRelay // zero value if learned via [disco.CallMeMaybeVia] + preferenceRank int // -1 when no ordered preference applies + preferred bool allocGen uint32 @@ -536,15 +543,17 @@ type endpointWithLastBest struct { ep *endpoint lastBest addrQuality lastBestIsTrusted bool + relayPreference relayPreferenceForEndpoint } // startUDPRelayPathDiscoveryFor starts UDP relay path discovery for ep on all // known relay servers if ep has no in-progress work. -func (r *relayManager) startUDPRelayPathDiscoveryFor(ep *endpoint, lastBest addrQuality, lastBestIsTrusted bool) { +func (r *relayManager) startUDPRelayPathDiscoveryFor(ep *endpoint, lastBest addrQuality, lastBestIsTrusted bool, relayPreference relayPreferenceForEndpoint) { relayManagerInputEvent(r, nil, &r.startDiscoveryCh, endpointWithLastBest{ ep: ep, lastBest: lastBest, lastBestIsTrusted: lastBestIsTrusted, + relayPreference: relayPreference, }) } @@ -699,9 +708,11 @@ func (r *relayManager) handleAllocWorkDoneRunLoop(done relayEndpointAllocWorkDon } if !done.allocated.ServerDisco.IsZero() { r.handleNewServerEndpointRunLoop(newRelayServerEndpointEvent{ - wlb: done.work.wlb, - se: done.allocated, - server: done.work.candidatePeerRelay, + wlb: done.work.wlb, + se: done.allocated, + server: done.work.candidatePeerRelay, + preferenceRank: done.work.preferenceRank, + preferred: done.work.preferred, }) } } @@ -737,10 +748,12 @@ func (r *relayManager) handleHandshakeWorkDoneRunLoop(done relayEndpointHandshak // deadlocks as it acquires [endpoint] & [Conn] mutexes. See [relayManager] // docs for details. go done.work.wlb.ep.udpRelayEndpointReady(addrQuality{ - epAddr: addr, - relayServerDisco: done.work.se.ServerDisco, - latency: done.latency, - wireMTU: pingSizeToPktLen(0, addr), + epAddr: addr, + relayServerDisco: done.work.se.ServerDisco, + preferredRelay: done.work.preferred, + relayPreferenceRank: done.work.preferenceRank, + latency: done.latency, + wireMTU: pingSizeToPktLen(0, addr), }) } @@ -817,13 +830,15 @@ func (r *relayManager) handleNewServerEndpointRunLoop(newServerEndpoint newRelay // We're ready to start a new handshake. ctx, cancel := context.WithCancel(context.Background()) work := &relayHandshakeWork{ - wlb: newServerEndpoint.wlb, - se: newServerEndpoint.se, - server: newServerEndpoint.server, - rxDiscoMsgCh: make(chan relayDiscoMsgEvent), - doneCh: make(chan relayEndpointHandshakeWorkDoneEvent, 1), - ctx: ctx, - cancel: cancel, + wlb: newServerEndpoint.wlb, + se: newServerEndpoint.se, + server: newServerEndpoint.server, + preferenceRank: newServerEndpoint.preferenceRank, + preferred: newServerEndpoint.preferred, + rxDiscoMsgCh: make(chan relayDiscoMsgEvent), + doneCh: make(chan relayEndpointHandshakeWorkDoneEvent, 1), + ctx: ctx, + cancel: cancel, } // We must look up byServerDisco again. The previous value may have been // deleted from the outer map when cleaning up duplicate work. @@ -1097,8 +1112,27 @@ func (r *relayManager) allocateAllServersRunLoop(wlb endpointWithLastBest) { if remoteDisco == nil { return } + type candidateWithRank struct { + candidate candidatePeerRelay + rank int + } + candidates := make([]candidateWithRank, 0, len(r.serversByNodeKey)) + for _, candidate := range r.serversByNodeKey { + rank := -1 + if wlb.relayPreference.enabled { + var ok bool + rank, ok = wlb.relayPreference.peerRelayRanks[candidate.tailscaleIP] + if !ok { + continue + } + } + candidates = append(candidates, candidateWithRank{candidate, rank}) + } + sort.Slice(candidates, func(i, j int) bool { return candidates[i].rank < candidates[j].rank }) + discoKeys := key.NewSortedPairOfDiscoPublic(wlb.ep.c.discoAtomic.Public(), remoteDisco.key) - for _, v := range r.serversByNodeKey { + for _, candidate := range candidates { + v := candidate.candidate byDiscoKeys, ok := r.allocWorkByDiscoKeysByServerNodeKey[v.nodeKey] if !ok { byDiscoKeys = make(map[key.SortedPairOfDiscoPublic]*relayEndpointAllocWork) @@ -1118,6 +1152,8 @@ func (r *relayManager) allocateAllServersRunLoop(wlb endpointWithLastBest) { wlb: wlb, discoKeys: discoKeys, candidatePeerRelay: v, + preferenceRank: candidate.rank, + preferred: candidate.rank >= 0, rxDiscoMsgCh: make(chan *disco.AllocateUDPRelayEndpointResponse), doneCh: make(chan relayEndpointAllocWorkDoneEvent, 1), ctx: ctx, From 3b38f58ab523840481c860811a16996bd388691a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:12:37 +0000 Subject: [PATCH 2/2] .github: Bump github/codeql-action/analyze from 4.34.1 to 4.37.3 Bumps [github/codeql-action/analyze](https://github.com/github/codeql-action) from 4.34.1 to 4.37.3. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/38697555549f1db7851b81482ff19f1fa5c4fedc...e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81) --- updated-dependencies: - dependency-name: github/codeql-action/analyze dependency-version: 4.37.3 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index abe6a2c3a..70b95d2ab 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -80,4 +80,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 + uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3