client/local,ipn/localapi: add /localapi/v0/routecheck endpoint (#19640)
In order to support a `tailscale routecheck` command, we introduce the `/localapi/v0/routecheck` endpoint to the local API. This endpoint returns the most recent report collected by the routecheck client. If `force=true` is an argument in the query string, then this endpoint will actively probe before returning the report. Updates #17366 Updates tailscale/corp#33033 Signed-off-by: Simon Law <sfllaw@tailscale.com>
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_routecheck
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"tailscale.com/net/routecheck"
|
||||
)
|
||||
|
||||
// ErrReportPending is returned by [Client.RouteCheck] and [Client.RouteCheckProbe]
|
||||
// when the report is pending.
|
||||
var ErrRouteCheckReportUnavailable = errors.New("report pending")
|
||||
|
||||
// RouteCheckProbe performs a routecheck probe and waits for its report.
|
||||
func (lc *Client) RouteCheckProbe(ctx context.Context) (*routecheck.Report, error) {
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/routecheck?probe=true", http.StatusOK, nil)
|
||||
if err != nil {
|
||||
if hs, ok := errors.AsType[httpStatusError](err); ok && hs.HTTPStatus == http.StatusNoContent {
|
||||
return nil, ErrRouteCheckReportUnavailable
|
||||
}
|
||||
return nil, fmt.Errorf("error %w: %s", err, body)
|
||||
}
|
||||
return decodeJSON[*routecheck.Report](body)
|
||||
}
|
||||
|
||||
// RouteCheck requests the report compiled by the latest routecheck probe.
|
||||
func (lc *Client) RouteCheck(ctx context.Context) (*routecheck.Report, error) {
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/routecheck", http.StatusOK, nil)
|
||||
if err != nil {
|
||||
if hs, ok := errors.AsType[httpStatusError](err); ok && hs.HTTPStatus == http.StatusNoContent {
|
||||
return nil, ErrRouteCheckReportUnavailable
|
||||
}
|
||||
return nil, fmt.Errorf("error %w: %s", err, body)
|
||||
}
|
||||
return decodeJSON[*routecheck.Report](body)
|
||||
}
|
||||
@@ -107,12 +107,14 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
💣 tailscale.com/net/netns from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/netutil from tailscale.com/client/local
|
||||
tailscale.com/net/netx from tailscale.com/net/dnscache+
|
||||
tailscale.com/net/routecheck from tailscale.com/client/local
|
||||
tailscale.com/net/sockstats from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/stun from tailscale.com/net/stunserver
|
||||
tailscale.com/net/stunserver from tailscale.com/cmd/derper
|
||||
L tailscale.com/net/tcpinfo from tailscale.com/derp/derpserver
|
||||
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/tlsdial/blockblame from tailscale.com/net/tlsdial
|
||||
tailscale.com/net/traffic from tailscale.com/net/routecheck
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn+
|
||||
tailscale.com/net/udprelay/status from tailscale.com/client/local
|
||||
tailscale.com/net/wsconn from tailscale.com/derp/derpserver
|
||||
@@ -135,7 +137,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
tailscale.com/types/key from tailscale.com/client/local+
|
||||
tailscale.com/types/lazy from tailscale.com/version+
|
||||
tailscale.com/types/logger from tailscale.com/cmd/derper+
|
||||
tailscale.com/types/netmap from tailscale.com/ipn
|
||||
tailscale.com/types/netmap from tailscale.com/ipn+
|
||||
tailscale.com/types/opt from tailscale.com/envknob+
|
||||
tailscale.com/types/persist from tailscale.com/ipn+
|
||||
tailscale.com/types/preftype from tailscale.com/ipn
|
||||
@@ -310,7 +312,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
go/token from google.golang.org/protobuf/internal/strs
|
||||
hash from crypto+
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/fnv from google.golang.org/protobuf/internal/detrand
|
||||
hash/fnv from google.golang.org/protobuf/internal/detrand+
|
||||
hash/maphash from go4.org/mem+
|
||||
html from net/http/pprof+
|
||||
html/template from tailscale.com/cmd/derper+
|
||||
|
||||
@@ -807,13 +807,14 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
tailscale.com/net/portmapper from tailscale.com/feature/portmapper
|
||||
tailscale.com/net/portmapper/portmappertype from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/proxymux from tailscale.com/tsnet
|
||||
tailscale.com/net/routecheck from tailscale.com/client/local
|
||||
💣 tailscale.com/net/sockopts from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/net/socks5 from tailscale.com/tsnet
|
||||
tailscale.com/net/sockstats from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/stun from tailscale.com/ipn/localapi+
|
||||
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tlsdial/blockblame from tailscale.com/net/tlsdial
|
||||
tailscale.com/net/traffic from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/net/traffic from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/tsaddr from tailscale.com/client/web+
|
||||
tailscale.com/net/tsdial from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/feature/useproxy
|
||||
|
||||
@@ -220,10 +220,12 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/net/ping from tailscale.com/net/netcheck
|
||||
tailscale.com/net/portmapper from tailscale.com/feature/portmapper
|
||||
tailscale.com/net/portmapper/portmappertype from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/routecheck from tailscale.com/client/local
|
||||
tailscale.com/net/sockstats from tailscale.com/control/controlhttp+
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck
|
||||
tailscale.com/net/tlsdial from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/net/tlsdial/blockblame from tailscale.com/net/tlsdial
|
||||
tailscale.com/net/traffic from tailscale.com/net/routecheck
|
||||
tailscale.com/net/tsaddr from tailscale.com/client/web+
|
||||
tailscale.com/net/tsdial from tailscale.com/cmd/tailscale/cli+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/feature/useproxy
|
||||
@@ -466,6 +468,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
hash from compress/zlib+
|
||||
hash/adler32 from compress/zlib
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/fnv from tailscale.com/net/traffic
|
||||
hash/maphash from go4.org/mem
|
||||
html from html/template+
|
||||
html/template from tailscale.com/util/eventbus
|
||||
|
||||
@@ -112,6 +112,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json/jsontext+
|
||||
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json/jsontext+
|
||||
github.com/go-json-experiment/json/jsontext from tailscale.com/logtail+
|
||||
github.com/go-json-experiment/json/v1 from tailscale.com/feature/routecheck
|
||||
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
|
||||
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
|
||||
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns+
|
||||
@@ -372,7 +373,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/net/portmapper from tailscale.com/feature/portmapper+
|
||||
tailscale.com/net/portmapper/portmappertype from tailscale.com/feature/portmapper+
|
||||
tailscale.com/net/proxymux from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/net/routecheck from tailscale.com/feature/routecheck
|
||||
tailscale.com/net/routecheck from tailscale.com/feature/routecheck+
|
||||
tailscale.com/net/routetable from tailscale.com/doctor/routetable
|
||||
💣 tailscale.com/net/sockopts from tailscale.com/wgengine/magicsock+
|
||||
tailscale.com/net/socks5 from tailscale.com/cmd/tailscaled
|
||||
|
||||
@@ -206,13 +206,14 @@ tailscale.com/cmd/tsidp dependencies: (generated by github.com/tailscale/depawar
|
||||
tailscale.com/net/portmapper from tailscale.com/feature/portmapper
|
||||
tailscale.com/net/portmapper/portmappertype from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/proxymux from tailscale.com/tsnet
|
||||
tailscale.com/net/routecheck from tailscale.com/client/local
|
||||
💣 tailscale.com/net/sockopts from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/net/socks5 from tailscale.com/tsnet
|
||||
tailscale.com/net/sockstats from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/stun from tailscale.com/ipn/localapi+
|
||||
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tlsdial/blockblame from tailscale.com/net/tlsdial
|
||||
tailscale.com/net/traffic from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/net/traffic from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/tsaddr from tailscale.com/client/web+
|
||||
tailscale.com/net/tsdial from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/feature/useproxy
|
||||
|
||||
@@ -5,9 +5,23 @@ package routecheck
|
||||
|
||||
import (
|
||||
"tailscale.com/ipn/ipnext"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/net/routecheck"
|
||||
)
|
||||
|
||||
// ClientFor returns the [routecheck.Client] for a given backend,
|
||||
// or nil if route checking is not available for that backend.
|
||||
func ClientFor(b *ipnlocal.LocalBackend) *routecheck.Client {
|
||||
e, ok := ipnlocal.GetExt[*Extension](b)
|
||||
if e == nil || !ok {
|
||||
return nil
|
||||
}
|
||||
return e.Client
|
||||
}
|
||||
|
||||
// Report contains the result of a single routecheck.
|
||||
type Report = routecheck.Report
|
||||
|
||||
// NodeBackender is a shim between [ipnext.Host] and [routecheck.NodeBackender].
|
||||
type nodeBackender struct{ ipnext.Host }
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package routecheck
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"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/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 doesn’t 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
|
||||
}
|
||||
|
||||
var err error
|
||||
var report *routecheck.Report
|
||||
if defBool(r.FormValue("probe"), false) {
|
||||
timeout := defDuration(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
|
||||
}
|
||||
|
||||
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 defBool(a string, def bool) bool {
|
||||
if a == "" {
|
||||
return def
|
||||
}
|
||||
v, err := strconv.ParseBool(a)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func defDuration(a string, def time.Duration) time.Duration {
|
||||
if a == "" {
|
||||
return def
|
||||
}
|
||||
v, err := time.ParseDuration(a)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
return v
|
||||
}
|
||||
@@ -50,6 +50,12 @@ func (c *Client) probe(ctx context.Context, nodes iter.Seq[probed], limit int, t
|
||||
var mu syncs.Mutex
|
||||
r := &Report{}
|
||||
|
||||
timestampProbe := func(n probed) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
mak.Set(&r.LastProbed, n.ID(), time.Now())
|
||||
}
|
||||
|
||||
markReachable := func(n probed) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
@@ -81,12 +87,22 @@ func (c *Client) probe(ctx context.Context, nodes iter.Seq[probed], limit int, t
|
||||
// TODO(sfllaw): Add a mechanism to mark a node as unreachable
|
||||
// because it fails of establish a new WireGuard connection.
|
||||
if n.IsWireGuardOnly() {
|
||||
timestampProbe(n)
|
||||
markReachable(n)
|
||||
continue
|
||||
}
|
||||
|
||||
g.Go(func() error {
|
||||
metricPing.Add(1)
|
||||
|
||||
// We record the timestamp of each node’s latest probe
|
||||
// so we can probe in incremental batches
|
||||
// and to limit the rate that any given node is pinged.
|
||||
//
|
||||
// TODO(sfllaw): We currently record the timestamp
|
||||
// but haven’t implemented batching or rate-limiting yet.
|
||||
defer timestampProbe(n)
|
||||
|
||||
// TODO(sfllaw): Why did we choose Disco ping instead of TSMP ping?
|
||||
// After all, a TSMP ping proves that the peer Tailscale node is there
|
||||
// and that both nodes know each other’s WireGuard keys,
|
||||
|
||||
@@ -4,10 +4,16 @@
|
||||
package routecheck
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"maps"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/clientmetric"
|
||||
)
|
||||
@@ -26,9 +32,9 @@ func (c *Client) Report() *Report {
|
||||
}
|
||||
|
||||
// TODO(sfllaw): Return the latest snapshot produced by background probing.
|
||||
r, err := c.ProbeAllHARouters(context.TODO(), 5, DefaultTimeout)
|
||||
r, err := c.Refresh(context.TODO(), DefaultTimeout)
|
||||
if err != nil {
|
||||
c.logf("reachability report error: %v", err)
|
||||
c.logf("%v", err)
|
||||
}
|
||||
return r
|
||||
}
|
||||
@@ -36,26 +42,61 @@ func (c *Client) Report() *Report {
|
||||
// Report contains the result of a single routecheck.
|
||||
type Report struct {
|
||||
// Done is the time when the report was finished.
|
||||
Done time.Time
|
||||
Done time.Time `json:"done"`
|
||||
|
||||
// Reachable is the set of nodes that were reachable from the current host
|
||||
// when this report was compiled. Missing nodes may or may not be reachable.
|
||||
Reachable map[tailcfg.NodeID]Node
|
||||
Reachable NodeSet `json:"reachable"`
|
||||
|
||||
// LastProbed tracks the last time a given node was probed.
|
||||
// This is used to rate-limit reachability probing, so an entry’s
|
||||
// presence doesn’t imply that it is reachable.
|
||||
LastProbed map[tailcfg.NodeID]time.Time `json:"-"` // not marshaled
|
||||
}
|
||||
|
||||
// Node represents a node in the reachability report.
|
||||
type Node struct {
|
||||
ID tailcfg.NodeID
|
||||
ID tailcfg.NodeID `json:"id"`
|
||||
|
||||
// Name is the FQDN of the node.
|
||||
// It is also the MagicDNS name for the node.
|
||||
// It has a trailing dot.
|
||||
// e.g. "host.tail-scale.ts.net."
|
||||
Name string
|
||||
Name string `json:"name"`
|
||||
|
||||
// Addr is the IP address that was probed.
|
||||
Addr netip.Addr
|
||||
Addr netip.Addr `json:"addr"`
|
||||
|
||||
// Routes are the subnets that the node will route.
|
||||
Routes []netip.Prefix
|
||||
Routes []netip.Prefix `json:"routes"`
|
||||
}
|
||||
|
||||
// NodeSet is a set of nodes keyed by node ID, so duplicates are easily detected.
|
||||
// To prevent stuttering, it marshals itself as a JSON array, sorted by node ID.
|
||||
type NodeSet map[tailcfg.NodeID]Node
|
||||
|
||||
var _ jsonv2.MarshalerTo = &NodeSet{}
|
||||
var _ jsonv2.UnmarshalerFrom = &NodeSet{}
|
||||
|
||||
// MarshalJSONTo implements [jsonv2.MarshalerTo].
|
||||
func (ns NodeSet) MarshalJSONTo(enc *jsontext.Encoder) error {
|
||||
nodes := slices.SortedFunc(maps.Values(ns), func(a, b Node) int {
|
||||
return cmp.Compare(a.ID, b.ID)
|
||||
})
|
||||
return jsonv2.MarshalEncode(enc, nodes)
|
||||
}
|
||||
|
||||
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
|
||||
func (ns *NodeSet) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
|
||||
var nodes []Node
|
||||
if err := jsonv2.UnmarshalDecode(dec, &nodes); err != nil {
|
||||
return err
|
||||
}
|
||||
if *ns == nil {
|
||||
*ns = make(NodeSet, len(nodes))
|
||||
}
|
||||
for _, n := range nodes {
|
||||
(*ns)[n.ID] = n
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -7,13 +7,20 @@ package routecheck
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/util/clientmetric"
|
||||
)
|
||||
|
||||
var (
|
||||
metricRefresh = clientmetric.NewCounter("routecheck_refresh")
|
||||
)
|
||||
|
||||
// Client generates Reports describing the result of both passive and active
|
||||
@@ -150,6 +157,17 @@ func (c *Client) waitForNetMap(ctx context.Context) (*netmap.NetworkMap, error)
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh generates a new reachability report and returns it.
|
||||
// A peer is considered unreachable if it doesn’t respond within the timeout.
|
||||
func (c *Client) Refresh(ctx context.Context, timeout time.Duration) (*Report, error) {
|
||||
metricRefresh.Add(1)
|
||||
r, err := c.ProbeAllHARouters(ctx, 5, timeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error probing routers: %w", err)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Close immediately stops all active probes.
|
||||
func (c *Client) Close() error {
|
||||
if c == nil {
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
package routecheck_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
"time"
|
||||
@@ -24,7 +26,7 @@ import (
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
func TestReport(t *testing.T) {
|
||||
func TestRefresh(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
init bool // true before the netmap has been loaded
|
||||
@@ -33,9 +35,13 @@ func TestReport(t *testing.T) {
|
||||
want []tailcfg.NodeID // Report.Reachable nodes
|
||||
}{
|
||||
{
|
||||
name: "before-netmap",
|
||||
name: "wait-for-netmap",
|
||||
init: true,
|
||||
want: nil,
|
||||
peers: []tailcfg.NodeView{
|
||||
makeNode(11, withName("exit11"), withExitRoutes()),
|
||||
makeNode(12, withName("exit12"), withExitRoutes()),
|
||||
},
|
||||
want: []tailcfg.NodeID{11, 12},
|
||||
},
|
||||
{
|
||||
name: "no-peers",
|
||||
@@ -126,28 +132,59 @@ func TestReport(t *testing.T) {
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
// The backend is initialized without a NetMap.
|
||||
b := newStubBackend(tailcfg.NodeView{}, nil, withGone(tt.gone...))
|
||||
self := makeNode(99, withName("self"))
|
||||
var b *stubBackend
|
||||
if !tt.init {
|
||||
self := makeNode(99, withName("self"))
|
||||
b = newStubBackend(self, tt.peers, withGone(tt.gone...))
|
||||
b = newStubBackend(self, tt.peers,
|
||||
withGone(t, tt.gone...))
|
||||
} else {
|
||||
// The backend is initialized without a NetMap,
|
||||
// which gets “retrieved” after a delay.
|
||||
b = newStubBackend(self, tt.peers,
|
||||
withGone(t, tt.gone...),
|
||||
withDelay(t, 10*time.Second))
|
||||
}
|
||||
t.Cleanup(func() { b.Close() })
|
||||
c, err := routecheck.NewClient(t.Logf, b, b, b)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
got := c.Report()
|
||||
now := time.Now() // synctest will freeze time.
|
||||
if tt.init {
|
||||
// This callback simulates the delay between
|
||||
// connecting to the backend and receiving the NetMap.
|
||||
donef := func() { c.NotifyNetMapAvailable(b.NetMapWithPeers()) }
|
||||
b.donef.Store(&donef)
|
||||
}
|
||||
|
||||
before := time.Now()
|
||||
got, err := c.Refresh(t.Context(), routecheck.DefaultTimeout)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
after := time.Now() // synctest will freeze time.
|
||||
|
||||
var want *routecheck.Report
|
||||
peers := makeDB(tt.peers)
|
||||
if !tt.init {
|
||||
want = &routecheck.Report{
|
||||
Done: now,
|
||||
want := &routecheck.Report{
|
||||
Done: after,
|
||||
}
|
||||
for _, nid := range tt.want {
|
||||
mak.Set(&want.Reachable, nid, peers[nid])
|
||||
}
|
||||
|
||||
for _, nodes := range c.RoutersByPrefix() {
|
||||
if len(nodes) <= 1 {
|
||||
continue // no choice
|
||||
}
|
||||
for _, nid := range tt.want {
|
||||
mak.Set(&want.Reachable, nid, peers[nid])
|
||||
for _, n := range nodes {
|
||||
ts := before
|
||||
if tt.init {
|
||||
ts = after // waiting for netmap
|
||||
}
|
||||
if slices.Contains(tt.gone, n.ID()) {
|
||||
ts = after // ping timed out
|
||||
}
|
||||
mak.Set(&want.LastProbed, n.ID(), ts)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,6 +387,7 @@ func TestRoutersByPrefix(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
self := makeNode(99, withName("self"))
|
||||
b := newStubBackend(self, tt.peers)
|
||||
t.Cleanup(func() { b.Close() })
|
||||
c, err := routecheck.NewClient(t.Logf, b, b, b)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
@@ -412,14 +450,26 @@ type stubBackend struct {
|
||||
self tailcfg.NodeView
|
||||
peers []tailcfg.NodeView
|
||||
gone set.Set[tailcfg.NodeID]
|
||||
|
||||
delay context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
donef atomic.Pointer[func()]
|
||||
}
|
||||
|
||||
type backendOptFunc func(*stubBackend)
|
||||
|
||||
func newStubBackend(self tailcfg.NodeView, peers []tailcfg.NodeView, opts ...backendOptFunc) *stubBackend {
|
||||
if !self.Valid() {
|
||||
panic("invalid self")
|
||||
}
|
||||
|
||||
delay, cancel := context.WithTimeout(context.Background(), 0) // No delay
|
||||
b := &stubBackend{
|
||||
self: self,
|
||||
peers: slices.Clone(peers),
|
||||
self: self,
|
||||
peers: slices.Clone(peers),
|
||||
delay: delay,
|
||||
cancel: cancel,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(b)
|
||||
@@ -427,8 +477,16 @@ func newStubBackend(self tailcfg.NodeView, peers []tailcfg.NodeView, opts ...bac
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *stubBackend) Close() error {
|
||||
if b.cancel != nil {
|
||||
b.cancel()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *stubBackend) NetMapNoPeers() *netmap.NetworkMap {
|
||||
if !b.self.Valid() {
|
||||
if b.delay.Err() == nil {
|
||||
// Simulate the delay between startup and receiving the NetMap.
|
||||
return nil
|
||||
}
|
||||
return &netmap.NetworkMap{
|
||||
@@ -479,8 +537,25 @@ func (b *stubBackend) Ping(ip netip.Addr, pingType tailcfg.PingType, size int, c
|
||||
}
|
||||
}
|
||||
|
||||
func withGone(gone ...tailcfg.NodeID) backendOptFunc {
|
||||
func withDelay(t *testing.T, d time.Duration) backendOptFunc {
|
||||
return func(b *stubBackend) {
|
||||
t.Helper()
|
||||
var stopf func() bool
|
||||
ctx, cancel := context.WithTimeout(t.Context(), d)
|
||||
stopf = context.AfterFunc(ctx, func() {
|
||||
if donef := b.donef.Load(); donef != nil {
|
||||
(*donef)()
|
||||
}
|
||||
cancel()
|
||||
stopf()
|
||||
})
|
||||
b.delay = ctx
|
||||
}
|
||||
}
|
||||
|
||||
func withGone(t *testing.T, gone ...tailcfg.NodeID) backendOptFunc {
|
||||
return func(b *stubBackend) {
|
||||
t.Helper()
|
||||
b.gone = set.SetOf(gone)
|
||||
}
|
||||
|
||||
|
||||
@@ -39,11 +39,10 @@ func (c *Client) RoutersByPrefix() RoutersByPrefix {
|
||||
// The result omits any prefix that is one of the node’s local addresses.
|
||||
func routes(n tailcfg.NodeView) []netip.Prefix {
|
||||
var routes []netip.Prefix
|
||||
AllowedIPs:
|
||||
for _, pfx := range n.AllowedIPs().All() {
|
||||
// Routers never forward their own local addresses.
|
||||
if views.SliceContains(n.Addresses(), pfx) {
|
||||
continue AllowedIPs
|
||||
continue
|
||||
}
|
||||
routes = append(routes, pfx)
|
||||
}
|
||||
|
||||
+2
-1
@@ -202,13 +202,14 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
|
||||
tailscale.com/net/portmapper from tailscale.com/feature/portmapper
|
||||
tailscale.com/net/portmapper/portmappertype from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/proxymux from tailscale.com/tsnet
|
||||
tailscale.com/net/routecheck from tailscale.com/client/local
|
||||
💣 tailscale.com/net/sockopts from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/net/socks5 from tailscale.com/tsnet
|
||||
tailscale.com/net/sockstats from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/stun from tailscale.com/ipn/localapi+
|
||||
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tlsdial/blockblame from tailscale.com/net/tlsdial
|
||||
tailscale.com/net/traffic from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/net/traffic from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/tsaddr from tailscale.com/client/web+
|
||||
tailscale.com/net/tsdial from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/feature/useproxy
|
||||
|
||||
Reference in New Issue
Block a user