Files
tailscale/feature/routecheck/localapi.go
T
Simon Law cb7e536804 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>
2026-07-02 20:26:27 -07:00

69 lines
1.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package routecheck
import (
"net/http"
"time"
jsonv2 "github.com/go-json-experiment/json"
jsonv1 "github.com/go-json-experiment/json/v1"
"tailscale.com/ipn/localapi"
"tailscale.com/net/routecheck"
"tailscale.com/util/def"
"tailscale.com/util/httpm"
)
func init() {
localapi.Register("routecheck", serveRouteCheck)
}
// ServeRouteCheck handles the API endpoint that serves the routecheck Report.
// If the probe form field is true, then this handler will refresh the Report
// before serving it.
// If the timeout form field is a valid duration, the probe will consider a node
// to be unreachable if it doesnt respond before the timeout expires.
func serveRouteCheck(h *localapi.Handler, w http.ResponseWriter, r *http.Request) {
rc := ClientFor(h.LocalBackend())
if rc == nil {
http.Error(w, "routecheck is not enabled", http.StatusServiceUnavailable)
return
}
if r.Method != httpm.POST {
http.Error(w, "want POST", http.StatusMethodNotAllowed)
return
}
report := rc.Report()
if def.Bool(r.FormValue("probe"), false) {
timeout := def.Duration(r.FormValue("timeout"), routecheck.DefaultTimeout)
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")
if report == nil {
w.WriteHeader(http.StatusNoContent)
return
}
// TODO(sfllaw): Since ipn/localapi is still using encoding/json
// 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]
}