diff --git a/client/local/routecheck.go b/client/local/routecheck.go new file mode 100644 index 000000000..bf64842f7 --- /dev/null +++ b/client/local/routecheck.go @@ -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) +} diff --git a/cmd/derper/depaware.txt b/cmd/derper/depaware.txt index c1a50d219..bac6d3546 100644 --- a/cmd/derper/depaware.txt +++ b/cmd/derper/depaware.txt @@ -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+ diff --git a/cmd/k8s-operator/depaware.txt b/cmd/k8s-operator/depaware.txt index ec5824854..fbe9716cd 100644 --- a/cmd/k8s-operator/depaware.txt +++ b/cmd/k8s-operator/depaware.txt @@ -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 diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index f0c001ac3..467c523f0 100644 --- a/cmd/tailscale/depaware.txt +++ b/cmd/tailscale/depaware.txt @@ -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 diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 6e9fd7986..f98b05ac2 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -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 diff --git a/cmd/tsidp/depaware.txt b/cmd/tsidp/depaware.txt index cfcf72f6a..23fbdb846 100644 --- a/cmd/tsidp/depaware.txt +++ b/cmd/tsidp/depaware.txt @@ -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 diff --git a/feature/routecheck/ipn.go b/feature/routecheck/ipn.go index 5f960b270..b1f30454f 100644 --- a/feature/routecheck/ipn.go +++ b/feature/routecheck/ipn.go @@ -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 } diff --git a/feature/routecheck/localapi.go b/feature/routecheck/localapi.go new file mode 100644 index 000000000..d0cc070de --- /dev/null +++ b/feature/routecheck/localapi.go @@ -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 +} diff --git a/net/routecheck/probe.go b/net/routecheck/probe.go index 58548d52e..5222e10a7 100644 --- a/net/routecheck/probe.go +++ b/net/routecheck/probe.go @@ -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, diff --git a/net/routecheck/report.go b/net/routecheck/report.go index f80df9179..4707dc040 100644 --- a/net/routecheck/report.go +++ b/net/routecheck/report.go @@ -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 } diff --git a/net/routecheck/routecheck.go b/net/routecheck/routecheck.go index c458a93a9..fb57b5fa2 100644 --- a/net/routecheck/routecheck.go +++ b/net/routecheck/routecheck.go @@ -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 { diff --git a/net/routecheck/routecheck_test.go b/net/routecheck/routecheck_test.go index f5dd4800d..682e4ca67 100644 --- a/net/routecheck/routecheck_test.go +++ b/net/routecheck/routecheck_test.go @@ -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) } diff --git a/net/routecheck/routes.go b/net/routecheck/routes.go index c646f3894..ad1b663e2 100644 --- a/net/routecheck/routes.go +++ b/net/routecheck/routes.go @@ -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) } diff --git a/tsnet/depaware.txt b/tsnet/depaware.txt index c117384aa..eea1e62bf 100644 --- a/tsnet/depaware.txt +++ b/tsnet/depaware.txt @@ -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