summaryrefslogtreecommitdiffhomepage
path: root/cmd
diff options
context:
space:
mode:
authorBrad Fitzpatrick <bradfitz@tailscale.com>2020-03-27 13:26:35 -0700
committerBrad Fitzpatrick <brad@danga.com>2020-03-27 20:34:36 -0700
commita4ef345737abf756ccb671dc5863dfc0518e67ec (patch)
tree369b5da0245ae78d47c8311de4cc7d7134fbafd2 /cmd
parent810c1e9704e4898543cc3b050c5b80f9f7d64df5 (diff)
downloadtailscale-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.go10
-rw-r--r--cmd/tailscale/status.go142
-rw-r--r--cmd/tailscale/tailscale.go93
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)
+ }
+}