cmd/tailscale/cli: prefix all --help usages with "tailscale ...", some tidying

Also capitalises the start of all ShortHelp, allows subcommands to be hidden
with a "HIDDEN: " prefix in their ShortHelp, and adds a TS_DUMP_HELP envknob
to look at all --help messages together.

Fixes #11664

Signed-off-by: Paul Scott <paul@tailscale.com>
pull/11601/head
Paul Scott 2 months ago committed by Paul Scott
parent 9da135dd64
commit da4e92bf01

@ -17,7 +17,7 @@ var bugReportCmd = &ffcli.Command{
Name: "bugreport", Name: "bugreport",
Exec: runBugReport, Exec: runBugReport,
ShortHelp: "Print a shareable identifier to help diagnose issues", ShortHelp: "Print a shareable identifier to help diagnose issues",
ShortUsage: "bugreport [note]", ShortUsage: "tailscale bugreport [note]",
FlagSet: (func() *flag.FlagSet { FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("bugreport") fs := newFlagSet("bugreport")
fs.BoolVar(&bugReportArgs.diagnose, "diagnose", false, "run additional in-depth checks") fs.BoolVar(&bugReportArgs.diagnose, "diagnose", false, "run additional in-depth checks")

@ -28,7 +28,7 @@ var certCmd = &ffcli.Command{
Name: "cert", Name: "cert",
Exec: runCert, Exec: runCert,
ShortHelp: "Get TLS certs", ShortHelp: "Get TLS certs",
ShortUsage: "cert [flags] <domain>", ShortUsage: "tailscale cert [flags] <domain>",
FlagSet: (func() *flag.FlagSet { FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("cert") fs := newFlagSet("cert")
fs.StringVar(&certArgs.certFile, "cert-file", "", "output cert file or \"-\" for stdout; defaults to DOMAIN.crt if --cert-file and --key-file are both unset") fs.StringVar(&certArgs.certFile, "cert-file", "", "output cert file or \"-\" for stdout; defaults to DOMAIN.crt if --cert-file and --key-file are both unset")

@ -14,7 +14,6 @@ import (
"log" "log"
"os" "os"
"runtime" "runtime"
"slices"
"strings" "strings"
"sync" "sync"
"text/tabwriter" "text/tabwriter"
@ -95,6 +94,49 @@ func Run(args []string) (err error) {
}) })
}) })
rootCmd := newRootCmd()
if err := rootCmd.Parse(args); err != nil {
if errors.Is(err, flag.ErrHelp) {
return nil
}
return err
}
if envknob.Bool("TS_DUMP_HELP") {
walkCommands(rootCmd, func(c *ffcli.Command) {
fmt.Println("===")
// UsageFuncs are typically called during Command.Run which ensures
// FlagSet is not nil.
if c.FlagSet == nil {
c.FlagSet = flag.NewFlagSet(c.Name, flag.ContinueOnError)
}
if c.UsageFunc != nil {
fmt.Println(c.UsageFunc(c))
} else {
fmt.Println(ffcli.DefaultUsageFunc(c))
}
})
return
}
localClient.Socket = rootArgs.socket
rootCmd.FlagSet.Visit(func(f *flag.Flag) {
if f.Name == "socket" {
localClient.UseSocketOnly = true
}
})
err = rootCmd.Run(context.Background())
if tailscale.IsAccessDeniedError(err) && os.Getuid() != 0 && runtime.GOOS != "windows" {
return fmt.Errorf("%v\n\nUse 'sudo tailscale %s' or 'tailscale up --operator=$USER' to not require root.", err, strings.Join(args, " "))
}
if errors.Is(err, flag.ErrHelp) {
return nil
}
return err
}
func newRootCmd() *ffcli.Command {
rootfs := newFlagSet("tailscale") rootfs := newFlagSet("tailscale")
rootfs.StringVar(&rootArgs.socket, "socket", paths.DefaultTailscaledSocket(), "path to tailscaled socket") rootfs.StringVar(&rootArgs.socket, "socket", paths.DefaultTailscaledSocket(), "path to tailscaled socket")
@ -134,10 +176,11 @@ change in the future.
exitNodeCmd(), exitNodeCmd(),
updateCmd, updateCmd,
whoisCmd, whoisCmd,
debugCmd,
driveCmd,
}, },
FlagSet: rootfs, FlagSet: rootfs,
Exec: func(context.Context, []string) error { return flag.ErrHelp }, Exec: func(context.Context, []string) error { return flag.ErrHelp },
UsageFunc: usageFunc,
} }
if envknob.UseWIPCode() { if envknob.UseWIPCode() {
rootCmd.Subcommands = append(rootCmd.Subcommands, rootCmd.Subcommands = append(rootCmd.Subcommands,
@ -145,45 +188,16 @@ change in the future.
) )
} }
// Don't advertise these commands, but they're still explicitly available.
switch {
case slices.Contains(args, "debug"):
rootCmd.Subcommands = append(rootCmd.Subcommands, debugCmd)
case slices.Contains(args, "drive"):
rootCmd.Subcommands = append(rootCmd.Subcommands, driveCmd)
}
if runtime.GOOS == "linux" && distro.Get() == distro.Synology { if runtime.GOOS == "linux" && distro.Get() == distro.Synology {
rootCmd.Subcommands = append(rootCmd.Subcommands, configureHostCmd) rootCmd.Subcommands = append(rootCmd.Subcommands, configureHostCmd)
} }
for _, c := range rootCmd.Subcommands { walkCommands(rootCmd, func(c *ffcli.Command) {
if c.UsageFunc == nil { if c.UsageFunc == nil {
c.UsageFunc = usageFunc c.UsageFunc = usageFunc
} }
}
if err := rootCmd.Parse(args); err != nil {
if errors.Is(err, flag.ErrHelp) {
return nil
}
return err
}
localClient.Socket = rootArgs.socket
rootfs.Visit(func(f *flag.Flag) {
if f.Name == "socket" {
localClient.UseSocketOnly = true
}
}) })
return rootCmd
err = rootCmd.Run(context.Background())
if tailscale.IsAccessDeniedError(err) && os.Getuid() != 0 && runtime.GOOS != "windows" {
return fmt.Errorf("%v\n\nUse 'sudo tailscale %s' or 'tailscale up --operator=$USER' to not require root.", err, strings.Join(args, " "))
}
if errors.Is(err, flag.ErrHelp) {
return nil
}
return err
} }
func fatalf(format string, a ...any) { func fatalf(format string, a ...any) {
@ -202,6 +216,13 @@ var rootArgs struct {
socket string socket string
} }
func walkCommands(cmd *ffcli.Command, f func(*ffcli.Command)) {
f(cmd)
for _, sub := range cmd.Subcommands {
walkCommands(sub, f)
}
}
// usageFuncNoDefaultValues is like usageFunc but doesn't print default values. // usageFuncNoDefaultValues is like usageFunc but doesn't print default values.
func usageFuncNoDefaultValues(c *ffcli.Command) string { func usageFuncNoDefaultValues(c *ffcli.Command) string {
return usageFuncOpt(c, false) return usageFuncOpt(c, false)
@ -213,23 +234,32 @@ func usageFunc(c *ffcli.Command) string {
func usageFuncOpt(c *ffcli.Command, withDefaults bool) string { func usageFuncOpt(c *ffcli.Command, withDefaults bool) string {
var b strings.Builder var b strings.Builder
const hiddenPrefix = "HIDDEN: "
if c.ShortHelp != "" {
fmt.Fprintf(&b, "%s\n\n", c.ShortHelp)
}
fmt.Fprintf(&b, "USAGE\n") fmt.Fprintf(&b, "USAGE\n")
if c.ShortUsage != "" { if c.ShortUsage != "" {
fmt.Fprintf(&b, " %s\n", c.ShortUsage) fmt.Fprintf(&b, " %s\n", strings.ReplaceAll(c.ShortUsage, "\n", "\n "))
} else { } else {
fmt.Fprintf(&b, " %s\n", c.Name) fmt.Fprintf(&b, " %s\n", c.Name)
} }
fmt.Fprintf(&b, "\n") fmt.Fprintf(&b, "\n")
if c.LongHelp != "" { if c.LongHelp != "" {
fmt.Fprintf(&b, "%s\n\n", c.LongHelp) help, _ := strings.CutPrefix(c.LongHelp, hiddenPrefix)
fmt.Fprintf(&b, "%s\n\n", help)
} }
if len(c.Subcommands) > 0 { if len(c.Subcommands) > 0 {
fmt.Fprintf(&b, "SUBCOMMANDS\n") fmt.Fprintf(&b, "SUBCOMMANDS\n")
tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0) tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
for _, subcommand := range c.Subcommands { for _, subcommand := range c.Subcommands {
if strings.HasPrefix(subcommand.LongHelp, hiddenPrefix) {
continue
}
fmt.Fprintf(tw, " %s\t%s\n", subcommand.Name, subcommand.ShortHelp) fmt.Fprintf(tw, " %s\t%s\n", subcommand.Name, subcommand.ShortHelp)
} }
tw.Flush() tw.Flush()
@ -242,7 +272,7 @@ func usageFuncOpt(c *ffcli.Command, withDefaults bool) string {
c.FlagSet.VisitAll(func(f *flag.Flag) { c.FlagSet.VisitAll(func(f *flag.Flag) {
var s string var s string
name, usage := flag.UnquoteUsage(f) name, usage := flag.UnquoteUsage(f)
if strings.HasPrefix(usage, "HIDDEN: ") { if strings.HasPrefix(usage, hiddenPrefix) {
return return
} }
if isBoolFlag(f) { if isBoolFlag(f) {

@ -16,6 +16,7 @@ import (
qt "github.com/frankban/quicktest" qt "github.com/frankban/quicktest"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/envknob" "tailscale.com/envknob"
"tailscale.com/health/healthmsg" "tailscale.com/health/healthmsg"
"tailscale.com/ipn" "tailscale.com/ipn"
@ -29,15 +30,37 @@ import (
"tailscale.com/version/distro" "tailscale.com/version/distro"
) )
func TestPanicIfAnyEnvCheckedInInit(t *testing.T) {
envknob.PanicIfAnyEnvCheckedInInit()
}
func TestShortUsage_FullCmd(t *testing.T) {
t.Setenv("TAILSCALE_USE_WIP_CODE", "1")
if !envknob.UseWIPCode() {
t.Fatal("expected envknob.UseWIPCode() to be true")
}
// Some commands have more than one path from the root, so investigate all
// paths before we report errors.
ok := make(map[*ffcli.Command]bool)
root := newRootCmd()
walkCommands(root, func(c *ffcli.Command) {
if !ok[c] {
ok[c] = strings.HasPrefix(c.ShortUsage, "tailscale ") && (c.Name == "tailscale" || strings.Contains(c.ShortUsage, " "+c.Name+" ") || strings.HasSuffix(c.ShortUsage, " "+c.Name))
}
})
walkCommands(root, func(c *ffcli.Command) {
if !ok[c] {
t.Errorf("subcommand %s should show full usage ('tailscale ... %s ...') in ShortUsage (%q)", c.Name, c.Name, c.ShortUsage)
}
})
}
// geese is a collection of gooses. It need not be complete. // geese is a collection of gooses. It need not be complete.
// But it should include anything handled specially (e.g. linux, windows) // But it should include anything handled specially (e.g. linux, windows)
// and at least one thing that's not (darwin, freebsd). // and at least one thing that's not (darwin, freebsd).
var geese = []string{"linux", "darwin", "windows", "freebsd"} var geese = []string{"linux", "darwin", "windows", "freebsd"}
func TestPanicIfAnyEnvCheckedInInit(t *testing.T) {
envknob.PanicIfAnyEnvCheckedInInit()
}
// Test that checkForAccidentalSettingReverts's updateMaskedPrefsFromUpFlag can handle // Test that checkForAccidentalSettingReverts's updateMaskedPrefsFromUpFlag can handle
// all flags. This will panic if a new flag creeps in that's unhandled. // all flags. This will panic if a new flag creeps in that's unhandled.
// //

@ -27,7 +27,7 @@ func init() {
var configureKubeconfigCmd = &ffcli.Command{ var configureKubeconfigCmd = &ffcli.Command{
Name: "kubeconfig", Name: "kubeconfig",
ShortHelp: "[ALPHA] Connect to a Kubernetes cluster using a Tailscale Auth Proxy", ShortHelp: "[ALPHA] Connect to a Kubernetes cluster using a Tailscale Auth Proxy",
ShortUsage: "kubeconfig <hostname-or-fqdn>", ShortUsage: "tailscale configure kubeconfig <hostname-or-fqdn>",
LongHelp: strings.TrimSpace(` LongHelp: strings.TrimSpace(`
Run this command to configure kubectl to connect to a Kubernetes cluster over Tailscale. Run this command to configure kubectl to connect to a Kubernetes cluster over Tailscale.

@ -22,10 +22,11 @@ import (
// used to configure Synology devices, but is now a compatibility alias to // used to configure Synology devices, but is now a compatibility alias to
// "tailscale configure synology". // "tailscale configure synology".
var configureHostCmd = &ffcli.Command{ var configureHostCmd = &ffcli.Command{
Name: "configure-host", Name: "configure-host",
Exec: runConfigureSynology, Exec: runConfigureSynology,
ShortHelp: synologyConfigureCmd.ShortHelp, ShortUsage: "tailscale configure-host",
LongHelp: synologyConfigureCmd.LongHelp, ShortHelp: synologyConfigureCmd.ShortHelp,
LongHelp: synologyConfigureCmd.LongHelp,
FlagSet: (func() *flag.FlagSet { FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("configure-host") fs := newFlagSet("configure-host")
return fs return fs
@ -33,9 +34,10 @@ var configureHostCmd = &ffcli.Command{
} }
var synologyConfigureCmd = &ffcli.Command{ var synologyConfigureCmd = &ffcli.Command{
Name: "synology", Name: "synology",
Exec: runConfigureSynology, Exec: runConfigureSynology,
ShortHelp: "Configure Synology to enable outbound connections", ShortUsage: "tailscale configure synology",
ShortHelp: "Configure Synology to enable outbound connections",
LongHelp: strings.TrimSpace(` LongHelp: strings.TrimSpace(`
This command is intended to run at boot as root on a Synology device to This command is intended to run at boot as root on a Synology device to
create the /dev/net/tun device and give the tailscaled binary permission create the /dev/net/tun device and give the tailscaled binary permission

@ -14,8 +14,9 @@ import (
) )
var configureCmd = &ffcli.Command{ var configureCmd = &ffcli.Command{
Name: "configure", Name: "configure",
ShortHelp: "[ALPHA] Configure the host to enable more Tailscale features", ShortUsage: "tailscale configure <subcommand>",
ShortHelp: "[ALPHA] Configure the host to enable more Tailscale features",
LongHelp: strings.TrimSpace(` LongHelp: strings.TrimSpace(`
The 'configure' set of commands are intended to provide a way to enable different The 'configure' set of commands are intended to provide a way to enable different
services on the host to use Tailscale in more ways. services on the host to use Tailscale in more ways.

@ -45,9 +45,10 @@ import (
) )
var debugCmd = &ffcli.Command{ var debugCmd = &ffcli.Command{
Name: "debug", Name: "debug",
Exec: runDebug, Exec: runDebug,
LongHelp: `"tailscale debug" contains misc debug facilities; it is not a stable interface.`, ShortUsage: "tailscale debug <debug-flags | subcommand>",
LongHelp: `HIDDEN: "tailscale debug" contains misc debug facilities; it is not a stable interface.`,
FlagSet: (func() *flag.FlagSet { FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("debug") fs := newFlagSet("debug")
fs.StringVar(&debugArgs.file, "file", "", "get, delete:NAME, or NAME") fs.StringVar(&debugArgs.file, "file", "", "get, delete:NAME, or NAME")
@ -58,15 +59,16 @@ var debugCmd = &ffcli.Command{
})(), })(),
Subcommands: []*ffcli.Command{ Subcommands: []*ffcli.Command{
{ {
Name: "derp-map", Name: "derp-map",
Exec: runDERPMap, ShortUsage: "tailscale debug derp-map",
ShortHelp: "print DERP map", Exec: runDERPMap,
ShortHelp: "Print DERP map",
}, },
{ {
Name: "component-logs", Name: "component-logs",
Exec: runDebugComponentLogs,
ShortHelp: "enable/disable debug logs for a component",
ShortUsage: "tailscale debug component-logs [" + strings.Join(ipn.DebuggableComponents, "|") + "]", ShortUsage: "tailscale debug component-logs [" + strings.Join(ipn.DebuggableComponents, "|") + "]",
Exec: runDebugComponentLogs,
ShortHelp: "Enable/disable debug logs for a component",
FlagSet: (func() *flag.FlagSet { FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("component-logs") fs := newFlagSet("component-logs")
fs.DurationVar(&debugComponentLogsArgs.forDur, "for", time.Hour, "how long to enable debug logs for; zero or negative means to disable") fs.DurationVar(&debugComponentLogsArgs.forDur, "for", time.Hour, "how long to enable debug logs for; zero or negative means to disable")
@ -74,14 +76,16 @@ var debugCmd = &ffcli.Command{
})(), })(),
}, },
{ {
Name: "daemon-goroutines", Name: "daemon-goroutines",
Exec: runDaemonGoroutines, ShortUsage: "tailscale debug daemon-goroutines",
ShortHelp: "print tailscaled's goroutines", Exec: runDaemonGoroutines,
ShortHelp: "Print tailscaled's goroutines",
}, },
{ {
Name: "daemon-logs", Name: "daemon-logs",
Exec: runDaemonLogs, ShortUsage: "tailscale debug daemon-logs",
ShortHelp: "watch tailscaled's server logs", Exec: runDaemonLogs,
ShortHelp: "Watch tailscaled's server logs",
FlagSet: (func() *flag.FlagSet { FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("daemon-logs") fs := newFlagSet("daemon-logs")
fs.IntVar(&daemonLogsArgs.verbose, "verbose", 0, "verbosity level") fs.IntVar(&daemonLogsArgs.verbose, "verbose", 0, "verbosity level")
@ -90,9 +94,10 @@ var debugCmd = &ffcli.Command{
})(), })(),
}, },
{ {
Name: "metrics", Name: "metrics",
Exec: runDaemonMetrics, ShortUsage: "tailscale debug metrics",
ShortHelp: "print tailscaled's metrics", Exec: runDaemonMetrics,
ShortHelp: "Print tailscaled's metrics",
FlagSet: (func() *flag.FlagSet { FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("metrics") fs := newFlagSet("metrics")
fs.BoolVar(&metricsArgs.watch, "watch", false, "print JSON dump of delta values") fs.BoolVar(&metricsArgs.watch, "watch", false, "print JSON dump of delta values")
@ -100,80 +105,95 @@ var debugCmd = &ffcli.Command{
})(), })(),
}, },
{ {
Name: "env", Name: "env",
Exec: runEnv, ShortUsage: "tailscale debug env",
ShortHelp: "print cmd/tailscale environment", Exec: runEnv,
ShortHelp: "Print cmd/tailscale environment",
}, },
{ {
Name: "stat", Name: "stat",
Exec: runStat, ShortUsage: "tailscale debug stat <files...>",
ShortHelp: "stat a file", Exec: runStat,
ShortHelp: "Stat a file",
}, },
{ {
Name: "hostinfo", Name: "hostinfo",
Exec: runHostinfo, ShortUsage: "tailscale debug hostinfo",
ShortHelp: "print hostinfo", Exec: runHostinfo,
ShortHelp: "Print hostinfo",
}, },
{ {
Name: "local-creds", Name: "local-creds",
Exec: runLocalCreds, ShortUsage: "tailscale debug local-creds",
ShortHelp: "print how to access Tailscale LocalAPI", Exec: runLocalCreds,
ShortHelp: "Print how to access Tailscale LocalAPI",
}, },
{ {
Name: "restun", Name: "restun",
Exec: localAPIAction("restun"), ShortUsage: "tailscale debug restun",
ShortHelp: "force a magicsock restun", Exec: localAPIAction("restun"),
ShortHelp: "Force a magicsock restun",
}, },
{ {
Name: "rebind", Name: "rebind",
Exec: localAPIAction("rebind"), ShortUsage: "tailscale debug rebind",
ShortHelp: "force a magicsock rebind", Exec: localAPIAction("rebind"),
ShortHelp: "Force a magicsock rebind",
}, },
{ {
Name: "derp-set-on-demand", Name: "derp-set-on-demand",
Exec: localAPIAction("derp-set-homeless"), ShortUsage: "tailscale debug derp-set-on-demand",
ShortHelp: "enable DERP on-demand mode (breaks reachability)", Exec: localAPIAction("derp-set-homeless"),
ShortHelp: "Enable DERP on-demand mode (breaks reachability)",
}, },
{ {
Name: "derp-unset-on-demand", Name: "derp-unset-on-demand",
Exec: localAPIAction("derp-unset-homeless"), ShortUsage: "tailscale debug derp-unset-on-demand",
ShortHelp: "disable DERP on-demand mode", Exec: localAPIAction("derp-unset-homeless"),
ShortHelp: "Disable DERP on-demand mode",
}, },
{ {
Name: "break-tcp-conns", Name: "break-tcp-conns",
Exec: localAPIAction("break-tcp-conns"), ShortUsage: "tailscale debug break-tcp-conns",
ShortHelp: "break any open TCP connections from the daemon", Exec: localAPIAction("break-tcp-conns"),
ShortHelp: "Break any open TCP connections from the daemon",
}, },
{ {
Name: "break-derp-conns", Name: "break-derp-conns",
Exec: localAPIAction("break-derp-conns"), ShortUsage: "tailscale debug break-derp-conns",
ShortHelp: "break any open DERP connections from the daemon", Exec: localAPIAction("break-derp-conns"),
ShortHelp: "Break any open DERP connections from the daemon",
}, },
{ {
Name: "pick-new-derp", Name: "pick-new-derp",
Exec: localAPIAction("pick-new-derp"), ShortUsage: "tailscale debug pick-new-derp",
ShortHelp: "switch to some other random DERP home region for a short time", Exec: localAPIAction("pick-new-derp"),
ShortHelp: "Switch to some other random DERP home region for a short time",
}, },
{ {
Name: "force-netmap-update", Name: "force-netmap-update",
Exec: localAPIAction("force-netmap-update"), ShortUsage: "tailscale debug force-netmap-update",
ShortHelp: "force a full no-op netmap update (for load testing)", Exec: localAPIAction("force-netmap-update"),
ShortHelp: "Force a full no-op netmap update (for load testing)",
}, },
{ {
// TODO(bradfitz,maisem): eventually promote this out of debug // TODO(bradfitz,maisem): eventually promote this out of debug
Name: "reload-config", Name: "reload-config",
Exec: reloadConfig, ShortUsage: "tailscale debug reload-config",
ShortHelp: "reload config", Exec: reloadConfig,
ShortHelp: "Reload config",
}, },
{ {
Name: "control-knobs", Name: "control-knobs",
Exec: debugControlKnobs, ShortUsage: "tailscale debug control-knobs",
ShortHelp: "see current control knobs", Exec: debugControlKnobs,
ShortHelp: "See current control knobs",
}, },
{ {
Name: "prefs", Name: "prefs",
Exec: runPrefs, ShortUsage: "tailscale debug prefs",
ShortHelp: "print prefs", Exec: runPrefs,
ShortHelp: "Print prefs",
FlagSet: (func() *flag.FlagSet { FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("prefs") fs := newFlagSet("prefs")
fs.BoolVar(&prefsArgs.pretty, "pretty", false, "If true, pretty-print output") fs.BoolVar(&prefsArgs.pretty, "pretty", false, "If true, pretty-print output")
@ -181,9 +201,10 @@ var debugCmd = &ffcli.Command{
})(), })(),
}, },
{ {
Name: "watch-ipn", Name: "watch-ipn",
Exec: runWatchIPN, ShortUsage: "tailscale debug watch-ipn",
ShortHelp: "subscribe to IPN message bus", Exec: runWatchIPN,
ShortHelp: "Subscribe to IPN message bus",
FlagSet: (func() *flag.FlagSet { FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("watch-ipn") fs := newFlagSet("watch-ipn")
fs.BoolVar(&watchIPNArgs.netmap, "netmap", true, "include netmap in messages") fs.BoolVar(&watchIPNArgs.netmap, "netmap", true, "include netmap in messages")
@ -194,9 +215,10 @@ var debugCmd = &ffcli.Command{
})(), })(),
}, },
{ {
Name: "netmap", Name: "netmap",
Exec: runNetmap, ShortUsage: "tailscale debug netmap",
ShortHelp: "print the current network map", Exec: runNetmap,
ShortHelp: "Print the current network map",
FlagSet: (func() *flag.FlagSet { FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("netmap") fs := newFlagSet("netmap")
fs.BoolVar(&netmapArgs.showPrivateKey, "show-private-key", false, "include node private key in printed netmap") fs.BoolVar(&netmapArgs.showPrivateKey, "show-private-key", false, "include node private key in printed netmap")
@ -204,14 +226,17 @@ var debugCmd = &ffcli.Command{
})(), })(),
}, },
{ {
Name: "via", Name: "via",
ShortUsage: "tailscale via <site-id> <v4-cidr>\n" +
"tailscale via <v6-route>",
Exec: runVia, Exec: runVia,
ShortHelp: "convert between site-specific IPv4 CIDRs and IPv6 'via' routes", ShortHelp: "Convert between site-specific IPv4 CIDRs and IPv6 'via' routes",
}, },
{ {
Name: "ts2021", Name: "ts2021",
Exec: runTS2021, ShortUsage: "tailscale debug ts2021",
ShortHelp: "debug ts2021 protocol connectivity", Exec: runTS2021,
ShortHelp: "Debug ts2021 protocol connectivity",
FlagSet: (func() *flag.FlagSet { FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("ts2021") fs := newFlagSet("ts2021")
fs.StringVar(&ts2021Args.host, "host", "controlplane.tailscale.com", "hostname of control plane") fs.StringVar(&ts2021Args.host, "host", "controlplane.tailscale.com", "hostname of control plane")
@ -221,9 +246,10 @@ var debugCmd = &ffcli.Command{
})(), })(),
}, },
{ {
Name: "set-expire", Name: "set-expire",
Exec: runSetExpire, ShortUsage: "tailscale debug set-expire --in=1m",
ShortHelp: "manipulate node key expiry for testing", Exec: runSetExpire,
ShortHelp: "Manipulate node key expiry for testing",
FlagSet: (func() *flag.FlagSet { FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("set-expire") fs := newFlagSet("set-expire")
fs.DurationVar(&setExpireArgs.in, "in", 0, "if non-zero, set node key to expire this duration from now") fs.DurationVar(&setExpireArgs.in, "in", 0, "if non-zero, set node key to expire this duration from now")
@ -231,9 +257,10 @@ var debugCmd = &ffcli.Command{
})(), })(),
}, },
{ {
Name: "dev-store-set", Name: "dev-store-set",
Exec: runDevStoreSet, ShortUsage: "tailscale debug dev-store-set",
ShortHelp: "set a key/value pair during development", Exec: runDevStoreSet,
ShortHelp: "Set a key/value pair during development",
FlagSet: (func() *flag.FlagSet { FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("store-set") fs := newFlagSet("store-set")
fs.BoolVar(&devStoreSetArgs.danger, "danger", false, "accept danger") fs.BoolVar(&devStoreSetArgs.danger, "danger", false, "accept danger")
@ -241,14 +268,16 @@ var debugCmd = &ffcli.Command{
})(), })(),
}, },
{ {
Name: "derp", Name: "derp",
Exec: runDebugDERP, ShortUsage: "tailscale debug derp",
ShortHelp: "test a DERP configuration", Exec: runDebugDERP,
ShortHelp: "Test a DERP configuration",
}, },
{ {
Name: "capture", Name: "capture",
Exec: runCapture, ShortUsage: "tailscale debug capture",
ShortHelp: "streams pcaps for debugging", Exec: runCapture,
ShortHelp: "Streams pcaps for debugging",
FlagSet: (func() *flag.FlagSet { FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("capture") fs := newFlagSet("capture")
fs.StringVar(&captureArgs.outFile, "o", "", "path to stream the pcap (or - for stdout), leave empty to start wireshark") fs.StringVar(&captureArgs.outFile, "o", "", "path to stream the pcap (or - for stdout), leave empty to start wireshark")
@ -256,9 +285,10 @@ var debugCmd = &ffcli.Command{
})(), })(),
}, },
{ {
Name: "portmap", Name: "portmap",
Exec: debugPortmap, ShortUsage: "tailscale debug portmap",
ShortHelp: "run portmap debugging", Exec: debugPortmap,
ShortHelp: "Run portmap debugging",
FlagSet: (func() *flag.FlagSet { FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("portmap") fs := newFlagSet("portmap")
fs.DurationVar(&debugPortmapArgs.duration, "duration", 5*time.Second, "timeout for port mapping") fs.DurationVar(&debugPortmapArgs.duration, "duration", 5*time.Second, "timeout for port mapping")
@ -270,14 +300,16 @@ var debugCmd = &ffcli.Command{
})(), })(),
}, },
{ {
Name: "peer-endpoint-changes", Name: "peer-endpoint-changes",
Exec: runPeerEndpointChanges, ShortUsage: "tailscale debug peer-endpoint-changes <hostname-or-IP>",
ShortHelp: "prints debug information about a peer's endpoint changes", Exec: runPeerEndpointChanges,
ShortHelp: "Prints debug information about a peer's endpoint changes",
}, },
{ {
Name: "dial-types", Name: "dial-types",
Exec: runDebugDialTypes, ShortUsage: "tailscale debug dial-types <hostname-or-IP> <port>",
ShortHelp: "prints debug information about connecting to a given host or IP", Exec: runDebugDialTypes,
ShortHelp: "Prints debug information about connecting to a given host or IP",
FlagSet: (func() *flag.FlagSet { FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("dial-types") fs := newFlagSet("dial-types")
fs.StringVar(&debugDialTypesArgs.network, "network", "tcp", `network type to dial ("tcp", "udp", etc.)`) fs.StringVar(&debugDialTypesArgs.network, "network", "tcp", `network type to dial ("tcp", "udp", etc.)`)
@ -867,7 +899,7 @@ var setExpireArgs struct {
func runSetExpire(ctx context.Context, args []string) error { func runSetExpire(ctx context.Context, args []string) error {
if len(args) != 0 || setExpireArgs.in == 0 { if len(args) != 0 || setExpireArgs.in == 0 {
return errors.New("usage --in=<duration>") return errors.New("usage: tailscale debug set-expire --in=<duration>")
} }
return localClient.DebugSetExpireIn(ctx, setExpireArgs.in) return localClient.DebugSetExpireIn(ctx, setExpireArgs.in)
} }
@ -966,7 +998,7 @@ func runPeerEndpointChanges(ctx context.Context, args []string) error {
} }
if len(args) != 1 || args[0] == "" { if len(args) != 1 || args[0] == "" {
return errors.New("usage: peer-status <hostname-or-IP>") return errors.New("usage: tailscale debug peer-endpoint-changes <hostname-or-IP>")
} }
var ip string var ip string
@ -1042,7 +1074,7 @@ func runDebugDialTypes(ctx context.Context, args []string) error {
} }
if len(args) != 2 || args[0] == "" || args[1] == "" { if len(args) != 2 || args[0] == "" || args[1] == "" {
return errors.New("usage: dial-types <hostname-or-IP> <port>") return errors.New("usage: tailscale debug dial-types <hostname-or-IP> <port>")
} }
port, err := strconv.ParseUint(args[1], 10, 16) port, err := strconv.ParseUint(args[1], 10, 16)

@ -14,7 +14,7 @@ import (
var downCmd = &ffcli.Command{ var downCmd = &ffcli.Command{
Name: "down", Name: "down",
ShortUsage: "down", ShortUsage: "tailscale down",
ShortHelp: "Disconnect from Tailscale", ShortHelp: "Disconnect from Tailscale",
Exec: runDown, Exec: runDown,

@ -14,10 +14,10 @@ import (
) )
const ( const (
driveShareUsage = "drive share <name> <path>" driveShareUsage = "tailscale drive share <name> <path>"
driveRenameUsage = "drive rename <oldname> <newname>" driveRenameUsage = "tailscale drive rename <oldname> <newname>"
driveUnshareUsage = "drive unshare <name>" driveUnshareUsage = "tailscale drive unshare <name>"
driveListUsage = "drive list" driveListUsage = "tailscale drive list"
) )
var driveCmd = &ffcli.Command{ var driveCmd = &ffcli.Command{
@ -33,28 +33,32 @@ var driveCmd = &ffcli.Command{
UsageFunc: usageFuncNoDefaultValues, UsageFunc: usageFuncNoDefaultValues,
Subcommands: []*ffcli.Command{ Subcommands: []*ffcli.Command{
{ {
Name: "share", Name: "share",
Exec: runDriveShare, ShortUsage: driveShareUsage,
ShortHelp: "[ALPHA] create or modify a share", Exec: runDriveShare,
UsageFunc: usageFunc, ShortHelp: "[ALPHA] create or modify a share",
UsageFunc: usageFunc,
}, },
{ {
Name: "rename", Name: "rename",
ShortHelp: "[ALPHA] rename a share", ShortUsage: driveRenameUsage,
Exec: runDriveRename, ShortHelp: "[ALPHA] rename a share",
UsageFunc: usageFunc, Exec: runDriveRename,
UsageFunc: usageFunc,
}, },
{ {
Name: "unshare", Name: "unshare",
ShortHelp: "[ALPHA] remove a share", ShortUsage: driveUnshareUsage,
Exec: runDriveUnshare, ShortHelp: "[ALPHA] remove a share",
UsageFunc: usageFunc, Exec: runDriveUnshare,
UsageFunc: usageFunc,
}, },
{ {
Name: "list", Name: "list",
ShortHelp: "[ALPHA] list current shares", ShortUsage: driveListUsage,
Exec: runDriveList, ShortHelp: "[ALPHA] list current shares",
UsageFunc: usageFunc, Exec: runDriveList,
UsageFunc: usageFunc,
}, },
}, },
Exec: func(context.Context, []string) error { Exec: func(context.Context, []string) error {
@ -237,8 +241,8 @@ You can get a list of currently published shares by running:
$ tailscale drive list` $ tailscale drive list`
var shareLongHelpAs = ` const shareLongHelpAs = `
If you want a share to be accessed as a different user, you can use sudo to accomplish this. For example, to create the aforementioned share as "theuser", you could run: If you want a share to be accessed as a different user, you can use sudo to accomplish this. For example, to create the aforementioned share as "theuser", you could run:
$ sudo -u theuser tailscale drive share docs /Users/theuser/Documents` $ sudo -u theuser tailscale drive share docs /Users/theuser/Documents`

@ -23,7 +23,7 @@ import (
func exitNodeCmd() *ffcli.Command { func exitNodeCmd() *ffcli.Command {
return &ffcli.Command{ return &ffcli.Command{
Name: "exit-node", Name: "exit-node",
ShortUsage: "exit-node [flags]", ShortUsage: "tailscale exit-node [flags]",
ShortHelp: "Show machines on your tailnet configured as exit nodes", ShortHelp: "Show machines on your tailnet configured as exit nodes",
LongHelp: "Show machines on your tailnet configured as exit nodes", LongHelp: "Show machines on your tailnet configured as exit nodes",
Exec: func(context.Context, []string) error { Exec: func(context.Context, []string) error {
@ -32,7 +32,7 @@ func exitNodeCmd() *ffcli.Command {
Subcommands: append([]*ffcli.Command{ Subcommands: append([]*ffcli.Command{
{ {
Name: "list", Name: "list",
ShortUsage: "exit-node list [flags]", ShortUsage: "tailscale exit-node list [flags]",
ShortHelp: "Show exit nodes", ShortHelp: "Show exit nodes",
Exec: runExitNodeList, Exec: runExitNodeList,
FlagSet: (func() *flag.FlagSet { FlagSet: (func() *flag.FlagSet {
@ -48,13 +48,13 @@ func exitNodeCmd() *ffcli.Command {
return []*ffcli.Command{ return []*ffcli.Command{
{ {
Name: "connect", Name: "connect",
ShortUsage: "exit-node connect", ShortUsage: "tailscale exit-node connect",
ShortHelp: "connect to most recently used exit node", ShortHelp: "connect to most recently used exit node",
Exec: exitNodeSetUse(true), Exec: exitNodeSetUse(true),
}, },
{ {
Name: "disconnect", Name: "disconnect",
ShortUsage: "exit-node disconnect", ShortUsage: "tailscale exit-node disconnect",
ShortHelp: "disconnect from current exit node, if any", ShortHelp: "disconnect from current exit node, if any",
Exec: exitNodeSetUse(false), Exec: exitNodeSetUse(false),
}, },

@ -38,7 +38,7 @@ import (
var fileCmd = &ffcli.Command{ var fileCmd = &ffcli.Command{
Name: "file", Name: "file",
ShortUsage: "file <cp|get> ...", ShortUsage: "tailscale file <cp|get> ...",
ShortHelp: "Send or receive files", ShortHelp: "Send or receive files",
Subcommands: []*ffcli.Command{ Subcommands: []*ffcli.Command{
fileCpCmd, fileCpCmd,
@ -65,7 +65,7 @@ func (c *countingReader) Read(buf []byte) (int, error) {
var fileCpCmd = &ffcli.Command{ var fileCpCmd = &ffcli.Command{
Name: "cp", Name: "cp",
ShortUsage: "file cp <files...> <target>:", ShortUsage: "tailscale file cp <files...> <target>:",
ShortHelp: "Copy file(s) to a host", ShortHelp: "Copy file(s) to a host",
Exec: runCp, Exec: runCp,
FlagSet: (func() *flag.FlagSet { FlagSet: (func() *flag.FlagSet {
@ -412,7 +412,7 @@ func (v *onConflict) Set(s string) error {
var fileGetCmd = &ffcli.Command{ var fileGetCmd = &ffcli.Command{
Name: "get", Name: "get",
ShortUsage: "file get [--wait] [--verbose] [--conflict=(skip|overwrite|rename)] <target-directory>", ShortUsage: "tailscale file get [--wait] [--verbose] [--conflict=(skip|overwrite|rename)] <target-directory>",
ShortHelp: "Move files out of the Tailscale file inbox", ShortHelp: "Move files out of the Tailscale file inbox",
Exec: runFileGet, Exec: runFileGet,
FlagSet: (func() *flag.FlagSet { FlagSet: (func() *flag.FlagSet {
@ -420,7 +420,7 @@ var fileGetCmd = &ffcli.Command{
fs.BoolVar(&getArgs.wait, "wait", false, "wait for a file to arrive if inbox is empty") fs.BoolVar(&getArgs.wait, "wait", false, "wait for a file to arrive if inbox is empty")
fs.BoolVar(&getArgs.loop, "loop", false, "run get in a loop, receiving files as they come in") fs.BoolVar(&getArgs.loop, "loop", false, "run get in a loop, receiving files as they come in")
fs.BoolVar(&getArgs.verbose, "verbose", false, "verbose output") fs.BoolVar(&getArgs.verbose, "verbose", false, "verbose output")
fs.Var(&getArgs.conflict, "conflict", `behavior when a conflicting (same-named) file already exists in the target directory. fs.Var(&getArgs.conflict, "conflict", "`behavior`"+` when a conflicting (same-named) file already exists in the target directory.
skip: skip conflicting files: leave them in the taildrop inbox and print an error. get any non-conflicting files skip: skip conflicting files: leave them in the taildrop inbox and print an error. get any non-conflicting files
overwrite: overwrite existing file overwrite: overwrite existing file
rename: write to a new number-suffixed filename`) rename: write to a new number-suffixed filename`)

@ -36,9 +36,9 @@ func newFunnelCommand(e *serveEnv) *ffcli.Command {
Name: "funnel", Name: "funnel",
ShortHelp: "Turn on/off Funnel service", ShortHelp: "Turn on/off Funnel service",
ShortUsage: strings.Join([]string{ ShortUsage: strings.Join([]string{
"funnel <serve-port> {on|off}", "tailscale funnel <serve-port> {on|off}",
"funnel status [--json]", "tailscale funnel status [--json]",
}, "\n "), }, "\n"),
LongHelp: strings.Join([]string{ LongHelp: strings.Join([]string{
"Funnel allows you to publish a 'tailscale serve'", "Funnel allows you to publish a 'tailscale serve'",
"server publicly, open to the entire internet.", "server publicly, open to the entire internet.",
@ -46,17 +46,16 @@ func newFunnelCommand(e *serveEnv) *ffcli.Command {
"Turning off Funnel only turns off serving to the internet.", "Turning off Funnel only turns off serving to the internet.",
"It does not affect serving to your tailnet.", "It does not affect serving to your tailnet.",
}, "\n"), }, "\n"),
Exec: e.runFunnel, Exec: e.runFunnel,
UsageFunc: usageFunc,
Subcommands: []*ffcli.Command{ Subcommands: []*ffcli.Command{
{ {
Name: "status", Name: "status",
Exec: e.runServeStatus, Exec: e.runServeStatus,
ShortHelp: "show current serve/funnel status", ShortUsage: "tailscale funnel status [--json]",
ShortHelp: "Show current serve/funnel status",
FlagSet: e.newFlags("funnel-status", func(fs *flag.FlagSet) { FlagSet: e.newFlags("funnel-status", func(fs *flag.FlagSet) {
fs.BoolVar(&e.json, "json", false, "output JSON") fs.BoolVar(&e.json, "json", false, "output JSON")
}), }),
UsageFunc: usageFunc,
}, },
}, },
} }

@ -12,8 +12,8 @@ import (
var idTokenCmd = &ffcli.Command{ var idTokenCmd = &ffcli.Command{
Name: "id-token", Name: "id-token",
ShortUsage: "id-token <aud>", ShortUsage: "tailscale id-token <aud>",
ShortHelp: "fetch an OIDC id-token for the Tailscale machine", ShortHelp: "Fetch an OIDC id-token for the Tailscale machine",
Exec: runIDToken, Exec: runIDToken,
} }

@ -16,7 +16,7 @@ import (
var ipCmd = &ffcli.Command{ var ipCmd = &ffcli.Command{
Name: "ip", Name: "ip",
ShortUsage: "ip [-1] [-4] [-6] [peer hostname or ip address]", ShortUsage: "tailscale ip [-1] [-4] [-6] [peer hostname or ip address]",
ShortHelp: "Show Tailscale IP addresses", ShortHelp: "Show Tailscale IP addresses",
LongHelp: "Show Tailscale IP addresses for peer. Peer defaults to the current machine.", LongHelp: "Show Tailscale IP addresses for peer. Peer defaults to the current machine.",
Exec: runIP, Exec: runIP,

@ -12,7 +12,7 @@ import (
var licensesCmd = &ffcli.Command{ var licensesCmd = &ffcli.Command{
Name: "licenses", Name: "licenses",
ShortUsage: "licenses", ShortUsage: "tailscale licenses",
ShortHelp: "Get open source license information", ShortHelp: "Get open source license information",
LongHelp: "Get open source license information", LongHelp: "Get open source license information",
Exec: runLicenses, Exec: runLicenses,

@ -14,11 +14,10 @@ var loginArgs upArgsT
var loginCmd = &ffcli.Command{ var loginCmd = &ffcli.Command{
Name: "login", Name: "login",
ShortUsage: "login [flags]", ShortUsage: "tailscale login [flags]",
ShortHelp: "Log in to a Tailscale account", ShortHelp: "Log in to a Tailscale account",
LongHelp: `"tailscale login" logs this machine in to your Tailscale network. LongHelp: `"tailscale login" logs this machine in to your Tailscale network.
This command is currently in alpha and may change in the future.`, This command is currently in alpha and may change in the future.`,
UsageFunc: usageFunc,
FlagSet: func() *flag.FlagSet { FlagSet: func() *flag.FlagSet {
return newUpFlagSet(effectiveGOOS(), &loginArgs, "login") return newUpFlagSet(effectiveGOOS(), &loginArgs, "login")
}(), }(),

@ -13,7 +13,7 @@ import (
var logoutCmd = &ffcli.Command{ var logoutCmd = &ffcli.Command{
Name: "logout", Name: "logout",
ShortUsage: "logout [flags]", ShortUsage: "tailscale logout",
ShortHelp: "Disconnect from Tailscale and expire current node key", ShortHelp: "Disconnect from Tailscale and expire current node key",
LongHelp: strings.TrimSpace(` LongHelp: strings.TrimSpace(`

@ -16,7 +16,7 @@ import (
var ncCmd = &ffcli.Command{ var ncCmd = &ffcli.Command{
Name: "nc", Name: "nc",
ShortUsage: "nc <hostname-or-IP> <port>", ShortUsage: "tailscale nc <hostname-or-IP> <port>",
ShortHelp: "Connect to a port on a host, connected to stdin/stdout", ShortHelp: "Connect to a port on a host, connected to stdin/stdout",
Exec: runNC, Exec: runNC,
} }

@ -28,7 +28,7 @@ import (
var netcheckCmd = &ffcli.Command{ var netcheckCmd = &ffcli.Command{
Name: "netcheck", Name: "netcheck",
ShortUsage: "netcheck", ShortUsage: "tailscale netcheck",
ShortHelp: "Print an analysis of local network conditions", ShortHelp: "Print an analysis of local network conditions",
Exec: runNetcheck, Exec: runNetcheck,
FlagSet: (func() *flag.FlagSet { FlagSet: (func() *flag.FlagSet {

@ -26,7 +26,7 @@ import (
var netlockCmd = &ffcli.Command{ var netlockCmd = &ffcli.Command{
Name: "lock", Name: "lock",
ShortUsage: "lock <sub-command> <arguments>", ShortUsage: "tailscale lock <sub-command> <arguments>",
ShortHelp: "Manage tailnet lock", ShortHelp: "Manage tailnet lock",
LongHelp: "Manage tailnet lock", LongHelp: "Manage tailnet lock",
Subcommands: []*ffcli.Command{ Subcommands: []*ffcli.Command{
@ -61,7 +61,7 @@ var nlInitArgs struct {
var nlInitCmd = &ffcli.Command{ var nlInitCmd = &ffcli.Command{
Name: "init", Name: "init",
ShortUsage: "init [--gen-disablement-for-support] --gen-disablements N <trusted-key>...", ShortUsage: "tailscale lock init [--gen-disablement-for-support] --gen-disablements N <trusted-key>...",
ShortHelp: "Initialize tailnet lock", ShortHelp: "Initialize tailnet lock",
LongHelp: strings.TrimSpace(` LongHelp: strings.TrimSpace(`
@ -183,7 +183,7 @@ var nlStatusArgs struct {
var nlStatusCmd = &ffcli.Command{ var nlStatusCmd = &ffcli.Command{
Name: "status", Name: "status",
ShortUsage: "status", ShortUsage: "tailscale lock status",
ShortHelp: "Outputs the state of tailnet lock", ShortHelp: "Outputs the state of tailnet lock",
LongHelp: "Outputs the state of tailnet lock", LongHelp: "Outputs the state of tailnet lock",
Exec: runNetworkLockStatus, Exec: runNetworkLockStatus,
@ -280,7 +280,7 @@ func runNetworkLockStatus(ctx context.Context, args []string) error {
var nlAddCmd = &ffcli.Command{ var nlAddCmd = &ffcli.Command{
Name: "add", Name: "add",
ShortUsage: "add <public-key>...", ShortUsage: "tailscale lock add <public-key>...",
ShortHelp: "Adds one or more trusted signing keys to tailnet lock", ShortHelp: "Adds one or more trusted signing keys to tailnet lock",
LongHelp: "Adds one or more trusted signing keys to tailnet lock", LongHelp: "Adds one or more trusted signing keys to tailnet lock",
Exec: func(ctx context.Context, args []string) error { Exec: func(ctx context.Context, args []string) error {
@ -294,7 +294,7 @@ var nlRemoveArgs struct {
var nlRemoveCmd = &ffcli.Command{ var nlRemoveCmd = &ffcli.Command{
Name: "remove", Name: "remove",
ShortUsage: "remove [--re-sign=false] <public-key>...", ShortUsage: "tailscale lock remove [--re-sign=false] <public-key>...",
ShortHelp: "Removes one or more trusted signing keys from tailnet lock", ShortHelp: "Removes one or more trusted signing keys from tailnet lock",
LongHelp: "Removes one or more trusted signing keys from tailnet lock", LongHelp: "Removes one or more trusted signing keys from tailnet lock",
Exec: runNetworkLockRemove, Exec: runNetworkLockRemove,
@ -435,7 +435,7 @@ func runNetworkLockModify(ctx context.Context, addArgs, removeArgs []string) err
var nlSignCmd = &ffcli.Command{ var nlSignCmd = &ffcli.Command{
Name: "sign", Name: "sign",
ShortUsage: "sign <node-key> [<rotation-key>] or sign <auth-key>", ShortUsage: "tailscale lock sign <node-key> [<rotation-key>] or sign <auth-key>",
ShortHelp: "Signs a node or pre-approved auth key", ShortHelp: "Signs a node or pre-approved auth key",
LongHelp: `Either: LongHelp: `Either:
- signs a node key and transmits the signature to the coordination server, or - signs a node key and transmits the signature to the coordination server, or
@ -479,7 +479,7 @@ func runNetworkLockSign(ctx context.Context, args []string) error {
var nlDisableCmd = &ffcli.Command{ var nlDisableCmd = &ffcli.Command{
Name: "disable", Name: "disable",
ShortUsage: "disable <disablement-secret>", ShortUsage: "tailscale lock disable <disablement-secret>",
ShortHelp: "Consumes a disablement secret to shut down tailnet lock for the tailnet", ShortHelp: "Consumes a disablement secret to shut down tailnet lock for the tailnet",
LongHelp: strings.TrimSpace(` LongHelp: strings.TrimSpace(`
@ -508,7 +508,7 @@ func runNetworkLockDisable(ctx context.Context, args []string) error {
var nlLocalDisableCmd = &ffcli.Command{ var nlLocalDisableCmd = &ffcli.Command{
Name: "local-disable", Name: "local-disable",
ShortUsage: "local-disable", ShortUsage: "tailscale lock local-disable",
ShortHelp: "Disables tailnet lock for this node only", ShortHelp: "Disables tailnet lock for this node only",
LongHelp: strings.TrimSpace(` LongHelp: strings.TrimSpace(`
@ -530,7 +530,7 @@ func runNetworkLockLocalDisable(ctx context.Context, args []string) error {
var nlDisablementKDFCmd = &ffcli.Command{ var nlDisablementKDFCmd = &ffcli.Command{
Name: "disablement-kdf", Name: "disablement-kdf",
ShortUsage: "disablement-kdf <hex-encoded-disablement-secret>", ShortUsage: "tailscale lock disablement-kdf <hex-encoded-disablement-secret>",
ShortHelp: "Computes a disablement value from a disablement secret (advanced users only)", ShortHelp: "Computes a disablement value from a disablement secret (advanced users only)",
LongHelp: "Computes a disablement value from a disablement secret (advanced users only)", LongHelp: "Computes a disablement value from a disablement secret (advanced users only)",
Exec: runNetworkLockDisablementKDF, Exec: runNetworkLockDisablementKDF,
@ -555,7 +555,7 @@ var nlLogArgs struct {
var nlLogCmd = &ffcli.Command{ var nlLogCmd = &ffcli.Command{
Name: "log", Name: "log",
ShortUsage: "log [--limit N]", ShortUsage: "tailscale lock log [--limit N]",
ShortHelp: "List changes applied to tailnet lock", ShortHelp: "List changes applied to tailnet lock",
LongHelp: "List changes applied to tailnet lock", LongHelp: "List changes applied to tailnet lock",
Exec: runNetworkLockLog, Exec: runNetworkLockLog,
@ -719,7 +719,7 @@ var nlRevokeKeysArgs struct {
var nlRevokeKeysCmd = &ffcli.Command{ var nlRevokeKeysCmd = &ffcli.Command{
Name: "revoke-keys", Name: "revoke-keys",
ShortUsage: "revoke-keys <tailnet-lock-key>...\n revoke-keys [--cosign] [--finish] <recovery-blob>", ShortUsage: "tailscale lock revoke-keys <tailnet-lock-key>...\n revoke-keys [--cosign] [--finish] <recovery-blob>",
ShortHelp: "Revoke compromised tailnet-lock keys", ShortHelp: "Revoke compromised tailnet-lock keys",
LongHelp: `Retroactively revoke the specified tailnet lock keys (tlpub:abc). LongHelp: `Retroactively revoke the specified tailnet lock keys (tlpub:abc).

@ -23,7 +23,7 @@ import (
var pingCmd = &ffcli.Command{ var pingCmd = &ffcli.Command{
Name: "ping", Name: "ping",
ShortUsage: "ping <hostname-or-IP>", ShortUsage: "tailscale ping <hostname-or-IP>",
ShortHelp: "Ping a host at the Tailscale layer, see how it routed", ShortHelp: "Ping a host at the Tailscale layer, see how it routed",
LongHelp: strings.TrimSpace(` LongHelp: strings.TrimSpace(`

@ -44,13 +44,13 @@ func newServeLegacyCommand(e *serveEnv) *ffcli.Command {
Name: "serve", Name: "serve",
ShortHelp: "Serve content and local servers", ShortHelp: "Serve content and local servers",
ShortUsage: strings.Join([]string{ ShortUsage: strings.Join([]string{
"serve http:<port> <mount-point> <source> [off]", "tailscale serve http:<port> <mount-point> <source> [off]",
"serve https:<port> <mount-point> <source> [off]", "tailscale serve https:<port> <mount-point> <source> [off]",
"serve tcp:<port> tcp://localhost:<local-port> [off]", "tailscale serve tcp:<port> tcp://localhost:<local-port> [off]",
"serve tls-terminated-tcp:<port> tcp://localhost:<local-port> [off]", "tailscale serve tls-terminated-tcp:<port> tcp://localhost:<local-port> [off]",
"serve status [--json]", "tailscale serve status [--json]",
"serve reset", "tailscale serve reset",
}, "\n "), }, "\n"),
LongHelp: strings.TrimSpace(` LongHelp: strings.TrimSpace(`
*** BETA; all of this is subject to change *** *** BETA; all of this is subject to change ***
@ -91,24 +91,21 @@ EXAMPLES
local plaintext server on port 80: local plaintext server on port 80:
$ tailscale serve tls-terminated-tcp:443 tcp://localhost:80 $ tailscale serve tls-terminated-tcp:443 tcp://localhost:80
`), `),
Exec: e.runServe, Exec: e.runServe,
UsageFunc: usageFunc,
Subcommands: []*ffcli.Command{ Subcommands: []*ffcli.Command{
{ {
Name: "status", Name: "status",
Exec: e.runServeStatus, Exec: e.runServeStatus,
ShortHelp: "show current serve/funnel status", ShortHelp: "Show current serve/funnel status",
FlagSet: e.newFlags("serve-status", func(fs *flag.FlagSet) { FlagSet: e.newFlags("serve-status", func(fs *flag.FlagSet) {
fs.BoolVar(&e.json, "json", false, "output JSON") fs.BoolVar(&e.json, "json", false, "output JSON")
}), }),
UsageFunc: usageFunc,
}, },
{ {
Name: "reset", Name: "reset",
Exec: e.runServeReset, Exec: e.runServeReset,
ShortHelp: "reset current serve/funnel config", ShortHelp: "Reset current serve/funnel config",
FlagSet: e.newFlags("serve-reset", nil), FlagSet: e.newFlags("serve-reset", nil),
UsageFunc: usageFunc,
}, },
}, },
} }

@ -110,10 +110,10 @@ func newServeV2Command(e *serveEnv, subcmd serveMode) *ffcli.Command {
Name: info.Name, Name: info.Name,
ShortHelp: info.ShortHelp, ShortHelp: info.ShortHelp,
ShortUsage: strings.Join([]string{ ShortUsage: strings.Join([]string{
fmt.Sprintf("%s <target>", info.Name), fmt.Sprintf("tailscale %s <target>", info.Name),
fmt.Sprintf("%s status [--json]", info.Name), fmt.Sprintf("tailscale %s status [--json]", info.Name),
fmt.Sprintf("%s reset", info.Name), fmt.Sprintf("tailscale %s reset", info.Name),
}, "\n "), }, "\n"),
LongHelp: info.LongHelp + fmt.Sprintf(strings.TrimSpace(serveHelpCommon), info.Name), LongHelp: info.LongHelp + fmt.Sprintf(strings.TrimSpace(serveHelpCommon), info.Name),
Exec: e.runServeCombined(subcmd), Exec: e.runServeCombined(subcmd),
@ -131,20 +131,20 @@ func newServeV2Command(e *serveEnv, subcmd serveMode) *ffcli.Command {
UsageFunc: usageFuncNoDefaultValues, UsageFunc: usageFuncNoDefaultValues,
Subcommands: []*ffcli.Command{ Subcommands: []*ffcli.Command{
{ {
Name: "status", Name: "status",
Exec: e.runServeStatus, ShortUsage: "tailscale " + info.Name + " status [--json]",
ShortHelp: "view current proxy configuration", Exec: e.runServeStatus,
ShortHelp: "View current " + info.Name + " configuration",
FlagSet: e.newFlags("serve-status", func(fs *flag.FlagSet) { FlagSet: e.newFlags("serve-status", func(fs *flag.FlagSet) {
fs.BoolVar(&e.json, "json", false, "output JSON") fs.BoolVar(&e.json, "json", false, "output JSON")
}), }),
UsageFunc: usageFunc,
}, },
{ {
Name: "reset", Name: "reset",
ShortHelp: "reset current serve/funnel config", ShortUsage: "tailscale " + info.Name + " reset",
Exec: e.runServeReset, ShortHelp: "Reset current " + info.Name + " config",
FlagSet: e.newFlags("serve-reset", nil), Exec: e.runServeReset,
UsageFunc: usageFunc, FlagSet: e.newFlags("serve-reset", nil),
}, },
}, },
} }

@ -25,7 +25,7 @@ import (
var setCmd = &ffcli.Command{ var setCmd = &ffcli.Command{
Name: "set", Name: "set",
ShortUsage: "set [flags]", ShortUsage: "tailscale set [flags]",
ShortHelp: "Change specified preferences", ShortHelp: "Change specified preferences",
LongHelp: `"tailscale set" allows changing specific preferences. LongHelp: `"tailscale set" allows changing specific preferences.

@ -26,7 +26,7 @@ import (
var sshCmd = &ffcli.Command{ var sshCmd = &ffcli.Command{
Name: "ssh", Name: "ssh",
ShortUsage: "ssh [user@]<host> [args...]", ShortUsage: "tailscale ssh [user@]<host> [args...]",
ShortHelp: "SSH to a Tailscale machine", ShortHelp: "SSH to a Tailscale machine",
LongHelp: strings.TrimSpace(` LongHelp: strings.TrimSpace(`

@ -29,7 +29,7 @@ import (
var statusCmd = &ffcli.Command{ var statusCmd = &ffcli.Command{
Name: "status", Name: "status",
ShortUsage: "status [--active] [--web] [--json]", ShortUsage: "tailscale status [--active] [--web] [--json]",
ShortHelp: "Show state of tailscaled and its connections", ShortHelp: "Show state of tailscaled and its connections",
LongHelp: strings.TrimSpace(` LongHelp: strings.TrimSpace(`

@ -17,26 +17,22 @@ import (
) )
var switchCmd = &ffcli.Command{ var switchCmd = &ffcli.Command{
Name: "switch", Name: "switch",
ShortHelp: "Switches to a different Tailscale account", ShortUsage: "tailscale switch <id>",
ShortHelp: "Switches to a different Tailscale account",
LongHelp: `"tailscale switch" switches between logged in accounts. You can
use the ID that's returned from 'tailnet switch -list'
to pick which profile you want to switch to. Alternatively, you
can use the Tailnet or the account names to switch as well.
This command is currently in alpha and may change in the future.`,
FlagSet: func() *flag.FlagSet { FlagSet: func() *flag.FlagSet {
fs := flag.NewFlagSet("switch", flag.ExitOnError) fs := flag.NewFlagSet("switch", flag.ExitOnError)
fs.BoolVar(&switchArgs.list, "list", false, "list available accounts") fs.BoolVar(&switchArgs.list, "list", false, "list available accounts")
return fs return fs
}(), }(),
Exec: switchProfile, Exec: switchProfile,
UsageFunc: func(*ffcli.Command) string {
return `USAGE
switch <id>
switch --list
"tailscale switch" switches between logged in accounts. You can
use the ID that's returned from 'tailnet switch -list'
to pick which profile you want to switch to. Alternatively, you
can use the Tailnet or the account names to switch as well.
This command is currently in alpha and may change in the future.`
},
} }
var switchArgs struct { var switchArgs struct {

@ -44,7 +44,7 @@ import (
var upCmd = &ffcli.Command{ var upCmd = &ffcli.Command{
Name: "up", Name: "up",
ShortUsage: "up [flags]", ShortUsage: "tailscale up [flags]",
ShortHelp: "Connect to Tailscale, logging in if needed", ShortHelp: "Connect to Tailscale, logging in if needed",
LongHelp: strings.TrimSpace(` LongHelp: strings.TrimSpace(`

@ -19,7 +19,7 @@ import (
var updateCmd = &ffcli.Command{ var updateCmd = &ffcli.Command{
Name: "update", Name: "update",
ShortUsage: "update", ShortUsage: "tailscale update",
ShortHelp: "Update Tailscale to the latest/different version", ShortHelp: "Update Tailscale to the latest/different version",
Exec: runUpdate, Exec: runUpdate,
FlagSet: (func() *flag.FlagSet { FlagSet: (func() *flag.FlagSet {

@ -17,7 +17,7 @@ import (
var versionCmd = &ffcli.Command{ var versionCmd = &ffcli.Command{
Name: "version", Name: "version",
ShortUsage: "version [flags]", ShortUsage: "tailscale version [flags]",
ShortHelp: "Print Tailscale version", ShortHelp: "Print Tailscale version",
FlagSet: (func() *flag.FlagSet { FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("version") fs := newFlagSet("version")

@ -26,7 +26,7 @@ import (
var webCmd = &ffcli.Command{ var webCmd = &ffcli.Command{
Name: "web", Name: "web",
ShortUsage: "web [flags]", ShortUsage: "tailscale web [flags]",
ShortHelp: "Run a web server for controlling Tailscale", ShortHelp: "Run a web server for controlling Tailscale",
LongHelp: strings.TrimSpace(` LongHelp: strings.TrimSpace(`

@ -17,13 +17,12 @@ import (
var whoisCmd = &ffcli.Command{ var whoisCmd = &ffcli.Command{
Name: "whois", Name: "whois",
ShortUsage: "whois [--json] ip[:port]", ShortUsage: "tailscale whois [--json] ip[:port]",
ShortHelp: "Show the machine and user associated with a Tailscale IP (v4 or v6)", ShortHelp: "Show the machine and user associated with a Tailscale IP (v4 or v6)",
LongHelp: strings.TrimSpace(` LongHelp: strings.TrimSpace(`
'tailscale whois' shows the machine and user associated with a Tailscale IP (v4 or v6). 'tailscale whois' shows the machine and user associated with a Tailscale IP (v4 or v6).
`), `),
UsageFunc: usageFunc, Exec: runWhoIs,
Exec: runWhoIs,
FlagSet: func() *flag.FlagSet { FlagSet: func() *flag.FlagSet {
fs := newFlagSet("whois") fs := newFlagSet("whois")
fs.BoolVar(&whoIsArgs.json, "json", false, "output in JSON format") fs.BoolVar(&whoIsArgs.json, "json", false, "output in JSON format")

Loading…
Cancel
Save