diff options
Diffstat (limited to 'cmd/tailscaled/cli/cli.go')
| -rw-r--r-- | cmd/tailscaled/cli/cli.go | 276 |
1 files changed, 276 insertions, 0 deletions
diff --git a/cmd/tailscaled/cli/cli.go b/cmd/tailscaled/cli/cli.go new file mode 100644 index 000000000..ab58eb4a3 --- /dev/null +++ b/cmd/tailscaled/cli/cli.go @@ -0,0 +1,276 @@ +// 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" + "errors" + "flag" + "fmt" + "io" + "log" + "net" + "os" + "os/signal" + "runtime" + "strconv" + "strings" + "syscall" + "text/tabwriter" + + "github.com/peterbourgon/ff/v2/ffcli" + "tailscale.com/client/tailscale" + "tailscale.com/ipn" + "tailscale.com/paths" + "tailscale.com/safesocket" + "tailscale.com/syncs" +) + +// 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 { + // This function is only used on macOS. + if runtime.GOOS != "darwin" { + return false + } + + // Escape hatch to let people force running the macOS + // GUI Tailscale binary as the CLI. + if v, _ := strconv.ParseBool(os.Getenv("TAILSCALE_BE_CLI")); v { + return true + } + + // If our parent is launchd, we're definitely not + // being run as a CLI. + if os.Getppid() == 1 { + return false + } + + // Xcode adds the -NSDocumentRevisionsDebugMode flag on execution. + // If present, we are almost certainly being run as a GUI. + for _, arg := range os.Args { + if arg == "-NSDocumentRevisionsDebugMode" { + return false + } + } + + // Looking at the environment of the GUI Tailscale app (ps eww + // $PID), empirically none of these environment variables are + // present. But all or some of these should be present with + // Terminal.all and bash or zsh. + for _, e := range []string{ + "SHLVL", + "TERM", + "TERM_PROGRAM", + "PS1", + } { + if os.Getenv(e) != "" { + 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] <subcommand> [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, + logoutCmd, + netcheckCmd, + ipCmd, + statusCmd, + pingCmd, + versionCmd, + webCmd, + fileCmd, + bugReportCmd, + }, + 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 + } + + tailscale.TailscaledSocket = rootArgs.socket + + 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 +} + +var gotSignal syncs.AtomicBool + +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) + select { + case <-interrupt: + case <-ctx.Done(): + // Context canceled elsewhere. + signal.Reset(syscall.SIGINT, syscall.SIGTERM) + return + } + gotSignal.Set(true) + 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) error { + defer conn.Close() + for ctx.Err() == nil { + msg, err := ipn.ReadMsg(conn) + if err != nil { + if ctx.Err() != nil { + return ctx.Err() + } + if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) { + return fmt.Errorf("%w (tailscaled stopped running?)", err) + } + return err + } + bc.GotNotifyMsg(msg) + } + return ctx.Err() +} + +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 +} |
