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:
Alex Chan
2025-11-25 10:22:08 +00:00
committed by Alex Chan
parent f4a4bab105
commit b38dd1ae06
2 changed files with 66 additions and 0 deletions
+10
View File
@@ -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")