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:
@@ -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]
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"tailscale.com/net/routecheck"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/eventbus"
|
||||
)
|
||||
|
||||
// FeatureName is the name of the feature implemented by this package.
|
||||
@@ -43,6 +44,7 @@ type Extension struct {
|
||||
|
||||
logf logger.Logf
|
||||
backend ipnext.SafeBackend
|
||||
ec *eventbus.Client
|
||||
nb nodeBackender
|
||||
nm routecheck.NetMapper
|
||||
routers *RouterTracker
|
||||
@@ -83,7 +85,7 @@ func (e *Extension) Init(h ipnext.Host) error {
|
||||
|
||||
pinger := e.backend.Sys().Engine.Get()
|
||||
|
||||
c, err := routecheck.NewClient(e.logf, e.nb, e.nm, pinger)
|
||||
c, err := routecheck.NewClient(context.Background(), e.logf, e.nb, e.nm, pinger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -91,7 +93,11 @@ func (e *Extension) Init(h ipnext.Host) error {
|
||||
|
||||
e.routers = TrackRouters(context.Background(), e.logf, ipnbus)
|
||||
e.routers.OnNetMapAvailable = e.Client.NotifyNetMapAvailable
|
||||
e.routers.OnRoutersChange = e.incrementalRefresh
|
||||
e.routers.OnRoutersChange = e.Client.NeedsIncrRefresh
|
||||
|
||||
bus := e.backend.Sys().Bus.Get()
|
||||
e.ec = bus.Client("routecheck")
|
||||
eventbus.SubscribeFunc(e.ec, e.Client.WatchForNetMonRebind)
|
||||
|
||||
// Watch for changes to the self node that would toggle the routecheck feature.
|
||||
e.reconcile.args = make(chan tailcfg.NodeView, 1)
|
||||
@@ -99,6 +105,9 @@ func (e *Extension) Init(h ipnext.Host) error {
|
||||
go e.reconcileLoop()
|
||||
h.Hooks().OnSelfChange.Add(e.reconcileWatcher)
|
||||
|
||||
// Probe for reachable peers.
|
||||
go e.Client.Start()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -109,22 +118,12 @@ func (e *Extension) Shutdown() error {
|
||||
close(e.reconcile.args) // lock prevents reconcileWatcher from writing to this channel
|
||||
e.reconcile.Unlock()
|
||||
|
||||
e.ec.Close()
|
||||
e.routers.Close() // stop the watcher before waiting for reconcile.done
|
||||
<-e.reconcile.done
|
||||
return e.Client.Close()
|
||||
}
|
||||
|
||||
func (e *Extension) needsRefresh() {
|
||||
// TODO(sfllaw): Call e.Client.NeedsRefresh() after implementing it.
|
||||
}
|
||||
|
||||
func (e *Extension) incrementalRefresh(added, modified, removed []tailcfg.NodeID) {
|
||||
// TODO(sfllaw): This refresh should be incremental,
|
||||
// based on the added, modified, and removed nodes.
|
||||
// Currently it refreshes everything.
|
||||
e.needsRefresh()
|
||||
}
|
||||
|
||||
// reconcileWatcher is called whenever e.routers should start, stop, or restart its watcher.
|
||||
// It may trigger a restart when self indicates that we have switched to a different tailnet or user,
|
||||
// in order to reset the internal state of e.routers and start tracking from scratch.
|
||||
@@ -159,7 +158,7 @@ func (e *Extension) reconcileLoop() {
|
||||
continue // can be started by toggling the nodeattr
|
||||
}
|
||||
if started {
|
||||
e.needsRefresh()
|
||||
e.Client.NeedsRefresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user