ipn/ipnlocal: use routecheck reports to make exit node suggestions

Now that the routecheck subsystem is continuously collecting
reachability reports in the background, we can add a hook to
LocalBackend for fetching its report. That allows
suggestExitNodeUsingTrafficSteering to consult that report when
disqualifying candidates, instead of blocking on an immediate probe.

Exit node suggestions will only consult the report when the
`client-side-reachability` and `client-side-reachability-routecheck`
node attributes are both set on the current node.

Updates #17366
Updates tailscale/corp#33033

Signed-off-by: Simon Law <sfllaw@tailscale.com>
This commit is contained in:
Simon Law
2026-07-02 20:26:27 -07:00
committed by Simon Law
parent cb7e536804
commit 932260511e
19 changed files with 306 additions and 74 deletions
+115 -33
View File
@@ -6,13 +6,18 @@ package ipnlocal
import (
"context"
"errors"
"maps"
"slices"
"testing"
"time"
"tailscale.com/net/routecheck/peernode"
"tailscale.com/tailcfg"
"tailscale.com/tstest"
"tailscale.com/types/netmap"
"tailscale.com/util/eventbus"
"tailscale.com/util/mak"
"tailscale.com/util/set"
)
func TestNodeBackendReadiness(t *testing.T) {
@@ -134,58 +139,135 @@ func TestNodeBackendReachability(t *testing.T) {
//
// When disabled, the client relies on the control plane sending
// an accurate peer.Online flag. When enabled, the client
// ignores peer.Online and determines whether it can reach the
// peer node.
// ignores peer.Online and is forced to return true.
cap bool
// rchk sets [tailcfg.NodeAttrClientSideReachabilityRouteCheck]
// on the self node.
//
// When enabled with [tailcfg.NodeAttrClientSideReachability]
// above, the client ignores peer.Online and determines whether
// it can reach the peer node using [routecheck] reports.
rchk bool
peer tailcfg.Node
want bool
online bool
pong peernode.Reachability
want bool
}{
{
name: "disabled/offline",
cap: false,
peer: tailcfg.Node{
Online: new(false),
},
want: false,
name: "disabled/offline",
cap: false,
online: false,
want: false,
},
{
name: "disabled/online",
cap: false,
peer: tailcfg.Node{
Online: new(true),
},
want: true,
name: "disabled/online",
cap: false,
online: true,
want: true,
},
{
name: "enabled/offline",
cap: true,
peer: tailcfg.Node{
Online: new(false),
},
want: true,
name: "forced/offline",
cap: true,
rchk: false,
online: false,
want: true,
},
{
name: "enabled/online",
cap: true,
peer: tailcfg.Node{
Online: new(true),
},
want: true,
name: "forced/online",
cap: true,
rchk: false,
online: true,
want: true,
},
{
name: "routecheck/offline/needs-probe",
cap: true,
rchk: true,
online: false,
pong: peernode.Unknown,
want: false,
},
{
name: "routecheck/offline/unreachable",
cap: true,
rchk: true,
online: false,
pong: peernode.Unreachable,
want: false,
},
{
name: "routecheck/offline/reachable",
cap: true,
rchk: true,
online: false,
pong: peernode.Reachable,
want: true,
},
{
name: "routecheck/online/needs-probe",
cap: true,
rchk: true,
online: true,
pong: peernode.Unknown,
want: true,
},
{
name: "routecheck/online/unreachable",
cap: true,
rchk: true,
online: true,
pong: peernode.Unreachable,
want: false,
},
{
name: "routecheck/online/reachable",
cap: true,
rchk: true,
online: true,
pong: peernode.Reachable,
want: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
nb := newNodeBackend(t.Context(), tstest.WhileTestRunningLogger(t), eventbus.New())
nb.netMap = &netmap.NetworkMap{}
self := &tailcfg.Node{
ID: 1,
StableID: "stable1",
Name: "self",
}
if tc.cap {
nb.netMap.AllCaps.Make()
nb.netMap.AllCaps.Add(tailcfg.NodeAttrClientSideReachability)
mak.Set(&self.CapMap, tailcfg.NodeAttrClientSideReachability, nil)
}
if tc.rchk {
mak.Set(&self.CapMap, tailcfg.NodeAttrClientSideReachabilityRouteCheck, nil)
}
got := nb.PeerIsReachable(t.Context(), tc.peer.View())
peer := &tailcfg.Node{
ID: 2,
StableID: "stable2",
Name: "peer",
Online: &tc.online,
}
nb := newNodeBackend(t.Context(), tstest.WhileTestRunningLogger(t), eventbus.New())
nb.netMap = &netmap.NetworkMap{
SelfNode: self.View(),
Peers: []tailcfg.NodeView{peer.View()},
// HACK: AllCaps is usually populated by Control
AllCaps: set.SetOf(slices.Collect(maps.Keys(self.CapMap))),
}
got := nb.PeerIsReachable(routecheckReport(tc.pong), peer.View())
if got != tc.want {
t.Errorf("got %v, want %v", got, tc.want)
}
})
}
}
type routecheckReport peernode.Reachability
var _ RouteCheckReport = *new(routecheckReport)
func (rp routecheckReport) IsReachable(_ tailcfg.NodeID) peernode.Reachability {
return peernode.Reachability(rp)
}