net/traffic,ipn/ipnlocal: extract traffic steering utilities (#19682)

The traffic package contains helpers for evaluating traffic steering
scores and picking appropriate nodes. These were extracted from
ipnlocal.suggestExitNodeUsingTrafficSteering so they can be reused by
the new routecheck package to probe exit nodes in priority order.

Updates #17366
Updates tailscale/corp#33033

Signed-off-by: Simon Law <sfllaw@tailscale.com>
This commit is contained in:
Simon Law
2026-05-21 08:28:27 -07:00
committed by GitHub
parent dbe92f98b5
commit 7ebca58042
9 changed files with 276 additions and 43 deletions
+5 -43
View File
@@ -9,8 +9,6 @@ import (
"bufio"
"cmp"
"context"
"crypto/sha256"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
@@ -64,6 +62,7 @@ import (
"tailscale.com/net/netns"
"tailscale.com/net/netutil"
"tailscale.com/net/packet"
"tailscale.com/net/traffic"
"tailscale.com/net/tsaddr"
"tailscale.com/net/tsdial"
"tailscale.com/paths"
@@ -8133,42 +8132,18 @@ func suggestExitNodeUsingTrafficSteering(nb *nodeBackend, allowed set.Set[tailcf
return true
})
scores := make(map[tailcfg.NodeID]int, len(nodes))
score := func(n tailcfg.NodeView) int {
id := n.ID()
s, ok := scores[id]
if !ok {
s = 0 // score of zero means incomparable
if hi := n.Hostinfo(); hi.Valid() {
if loc := hi.Location(); loc.Valid() {
s = loc.Priority()
}
}
scores[id] = s
}
return s
}
rdvHash := makeRendezvousHasher(self.ID())
ss := traffic.ScoresFor(self.ID(), nodes)
var pick tailcfg.NodeView
if len(nodes) == 1 {
pick = nodes[0]
}
if len(nodes) > 1 {
// Find the highest scoring exit nodes.
slices.SortFunc(nodes, func(a, b tailcfg.NodeView) int {
c := cmp.Compare(score(b), score(a)) // Highest score first.
if c == 0 {
// Rendezvous hashing for reliably picking the
// same node from a list: tailscale/tailscale#16551.
return cmp.Compare(rdvHash(b.ID()), rdvHash(a.ID()))
}
return c
})
ss.SortNodes(nodes)
// TODO(sfllaw): add a temperature knob so that this client has
// a chance of picking the next best option.
pick = nodes[0]
pick = nodes[0] // Pick the highest score.
}
nb.logf("netmap: traffic steering: exit node scores: %v", logger.ArgWriter(func(bw *bufio.Writer) {
@@ -8182,7 +8157,7 @@ func suggestExitNodeUsingTrafficSteering(nb *nodeBackend, allowed set.Set[tailcf
bw.WriteString(", ")
}
name, _, _ := strings.Cut(n.Name(), ".")
fmt.Fprintf(bw, "%d:%s", score(n), name)
fmt.Fprintf(bw, "%d:%s", ss.Score(n), name)
}
}))
@@ -8284,19 +8259,6 @@ func longLatDistance(fromLat, fromLong, toLat, toLong float64) float64 {
return earthRadiusMeters * c
}
// makeRendezvousHasher returns a function that hashes a node ID to a uint64.
// https://en.wikipedia.org/wiki/Rendezvous_hashing
func makeRendezvousHasher(seed tailcfg.NodeID) func(tailcfg.NodeID) uint64 {
en := binary.BigEndian
return func(n tailcfg.NodeID) uint64 {
var b [16]byte
en.PutUint64(b[:], uint64(seed))
en.PutUint64(b[8:], uint64(n))
v := sha256.Sum256(b[:])
return en.Uint64(v[:])
}
}
const (
// unresolvedExitNodeID is a special [tailcfg.StableNodeID] value
// used as an exit node ID to install a blackhole route, preventing