2020-03-27 13:26:35 -07:00
|
|
|
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
|
|
|
// Use of this source code is governed by a BSD-style
|
2020-07-15 12:48:35 -04:00
|
|
|
// license that can be found in the LICENSE file.
|
2020-03-27 13:26:35 -07:00
|
|
|
|
2020-07-15 07:56:48 -07:00
|
|
|
package cli
|
2020-03-27 13:26:35 -07:00
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
|
|
|
"flag"
|
|
|
|
"fmt"
|
|
|
|
"net"
|
|
|
|
"net/http"
|
|
|
|
"os"
|
2021-01-10 12:03:01 -08:00
|
|
|
"strings"
|
2020-07-03 13:44:22 -07:00
|
|
|
"time"
|
2020-03-27 13:26:35 -07:00
|
|
|
|
|
|
|
"github.com/peterbourgon/ff/v2/ffcli"
|
|
|
|
"github.com/toqueteos/webbrowser"
|
2021-04-14 07:20:27 -07:00
|
|
|
"inet.af/netaddr"
|
2021-03-18 19:34:59 -07:00
|
|
|
"tailscale.com/client/tailscale"
|
2020-03-27 13:26:35 -07:00
|
|
|
"tailscale.com/ipn"
|
|
|
|
"tailscale.com/ipn/ipnstate"
|
|
|
|
"tailscale.com/net/interfaces"
|
2021-01-10 12:03:01 -08:00
|
|
|
"tailscale.com/util/dnsname"
|
2020-03-27 13:26:35 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
var statusCmd = &ffcli.Command{
|
|
|
|
Name: "status",
|
2021-03-19 13:09:10 -07:00
|
|
|
ShortUsage: "status [--active] [--web] [--json]",
|
2020-03-27 13:26:35 -07:00
|
|
|
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")
|
2020-07-09 10:37:34 -07:00
|
|
|
fs.BoolVar(&statusArgs.active, "active", false, "filter output to only peers with active sessions (not applicable to web mode)")
|
2020-08-26 07:26:10 +08:00
|
|
|
fs.BoolVar(&statusArgs.self, "self", true, "show status of local machine")
|
2021-01-10 12:03:01 -08:00
|
|
|
fs.BoolVar(&statusArgs.peers, "peers", true, "show status of peers")
|
2021-03-09 12:04:12 -08:00
|
|
|
fs.StringVar(&statusArgs.listen, "listen", "127.0.0.1:8384", "listen address for web mode; use port 0 for automatic")
|
2020-03-27 13:26:35 -07:00
|
|
|
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
|
2020-07-09 10:37:34 -07:00
|
|
|
active bool // in CLI mode, filter output to only peers with active sessions
|
2020-08-26 07:26:10 +08:00
|
|
|
self bool // in CLI mode, show status of local machine
|
2021-01-10 12:03:01 -08:00
|
|
|
peers bool // in CLI mode, show status of peer machines
|
2020-03-27 13:26:35 -07:00
|
|
|
}
|
|
|
|
|
2021-03-15 15:44:56 -04:00
|
|
|
func runStatus(ctx context.Context, args []string) error {
|
2021-03-18 19:34:59 -07:00
|
|
|
st, err := tailscale.Status(ctx)
|
2020-03-27 13:26:35 -07:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if statusArgs.json {
|
2020-07-15 09:27:48 -07:00
|
|
|
if statusArgs.active {
|
|
|
|
for peer, ps := range st.Peer {
|
|
|
|
if !peerActive(ps) {
|
|
|
|
delete(st.Peer, peer)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-03-27 13:26:35 -07:00
|
|
|
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
|
|
|
|
}
|
2021-03-18 19:34:59 -07:00
|
|
|
st, err := tailscale.Status(ctx)
|
2020-03-27 13:26:35 -07:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2021-04-09 18:20:50 -07:00
|
|
|
switch st.BackendState {
|
|
|
|
default:
|
|
|
|
fmt.Fprintf(os.Stderr, "unexpected state: %s\n", st.BackendState)
|
|
|
|
os.Exit(1)
|
|
|
|
case ipn.Stopped.String():
|
2020-08-28 10:02:32 +08:00
|
|
|
fmt.Println("Tailscale is stopped.")
|
|
|
|
os.Exit(1)
|
2021-04-09 18:20:50 -07:00
|
|
|
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.
|
2020-08-28 10:02:32 +08:00
|
|
|
}
|
|
|
|
|
2020-03-27 13:26:35 -07:00
|
|
|
var buf bytes.Buffer
|
|
|
|
f := func(format string, a ...interface{}) { fmt.Fprintf(&buf, format, a...) }
|
2020-08-26 07:26:10 +08:00
|
|
|
printPS := func(ps *ipnstate.PeerStatus) {
|
2020-07-15 09:27:48 -07:00
|
|
|
active := peerActive(ps)
|
2021-01-10 12:03:01 -08:00
|
|
|
f("%-15s %-20s %-12s %-7s ",
|
2021-04-14 07:20:27 -07:00
|
|
|
firstIPString(ps.TailscaleIPs),
|
2021-01-10 12:03:01 -08:00
|
|
|
dnsOrQuoteHostname(st, ps),
|
|
|
|
ownerLogin(st, ps),
|
|
|
|
ps.OS,
|
2020-03-27 13:26:35 -07:00
|
|
|
)
|
2020-07-03 13:44:22 -07:00
|
|
|
relay := ps.Relay
|
2021-01-10 12:03:01 -08:00
|
|
|
anyTraffic := ps.TxBytes != 0 || ps.RxBytes != 0
|
|
|
|
if !active {
|
2021-02-05 13:07:48 -08:00
|
|
|
if ps.ExitNode {
|
|
|
|
f("idle; exit node")
|
|
|
|
} else if anyTraffic {
|
2021-01-10 12:03:01 -08:00
|
|
|
f("idle")
|
2020-03-27 13:26:35 -07:00
|
|
|
} else {
|
2021-01-10 12:03:01 -08:00
|
|
|
f("-")
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
f("active; ")
|
2021-02-05 13:07:48 -08:00
|
|
|
if ps.ExitNode {
|
|
|
|
f("exit node; ")
|
|
|
|
}
|
2021-01-10 12:03:01 -08:00
|
|
|
if relay != "" && ps.CurAddr == "" {
|
|
|
|
f("relay %q", relay)
|
|
|
|
} else if ps.CurAddr != "" {
|
|
|
|
f("direct %s", ps.CurAddr)
|
2020-03-27 13:26:35 -07:00
|
|
|
}
|
|
|
|
}
|
2021-01-10 12:03:01 -08:00
|
|
|
if anyTraffic {
|
|
|
|
f(", tx %d rx %d", ps.TxBytes, ps.RxBytes)
|
|
|
|
}
|
2020-03-27 13:26:35 -07:00
|
|
|
f("\n")
|
|
|
|
}
|
2020-08-26 07:26:10 +08:00
|
|
|
|
|
|
|
if statusArgs.self && st.Self != nil {
|
|
|
|
printPS(st.Self)
|
|
|
|
}
|
2021-01-10 12:03:01 -08:00
|
|
|
if statusArgs.peers {
|
|
|
|
var peers []*ipnstate.PeerStatus
|
|
|
|
for _, peer := range st.Peers() {
|
|
|
|
ps := st.Peer[peer]
|
|
|
|
if ps.ShareeNode {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
peers = append(peers, ps)
|
2020-11-30 18:05:51 -08:00
|
|
|
}
|
2021-01-26 08:28:34 -08:00
|
|
|
ipnstate.SortPeers(peers)
|
2021-01-10 12:03:01 -08:00
|
|
|
for _, ps := range peers {
|
|
|
|
active := peerActive(ps)
|
|
|
|
if statusArgs.active && !active {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
printPS(ps)
|
2020-08-26 07:26:10 +08:00
|
|
|
}
|
|
|
|
}
|
2020-03-27 13:26:35 -07:00
|
|
|
os.Stdout.Write(buf.Bytes())
|
|
|
|
return nil
|
|
|
|
}
|
2020-07-15 09:27:48 -07:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
2021-01-10 12:03:01 -08:00
|
|
|
|
|
|
|
func dnsOrQuoteHostname(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
|
2021-02-18 17:15:38 -05:00
|
|
|
baseName := dnsname.TrimSuffix(ps.DNSName, st.MagicDNSSuffix)
|
|
|
|
if baseName != "" {
|
|
|
|
return baseName
|
2021-01-10 12:03:01 -08:00
|
|
|
}
|
2021-02-18 17:15:38 -05:00
|
|
|
return fmt.Sprintf("(%q)", dnsname.SanitizeHostname(ps.HostName))
|
2021-01-10 12:03:01 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
2021-04-14 07:20:27 -07:00
|
|
|
|
|
|
|
func firstIPString(v []netaddr.IP) string {
|
|
|
|
if len(v) == 0 {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
return v[0].String()
|
|
|
|
}
|