cmd/tailscale/cli: add routecheck command

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:
Simon Law 2026-04-09 22:57:09 -07:00
parent 0af7dc12ba
commit 8c714b2ac5
No known key found for this signature in database
GPG Key ID: B83D1EE07548341D
4 changed files with 106 additions and 3 deletions

View File

@ -211,6 +211,7 @@ func noDupFlagify(c *ffcli.Command) {
var (
fileCmd,
sysPolicyCmd,
maybeRoutecheckCmd,
maybeWebCmd,
maybeDriveCmd,
maybeNetlockCmd,
@ -252,6 +253,7 @@ change in the future.
configureCmd(),
nilOrCall(sysPolicyCmd),
netcheckCmd,
nilOrCall(maybeRoutecheckCmd),
ipCmd,
dnsCmd,
statusCmd,

View File

@ -143,7 +143,7 @@ func runNetcheck(ctx context.Context, args []string) error {
if err != nil {
return fmt.Errorf("netcheck: %w", err)
}
if err := printReport(dm, report); err != nil {
if err := printNetCheckReport(dm, report); err != nil {
return err
}
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 err error
switch netcheckArgs.format {

View File

@ -0,0 +1,101 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_routecheck
package cli
import (
"context"
"flag"
"fmt"
"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/ipn/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",
Exec: runRoutecheck,
FlagSet: routecheckFlagSet,
}
}
var routecheckFlagSet = func() *flag.FlagSet {
fs := newFlagSet("routecheck")
fs.BoolVar(&routecheckArgs.force, "force", false, "force probe 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 {
force bool
format string
}
func runRoutecheck(ctx context.Context, args []string) error {
rp, err := localClient.RouteCheck(ctx, routecheckArgs.force)
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)
}
routes := rp.RoutablePrefixes()
if enc != nil {
out := struct {
Done time.Time `json:"done"`
Routes routecheck.RoutingTable `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.UTC().Format(time.DateTime+"Z07:00"))
fmt.Fprintf(w, "\n %s\t%s\t%s\t", "PREFIX", "IP", "HOSTNAME")
for prefix, nodes := range routes {
for _, n := range nodes {
fmt.Fprintf(w, "\n %s\t%s\t%s\t", prefix, n.Addr, strings.Trim(n.Name, "."))
}
}
fmt.Fprintln(w)
return nil
}

View File

@ -201,7 +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/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+