mirror of
https://github.com/tailscale/tailscale.git
synced 2026-05-05 20:26:47 +02:00
client/local,ipn/localapi: add /localapi/v0/routecheck endpoint
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:
parent
674d438282
commit
0af7dc12ba
26
client/local/routecheck.go
Normal file
26
client/local/routecheck.go
Normal file
@ -0,0 +1,26 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_routecheck
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"tailscale.com/ipn/routecheck"
|
||||
)
|
||||
|
||||
// RouteCheck performs a routecheck probe to the provided IPs and waits for its report.
|
||||
func (lc *Client) RouteCheck(ctx context.Context, force bool) (*routecheck.Report, error) {
|
||||
v := url.Values{}
|
||||
v.Set("force", strconv.FormatBool(force))
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/routecheck?"+v.Encode(), 200, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error %w: %s", err, body)
|
||||
}
|
||||
return decodeJSON[*routecheck.Report](body)
|
||||
}
|
||||
@ -94,6 +94,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
tailscale.com/hostinfo from tailscale.com/net/netmon+
|
||||
tailscale.com/ipn from tailscale.com/client/local
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/client/local+
|
||||
tailscale.com/ipn/routecheck from tailscale.com/client/local
|
||||
tailscale.com/kube/kubetypes from tailscale.com/envknob
|
||||
tailscale.com/metrics from tailscale.com/cmd/derper+
|
||||
tailscale.com/net/bakedroots from tailscale.com/net/tlsdial
|
||||
@ -134,7 +135,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
|
||||
|
||||
@ -811,6 +811,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
tailscale.com/feature/identityfederation from tailscale.com/feature/condregister/identityfederation
|
||||
tailscale.com/feature/oauthkey from tailscale.com/feature/condregister/oauthkey
|
||||
tailscale.com/feature/portmapper from tailscale.com/feature/condregister/portmapper
|
||||
tailscale.com/feature/routecheck from tailscale.com/ipn/localapi
|
||||
tailscale.com/feature/syspolicy from tailscale.com/logpolicy
|
||||
tailscale.com/feature/useproxy from tailscale.com/feature/condregister/useproxy
|
||||
tailscale.com/health from tailscale.com/control/controlclient+
|
||||
@ -825,6 +826,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
tailscale.com/ipn/ipnlocal/netmapcache from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/client/local+
|
||||
tailscale.com/ipn/localapi from tailscale.com/tsnet
|
||||
tailscale.com/ipn/routecheck from tailscale.com/feature/routecheck+
|
||||
tailscale.com/ipn/store from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/ipn/store/kubestore from tailscale.com/cmd/k8s-operator
|
||||
tailscale.com/ipn/store/mem from tailscale.com/ipn/ipnlocal+
|
||||
|
||||
@ -201,6 +201,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/ipn from tailscale.com/client/local+
|
||||
tailscale.com/ipn/conffile from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/client/local+
|
||||
tailscale.com/ipn/routecheck from tailscale.com/client/local
|
||||
tailscale.com/kube/kubetypes from tailscale.com/envknob
|
||||
tailscale.com/licenses from tailscale.com/client/web+
|
||||
tailscale.com/metrics from tailscale.com/tsweb+
|
||||
|
||||
@ -303,7 +303,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/feature/portmapper from tailscale.com/feature/condregister/portmapper
|
||||
tailscale.com/feature/posture from tailscale.com/feature/condregister
|
||||
tailscale.com/feature/relayserver from tailscale.com/feature/condregister
|
||||
tailscale.com/feature/routecheck from tailscale.com/feature/condregister
|
||||
tailscale.com/feature/routecheck from tailscale.com/feature/condregister+
|
||||
L tailscale.com/feature/sdnotify from tailscale.com/feature/condregister
|
||||
LD tailscale.com/feature/ssh from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/feature/syspolicy from tailscale.com/feature/condregister+
|
||||
@ -328,7 +328,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/client/local+
|
||||
tailscale.com/ipn/localapi from tailscale.com/ipn/ipnserver+
|
||||
tailscale.com/ipn/policy from tailscale.com/feature/portlist
|
||||
tailscale.com/ipn/routecheck from tailscale.com/feature/routecheck
|
||||
tailscale.com/ipn/routecheck from tailscale.com/feature/routecheck+
|
||||
tailscale.com/ipn/store from tailscale.com/cmd/tailscaled+
|
||||
L tailscale.com/ipn/store/awsstore from tailscale.com/feature/condregister
|
||||
L tailscale.com/ipn/store/kubestore from tailscale.com/feature/condregister
|
||||
|
||||
@ -230,6 +230,7 @@ tailscale.com/cmd/tsidp dependencies: (generated by github.com/tailscale/depawar
|
||||
tailscale.com/feature/identityfederation from tailscale.com/feature/condregister/identityfederation
|
||||
tailscale.com/feature/oauthkey from tailscale.com/feature/condregister/oauthkey
|
||||
tailscale.com/feature/portmapper from tailscale.com/feature/condregister/portmapper
|
||||
tailscale.com/feature/routecheck from tailscale.com/ipn/localapi
|
||||
tailscale.com/feature/syspolicy from tailscale.com/logpolicy
|
||||
tailscale.com/feature/useproxy from tailscale.com/feature/condregister/useproxy
|
||||
tailscale.com/health from tailscale.com/control/controlclient+
|
||||
@ -244,6 +245,7 @@ tailscale.com/cmd/tsidp dependencies: (generated by github.com/tailscale/depawar
|
||||
tailscale.com/ipn/ipnlocal/netmapcache from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/client/local+
|
||||
tailscale.com/ipn/localapi from tailscale.com/tsnet
|
||||
tailscale.com/ipn/routecheck from tailscale.com/feature/routecheck+
|
||||
tailscale.com/ipn/store from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/ipn/store/mem from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/kube/kubetypes from tailscale.com/envknob
|
||||
|
||||
@ -7,9 +7,30 @@ package routecheck
|
||||
|
||||
import (
|
||||
"tailscale.com/ipn/ipnext"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/routecheck"
|
||||
)
|
||||
|
||||
// DefaultTimeout is the default time allowed for a response before a peer is considered unreachable.
|
||||
const DefaultTimeout = routecheck.DefaultTimeout
|
||||
|
||||
// Client generates Reports describing the result of both passive and active
|
||||
// reachability probing.
|
||||
type Client = routecheck.Client
|
||||
|
||||
// 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) *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 }
|
||||
|
||||
|
||||
@ -125,6 +125,9 @@ func init() {
|
||||
if buildfeatures.HasUserMetrics {
|
||||
Register("usermetrics", (*Handler).serveUserMetrics)
|
||||
}
|
||||
if buildfeatures.HasRouteCheck {
|
||||
Register("routecheck", (*Handler).serveRouteCheck)
|
||||
}
|
||||
if buildfeatures.HasServe {
|
||||
Register("query-feature", (*Handler).serveQueryFeature)
|
||||
}
|
||||
@ -1152,6 +1155,17 @@ func (h *Handler) servePing(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (h *Handler) serveDial(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != httpm.POST {
|
||||
http.Error(w, "POST required", http.StatusMethodNotAllowed)
|
||||
|
||||
16
ipn/localapi/routecheck_disabled.go
Normal file
16
ipn/localapi/routecheck_disabled.go
Normal file
@ -0,0 +1,16 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ts_omit_routecheck
|
||||
|
||||
package localapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"tailscale.com/feature"
|
||||
)
|
||||
|
||||
func (h *Handler) serveRouteCheck(w http.ResponseWriter, r *http.Request) {
|
||||
panic(feature.ErrUnavailable.Error())
|
||||
}
|
||||
58
ipn/localapi/routecheck_enabled.go
Normal file
58
ipn/localapi/routecheck_enabled.go
Normal file
@ -0,0 +1,58 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_routecheck
|
||||
|
||||
package localapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"tailscale.com/feature"
|
||||
"tailscale.com/feature/buildfeatures"
|
||||
"tailscale.com/feature/routecheck"
|
||||
"tailscale.com/util/httpm"
|
||||
)
|
||||
|
||||
func (h *Handler) serveRouteCheck(w http.ResponseWriter, r *http.Request) {
|
||||
if !buildfeatures.HasRouteCheck {
|
||||
http.Error(w, feature.ErrUnavailable.Error(), http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
rc := routecheck.ClientFor(h.b)
|
||||
if rc == nil {
|
||||
http.Error(w, "routecheck is not enabled", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method != httpm.POST {
|
||||
http.Error(w, "want POST", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
var report *routecheck.Report
|
||||
if defBool(r.FormValue("force"), 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 {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Header().Set("Content-Type", "text.plain")
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if report == nil {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(report)
|
||||
}
|
||||
@ -5,7 +5,10 @@ package routecheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"maps"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
@ -20,9 +23,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
|
||||
}
|
||||
@ -30,26 +33,51 @@ 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"`
|
||||
}
|
||||
|
||||
// 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 encodes itself as an array.
|
||||
type nodeset map[tailcfg.NodeID]Node
|
||||
|
||||
var _ json.Marshaler = nodeset{}
|
||||
var _ json.Unmarshaler = nodeset{}
|
||||
|
||||
// MarshalJSON implements the [json.Marshaler] interface.
|
||||
func (ns nodeset) MarshalJSON() ([]byte, error) {
|
||||
nodes := maps.Values(ns)
|
||||
return json.Marshal(slices.Collect(nodes))
|
||||
}
|
||||
|
||||
// MarshalJSON implements the [json.Unmarshaler] interface.
|
||||
func (ns nodeset) UnmarshalJSON(b []byte) error {
|
||||
var nodes []Node
|
||||
if err := json.Unmarshal(b, &nodes); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, n := range nodes {
|
||||
ns[n.ID] = n
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -7,7 +7,9 @@ package routecheck
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
@ -81,3 +83,13 @@ func NewClient(logf logger.Logf, nb NodeBackender, nm NetMapWaiter, pinger Pinge
|
||||
pinger: pinger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
r, err := c.ProbeAllHARouters(ctx, 5, timeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error probing routers: %w", err)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
@ -26,7 +26,7 @@ import (
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
func TestReport(t *testing.T) {
|
||||
func TestRefresh(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
init bool // true before the netmap has been loaded
|
||||
@ -35,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",
|
||||
@ -128,29 +132,32 @@ func TestReport(t *testing.T) {
|
||||
|
||||
t.Run(tc.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(tc.gone...))
|
||||
self := makeNode(99, withName("self"))
|
||||
var b *stubBackend
|
||||
if !tc.init {
|
||||
self := makeNode(99, withName("self"))
|
||||
b = newStubBackend(self, tc.peers, withGone(tc.gone...))
|
||||
} else {
|
||||
// The backend is initialized without a NetMap,
|
||||
// which gets “retrieved” after a delay.
|
||||
b = newStubBackend(self, tc.peers, withGone(tc.gone...), withDelay(10*time.Second))
|
||||
}
|
||||
c, err := routecheck.NewClient(t.Logf, b, b, b)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
got := c.Report()
|
||||
got, err := c.Refresh(t.Context(), routecheck.DefaultTimeout)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
now := time.Now() // synctest will freeze time.
|
||||
|
||||
var want *routecheck.Report
|
||||
peers := makeDB(tc.peers)
|
||||
if !tc.init {
|
||||
want = &routecheck.Report{
|
||||
Done: now,
|
||||
}
|
||||
for _, nid := range tc.want {
|
||||
mak.Set(&want.Reachable, nid, peers[nid])
|
||||
}
|
||||
want := &routecheck.Report{
|
||||
Done: now,
|
||||
}
|
||||
for _, nid := range tc.want {
|
||||
mak.Set(&want.Reachable, nid, peers[nid])
|
||||
}
|
||||
|
||||
if diff := cmpDiff(want, got); diff != "" {
|
||||
@ -414,14 +421,21 @@ type stubBackend struct {
|
||||
self tailcfg.NodeView
|
||||
peers []tailcfg.NodeView
|
||||
gone set.Set[tailcfg.NodeID]
|
||||
delay context.Context
|
||||
}
|
||||
|
||||
type backendOptFunc func(*stubBackend)
|
||||
|
||||
func newStubBackend(self tailcfg.NodeView, peers []tailcfg.NodeView, opts ...backendOptFunc) *stubBackend {
|
||||
if !self.Valid() {
|
||||
panic("invalid self")
|
||||
}
|
||||
|
||||
delay, _ := context.WithTimeout(context.Background(), 0) // No delay
|
||||
b := &stubBackend{
|
||||
self: self,
|
||||
peers: slices.Clone(peers),
|
||||
delay: delay,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(b)
|
||||
@ -430,7 +444,8 @@ func newStubBackend(self tailcfg.NodeView, peers []tailcfg.NodeView, opts ...bac
|
||||
}
|
||||
|
||||
func (b *stubBackend) NetMap() *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{
|
||||
@ -440,6 +455,7 @@ func (b *stubBackend) NetMap() *netmap.NetworkMap {
|
||||
}
|
||||
|
||||
func (b *stubBackend) WaitForNetMap(ctx context.Context) (*netmap.NetworkMap, error) {
|
||||
<-b.delay.Done()
|
||||
nm := b.NetMap()
|
||||
if nm == nil {
|
||||
<-ctx.Done()
|
||||
@ -485,6 +501,12 @@ func (b *stubBackend) Ping(ip netip.Addr, pingType tailcfg.PingType, size int, c
|
||||
}
|
||||
}
|
||||
|
||||
func withDelay(d time.Duration) backendOptFunc {
|
||||
return func(b *stubBackend) {
|
||||
b.delay, _ = context.WithTimeout(context.Background(), d)
|
||||
}
|
||||
}
|
||||
|
||||
func withGone(gone ...tailcfg.NodeID) backendOptFunc {
|
||||
return func(b *stubBackend) {
|
||||
b.gone = set.SetOf(gone)
|
||||
|
||||
@ -226,6 +226,7 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
|
||||
tailscale.com/feature/identityfederation from tailscale.com/feature/condregister/identityfederation
|
||||
tailscale.com/feature/oauthkey from tailscale.com/feature/condregister/oauthkey
|
||||
tailscale.com/feature/portmapper from tailscale.com/feature/condregister/portmapper
|
||||
tailscale.com/feature/routecheck from tailscale.com/ipn/localapi
|
||||
tailscale.com/feature/syspolicy from tailscale.com/logpolicy
|
||||
tailscale.com/feature/useproxy from tailscale.com/feature/condregister/useproxy
|
||||
tailscale.com/health from tailscale.com/control/controlclient+
|
||||
@ -240,6 +241,7 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
|
||||
tailscale.com/ipn/ipnlocal/netmapcache from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/client/local+
|
||||
tailscale.com/ipn/localapi from tailscale.com/tsnet
|
||||
tailscale.com/ipn/routecheck from tailscale.com/feature/routecheck+
|
||||
tailscale.com/ipn/store from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/ipn/store/mem from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/kube/kubetypes from tailscale.com/envknob
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user