ipn/ipnlocal: consider all DERP regions for exit node recommendations

When recommending an exit node, suggestExitNodeLocked ranks candidates by
the latency to their home DERP region, taken from the most recent netcheck
report. But netcheck alternates between full reports, which probe every
region, and incremental reports, which only re-probe the home region and a
handful of the fastest regions. When the most recent report is incremental,
the suggestion fell back to a random for exit nodes that are far away.

Now we rank candidates against the best recent latency, tracked by the
`netcheck.Client` - the same data that is used to pick the preferred
DERP. It uses a history of measurements which includes a full netcheck
report, so should cover all DERP regions.

Updates tailscale/corp#17516

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
This commit is contained in:
Anton Tolchanov
2026-06-22 12:28:09 +02:00
committed by Anton Tolchanov
parent 6a275c01db
commit f442cda999
9 changed files with 240 additions and 78 deletions
+29 -4
View File
@@ -4345,10 +4345,35 @@ func (c *Conn) GetLastNetcheckReport(ctx context.Context) *netcheck.Report {
return c.lastNetCheckReport.Load()
}
// SetLastNetcheckReportForTest sets the magicsock conn's last netcheck report.
// Used for testing purposes.
func (c *Conn) SetLastNetcheckReportForTest(ctx context.Context, report *netcheck.Report) {
c.lastNetCheckReport.Store(report)
// AddNetcheckReportForTest records report in the conn's netcheck client's
// recent-report history as if it had been produced at time now, seeding the
// netcheck client's per-region latency history. If report is newer than the
// currently stored last netcheck report, it also becomes the last netcheck
// report.
func (c *Conn) AddNetcheckReportForTest(dm *tailcfg.DERPMap, report *netcheck.Report, now time.Time) {
testenv.AssertInTest()
rep := report.Clone() // netchecker mutates the report, so create a copy
c.netChecker.AddReportHistoryForTest(dm, rep, now)
for {
if cur := c.lastNetCheckReport.Load(); cur == nil || rep.Now.After(cur.Now) {
if c.lastNetCheckReport.CompareAndSwap(cur, rep) {
break
}
}
}
}
// GetDERPRegionLatency returns the lowest latency seen per DERP region over
// netcheck's recent history, keyed by region ID. Unlike the most recent report
// from GetLastNetcheckReport (which for an incremental netcheck covers only a
// few regions), netcheck's history retains every region measured by the most
// recent full netcheck, so this can rank regions the latest report did not
// re-probe. It returns nil if the netcheck client is not yet initialized.
func (c *Conn) GetDERPRegionLatency() map[int]time.Duration {
if c.netChecker == nil {
return nil
}
return c.netChecker.RecentRegionLatency()
}
// lazyEndpoint is a wireguard [conn.Endpoint] for when magicsock received a