diff options
| author | Brad Fitzpatrick <bradfitz@tailscale.com> | 2020-03-27 13:26:35 -0700 |
|---|---|---|
| committer | Brad Fitzpatrick <brad@danga.com> | 2020-03-27 20:34:36 -0700 |
| commit | a4ef345737abf756ccb671dc5863dfc0518e67ec (patch) | |
| tree | 369b5da0245ae78d47c8311de4cc7d7134fbafd2 /cmd | |
| parent | 810c1e9704e4898543cc3b050c5b80f9f7d64df5 (diff) | |
| download | tailscale-a4ef345737abf756ccb671dc5863dfc0518e67ec.tar.xz tailscale-a4ef345737abf756ccb671dc5863dfc0518e67ec.zip | |
cmd/tailscale: add status subcommand
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/tailscale/netcheck.go | 10 | ||||
| -rw-r--r-- | cmd/tailscale/status.go | 142 | ||||
| -rw-r--r-- | cmd/tailscale/tailscale.go | 93 |
3 files changed, 204 insertions, 41 deletions
diff --git a/cmd/tailscale/netcheck.go b/cmd/tailscale/netcheck.go index ae5849c19..2ee1c23ca 100644 --- a/cmd/tailscale/netcheck.go +++ b/cmd/tailscale/netcheck.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package main // import "tailscale.com/cmd/tailscale" +package main import ( "context" @@ -10,11 +10,19 @@ import ( "log" "sort" + "github.com/peterbourgon/ff/v2/ffcli" "tailscale.com/derp/derpmap" "tailscale.com/net/dnscache" "tailscale.com/netcheck" ) +var netcheckCmd = &ffcli.Command{ + Name: "netcheck", + ShortUsage: "netcheck", + ShortHelp: "Print an analysis of local network conditions", + Exec: runNetcheck, +} + func runNetcheck(ctx context.Context, args []string) error { c := &netcheck.Client{ DERP: derpmap.Prod(), diff --git a/cmd/tailscale/status.go b/cmd/tailscale/status.go new file mode 100644 index 000000000..2b465838f --- /dev/null +++ b/cmd/tailscale/status.go @@ -0,0 +1,142 @@ +// 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 main + +import ( + "bytes" + "context" + "encoding/json" + "flag" + "fmt" + "log" + "net" + "net/http" + "os" + + "github.com/peterbourgon/ff/v2/ffcli" + "github.com/toqueteos/webbrowser" + "tailscale.com/ipn" + "tailscale.com/ipn/ipnstate" + "tailscale.com/net/interfaces" +) + +var statusCmd = &ffcli.Command{ + Name: "status", + ShortUsage: "status [-web] [-json]", + ShortHelp: "Show state of tailscaled and its connections", + Exec: runStatus, + FlagSet: (func() *flag.FlagSet { + fs := flag.NewFlagSet("status", flag.ExitOnError) + fs.BoolVar(&statusArgs.json, "json", false, "output in JSON format (WARNING: format subject to change)") + fs.BoolVar(&statusArgs.web, "web", false, "run webserver with HTML showing status") + fs.StringVar(&statusArgs.listen, "listen", "127.0.0.1:8384", "listen address; use port 0 for automatic") + fs.BoolVar(&statusArgs.browser, "browser", true, "Open a browser in web mode") + return fs + })(), +} + +var statusArgs struct { + json bool // JSON output mode + web bool // run webserver + listen string // in web mode, webserver address to listen on, empty means auto + browser bool // in web mode, whether to open browser +} + +func runStatus(ctx context.Context, args []string) error { + c, bc, ctx, cancel := connect(ctx) + defer cancel() + + ch := make(chan *ipnstate.Status, 1) + bc.SetNotifyCallback(func(n ipn.Notify) { + if n.ErrMessage != nil { + log.Fatal(*n.ErrMessage) + } + if n.Status != nil { + ch <- n.Status + } + }) + go pump(ctx, bc, c) + + getStatus := func() (*ipnstate.Status, error) { + bc.RequestStatus() + select { + case st := <-ch: + return st, nil + case <-ctx.Done(): + return nil, ctx.Err() + } + } + st, err := getStatus() + if err != nil { + return err + } + if statusArgs.json { + j, err := json.MarshalIndent(st, "", " ") + if err != nil { + return err + } + fmt.Printf("%s", j) + return nil + } + if statusArgs.web { + ln, err := net.Listen("tcp", statusArgs.listen) + if err != nil { + return err + } + statusURL := interfaces.HTTPOfListener(ln) + fmt.Printf("Serving Tailscale status at %v ...\n", statusURL) + go func() { + <-ctx.Done() + ln.Close() + }() + if statusArgs.browser { + go webbrowser.Open(statusURL) + } + err = http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.RequestURI != "/" { + http.NotFound(w, r) + return + } + st, err := getStatus() + if err != nil { + http.Error(w, err.Error(), 500) + return + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + st.WriteHTML(w) + })) + if ctx.Err() != nil { + return ctx.Err() + } + return err + } + + var buf bytes.Buffer + f := func(format string, a ...interface{}) { fmt.Fprintf(&buf, format, a...) } + for _, peer := range st.Peers() { + ps := st.Peer[peer] + f("%s %-7s %-15s %-18s tx=%8d rx=%8d ", + peer.ShortString(), + ps.OS, + ps.TailAddr, + ps.SimpleHostName(), + ps.TxBytes, + ps.RxBytes, + ) + for i, addr := range ps.Addrs { + if i != 0 { + f(", ") + } + if addr == ps.CurAddr { + f("*%s*", addr) + } else { + f("%s", addr) + } + } + f("\n") + } + os.Stdout.Write(buf.Bytes()) + return nil +} diff --git a/cmd/tailscale/tailscale.go b/cmd/tailscale/tailscale.go index 95b2b2817..57f7cbdf7 100644 --- a/cmd/tailscale/tailscale.go +++ b/cmd/tailscale/tailscale.go @@ -14,6 +14,7 @@ import ( "net" "os" "os/signal" + "runtime" "strings" "syscall" @@ -34,18 +35,8 @@ import ( // later, the global state key doesn't look like a username. const globalStateKey = "_daemon" -// pump receives backend messages on conn and pushes them into bc. -func pump(ctx context.Context, bc *ipn.BackendClient, conn net.Conn) { - defer log.Printf("Control connection done.\n") - defer conn.Close() - for ctx.Err() == nil { - msg, err := ipn.ReadMsg(conn) - if err != nil { - log.Printf("ReadMsg: %v\n", err) - break - } - bc.GotNotifyMsg(msg) - } +var rootArgs struct { + socket string } func main() { @@ -55,7 +46,6 @@ func main() { } upf := flag.NewFlagSet("up", flag.ExitOnError) - upf.StringVar(&upArgs.socket, "socket", paths.DefaultTailscaledSocket(), "path to tailscaled's unix socket") upf.StringVar(&upArgs.server, "login-server", "https://login.tailscale.com", "base URL of control server") upf.BoolVar(&upArgs.acceptRoutes, "accept-routes", false, "accept routes advertised by other Tailscale nodes") upf.BoolVar(&upArgs.noSingleRoutes, "no-single-routes", false, "don't install routes to single nodes") @@ -79,12 +69,8 @@ options are reset to their default. Exec: runUp, } - netcheckCmd := &ffcli.Command{ - Name: "netcheck", - ShortUsage: "netcheck", - ShortHelp: "Print an analysis of local network conditions", - Exec: runNetcheck, - } + rootfs := flag.NewFlagSet("tailscale", flag.ExitOnError) + rootfs.StringVar(&rootArgs.socket, "socket", paths.DefaultTailscaledSocket(), "path to tailscaled's unix socket") rootCmd := &ffcli.Command{ Name: "tailscale", @@ -97,8 +83,10 @@ change in the future. Subcommands: []*ffcli.Command{ upCmd, netcheckCmd, + statusCmd, }, - Exec: func(context.Context, []string) error { return flag.ErrHelp }, + FlagSet: rootfs, + Exec: func(context.Context, []string) error { return flag.ErrHelp }, } if err := rootCmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil && err != flag.ErrHelp { @@ -106,14 +94,13 @@ change in the future. } } -var upArgs = struct { - socket string +var upArgs struct { server string acceptRoutes bool noSingleRoutes bool noPacketFilter bool advertiseRoutes string -}{} +} func runUp(ctx context.Context, args []string) error { if len(args) > 0 { @@ -142,25 +129,9 @@ func runUp(ctx context.Context, args []string) error { prefs.UsePacketFilter = !upArgs.noPacketFilter prefs.AdvertiseRoutes = adv - c, err := safesocket.Connect(upArgs.socket, 41112) - if err != nil { - log.Fatalf("Failed to connect to connect to tailscaled. (safesocket.Connect: %v)\n", err) - } - clientToServer := func(b []byte) { - ipn.WriteMsg(c, b) - } - - ctx, cancel := context.WithCancel(ctx) + c, bc, ctx, cancel := connect(ctx) defer cancel() - go func() { - interrupt := make(chan os.Signal, 1) - signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM) - <-interrupt - c.Close() - }() - - bc := ipn.NewBackendClient(log.Printf, clientToServer) bc.SetPrefs(prefs) opts := ipn.Options{ StateKey: globalStateKey, @@ -197,3 +168,45 @@ func runUp(ctx context.Context, args []string) error { return nil } + +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 == "" { + log.Fatalf("--socket cannot be empty") + } + log.Fatalf("Failed to connect 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) + } +} |
