// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package cli contains the cmd/tailscale CLI code in a package that can be included // in other wrapper binaries such as the Mac and Windows clients. package cli import ( "context" "flag" "fmt" "log" "net" "os" "os/signal" "runtime" "strings" "syscall" "text/tabwriter" "github.com/peterbourgon/ff/v2/ffcli" "tailscale.com/ipn" "tailscale.com/paths" "tailscale.com/safesocket" ) // ActLikeCLI reports whether a GUI application should act like the // CLI based on os.Args, GOOS, the context the process is running in // (pty, parent PID), etc. func ActLikeCLI() bool { if len(os.Args) < 2 { return false } switch os.Args[1] { case "up", "down", "status", "netcheck", "ping", "version", "debug", "-V", "--version", "-h", "--help": return true } return false } // Run runs the CLI. The args do not include the binary name. func Run(args []string) error { if len(args) == 1 && (args[0] == "-V" || args[0] == "--version") { args = []string{"version"} } rootfs := flag.NewFlagSet("tailscale", flag.ExitOnError) rootfs.StringVar(&rootArgs.socket, "socket", paths.DefaultTailscaledSocket(), "path to tailscaled's unix socket") rootCmd := &ffcli.Command{ Name: "tailscale", ShortUsage: "tailscale [flags] [command flags]", ShortHelp: "The easiest, most secure way to use WireGuard.", LongHelp: strings.TrimSpace(` For help on subcommands, add --help after: "tailscale status --help". This CLI is still under active development. Commands and flags will change in the future. `), Subcommands: []*ffcli.Command{ upCmd, downCmd, netcheckCmd, ipCmd, statusCmd, pingCmd, versionCmd, }, FlagSet: rootfs, Exec: func(context.Context, []string) error { return flag.ErrHelp }, UsageFunc: usageFunc, } for _, c := range rootCmd.Subcommands { c.UsageFunc = usageFunc } // Don't advertise the debug command, but it exists. if strSliceContains(args, "debug") { rootCmd.Subcommands = append(rootCmd.Subcommands, debugCmd) } if err := rootCmd.Parse(args); err != nil { return err } err := rootCmd.Run(context.Background()) if err == flag.ErrHelp { return nil } return err } func fatalf(format string, a ...interface{}) { log.SetFlags(0) log.Fatalf(format, a...) } var rootArgs struct { socket string } func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context, context.CancelFunc) { c, err := safesocket.Connect(rootArgs.socket, 41112) if err != nil { if runtime.GOOS != "windows" && rootArgs.socket == "" { fatalf("--socket cannot be empty") } fatalf("Failed to connect to tailscaled. (safesocket.Connect: %v)\n", err) } clientToServer := func(b []byte) { ipn.WriteMsg(c, b) } ctx, cancel := context.WithCancel(ctx) go func() { interrupt := make(chan os.Signal, 1) signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM) <-interrupt c.Close() cancel() }() bc := ipn.NewBackendClient(log.Printf, clientToServer) return c, bc, ctx, cancel } // pump receives backend messages on conn and pushes them into bc. func pump(ctx context.Context, bc *ipn.BackendClient, conn net.Conn) { defer conn.Close() for ctx.Err() == nil { msg, err := ipn.ReadMsg(conn) if err != nil { if ctx.Err() != nil { return } log.Printf("ReadMsg: %v\n", err) break } bc.GotNotifyMsg(msg) } } func strSliceContains(ss []string, s string) bool { for _, v := range ss { if v == s { return true } } return false } func usageFunc(c *ffcli.Command) string { var b strings.Builder fmt.Fprintf(&b, "USAGE\n") if c.ShortUsage != "" { fmt.Fprintf(&b, " %s\n", c.ShortUsage) } else { fmt.Fprintf(&b, " %s\n", c.Name) } fmt.Fprintf(&b, "\n") if c.LongHelp != "" { fmt.Fprintf(&b, "%s\n\n", c.LongHelp) } if len(c.Subcommands) > 0 { fmt.Fprintf(&b, "SUBCOMMANDS\n") tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0) for _, subcommand := range c.Subcommands { fmt.Fprintf(tw, " %s\t%s\n", subcommand.Name, subcommand.ShortHelp) } tw.Flush() fmt.Fprintf(&b, "\n") } if countFlags(c.FlagSet) > 0 { fmt.Fprintf(&b, "FLAGS\n") tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0) c.FlagSet.VisitAll(func(f *flag.Flag) { var s string name, usage := flag.UnquoteUsage(f) if isBoolFlag(f) { s = fmt.Sprintf(" --%s, --%s=false", f.Name, f.Name) } else { s = fmt.Sprintf(" --%s", f.Name) // Two spaces before --; see next two comments. if len(name) > 0 { s += " " + name } } // Four spaces before the tab triggers good alignment // for both 4- and 8-space tab stops. s += "\n \t" s += strings.ReplaceAll(usage, "\n", "\n \t") if f.DefValue != "" { s += fmt.Sprintf(" (default %s)", f.DefValue) } fmt.Fprintln(&b, s) }) tw.Flush() fmt.Fprintf(&b, "\n") } return strings.TrimSpace(b.String()) } func isBoolFlag(f *flag.Flag) bool { bf, ok := f.Value.(interface { IsBoolFlag() bool }) return ok && bf.IsBoolFlag() } func countFlags(fs *flag.FlagSet) (n int) { fs.VisitAll(func(*flag.Flag) { n++ }) return n }