// 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" "log" "net" "net/http" "os" "strings" "time" "github.com/peterbourgon/ff/v2/ffcli" "github.com/toqueteos/webbrowser" "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; 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 { c, bc, ctx, cancel := connect(ctx) defer cancel() bc.AllowVersionSkew = true ch := make(chan *ipnstate.Status, 1) bc.SetNotifyCallback(func(n ipn.Notify) { if n.ErrMessage != nil { log.Fatal(*n.ErrMessage) } if n.Status != nil { select { case ch <- n.Status: default: // A status update from somebody else's request. // Ignoring this matters mostly for "tailscale status -web" // mode, otherwise the channel send would block forever // and pump would stop reading from tailscaled, which // previously caused tailscaled to block (while holding // a mutex), backing up unrelated clients. // See https://github.com/tailscale/tailscale/issues/1234 } } }) 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 { 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 := 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 } if st.BackendState == ipn.Stopped.String() { fmt.Println("Tailscale is stopped.") os.Exit(1) } 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 ", ps.TailAddr, dnsOrQuoteHostname(st, ps), ownerLogin(st, ps), ps.OS, ) relay := ps.Relay anyTraffic := ps.TxBytes != 0 || ps.RxBytes != 0 if !active { if anyTraffic { f("idle") } else { f("-") } } else { f("active; ") 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 { if i := strings.Index(ps.DNSName, "."); i != -1 && dnsname.HasSuffix(ps.DNSName, st.MagicDNSSuffix) { return ps.DNSName[:i] } if ps.DNSName != "" { return strings.TrimRight(ps.DNSName, ".") } return fmt.Sprintf("(%q)", strings.ReplaceAll(ps.SimpleHostName(), " ", "_")) } 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 }