Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ jobs:
- name: Save Cache
# Save cache even on failure, but only on cache miss and main branch to avoid thrashing.
if: always() && steps.restore-cache.outputs.cache-hit != 'true' && github.ref == 'refs/heads/main'
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
# Note: this is only saving the build cache. Mod cache is shared amongst
# all jobs in the workflow.
Expand Down Expand Up @@ -334,7 +334,7 @@ jobs:
- name: Save Cache
# Save cache even on failure, but only on cache miss and main branch to avoid thrashing.
if: always() && steps.restore-cache.outputs.cache-hit != 'true' && github.ref == 'refs/heads/main'
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/Library/Caches/go-build
key: ${{ runner.os }}-go-test-${{ hashFiles('**/go.sum') }}-${{ github.job }}-${{ github.run_id }}
Expand Down Expand Up @@ -472,7 +472,7 @@ jobs:
- name: Save Cache
# Save cache even on failure, but only on cache miss and main branch to avoid thrashing.
if: always() && steps.restore-cache.outputs.cache-hit != 'true' && github.ref == 'refs/heads/main'
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
# Note: this is only saving the build cache. Mod cache is shared amongst
# all jobs in the workflow.
Expand Down Expand Up @@ -564,7 +564,7 @@ jobs:
- name: Save Cache
# Save cache even on failure, but only on cache miss and main branch to avoid thrashing.
if: always() && steps.restore-cache.outputs.cache-hit != 'true' && github.ref == 'refs/heads/main'
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
# Note: this is only saving the build cache. Mod cache is shared amongst
# all jobs in the workflow.
Expand Down Expand Up @@ -650,7 +650,7 @@ jobs:
- name: Save Cache
# Save cache even on failure, but only on cache miss and main branch to avoid thrashing.
if: always() && steps.restore-cache.outputs.cache-hit != 'true' && github.ref == 'refs/heads/main'
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
# Note: this is only saving the build cache. Mod cache is shared amongst
# all jobs in the workflow.
Expand Down
18 changes: 18 additions & 0 deletions tsnet/tsnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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?
Expand Down
143 changes: 143 additions & 0 deletions wgengine/magicsock/connection_order.go
Original file line number Diff line number Diff line change
@@ -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
}
106 changes: 106 additions & 0 deletions wgengine/magicsock/connection_order_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
11 changes: 11 additions & 0 deletions wgengine/magicsock/derp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
}
Expand All @@ -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.
Expand Down
Loading
Loading