cli.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. // Package cli contains the cmd/tailscale CLI code in a package that can be included
  4. // in other wrapper binaries such as the Mac and Windows clients.
  5. package cli
  6. import (
  7. "context"
  8. "encoding/json"
  9. "errors"
  10. "flag"
  11. "fmt"
  12. "io"
  13. "log"
  14. "os"
  15. "runtime"
  16. "strings"
  17. "sync"
  18. "text/tabwriter"
  19. "github.com/mattn/go-colorable"
  20. "github.com/mattn/go-isatty"
  21. "github.com/peterbourgon/ff/v3/ffcli"
  22. "tailscale.com/client/local"
  23. "tailscale.com/client/tailscale"
  24. "tailscale.com/cmd/tailscale/cli/ffcomplete"
  25. "tailscale.com/envknob"
  26. "tailscale.com/paths"
  27. "tailscale.com/util/slicesx"
  28. "tailscale.com/version/distro"
  29. )
  30. var Stderr io.Writer = os.Stderr
  31. var Stdout io.Writer = os.Stdout
  32. func errf(format string, a ...any) {
  33. fmt.Fprintf(Stderr, format, a...)
  34. }
  35. func printf(format string, a ...any) {
  36. fmt.Fprintf(Stdout, format, a...)
  37. }
  38. // outln is like fmt.Println in the common case, except when Stdout is
  39. // changed (as in js/wasm).
  40. //
  41. // It's not named println because that looks like the Go built-in
  42. // which goes to stderr and formats slightly differently.
  43. func outln(a ...any) {
  44. fmt.Fprintln(Stdout, a...)
  45. }
  46. func newFlagSet(name string) *flag.FlagSet {
  47. onError := flag.ExitOnError
  48. if runtime.GOOS == "js" {
  49. onError = flag.ContinueOnError
  50. }
  51. fs := flag.NewFlagSet(name, onError)
  52. fs.SetOutput(Stderr)
  53. return fs
  54. }
  55. // CleanUpArgs rewrites command line arguments for simplicity and backwards compatibility.
  56. // In particular, it rewrites --authkey to --auth-key.
  57. func CleanUpArgs(args []string) []string {
  58. out := make([]string, 0, len(args))
  59. for _, arg := range args {
  60. switch {
  61. // Rewrite --authkey to --auth-key, and --authkey=x to --auth-key=x,
  62. // and the same for the -authkey variant.
  63. case arg == "--authkey", arg == "-authkey":
  64. arg = "--auth-key"
  65. case strings.HasPrefix(arg, "--authkey="), strings.HasPrefix(arg, "-authkey="):
  66. _, val, _ := strings.Cut(arg, "=")
  67. arg = "--auth-key=" + val
  68. // And the same, for posture-checking => report-posture
  69. case arg == "--posture-checking", arg == "-posture-checking":
  70. arg = "--report-posture"
  71. case strings.HasPrefix(arg, "--posture-checking="), strings.HasPrefix(arg, "-posture-checking="):
  72. _, val, _ := strings.Cut(arg, "=")
  73. arg = "--report-posture=" + val
  74. }
  75. out = append(out, arg)
  76. }
  77. return out
  78. }
  79. var localClient = local.Client{
  80. Socket: paths.DefaultTailscaledSocket(),
  81. }
  82. // Run runs the CLI. The args do not include the binary name.
  83. func Run(args []string) (err error) {
  84. if runtime.GOOS == "linux" && os.Getenv("GOKRAZY_FIRST_START") == "1" && distro.Get() == distro.Gokrazy && os.Getppid() == 1 && len(args) == 0 {
  85. // We're running on gokrazy and the user did not specify 'up'.
  86. // Don't run the tailscale CLI and spam logs with usage; just exit.
  87. // See https://gokrazy.org/development/process-interface/
  88. os.Exit(0)
  89. }
  90. args = CleanUpArgs(args)
  91. if len(args) == 1 {
  92. switch args[0] {
  93. case "-V", "--version":
  94. args = []string{"version"}
  95. case "help":
  96. args = []string{"--help"}
  97. }
  98. }
  99. var warnOnce sync.Once
  100. tailscale.SetVersionMismatchHandler(func(clientVer, serverVer string) {
  101. warnOnce.Do(func() {
  102. fmt.Fprintf(Stderr, "Warning: client version %q != tailscaled server version %q\n", clientVer, serverVer)
  103. })
  104. })
  105. rootCmd := newRootCmd()
  106. if err := rootCmd.Parse(args); err != nil {
  107. if errors.Is(err, flag.ErrHelp) {
  108. return nil
  109. }
  110. if noexec := (ffcli.NoExecError{}); errors.As(err, &noexec) {
  111. // When the user enters an unknown subcommand, ffcli tries to run
  112. // the closest valid parent subcommand with everything else as args,
  113. // returning NoExecError if it doesn't have an Exec function.
  114. cmd := noexec.Command
  115. args := cmd.FlagSet.Args()
  116. if len(cmd.Subcommands) > 0 {
  117. if len(args) > 0 {
  118. return fmt.Errorf("%s: unknown subcommand: %s", fullCmd(rootCmd, cmd), args[0])
  119. }
  120. subs := make([]string, 0, len(cmd.Subcommands))
  121. for _, sub := range cmd.Subcommands {
  122. subs = append(subs, sub.Name)
  123. }
  124. return fmt.Errorf("%s: missing subcommand: %s", fullCmd(rootCmd, cmd), strings.Join(subs, ", "))
  125. }
  126. }
  127. return err
  128. }
  129. if envknob.Bool("TS_DUMP_HELP") {
  130. walkCommands(rootCmd, func(w cmdWalk) bool {
  131. fmt.Println("===")
  132. // UsageFuncs are typically called during Command.Run which ensures
  133. // FlagSet is not nil.
  134. c := w.Command
  135. if c.FlagSet == nil {
  136. c.FlagSet = flag.NewFlagSet(c.Name, flag.ContinueOnError)
  137. }
  138. if c.UsageFunc != nil {
  139. fmt.Println(c.UsageFunc(c))
  140. } else {
  141. fmt.Println(ffcli.DefaultUsageFunc(c))
  142. }
  143. return true
  144. })
  145. return
  146. }
  147. err = rootCmd.Run(context.Background())
  148. if tailscale.IsAccessDeniedError(err) && os.Getuid() != 0 && runtime.GOOS != "windows" {
  149. return fmt.Errorf("%v\n\nUse 'sudo tailscale %s'.\nTo not require root, use 'sudo tailscale set --operator=$USER' once.", err, strings.Join(args, " "))
  150. }
  151. if errors.Is(err, flag.ErrHelp) {
  152. return nil
  153. }
  154. return err
  155. }
  156. type onceFlagValue struct {
  157. flag.Value
  158. set bool
  159. }
  160. func (v *onceFlagValue) Set(s string) error {
  161. if v.set {
  162. return fmt.Errorf("flag provided multiple times")
  163. }
  164. v.set = true
  165. return v.Value.Set(s)
  166. }
  167. func (v *onceFlagValue) IsBoolFlag() bool {
  168. type boolFlag interface {
  169. IsBoolFlag() bool
  170. }
  171. bf, ok := v.Value.(boolFlag)
  172. return ok && bf.IsBoolFlag()
  173. }
  174. // noDupFlagify modifies c recursively to make all the
  175. // flag values be wrappers that permit setting the value
  176. // at most once.
  177. func noDupFlagify(c *ffcli.Command) {
  178. if c.FlagSet != nil {
  179. c.FlagSet.VisitAll(func(f *flag.Flag) {
  180. f.Value = &onceFlagValue{Value: f.Value}
  181. })
  182. }
  183. for _, sub := range c.Subcommands {
  184. noDupFlagify(sub)
  185. }
  186. }
  187. var fileCmd func() *ffcli.Command
  188. var sysPolicyCmd func() *ffcli.Command
  189. func newRootCmd() *ffcli.Command {
  190. rootfs := newFlagSet("tailscale")
  191. rootfs.Func("socket", "path to tailscaled socket", func(s string) error {
  192. localClient.Socket = s
  193. localClient.UseSocketOnly = true
  194. return nil
  195. })
  196. rootfs.Lookup("socket").DefValue = localClient.Socket
  197. jsonDocs := rootfs.Bool("json-docs", false, hidden+"print JSON-encoded docs for all subcommands and flags")
  198. var rootCmd *ffcli.Command
  199. rootCmd = &ffcli.Command{
  200. Name: "tailscale",
  201. ShortUsage: "tailscale [flags] <subcommand> [command flags]",
  202. ShortHelp: "The easiest, most secure way to use WireGuard.",
  203. LongHelp: strings.TrimSpace(`
  204. For help on subcommands, add --help after: "tailscale status --help".
  205. This CLI is still under active development. Commands and flags will
  206. change in the future.
  207. `),
  208. Subcommands: nonNilCmds(
  209. upCmd,
  210. downCmd,
  211. setCmd,
  212. loginCmd,
  213. logoutCmd,
  214. switchCmd,
  215. configureCmd(),
  216. nilOrCall(sysPolicyCmd),
  217. netcheckCmd,
  218. ipCmd,
  219. dnsCmd,
  220. statusCmd,
  221. metricsCmd,
  222. pingCmd,
  223. ncCmd,
  224. sshCmd,
  225. funnelCmd(),
  226. serveCmd(),
  227. versionCmd,
  228. webCmd,
  229. nilOrCall(fileCmd),
  230. bugReportCmd,
  231. certCmd,
  232. netlockCmd,
  233. licensesCmd,
  234. exitNodeCmd(),
  235. updateCmd,
  236. whoisCmd,
  237. debugCmd(),
  238. driveCmd,
  239. idTokenCmd,
  240. configureHostCmd(),
  241. systrayCmd,
  242. ),
  243. FlagSet: rootfs,
  244. Exec: func(ctx context.Context, args []string) error {
  245. if *jsonDocs {
  246. return printJSONDocs(rootCmd)
  247. }
  248. if len(args) > 0 {
  249. return fmt.Errorf("tailscale: unknown subcommand: %s", args[0])
  250. }
  251. return flag.ErrHelp
  252. },
  253. }
  254. walkCommands(rootCmd, func(w cmdWalk) bool {
  255. if w.UsageFunc == nil {
  256. w.UsageFunc = usageFunc
  257. }
  258. return true
  259. })
  260. ffcomplete.Inject(rootCmd, func(c *ffcli.Command) { c.LongHelp = hidden + c.LongHelp }, usageFunc)
  261. noDupFlagify(rootCmd)
  262. return rootCmd
  263. }
  264. func nonNilCmds(cmds ...*ffcli.Command) []*ffcli.Command {
  265. return slicesx.AppendNonzero(cmds[:0], cmds)
  266. }
  267. func nilOrCall(f func() *ffcli.Command) *ffcli.Command {
  268. if f == nil {
  269. return nil
  270. }
  271. return f()
  272. }
  273. func fatalf(format string, a ...any) {
  274. if Fatalf != nil {
  275. Fatalf(format, a...)
  276. return
  277. }
  278. log.SetFlags(0)
  279. log.Fatalf(format, a...)
  280. }
  281. // Fatalf, if non-nil, is used instead of log.Fatalf.
  282. var Fatalf func(format string, a ...any)
  283. type cmdWalk struct {
  284. *ffcli.Command
  285. parents []*ffcli.Command
  286. }
  287. func (w cmdWalk) Path() string {
  288. if len(w.parents) == 0 {
  289. return w.Name
  290. }
  291. var sb strings.Builder
  292. for _, p := range w.parents {
  293. sb.WriteString(p.Name)
  294. sb.WriteString(" ")
  295. }
  296. sb.WriteString(w.Name)
  297. return sb.String()
  298. }
  299. // walkCommands calls f for root and all of its nested subcommands until f
  300. // returns false or all have been visited.
  301. func walkCommands(root *ffcli.Command, f func(w cmdWalk) (more bool)) {
  302. var walk func(cmd *ffcli.Command, parents []*ffcli.Command, f func(cmdWalk) bool) bool
  303. walk = func(cmd *ffcli.Command, parents []*ffcli.Command, f func(cmdWalk) bool) bool {
  304. if !f(cmdWalk{cmd, parents}) {
  305. return false
  306. }
  307. parents = append(parents, cmd)
  308. for _, sub := range cmd.Subcommands {
  309. if !walk(sub, parents, f) {
  310. return false
  311. }
  312. }
  313. return true
  314. }
  315. walk(root, nil, f)
  316. }
  317. // fullCmd returns the full "tailscale ... cmd" invocation for a subcommand.
  318. func fullCmd(root, cmd *ffcli.Command) (full string) {
  319. walkCommands(root, func(w cmdWalk) bool {
  320. if w.Command == cmd {
  321. full = w.Path()
  322. return false
  323. }
  324. return true
  325. })
  326. if full == "" {
  327. return cmd.Name
  328. }
  329. return full
  330. }
  331. // usageFuncNoDefaultValues is like usageFunc but doesn't print default values.
  332. func usageFuncNoDefaultValues(c *ffcli.Command) string {
  333. return usageFuncOpt(c, false)
  334. }
  335. func usageFunc(c *ffcli.Command) string {
  336. return usageFuncOpt(c, true)
  337. }
  338. // hidden is the prefix that hides subcommands and flags from --help output when
  339. // found at the start of the subcommand's LongHelp or flag's Usage.
  340. const hidden = "HIDDEN: "
  341. func usageFuncOpt(c *ffcli.Command, withDefaults bool) string {
  342. var b strings.Builder
  343. if c.ShortHelp != "" {
  344. fmt.Fprintf(&b, "%s\n\n", c.ShortHelp)
  345. }
  346. fmt.Fprintf(&b, "USAGE\n")
  347. if c.ShortUsage != "" {
  348. fmt.Fprintf(&b, " %s\n", strings.ReplaceAll(c.ShortUsage, "\n", "\n "))
  349. } else {
  350. fmt.Fprintf(&b, " %s\n", c.Name)
  351. }
  352. fmt.Fprintf(&b, "\n")
  353. if help := strings.TrimPrefix(c.LongHelp, hidden); help != "" {
  354. fmt.Fprintf(&b, "%s\n\n", help)
  355. }
  356. if len(c.Subcommands) > 0 {
  357. fmt.Fprintf(&b, "SUBCOMMANDS\n")
  358. tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
  359. for _, subcommand := range c.Subcommands {
  360. if strings.HasPrefix(subcommand.LongHelp, hidden) {
  361. continue
  362. }
  363. fmt.Fprintf(tw, " %s\t%s\n", subcommand.Name, subcommand.ShortHelp)
  364. }
  365. tw.Flush()
  366. fmt.Fprintf(&b, "\n")
  367. }
  368. if countFlags(c.FlagSet) > 0 {
  369. fmt.Fprintf(&b, "FLAGS\n")
  370. tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
  371. c.FlagSet.VisitAll(func(f *flag.Flag) {
  372. var s string
  373. name, usage := flag.UnquoteUsage(f)
  374. if strings.HasPrefix(usage, hidden) {
  375. return
  376. }
  377. if isBoolFlag(f) {
  378. s = fmt.Sprintf(" --%s, --%s=false", f.Name, f.Name)
  379. } else {
  380. s = fmt.Sprintf(" --%s", f.Name) // Two spaces before --; see next two comments.
  381. if len(name) > 0 {
  382. s += " " + name
  383. }
  384. }
  385. // Four spaces before the tab triggers good alignment
  386. // for both 4- and 8-space tab stops.
  387. s += "\n \t"
  388. s += strings.ReplaceAll(usage, "\n", "\n \t")
  389. showDefault := f.DefValue != "" && withDefaults
  390. // Issue 6766: don't show the default Windows socket path. It's long
  391. // and distracting. And people on on Windows aren't likely to ever
  392. // change it anyway.
  393. if runtime.GOOS == "windows" && f.Name == "socket" && strings.HasPrefix(f.DefValue, `\\.\pipe\ProtectedPrefix\`) {
  394. showDefault = false
  395. }
  396. if showDefault {
  397. s += fmt.Sprintf(" (default %s)", f.DefValue)
  398. }
  399. fmt.Fprintln(&b, s)
  400. })
  401. tw.Flush()
  402. fmt.Fprintf(&b, "\n")
  403. }
  404. return strings.TrimSpace(b.String())
  405. }
  406. func isBoolFlag(f *flag.Flag) bool {
  407. bf, ok := f.Value.(interface {
  408. IsBoolFlag() bool
  409. })
  410. return ok && bf.IsBoolFlag()
  411. }
  412. func countFlags(fs *flag.FlagSet) (n int) {
  413. fs.VisitAll(func(*flag.Flag) { n++ })
  414. return n
  415. }
  416. // colorableOutput returns a colorable writer if stdout is a terminal (not, say,
  417. // redirected to a file or pipe), the Stdout writer is os.Stdout (we're not
  418. // embedding the CLI in wasm or a mobile app), and NO_COLOR is not set (see
  419. // https://no-color.org/). If any of those is not the case, ok is false
  420. // and w is Stdout.
  421. func colorableOutput() (w io.Writer, ok bool) {
  422. if Stdout != os.Stdout ||
  423. os.Getenv("NO_COLOR") != "" ||
  424. !isatty.IsTerminal(os.Stdout.Fd()) {
  425. return Stdout, false
  426. }
  427. return colorable.NewColorableStdout(), true
  428. }
  429. type commandDoc struct {
  430. Name string
  431. Desc string
  432. Subcommands []commandDoc `json:",omitempty"`
  433. Flags []flagDoc `json:",omitempty"`
  434. }
  435. type flagDoc struct {
  436. Name string
  437. Desc string
  438. }
  439. func printJSONDocs(root *ffcli.Command) error {
  440. docs := jsonDocsWalk(root)
  441. return json.NewEncoder(os.Stdout).Encode(docs)
  442. }
  443. func jsonDocsWalk(cmd *ffcli.Command) *commandDoc {
  444. res := &commandDoc{
  445. Name: cmd.Name,
  446. }
  447. if cmd.LongHelp != "" {
  448. res.Desc = cmd.LongHelp
  449. } else if cmd.ShortHelp != "" {
  450. res.Desc = cmd.ShortHelp
  451. } else {
  452. res.Desc = cmd.ShortUsage
  453. }
  454. if strings.HasPrefix(res.Desc, hidden) {
  455. return nil
  456. }
  457. if cmd.FlagSet != nil {
  458. cmd.FlagSet.VisitAll(func(f *flag.Flag) {
  459. if strings.HasPrefix(f.Usage, hidden) {
  460. return
  461. }
  462. res.Flags = append(res.Flags, flagDoc{
  463. Name: f.Name,
  464. Desc: f.Usage,
  465. })
  466. })
  467. }
  468. for _, sub := range cmd.Subcommands {
  469. subj := jsonDocsWalk(sub)
  470. if subj != nil {
  471. res.Subcommands = append(res.Subcommands, *subj)
  472. }
  473. }
  474. return res
  475. }