feature/routecheck,ipn/routecheck: probe reachability in the background

Previously, refreshing the routecheck.Client would probe to generate a
new routecheck.Report, but this method was only wired up to the
LocalAPI and the `tailscale routecheck` command. However, waiting for
a probe to finish before choosing a router would take too long, so we
must keep a regularly updated report to be consulted as necessary.

This patch adds a Start and Close method to the routecheck.Client and
starts it in the background from features/routecheck. To enable this
feature for a given node, set both of the following node attributes:
`client-side-reachability` and `client-side-reachability-routecheck`.

This patch also wires up the RouterTracker.OnRoutersChange hook, which
fires a callback whenever a new network map includes information about
a router node, This signals to the routecheck.Client that it might
need to schedule another probe, if the shape of the routing table has
changed materially.

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 d8ee47d1cf
commit cb7e536804
6 changed files with 261 additions and 44 deletions
+15 -10
View File
@@ -37,18 +37,16 @@ func serveRouteCheck(h *localapi.Handler, w http.ResponseWriter, r *http.Request
return
}
var err error
var report *routecheck.Report
report := rc.Report()
if def.Bool(r.FormValue("probe"), false) {
timeout := def.Duration(r.FormValue("timeout"), routecheck.DefaultTimeout)
timeout = min(max(0, timeout), 60*time.Second) // clamp to [0s, 60s]
report, err = rc.Refresh(r.Context(), timeout)
} else {
report = rc.Report()
}
if err != nil {
localapi.WriteErrorJSON(w, err)
return
timeout = clampRouteCheckTimeout(timeout)
rp, err := rc.Refresh(r.Context(), timeout)
if err != nil {
localapi.WriteErrorJSON(w, err)
return
}
report = rp
}
w.Header().Set("Content-Type", "application/json")
@@ -61,3 +59,10 @@ func serveRouteCheck(h *localapi.Handler, w http.ResponseWriter, r *http.Request
// with its default options, marshal with DefaultOptionsV1.
jsonv2.MarshalWrite(w, report, jsonv1.DefaultOptionsV1())
}
func clampRouteCheckTimeout(timeout time.Duration) time.Duration {
if timeout < 0 {
timeout = routecheck.DefaultTimeout
}
return min(max(0, timeout), 60*time.Second) // clamp to [0s, 60s]
}