ipn/ipnlocal: parse priority out of suggest-exit-node capability

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 <sfllaw@tailscale.com>
This commit is contained in:
Simon Law
2025-08-04 11:51:42 -07:00
parent 0c6bf30585
commit b280aa9c44
3 changed files with 104 additions and 1 deletions

View File

@@ -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 peers location priority.
if hi := n.Hostinfo(); hi.Valid() {
if loc := hi.Location(); loc.Valid() {
s = loc.Priority()
}
}
SetScore:
scores[id] = s
}
return s

View File

@@ -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{

13
tailcfg/traffic.go Normal file
View File

@@ -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"`
}