mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-30 05:25:35 +00:00
19d77ce6a3
Signed-off-by: Dmytro Shynkevych <dmytro@tailscale.com>
173 lines
4.0 KiB
Go
173 lines
4.0 KiB
Go
// 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"
|
|
"time"
|
|
|
|
"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 [-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.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
|
|
}
|
|
|
|
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 {
|
|
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 {
|
|
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
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
f := func(format string, a ...interface{}) { fmt.Fprintf(&buf, format, a...) }
|
|
for _, peer := range st.Peers() {
|
|
ps := st.Peer[peer]
|
|
active := peerActive(ps)
|
|
if statusArgs.active && !active {
|
|
continue
|
|
}
|
|
f("%s %-7s %-15s %-18s tx=%8d rx=%8d ",
|
|
peer.ShortString(),
|
|
ps.OS,
|
|
ps.TailAddr,
|
|
ps.SimpleHostName(),
|
|
ps.TxBytes,
|
|
ps.RxBytes,
|
|
)
|
|
relay := ps.Relay
|
|
if active && relay != "" && ps.CurAddr == "" {
|
|
relay = "*" + relay + "*"
|
|
} else {
|
|
relay = " " + relay
|
|
}
|
|
f("%-6s", relay)
|
|
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
|
|
}
|
|
|
|
// 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
|
|
}
|