cmd/tailscale/cli: add routecheck command (#19641)
Introduce a new `tailscale routecheck` command which prints a report of high-availability routers that are reachable. This command rhymes with the `tailscale netcheck` command and but instead of reporting on local network conditions, `routecheck` reports on remote connectivity. Updates #17366 Updates tailscale/corp#33033 Signed-off-by: Simon Law <sfllaw@tailscale.com>
This commit is contained in:
@@ -20,6 +20,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
|||||||
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json+
|
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json+
|
||||||
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json+
|
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json+
|
||||||
github.com/go-json-experiment/json/jsontext from github.com/go-json-experiment/json+
|
github.com/go-json-experiment/json/jsontext from github.com/go-json-experiment/json+
|
||||||
|
github.com/go-json-experiment/json/v1 from tailscale.com/net/routecheck
|
||||||
💣 github.com/go4org/hashtriemap from tailscale.com/derp/derpserver
|
💣 github.com/go4org/hashtriemap from tailscale.com/derp/derpserver
|
||||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||||
github.com/hdevalence/ed25519consensus from tailscale.com/tka
|
github.com/hdevalence/ed25519consensus from tailscale.com/tka
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
|||||||
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json/jsontext+
|
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/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/jsontext from tailscale.com/logtail+
|
||||||
|
github.com/go-json-experiment/json/v1 from tailscale.com/net/routecheck
|
||||||
github.com/go-logr/logr from github.com/go-logr/logr/slogr+
|
github.com/go-logr/logr from github.com/go-logr/logr/slogr+
|
||||||
github.com/go-logr/logr/slogr from github.com/go-logr/zapr
|
github.com/go-logr/logr/slogr from github.com/go-logr/zapr
|
||||||
github.com/go-logr/zapr from sigs.k8s.io/controller-runtime/pkg/log/zap+
|
github.com/go-logr/zapr from sigs.k8s.io/controller-runtime/pkg/log/zap+
|
||||||
|
|||||||
@@ -239,6 +239,7 @@ func noDupFlagify(c *ffcli.Command, tb testenv.TB) {
|
|||||||
var (
|
var (
|
||||||
fileCmd,
|
fileCmd,
|
||||||
sysPolicyCmd,
|
sysPolicyCmd,
|
||||||
|
maybeRoutecheckCmd,
|
||||||
maybeWebCmd,
|
maybeWebCmd,
|
||||||
maybeDriveCmd,
|
maybeDriveCmd,
|
||||||
maybeTailnetLockCmd,
|
maybeTailnetLockCmd,
|
||||||
@@ -280,6 +281,7 @@ change in the future.
|
|||||||
configureCmd(),
|
configureCmd(),
|
||||||
nilOrCall(sysPolicyCmd),
|
nilOrCall(sysPolicyCmd),
|
||||||
netcheckCmd,
|
netcheckCmd,
|
||||||
|
nilOrCall(maybeRoutecheckCmd),
|
||||||
ipCmd,
|
ipCmd,
|
||||||
dnsCmd,
|
dnsCmd,
|
||||||
statusCmd,
|
statusCmd,
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ func runNetcheck(ctx context.Context, args []string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("netcheck: %w", err)
|
return fmt.Errorf("netcheck: %w", err)
|
||||||
}
|
}
|
||||||
if err := printReport(dm, report); err != nil {
|
if err := printNetCheckReport(dm, report); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if netcheckArgs.every == 0 {
|
if netcheckArgs.every == 0 {
|
||||||
@@ -153,7 +153,7 @@ func runNetcheck(ctx context.Context, args []string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func printReport(dm *tailcfg.DERPMap, report *netcheck.Report) error {
|
func printNetCheckReport(dm *tailcfg.DERPMap, report *netcheck.Report) error {
|
||||||
var j []byte
|
var j []byte
|
||||||
var err error
|
var err error
|
||||||
switch netcheckArgs.format {
|
switch netcheckArgs.format {
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & contributors
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build !ts_omit_routecheck
|
||||||
|
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cmp"
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"text/tabwriter"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
jsonv2 "github.com/go-json-experiment/json"
|
||||||
|
"github.com/go-json-experiment/json/jsontext"
|
||||||
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
|
"tailscale.com/net/routecheck"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
maybeRoutecheckCmd = routecheckCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
var routecheckCmd = func() *ffcli.Command {
|
||||||
|
return &ffcli.Command{
|
||||||
|
Name: "routecheck",
|
||||||
|
ShortUsage: "tailscale routecheck",
|
||||||
|
ShortHelp: "Print a reachability report for routes with multiple paths",
|
||||||
|
LongHelp: hidden + `"tailscale routecheck" is an experimental feature; it is not a stable interface`,
|
||||||
|
Exec: runRoutecheck,
|
||||||
|
FlagSet: routecheckFlagSet,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var routecheckFlagSet = func() *flag.FlagSet {
|
||||||
|
fs := newFlagSet("routecheck")
|
||||||
|
fs.BoolVar(&routecheckArgs.probe, "probe", false, "probe now to generate a new reachability report")
|
||||||
|
fs.StringVar(&routecheckArgs.format, "format", "", `output format: empty (for human-readable), "json" or "json-line"`)
|
||||||
|
return fs
|
||||||
|
}()
|
||||||
|
|
||||||
|
var routecheckArgs struct {
|
||||||
|
probe bool
|
||||||
|
format string
|
||||||
|
}
|
||||||
|
|
||||||
|
func runRoutecheck(ctx context.Context, args []string) error {
|
||||||
|
routeCheck := localClient.RouteCheck
|
||||||
|
if routecheckArgs.probe {
|
||||||
|
routeCheck = localClient.RouteCheckProbe
|
||||||
|
}
|
||||||
|
rp, err := routeCheck(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("routecheck: %w", err)
|
||||||
|
}
|
||||||
|
if err := printRouteCheckReport(rp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printRouteCheckReport(rp *routecheck.Report) error {
|
||||||
|
var enc *jsontext.Encoder
|
||||||
|
switch routecheckArgs.format {
|
||||||
|
case "":
|
||||||
|
case "json":
|
||||||
|
enc = jsontext.NewEncoder(Stdout, jsontext.WithIndent("\t"))
|
||||||
|
case "json-line":
|
||||||
|
enc = jsontext.NewEncoder(Stdout, jsontext.Multiline(false))
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown output format %q", routecheckArgs.format)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rp == nil {
|
||||||
|
return fmt.Errorf("routecheck: report unavailable")
|
||||||
|
}
|
||||||
|
routes := rp.RoutablePrefixes()
|
||||||
|
|
||||||
|
// Don’t render prefixes that only have one router:
|
||||||
|
for pfx, nodes := range routes {
|
||||||
|
if len(nodes) <= 1 {
|
||||||
|
delete(routes, pfx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if enc != nil {
|
||||||
|
out := struct {
|
||||||
|
Done time.Time `json:"done"`
|
||||||
|
Routes routecheck.RoutablePrefixes `json:"routes"`
|
||||||
|
}{
|
||||||
|
Done: rp.Done,
|
||||||
|
Routes: routes,
|
||||||
|
}
|
||||||
|
if err := jsonv2.MarshalEncode(enc, out); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := Stdout.Write([]byte("\n")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
w := tabwriter.NewWriter(Stdout, 10, 5, 5, ' ', 0)
|
||||||
|
defer w.Flush()
|
||||||
|
fmt.Fprintf(w, "\nReachable routers at %s:\n", rp.Done.Local().Format(time.DateTime+"Z07:00"))
|
||||||
|
fmt.Fprintf(w, "\n %s\t%s\t%s", "PREFIX", "IP", "HOSTNAME")
|
||||||
|
for prefix, nodes := range routes.Sorted() {
|
||||||
|
slices.SortFunc(nodes, func(a, b routecheck.Node) int {
|
||||||
|
return cmp.Compare(a.Name, b.Name) // order by hostname
|
||||||
|
})
|
||||||
|
for _, n := range nodes {
|
||||||
|
fmt.Fprintf(w, "\n %s\t%s\t%s", prefix, n.Addr, strings.TrimSuffix(n.Name, "."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Fprintln(w)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -109,6 +109,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
|||||||
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json+
|
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json+
|
||||||
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json+
|
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json+
|
||||||
github.com/go-json-experiment/json/jsontext from github.com/go-json-experiment/json+
|
github.com/go-json-experiment/json/jsontext from github.com/go-json-experiment/json+
|
||||||
|
github.com/go-json-experiment/json/v1 from tailscale.com/net/routecheck
|
||||||
L 💣 github.com/godbus/dbus/v5 from fyne.io/systray+
|
L 💣 github.com/godbus/dbus/v5 from fyne.io/systray+
|
||||||
L github.com/godbus/dbus/v5/introspect from fyne.io/systray+
|
L github.com/godbus/dbus/v5/introspect from fyne.io/systray+
|
||||||
L github.com/godbus/dbus/v5/prop from fyne.io/systray
|
L github.com/godbus/dbus/v5/prop from fyne.io/systray
|
||||||
@@ -220,7 +221,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
|||||||
tailscale.com/net/ping from tailscale.com/net/netcheck
|
tailscale.com/net/ping from tailscale.com/net/netcheck
|
||||||
tailscale.com/net/portmapper from tailscale.com/feature/portmapper
|
tailscale.com/net/portmapper from tailscale.com/feature/portmapper
|
||||||
tailscale.com/net/portmapper/portmappertype from tailscale.com/net/netcheck+
|
tailscale.com/net/portmapper/portmappertype from tailscale.com/net/netcheck+
|
||||||
tailscale.com/net/routecheck from tailscale.com/client/local
|
tailscale.com/net/routecheck from tailscale.com/client/local+
|
||||||
tailscale.com/net/sockstats from tailscale.com/control/controlhttp+
|
tailscale.com/net/sockstats from tailscale.com/control/controlhttp+
|
||||||
tailscale.com/net/stun from tailscale.com/net/netcheck
|
tailscale.com/net/stun from tailscale.com/net/netcheck
|
||||||
tailscale.com/net/tlsdial from tailscale.com/cmd/tailscale/cli+
|
tailscale.com/net/tlsdial from tailscale.com/cmd/tailscale/cli+
|
||||||
|
|||||||
@@ -112,7 +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/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/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/jsontext from tailscale.com/logtail+
|
||||||
github.com/go-json-experiment/json/v1 from tailscale.com/feature/routecheck
|
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 from github.com/go-ole/go-ole/oleutil+
|
||||||
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
|
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
|
||||||
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns+
|
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns+
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ tailscale.com/cmd/tsidp dependencies: (generated by github.com/tailscale/depawar
|
|||||||
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json+
|
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json+
|
||||||
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json+
|
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json+
|
||||||
github.com/go-json-experiment/json/jsontext from github.com/go-json-experiment/json+
|
github.com/go-json-experiment/json/jsontext from github.com/go-json-experiment/json+
|
||||||
|
github.com/go-json-experiment/json/v1 from tailscale.com/net/routecheck
|
||||||
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns
|
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns
|
||||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||||
github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
|
github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package routecheck
|
|||||||
import (
|
import (
|
||||||
"cmp"
|
"cmp"
|
||||||
"context"
|
"context"
|
||||||
|
"iter"
|
||||||
"maps"
|
"maps"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -13,9 +14,11 @@ import (
|
|||||||
|
|
||||||
jsonv2 "github.com/go-json-experiment/json"
|
jsonv2 "github.com/go-json-experiment/json"
|
||||||
"github.com/go-json-experiment/json/jsontext"
|
"github.com/go-json-experiment/json/jsontext"
|
||||||
|
jsonv1 "github.com/go-json-experiment/json/v1"
|
||||||
|
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/util/clientmetric"
|
"tailscale.com/util/clientmetric"
|
||||||
|
"tailscale.com/util/mak"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -54,6 +57,28 @@ type Report struct {
|
|||||||
LastProbed map[tailcfg.NodeID]time.Time `json:"-"` // not marshaled
|
LastProbed map[tailcfg.NodeID]time.Time `json:"-"` // not marshaled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RoutablePrefixes returns a map of routable network prefixes associated with
|
||||||
|
// each prefix’s routers that were reachable by the current host,
|
||||||
|
// at the time the report was finished.
|
||||||
|
// Each slice of routers are ordered by their node ID.
|
||||||
|
//
|
||||||
|
// Note: Fallback routes are not supported by design. If a subnet prefix
|
||||||
|
// contained within another more general prefix has no reachable routers,
|
||||||
|
// traffic is still sent to one of those unreachable routers.
|
||||||
|
// Routers for the general prefix aren’t candidates. See tailscale/tailscale#18550.
|
||||||
|
func (rp Report) RoutablePrefixes() RoutablePrefixes {
|
||||||
|
var out map[netip.Prefix][]Node
|
||||||
|
for _, n := range rp.Reachable {
|
||||||
|
for _, p := range n.Routes {
|
||||||
|
mak.Set(&out, p, append(out[p], n))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for p := range out {
|
||||||
|
slices.SortFunc(out[p], Node.Compare)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// Node represents a node in the reachability report.
|
// Node represents a node in the reachability report.
|
||||||
type Node struct {
|
type Node struct {
|
||||||
ID tailcfg.NodeID `json:"id"`
|
ID tailcfg.NodeID `json:"id"`
|
||||||
@@ -71,18 +96,26 @@ type Node struct {
|
|||||||
Routes []netip.Prefix `json:"routes"`
|
Routes []netip.Prefix `json:"routes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compare returns an integer comparing two nodes, ordered by their node ID.
|
||||||
|
// The result will be 0 if n.ID == n2.ID, -1 if n.ID < n2.ID, and +1 if n.ID > n2.ID.
|
||||||
|
func (n Node) Compare(n2 Node) int {
|
||||||
|
return cmp.Compare(n.ID, n2.ID)
|
||||||
|
}
|
||||||
|
|
||||||
// NodeSet is a set of nodes keyed by node ID, so duplicates are easily detected.
|
// 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.
|
// To prevent stuttering, it marshals itself as a JSON array, sorted by node ID.
|
||||||
type NodeSet map[tailcfg.NodeID]Node
|
type NodeSet map[tailcfg.NodeID]Node
|
||||||
|
|
||||||
var _ jsonv2.MarshalerTo = &NodeSet{}
|
var (
|
||||||
var _ jsonv2.UnmarshalerFrom = &NodeSet{}
|
_ jsonv1.Marshaler = &NodeSet{}
|
||||||
|
_ jsonv1.Unmarshaler = &NodeSet{}
|
||||||
|
_ jsonv2.MarshalerTo = &NodeSet{}
|
||||||
|
_ jsonv2.UnmarshalerFrom = &NodeSet{}
|
||||||
|
)
|
||||||
|
|
||||||
// MarshalJSONTo implements [jsonv2.MarshalerTo].
|
// MarshalJSONTo implements [jsonv2.MarshalerTo].
|
||||||
func (ns NodeSet) MarshalJSONTo(enc *jsontext.Encoder) error {
|
func (ns NodeSet) MarshalJSONTo(enc *jsontext.Encoder) error {
|
||||||
nodes := slices.SortedFunc(maps.Values(ns), func(a, b Node) int {
|
nodes := slices.SortedFunc(maps.Values(ns), Node.Compare)
|
||||||
return cmp.Compare(a.ID, b.ID)
|
|
||||||
})
|
|
||||||
return jsonv2.MarshalEncode(enc, nodes)
|
return jsonv2.MarshalEncode(enc, nodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,3 +133,31 @@ func (ns *NodeSet) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements [jsonv1.Marshaler].
|
||||||
|
func (ns *NodeSet) MarshalJSON() ([]byte, error) {
|
||||||
|
return jsonv2.Marshal(ns, jsonv1.DefaultOptionsV1())
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON implements [jsonv1.Unmarshaler].
|
||||||
|
func (ns *NodeSet) UnmarshalJSON(b []byte) error {
|
||||||
|
return jsonv2.Unmarshal(b, ns, jsonv1.DefaultOptionsV1())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoutablePrefixes is a map of routers,
|
||||||
|
// keyed by the network prefix for which they route.
|
||||||
|
type RoutablePrefixes map[netip.Prefix][]Node
|
||||||
|
|
||||||
|
// Sorted returns an iterator over the map of routers,
|
||||||
|
// ordered by the network prefix as described in [netip.Prefix.Compare].
|
||||||
|
func (rt RoutablePrefixes) Sorted() iter.Seq2[netip.Prefix, []Node] {
|
||||||
|
return func(yield func(netip.Prefix, []Node) bool) {
|
||||||
|
prefixes := slices.SortedFunc(maps.Keys(rt), netip.Prefix.Compare)
|
||||||
|
for _, p := range prefixes {
|
||||||
|
if !yield(p, rt[p]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
|
|||||||
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json+
|
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json+
|
||||||
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json+
|
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json+
|
||||||
github.com/go-json-experiment/json/jsontext from github.com/go-json-experiment/json+
|
github.com/go-json-experiment/json/jsontext from github.com/go-json-experiment/json+
|
||||||
|
github.com/go-json-experiment/json/v1 from tailscale.com/net/routecheck
|
||||||
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns
|
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns
|
||||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||||
github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
|
github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
|
||||||
|
|||||||
Reference in New Issue
Block a user