mirror of
https://github.com/tailscale/tailscale.git
synced 2025-12-01 17:49:02 +00:00
ipn/ipnlocal: don't panic if there are no suitable exit nodes
In suggestExitNodeLocked, if no exit node candidates have a home DERP or
valid location info, `bestCandidates` is an empty slice. This slice is
passed to `selectNode` (`randomNode` in prod):
```go func randomNode(nodes views.Slice[tailcfg.NodeView], …) tailcfg.NodeView {
…
return nodes.At(rand.IntN(nodes.Len()))
}
```
An empty slice becomes a call to `rand.IntN(0)`, which panics.
This patch changes the behaviour, so if we've filtered out all the
candidates before calling `selectNode`, reset the list and then pick
from any of the available candidates.
This patch also updates our tests to give us more coverage of `randomNode`,
so we can spot other potential issues.
Updates #17661
Change-Id: I63eb5e4494d45a1df5b1f4b1b5c6d5576322aa72
Signed-off-by: Alex Chan <alexc@tailscale.com>
This commit is contained in:
@@ -7432,6 +7432,16 @@ func suggestExitNodeUsingDERP(report *netcheck.Report, nb *nodeBackend, prevSugg
|
||||
}
|
||||
}
|
||||
bestCandidates := pickWeighted(pickFrom)
|
||||
|
||||
// We may have an empty list of candidates here, if none of the candidates
|
||||
// have home DERP info.
|
||||
//
|
||||
// We know that candidates is non-empty or we'd already have returned, so if
|
||||
// we've filtered everything out of bestCandidates, just use candidates.
|
||||
if len(bestCandidates) == 0 {
|
||||
bestCandidates = candidates
|
||||
}
|
||||
|
||||
chosen := selectNode(views.SliceOf(bestCandidates), prevSuggestion)
|
||||
if !chosen.Valid() {
|
||||
return res, errors.New("chosen candidate invalid: this is a bug")
|
||||
|
||||
@@ -4436,6 +4436,14 @@ func deterministicRegionForTest(t testing.TB, want views.Slice[int], use int) se
|
||||
}
|
||||
}
|
||||
|
||||
// deterministicNodeForTest returns a deterministic selectNodeFunc, which
|
||||
// allows us to make stable assertions about which exit node will be chosen
|
||||
// from a list of possible candidates.
|
||||
//
|
||||
// When given a list of candidates, it checks that `use` is in the list and
|
||||
// returns that.
|
||||
//
|
||||
// It verifies that `wantLast` was passed to `selectNode(…, want)`.
|
||||
func deterministicNodeForTest(t testing.TB, want views.Slice[tailcfg.StableNodeID], wantLast tailcfg.StableNodeID, use tailcfg.StableNodeID) selectNodeFunc {
|
||||
t.Helper()
|
||||
|
||||
@@ -4444,6 +4452,16 @@ func deterministicNodeForTest(t testing.TB, want views.Slice[tailcfg.StableNodeI
|
||||
}
|
||||
|
||||
return func(got views.Slice[tailcfg.NodeView], last tailcfg.StableNodeID) tailcfg.NodeView {
|
||||
// In the tests, we choose nodes deterministically so we can get
|
||||
// stable results, but in the real code, we choose nodes randomly.
|
||||
//
|
||||
// Call the randomNode function anyway, and ensure it returns
|
||||
// a sensible result.
|
||||
view := randomNode(got, last)
|
||||
if !views.SliceContains(got, view) {
|
||||
t.Fatalf("randomNode returns an unexpected node")
|
||||
}
|
||||
|
||||
var ret tailcfg.NodeView
|
||||
|
||||
gotIDs := make([]tailcfg.StableNodeID, got.Len())
|
||||
@@ -4529,6 +4547,7 @@ func TestSuggestExitNode(t *testing.T) {
|
||||
Longitude: -97.3325,
|
||||
Priority: 100,
|
||||
}
|
||||
var emptyLocation *tailcfg.Location
|
||||
|
||||
peer1 := makePeer(1,
|
||||
withExitRoutes(),
|
||||
@@ -4568,6 +4587,18 @@ func TestSuggestExitNode(t *testing.T) {
|
||||
withExitRoutes(),
|
||||
withSuggest(),
|
||||
withLocation(fortWorthLowPriority.View()))
|
||||
emptyLocationPeer9 := makePeer(9,
|
||||
withoutDERP(),
|
||||
withExitRoutes(),
|
||||
withSuggest(),
|
||||
withLocation(emptyLocation.View()),
|
||||
)
|
||||
emptyLocationPeer10 := makePeer(10,
|
||||
withoutDERP(),
|
||||
withExitRoutes(),
|
||||
withSuggest(),
|
||||
withLocation(emptyLocation.View()),
|
||||
)
|
||||
|
||||
selfNode := tailcfg.Node{
|
||||
Addresses: []netip.Prefix{
|
||||
@@ -4898,6 +4929,31 @@ func TestSuggestExitNode(t *testing.T) {
|
||||
wantName: "San Jose",
|
||||
wantLocation: sanJose.View(),
|
||||
},
|
||||
{
|
||||
// Regression test for https://github.com/tailscale/tailscale/issues/17661
|
||||
name: "exit nodes with no home DERP, randomly selected",
|
||||
lastReport: &netcheck.Report{
|
||||
RegionLatency: map[int]time.Duration{
|
||||
1: 10,
|
||||
2: 20,
|
||||
3: 10,
|
||||
},
|
||||
PreferredDERP: 1,
|
||||
},
|
||||
netMap: &netmap.NetworkMap{
|
||||
SelfNode: selfNode.View(),
|
||||
DERPMap: defaultDERPMap,
|
||||
Peers: []tailcfg.NodeView{
|
||||
emptyLocationPeer9,
|
||||
emptyLocationPeer10,
|
||||
},
|
||||
},
|
||||
wantRegions: []int{1, 2},
|
||||
wantName: "peer9",
|
||||
wantNodes: []tailcfg.StableNodeID{"stable9", "stable10"},
|
||||
wantID: "stable9",
|
||||
useRegion: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
Reference in New Issue
Block a user