Now that the routecheck subsystem is continuously collecting reachability reports in the background, we can add a hook to LocalBackend for fetching its report. That allows suggestExitNodeUsingTrafficSteering to consult that report when disqualifying candidates, instead of blocking on an immediate probe. Exit node suggestions will only consult the report when the `client-side-reachability` and `client-side-reachability-routecheck` node attributes are both set on the current node. Updates #17366 Updates tailscale/corp#33033 Signed-off-by: Simon Law <sfllaw@tailscale.com>
180 lines
5.1 KiB
Go
180 lines
5.1 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
||
// SPDX-License-Identifier: BSD-3-Clause
|
||
|
||
// Package routecheck registers support for RouteCheck,
|
||
// which checks the reachability of overlapping routers.
|
||
//
|
||
// When there are multiple network paths to an IP address, it is being routed by
|
||
// overlapping routers. The client uses reachability to pick between those
|
||
// paths: either sticking with an active WireGuard session or choosing from the
|
||
// peers that it has determined it can reach. It doesn’t need reachability for
|
||
// IP addresses that have only one network path, since it can naively attempt to
|
||
// establish a WireGuard session.
|
||
package routecheck
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"sync"
|
||
|
||
"tailscale.com/ipn/ipnext"
|
||
"tailscale.com/ipn/ipnlocal"
|
||
"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.
|
||
// It is also the [extension] name and the log prefix.
|
||
const featureName = "routecheck"
|
||
|
||
func init() {
|
||
ipnext.RegisterExtension(featureName, func(logf logger.Logf, b ipnext.SafeBackend) (ipnext.Extension, error) {
|
||
return &Extension{
|
||
logf: logger.WithPrefix(logf, featureName+": "),
|
||
backend: b,
|
||
}, nil
|
||
})
|
||
|
||
ipnlocal.HookRouteCheckReport.Set(routeCheckReport)
|
||
}
|
||
|
||
// Extension implements the [ipnext.Extension] interface.
|
||
type Extension struct {
|
||
Client *routecheck.Client
|
||
|
||
logf logger.Logf
|
||
backend ipnext.SafeBackend
|
||
ec *eventbus.Client
|
||
nb nodeBackender
|
||
nm routecheck.NetMapper
|
||
routers *RouterTracker
|
||
|
||
reconcile struct {
|
||
sync.Mutex
|
||
args chan tailcfg.NodeView // pending arguments for StartStopWatcher
|
||
closed bool
|
||
done chan struct{}
|
||
}
|
||
}
|
||
|
||
var _ ipnext.Extension = new(Extension)
|
||
|
||
// Name implements the [ipnext.Extension.Name] interface method.
|
||
func (e *Extension) Name() string {
|
||
return featureName
|
||
}
|
||
|
||
// Init implements the [ipnext.Extension.Init] interface method.
|
||
func (e *Extension) Init(h ipnext.Host) error {
|
||
if routecheck.DebugForceClientSideReachabilityRoutecheck().EqualBool(false) {
|
||
return ipnext.SkipExtension
|
||
}
|
||
|
||
e.nb = nodeBackender{h}
|
||
|
||
nm, ok := e.backend.(routecheck.NetMapper)
|
||
if !ok {
|
||
return fmt.Errorf("backend %T does not implement routecheck.NetMapper", e.backend)
|
||
}
|
||
e.nm = nm
|
||
|
||
ipnbus, ok := e.backend.(ipnext.NotifyWatcher)
|
||
if !ok {
|
||
return fmt.Errorf("backend %T does not implement ipnext.NotifyWatcher", e.backend)
|
||
}
|
||
|
||
pinger := e.backend.Sys().Engine.Get()
|
||
|
||
c, err := routecheck.NewClient(context.Background(), e.logf, e.nb, e.nm, pinger)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
e.Client = c
|
||
|
||
e.routers = TrackRouters(context.Background(), e.logf, ipnbus)
|
||
e.routers.OnNetMapAvailable = e.Client.NotifyNetMapAvailable
|
||
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)
|
||
e.reconcile.done = make(chan struct{})
|
||
go e.reconcileLoop()
|
||
h.Hooks().OnSelfChange.Add(e.reconcileWatcher)
|
||
|
||
// Probe for reachable peers.
|
||
go e.Client.Start()
|
||
|
||
return nil
|
||
}
|
||
|
||
// Shutdown implements the [ipnext.Extension.Shutdown] interface method.
|
||
func (e *Extension) Shutdown() error {
|
||
e.reconcile.Lock()
|
||
e.reconcile.closed = true
|
||
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()
|
||
}
|
||
|
||
// 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.
|
||
// This work is performed by [Extension.reconcileLoop].
|
||
//
|
||
// This function must never block, because it’s called from
|
||
// [ipnlocal.LocalBackend.SetControlClientStatus], which locks LocalBackend.mu.
|
||
// This lock is also acquired when unwinding [ipnlocal.LocalBackend.WatchNotificationsAs]
|
||
// which is what [RouterTracker.stopWatcherLocked] is waiting for.
|
||
func (e *Extension) reconcileWatcher(self tailcfg.NodeView) {
|
||
e.reconcile.Lock()
|
||
defer e.reconcile.Unlock()
|
||
if e.reconcile.closed {
|
||
return
|
||
}
|
||
select {
|
||
case <-e.reconcile.args: // drain stale args so StartStopWatcher is always called with the latest
|
||
default:
|
||
}
|
||
e.reconcile.args <- self
|
||
}
|
||
|
||
// reconcileLoop starts, stops, or restarts its watcher after calls to [Extension.reconcileWatcher].
|
||
func (e *Extension) reconcileLoop() {
|
||
defer close(e.reconcile.done)
|
||
for self := range e.reconcile.args {
|
||
started, err := e.routers.StartStopWatcher(self)
|
||
if err != nil {
|
||
if !errors.Is(err, ErrRouteCheckNotEnabled) {
|
||
e.logf("error tracking routers: %v", err)
|
||
}
|
||
continue // can be started by toggling the nodeattr
|
||
}
|
||
if started {
|
||
e.Client.NeedsRefresh()
|
||
}
|
||
}
|
||
}
|
||
|
||
func routeCheckReport(b *ipnlocal.LocalBackend) ipnlocal.RouteCheckReport {
|
||
c := ClientFor(b)
|
||
if c == nil {
|
||
return nil
|
||
}
|
||
r := c.Report()
|
||
if r == nil {
|
||
return nil
|
||
}
|
||
return r
|
||
}
|