From b280aa9c44844d833d8a27084b4be8ed326fb561 Mon Sep 17 00:00:00 2001 From: Simon Law Date: Mon, 4 Aug 2025 11:51:42 -0700 Subject: [PATCH] ipn/ipnlocal: parse priority out of suggest-exit-node capability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the `experimental:exit-node-steering` attribute is enabled for a node, it is likely that the priority score for an exit node is different when considering it as a suggested exit node versus considering it for the routers / connectors that it hosts. In order to distinguish between these two, the network map can now contain the priority score for suggesting the exit node in the `suggest-exit-node` capability, in that peer’s CapMap: "CapMap": { "suggest-exit-node": [ { "Priority": 42 } ] } Updates tailscale/corp#31011 Signed-off-by: Simon Law --- ipn/ipnlocal/local.go | 22 +++++++++++- ipn/ipnlocal/local_test.go | 70 ++++++++++++++++++++++++++++++++++++++ tailcfg/traffic.go | 13 +++++++ 3 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 tailcfg/traffic.go diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 5fb3d5771..6b8b35b53 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -7952,12 +7952,32 @@ func suggestExitNodeUsingTrafficSteering(nb *nodeBackend, allowed set.Set[tailcf id := n.ID() s, ok := scores[id] if !ok { - s = 0 // score of zero means incomparable + // score of zero means incomparable + s = 0 + + // Prefer the priority in the suggest-exit-node peer cap. + if caps, ok := n.CapMap().GetOk(tailcfg.NodeAttrSuggestExitNode); ok { + for _, cap := range caps.All() { + var c tailcfg.SuggestExitNode + if err := json.Unmarshal([]byte(cap), &c); err != nil { + break + } + if c.Priority == 0 { + break + } + s = c.Priority + goto SetScore + } + } + + // Fallback on the peer’s location priority. if hi := n.Hostinfo(); hi.Valid() { if loc := hi.Location(); loc.Valid() { s = loc.Priority() } } + + SetScore: scores[id] = s } return s diff --git a/ipn/ipnlocal/local_test.go b/ipn/ipnlocal/local_test.go index b7fe7f9d7..335c0c1e1 100644 --- a/ipn/ipnlocal/local_test.go +++ b/ipn/ipnlocal/local_test.go @@ -5076,6 +5076,76 @@ func TestSuggestExitNodeTrafficSteering(t *testing.T) { wantID: "stable3", wantName: "peer3", }, + { + name: "exit-node-empty-suggestions", + netMap: &netmap.NetworkMap{ + SelfNode: selfNode.View(), + Peers: []tailcfg.NodeView{ + makePeer(1, + withExitRoutes(), + withSuggest(nil)), + makePeer(2, + withExitRoutes(), + withSuggest([]tailcfg.RawMessage{})), + makePeer(3, + withExitRoutes(), + withSuggest([]tailcfg.RawMessage{ + `{}`, + })), + makePeer(4, + withExitRoutes(), + withSuggest([]tailcfg.RawMessage{ + `{"Priority": 0}`, + })), + }, + }, + // Change this, if the hashing function changes. + wantID: "stable3", + wantName: "peer3", + }, + { + name: "exit-node-with-suggestion-and-priority", + netMap: &netmap.NetworkMap{ + SelfNode: selfNode.View(), + Peers: []tailcfg.NodeView{ + makePeer(1, + withExitRoutes(), + withSuggest([]tailcfg.RawMessage{ + `{"Priority": 3}`, + }), + withLocationPriority(1)), // overridden + makePeer(2, + withExitRoutes(), + withLocationPriority(2)), + }, + }, + wantID: "stable1", + wantName: "peer1", + wantPri: 1, // Location.Priority + }, + { + name: "exit-node-with-priority", + netMap: &netmap.NetworkMap{ + SelfNode: selfNode.View(), + Peers: []tailcfg.NodeView{ + makePeer(1, + withExitRoutes(), + withSuggest(nil)), + makePeer(2, + withExitRoutes(), + withSuggest(nil)), + makePeer(3, + withExitRoutes(), + withSuggest(nil)), + makePeer(4, + withExitRoutes(), + withSuggest(nil)), + }, + }, + // Change this, if the hashing function changes. + wantID: "stable3", + wantName: "peer3", + }, { name: "exit-nodes-with-and-without-priority", netMap: &netmap.NetworkMap{ diff --git a/tailcfg/traffic.go b/tailcfg/traffic.go new file mode 100644 index 000000000..224a39bed --- /dev/null +++ b/tailcfg/traffic.go @@ -0,0 +1,13 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package tailcfg + +type SuggestExitNode struct { + // Priority is the relative priority of this exit node. Nodes with a + // higher priority are preferred over nodes with a lower priority, nodes + // of equal probability may be selected arbitrarily. A priority of 0 + // means the exit node has no a priority preference and a negative + // priority is not allowed. + Priority int `json:",omitempty"` +}