Update
This commit is contained in:
1 parent
3530afb854
commit
6903fcff27
2146 files changed
+112256
-190703
No files matched your search
+365
@@ -0,0 +1,365 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package routecheck performs status checks for routes from the current host.
|
||||
package routecheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tailcfg/nodecap"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
var (
|
||||
metricNeedsRefresh = clientmetric.NewCounter("routecheck_needs_refresh")
|
||||
metricRefresh = clientmetric.NewCounter("routecheck_refresh")
|
||||
)
|
||||
|
||||
// DebugForceClientSideReachabilityRoutecheck reports whether routecheck should be forced on or off.
|
||||
// If the TS_DEBUG_FORCE_CLIENT_SIDE_REACHABILITY_ROUTECHECK environment variable is true,
|
||||
// then routecheck is forced on. If it is false, then routecheck is forced off.
|
||||
// If unset, then the client respects the client-side-reachability and
|
||||
// client-side-reachability-routecheck node attributes.
|
||||
var DebugForceClientSideReachabilityRoutecheck = envknob.RegisterOptBool("TS_DEBUG_FORCE_CLIENT_SIDE_REACHABILITY_ROUTECHECK")
|
||||
|
||||
// IsEnabled reports whether routecheck probing has been enabled for this client.
|
||||
func IsEnabled(self tailcfg.NodeView) bool {
|
||||
if v, ok := DebugForceClientSideReachabilityRoutecheck().Get(); ok {
|
||||
return v // forced
|
||||
}
|
||||
if !self.Valid() {
|
||||
return false
|
||||
}
|
||||
// TODO(sfllaw): We intend to eventually enable this behaviour by default.
|
||||
return self.HasCap(nodecap.ClientSideReachability) &&
|
||||
self.HasCap(nodecap.ClientSideReachabilityRouteCheck)
|
||||
}
|
||||
|
||||
// Client generates Reports describing the result of both passive and active
|
||||
// reachability probing.
|
||||
type Client struct {
|
||||
// Verbose enables verbose logging.
|
||||
Verbose bool
|
||||
|
||||
// Logf optionally specifies where to log to.
|
||||
// If nil, log.Printf is used.
|
||||
Logf logger.Logf
|
||||
|
||||
// These elements are read-only after initialization.
|
||||
nb NodeBackender
|
||||
nm NetMapper
|
||||
pinger Pinger
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
// needsRefresh is sent a message by [Client.NeedsRefresh]
|
||||
// to signal that a new report is needed.
|
||||
// This message is received by the goroutine spawned by [Client.Start]
|
||||
// which probes the appropriate routers to compile a new [Client.report].
|
||||
// This channel doesn’t need to be closed because the goroutine is canceled by ctx.
|
||||
needsRefresh chan struct{}
|
||||
report atomic.Pointer[Report] // needsRefresh signals that this needs refreshing
|
||||
|
||||
// HasNetMap is a channel that can be closed to wake up goroutines
|
||||
// waiting for the netmap received after connecting to the control plane.
|
||||
// This channel gets swapped out for a new one whenever it is closed,
|
||||
// to handle disconnecting and reconnecting to the control plane.
|
||||
hasNetMap atomic.Pointer[chan struct{}]
|
||||
}
|
||||
|
||||
// NetMapper is the interface that returns the current [netmap.NetworkMap].
|
||||
type NetMapper interface {
|
||||
// NetMapNoPeers returns the latest cached network map received from
|
||||
// controlclient WITHOUT a freshly-built Peers slice.
|
||||
//
|
||||
// On a tailnet with frequent peer churn the cached netmap's Peers slice
|
||||
// can be stale relative to the live per-node-backend peers map; non-Peers
|
||||
// fields (SelfNode, DNS, PacketFilter, capabilities, ...) are always
|
||||
// current. Use this for any caller that does not need to iterate Peers,
|
||||
// since it's O(1) regardless of tailnet size.
|
||||
//
|
||||
// Returns nil if no network map has been received yet.
|
||||
NetMapNoPeers() *netmap.NetworkMap
|
||||
|
||||
// NetMapWithPeers returns the latest network map with the Peers slice
|
||||
// populated.
|
||||
//
|
||||
// Currently this is the same as [LocalBackend.NetMapNoPeers]: the cached
|
||||
// netmap's Peers slice may be stale relative to the live per-node-backend
|
||||
// peers map. A follow-up change will switch this method to return a
|
||||
// freshly-built netmap with up-to-date Peers, at O(N) cost per call.
|
||||
// Callers that genuinely need the up-to-date peer set should use this
|
||||
// method (and document why) so the upcoming change reaches them.
|
||||
//
|
||||
// Returns nil if no network map has been received yet.
|
||||
NetMapWithPeers() *netmap.NetworkMap
|
||||
}
|
||||
|
||||
// NodeBackender is the interface that returns the current [NodeBackend].
|
||||
type NodeBackender interface {
|
||||
NodeBackend() NodeBackend
|
||||
}
|
||||
|
||||
// NodeBackend is an interface to query the current node and its peers.
|
||||
//
|
||||
// It is not a snapshot in time but is locked to a particular node.
|
||||
type NodeBackend interface {
|
||||
// Self returns the current node.
|
||||
Self() tailcfg.NodeView
|
||||
|
||||
// Peers returns all the current peers.
|
||||
Peers() []tailcfg.NodeView
|
||||
}
|
||||
|
||||
// Pinger is the interface that wraps the [tailscale.com/ipn/ipnlocal.LocalBackend.Ping] method.
|
||||
type Pinger interface {
|
||||
Ping(ip netip.Addr, pingType tailcfg.PingType, size int, cb func(*ipnstate.PingResult))
|
||||
}
|
||||
|
||||
// NewClient returns a client that probes its peers using this LocalBackend.
|
||||
func NewClient(ctx context.Context, logf logger.Logf, nb NodeBackender, nm NetMapper, pinger Pinger) (*Client, error) {
|
||||
if nb == nil {
|
||||
return nil, errors.New("NodeBackender must be set")
|
||||
}
|
||||
if nm == nil {
|
||||
return nil, errors.New("NetMapper must be set")
|
||||
}
|
||||
if pinger == nil {
|
||||
return nil, errors.New("Pinger must be set")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
c := &Client{
|
||||
Logf: logf,
|
||||
nb: nb,
|
||||
nm: nm,
|
||||
pinger: pinger,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
|
||||
needsRefresh: make(chan struct{}, 1), // debounce using buffer of 1
|
||||
}
|
||||
c.hasNetMap.Store(new(make(chan struct{})))
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// NotifyNetMapAvailable wakes up goroutines that have been waiting for the
|
||||
// non-nil network map that the control plane sends after reconnecting.
|
||||
func (c *Client) NotifyNetMapAvailable() {
|
||||
if nm := c.nm.NetMapNoPeers(); nm == nil {
|
||||
return // client disconnected
|
||||
}
|
||||
var nextCh *chan struct{}
|
||||
for {
|
||||
ch := c.hasNetMap.Load()
|
||||
if ch == nil || *ch == nil {
|
||||
return // Client has been Closed
|
||||
}
|
||||
|
||||
if nextCh == nil {
|
||||
nextCh = new(make(chan struct{})) // prepare for next non-nil netmap
|
||||
}
|
||||
if c.hasNetMap.CompareAndSwap(ch, nextCh) {
|
||||
close(*ch)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) waitForNetMap(ctx context.Context) (*netmap.NetworkMap, error) {
|
||||
for {
|
||||
ch := c.hasNetMap.Load()
|
||||
if ch == nil || *ch == nil {
|
||||
return nil, errors.New("routecheck client closed")
|
||||
}
|
||||
|
||||
if nm := c.nm.NetMapNoPeers(); nm != nil {
|
||||
return nm, nil
|
||||
}
|
||||
|
||||
select {
|
||||
case <-c.ctx.Done(): // closed
|
||||
return nil, c.ctx.Err()
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-*ch: // woken up by NotifyNetMapAvailable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh generates and returns a new reachability report, after caching it in [Client.Report].
|
||||
// A peer is considered unreachable if it doesn’t respond within the timeout.
|
||||
// If the cached Client.Report is newer than the report that it just generated,
|
||||
// Refresh will return the cached report instead of clobbering it report.
|
||||
func (c *Client) Refresh(ctx context.Context, timeout time.Duration) (*Report, error) {
|
||||
metricRefresh.Add(1)
|
||||
c.vlogf("refreshing report")
|
||||
r, err := c.ProbeAllHARouters(ctx, 5, timeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error refreshing routers: %w", err)
|
||||
}
|
||||
for {
|
||||
saved := c.report.Load()
|
||||
if saved != nil && !saved.Done.Before(r.Done) {
|
||||
return saved, nil // don’t clobber newer reports
|
||||
}
|
||||
if c.report.CompareAndSwap(saved, r) { // retry if a concurrent Refresh stored first
|
||||
c.vlogf("saved new report at %v", r.Done)
|
||||
return r, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NeedsRefresh signals the need for a [Client.Refresh] to probe for a new report,
|
||||
// which will be done in the background by [Client.Start].
|
||||
func (c *Client) NeedsRefresh() {
|
||||
if !IsEnabled(c.nb.NodeBackend().Self()) {
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case c.needsRefresh <- struct{}{}:
|
||||
metricNeedsRefresh.Add(1)
|
||||
c.vlogf("report needs refresh")
|
||||
default:
|
||||
// needsRefresh has already been raised, so debounce.
|
||||
}
|
||||
}
|
||||
|
||||
// NeedsIncrRefresh signals the need for an incremental probe for a new report,
|
||||
// because routers have been added, modified, or removed,
|
||||
// which will be done in the background by [Client.Start].
|
||||
func (c *Client) NeedsIncrRefresh(added, modified, removed []tailcfg.NodeID) {
|
||||
// TODO(sfllaw): Currently, this refreshes everything.
|
||||
c.NeedsRefresh()
|
||||
}
|
||||
|
||||
// WatchForNetMonRebind watches the network monitor
|
||||
// for a signal that the sockets need to be rebound,
|
||||
// which implies that the cached report needs to be refreshed.
|
||||
// See [netmon.ChangeDelta.RebindLikelyRequired].
|
||||
func (c *Client) WatchForNetMonRebind(delta netmon.ChangeDelta) {
|
||||
if delta.RebindLikelyRequired {
|
||||
c.NeedsRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
// Start runs periodic probes that compile routecheck reports.
|
||||
// Use [Client.Close] to stop probing.
|
||||
// Returns an error if the client has already been closed.
|
||||
func (c *Client) Start() error {
|
||||
if c.ctx.Err() != nil {
|
||||
return c.ctx.Err()
|
||||
}
|
||||
|
||||
needsBootstrap := true
|
||||
for {
|
||||
select {
|
||||
case <-c.needsRefresh:
|
||||
nm := c.nm.NetMapWithPeers()
|
||||
if nm == nil {
|
||||
// There is no netmap: clear the cached report.
|
||||
c.report.Store(nil)
|
||||
needsBootstrap = true
|
||||
continue
|
||||
}
|
||||
|
||||
if needsBootstrap {
|
||||
r := c.bootstrap(nm)
|
||||
c.report.Store(r)
|
||||
needsBootstrap = false
|
||||
}
|
||||
|
||||
// TODO(sfllaw): Examine the shape of the overlapping
|
||||
// routers and only probe if the routing table has
|
||||
// changed sufficiently. For instance, a new router has
|
||||
// come online or a router has been removed or a set of
|
||||
// routers no longer overlap.
|
||||
if _, err := c.Refresh(c.ctx, DefaultTimeout); err != nil {
|
||||
c.logf("%v", err)
|
||||
}
|
||||
case <-c.ctx.Done(): // closed
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// bootstrap assumes that nodes that are connected to the control plane are reachable,
|
||||
// while waiting for the first probe to finish.
|
||||
//
|
||||
// This function requires a netmap with peers.
|
||||
func (c *Client) bootstrap(nm *netmap.NetworkMap) *Report {
|
||||
if nm == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
can4, can6 := supportsIPVersions(c.nb.NodeBackend().Self())
|
||||
if !can4 && !can6 {
|
||||
return nil
|
||||
}
|
||||
addrFor := addrPicker(can4, can6)
|
||||
|
||||
var r Report
|
||||
for _, nodes := range GroupRoutersByPrefix(nm.Peers) {
|
||||
if len(nodes) <= 1 {
|
||||
continue // Not an overlapping router
|
||||
}
|
||||
|
||||
// TODO(sfllaw): Instead of trusting the Node.Online flag,
|
||||
// which actually represents whether the node is connected
|
||||
// to the control plane and not that it is reachable,
|
||||
// we should cache reachability alongside the cached netmap
|
||||
// long enough to survive a restart or a brief disconnection.
|
||||
for _, n := range nodes {
|
||||
if !n.Online().Get() {
|
||||
continue // Not connected to the control plane.
|
||||
}
|
||||
|
||||
addr := addrFor(n)
|
||||
if !addr.IsValid() {
|
||||
continue // No valid addresses.
|
||||
}
|
||||
|
||||
mak.Set(&r.Reachable, n.ID(), Node{
|
||||
ID: n.ID(),
|
||||
Name: n.Name(),
|
||||
Addr: addr,
|
||||
Routes: routes(n),
|
||||
})
|
||||
}
|
||||
}
|
||||
r.Done = time.Now()
|
||||
c.vlogf("bootstrapped report from netmap at %v", r.Done)
|
||||
return &r
|
||||
}
|
||||
|
||||
// Close immediately stops all active probes.
|
||||
func (c *Client) Close() error {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.cancel != nil {
|
||||
c.cancel()
|
||||
}
|
||||
|
||||
hasNetMap := c.hasNetMap.Swap(nil) // clear before waking anything up
|
||||
if hasNetMap != nil && *hasNetMap != nil {
|
||||
close(*hasNetMap) // wake waitForNetMap
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user