summaryrefslogtreecommitdiffhomepage
path: root/cmd/tailscaled/cli/status.go
diff options
context:
space:
mode:
authorDenton Gentry <dgentry@tailscale.com>2021-05-17 08:15:50 -0700
committerDenton Gentry <dgentry@tailscale.com>2021-05-17 08:16:50 -0700
commit1dc90404f32e9f4437e188109622dc598cc185cb (patch)
tree0b853c62f1b28589de037cd975f95f3e64e4e7f3 /cmd/tailscaled/cli/status.go
parent25df067dd0c854eebcd2841b82ad92ebb1d77165 (diff)
downloadtailscale-onebinary.tar.xz
tailscale-onebinary.zip
cmd/tailscale{,d}: combine into a single binaryonebinary
To reduce size, combine tailscaled and tailscale into a single binary which will figure out what it should do based on argv[0]. Signed-off-by: Denton Gentry <dgentry@tailscale.com>
Diffstat (limited to 'cmd/tailscaled/cli/status.go')
-rw-r--r--cmd/tailscaled/cli/status.go226
1 files changed, 226 insertions, 0 deletions
diff --git a/cmd/tailscaled/cli/status.go b/cmd/tailscaled/cli/status.go
new file mode 100644
index 000000000..af887f427
--- /dev/null
+++ b/cmd/tailscaled/cli/status.go
@@ -0,0 +1,226 @@
+// 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
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "net"
+ "net/http"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/peterbourgon/ff/v2/ffcli"
+ "github.com/toqueteos/webbrowser"
+ "inet.af/netaddr"
+ "tailscale.com/client/tailscale"
+ "tailscale.com/ipn"
+ "tailscale.com/ipn/ipnstate"
+ "tailscale.com/net/interfaces"
+ "tailscale.com/util/dnsname"
+)
+
+var statusCmd = &ffcli.Command{
+ Name: "status",
+ ShortUsage: "status [--active] [--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.BoolVar(&statusArgs.active, "active", false, "filter output to only peers with active sessions (not applicable to web mode)")
+ fs.BoolVar(&statusArgs.self, "self", true, "show status of local machine")
+ fs.BoolVar(&statusArgs.peers, "peers", true, "show status of peers")
+ fs.StringVar(&statusArgs.listen, "listen", "127.0.0.1:8384", "listen address for web mode; 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
+ active bool // in CLI mode, filter output to only peers with active sessions
+ self bool // in CLI mode, show status of local machine
+ peers bool // in CLI mode, show status of peer machines
+}
+
+func runStatus(ctx context.Context, args []string) error {
+ st, err := tailscale.Status(ctx)
+ if err != nil {
+ return err
+ }
+ if statusArgs.json {
+ if statusArgs.active {
+ for peer, ps := range st.Peer {
+ if !peerActive(ps) {
+ delete(st.Peer, peer)
+ }
+ }
+ }
+ 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 := tailscale.Status(ctx)
+ 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
+ }
+
+ switch st.BackendState {
+ default:
+ fmt.Fprintf(os.Stderr, "unexpected state: %s\n", st.BackendState)
+ os.Exit(1)
+ case ipn.Stopped.String():
+ fmt.Println("Tailscale is stopped.")
+ os.Exit(1)
+ case ipn.NeedsLogin.String():
+ fmt.Println("Logged out.")
+ if st.AuthURL != "" {
+ fmt.Printf("\nLog in at: %s\n", st.AuthURL)
+ }
+ os.Exit(1)
+ case ipn.NeedsMachineAuth.String():
+ fmt.Println("Machine is not yet authorized by tailnet admin.")
+ os.Exit(1)
+ case ipn.Running.String():
+ // Run below.
+ }
+
+ var buf bytes.Buffer
+ f := func(format string, a ...interface{}) { fmt.Fprintf(&buf, format, a...) }
+ printPS := func(ps *ipnstate.PeerStatus) {
+ active := peerActive(ps)
+ f("%-15s %-20s %-12s %-7s ",
+ firstIPString(ps.TailscaleIPs),
+ dnsOrQuoteHostname(st, ps),
+ ownerLogin(st, ps),
+ ps.OS,
+ )
+ relay := ps.Relay
+ anyTraffic := ps.TxBytes != 0 || ps.RxBytes != 0
+ if !active {
+ if ps.ExitNode {
+ f("idle; exit node")
+ } else if anyTraffic {
+ f("idle")
+ } else {
+ f("-")
+ }
+ } else {
+ f("active; ")
+ if ps.ExitNode {
+ f("exit node; ")
+ }
+ if relay != "" && ps.CurAddr == "" {
+ f("relay %q", relay)
+ } else if ps.CurAddr != "" {
+ f("direct %s", ps.CurAddr)
+ }
+ }
+ if anyTraffic {
+ f(", tx %d rx %d", ps.TxBytes, ps.RxBytes)
+ }
+ f("\n")
+ }
+
+ if statusArgs.self && st.Self != nil {
+ printPS(st.Self)
+ }
+ if statusArgs.peers {
+ var peers []*ipnstate.PeerStatus
+ for _, peer := range st.Peers() {
+ ps := st.Peer[peer]
+ if ps.ShareeNode {
+ continue
+ }
+ peers = append(peers, ps)
+ }
+ ipnstate.SortPeers(peers)
+ for _, ps := range peers {
+ active := peerActive(ps)
+ if statusArgs.active && !active {
+ continue
+ }
+ printPS(ps)
+ }
+ }
+ os.Stdout.Write(buf.Bytes())
+ return nil
+}
+
+// peerActive reports whether ps has recent activity.
+//
+// TODO: have the server report this bool instead.
+func peerActive(ps *ipnstate.PeerStatus) bool {
+ return !ps.LastWrite.IsZero() && time.Since(ps.LastWrite) < 2*time.Minute
+}
+
+func dnsOrQuoteHostname(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
+ baseName := dnsname.TrimSuffix(ps.DNSName, st.MagicDNSSuffix)
+ if baseName != "" {
+ return baseName
+ }
+ return fmt.Sprintf("(%q)", dnsname.SanitizeHostname(ps.HostName))
+}
+
+func ownerLogin(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
+ if ps.UserID.IsZero() {
+ return "-"
+ }
+ u, ok := st.User[ps.UserID]
+ if !ok {
+ return fmt.Sprint(ps.UserID)
+ }
+ if i := strings.Index(u.LoginName, "@"); i != -1 {
+ return u.LoginName[:i+1]
+ }
+ return u.LoginName
+}
+
+func firstIPString(v []netaddr.IP) string {
+ if len(v) == 0 {
+ return ""
+ }
+ return v[0].String()
+}