cmd/tailscale: add status subcommand

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2020-03-27 13:26:35 -07:00
committed by Brad Fitzpatrick
parent 810c1e9704
commit a4ef345737
13 changed files with 411 additions and 166 deletions
+14 -9
View File
@@ -8,6 +8,7 @@ import (
"time"
"tailscale.com/control/controlclient"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
"tailscale.com/types/empty"
"tailscale.com/wgengine"
@@ -45,15 +46,16 @@ type NetworkMap = controlclient.NetworkMap
// that they have not changed.
// They are JSON-encoded on the wire, despite the lack of struct tags.
type Notify struct {
Version string // version number of IPN backend
ErrMessage *string // critical error message, if any
LoginFinished *empty.Message // event: non-nil when login process succeeded
State *State // current IPN state has changed
Prefs *Prefs // preferences were changed
NetMap *NetworkMap // new netmap received
Engine *EngineStatus // wireguard engine stats
BrowseToURL *string // UI should open a browser right now
BackendLogID *string // public logtail id used by backend
Version string // version number of IPN backend
ErrMessage *string // critical error message, if any
LoginFinished *empty.Message // event: non-nil when login process succeeded
State *State // current IPN state has changed
Prefs *Prefs // preferences were changed
NetMap *NetworkMap // new netmap received
Engine *EngineStatus // wireguard engine stats
Status *ipnstate.Status // full status
BrowseToURL *string // UI should open a browser right now
BackendLogID *string // public logtail id used by backend
// type is mirrored in xcode/Shared/IPN.swift
}
@@ -126,6 +128,9 @@ type Backend interface {
// counts. Connection events are emitted automatically without
// polling.
RequestEngineStatus()
// RequestStatus requests that a full Status update
// notification is sent.
RequestStatus()
// FakeExpireAfter pretends that the current key is going to
// expire after duration x. This is useful for testing GUIs to
// make sure they react properly with keys that are going to
+6
View File
@@ -7,6 +7,8 @@ package ipn
import (
"log"
"time"
"tailscale.com/ipn/ipnstate"
)
type FakeBackend struct {
@@ -71,6 +73,10 @@ func (b *FakeBackend) RequestEngineStatus() {
b.notify(Notify{Engine: &EngineStatus{}})
}
func (b *FakeBackend) RequestStatus() {
b.notify(Notify{Status: &ipnstate.Status{}})
}
func (b *FakeBackend) FakeExpireAfter(x time.Duration) {
b.notify(Notify{NetMap: &NetworkMap{}})
}
+4
View File
@@ -161,6 +161,10 @@ func (h *Handle) RequestEngineStatus() {
h.b.RequestEngineStatus()
}
func (h *Handle) RequestStatus() {
h.b.RequestStatus()
}
func (h *Handle) FakeExpireAfter(x time.Duration) {
h.b.FakeExpireAfter(x)
}
+4 -100
View File
@@ -8,7 +8,6 @@ import (
"bufio"
"context"
"fmt"
"html"
"log"
"net"
"net/http"
@@ -120,7 +119,10 @@ func Run(rctx context.Context, logf logger.Logf, logid string, opts Options, e w
if opts.DebugMux != nil {
opts.DebugMux.HandleFunc("/debug/ipn", func(w http.ResponseWriter, r *http.Request) {
serveDebugHandler(w, r, logid, opts, b, e)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
st := b.Status()
// TODO(bradfitz): add LogID and opts to st?
st.WriteHTML(w)
})
}
@@ -311,101 +313,3 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
}
}
}
func serveDebugHandler(w http.ResponseWriter, r *http.Request, logid string, opts Options, b *ipn.LocalBackend, e wgengine.Engine) {
f := func(format string, args ...interface{}) { fmt.Fprintf(w, format, args...) }
w.Header().Set("Content-Type", "text/html; charset=utf-8")
f(`<html><head><style>
.owner { font-size: 80%%; color: #444; }
.tailaddr { font-size: 80%%; font-family: monospace: }
</style></head>`)
f("<body><h1>IPN state</h1><h2>Run args</h2>")
f("<p><b>logid:</b> %s</p>\n", logid)
f("<p><b>opts:</b> <code>%s</code></p>\n", html.EscapeString(fmt.Sprintf("%+v", opts)))
st := b.Status()
f("<table border=1 cellpadding=5><tr><th>Peer</th><th>Node</th><th>Rx</th><th>Tx</th><th>Handshake</th><th>Endpoints</th></tr>")
now := time.Now()
// The tailcontrol server rounds LastSeen to 10 minutes. So we
// declare that a longAgo seen time of 15 minutes means
// they're not connected.
longAgo := now.Add(-15 * time.Minute)
for _, peer := range st.Peers() {
ps := st.Peer[peer]
var hsAgo string
if !ps.LastHandshake.IsZero() {
hsAgo = now.Sub(ps.LastHandshake).Round(time.Second).String() + " ago"
} else {
if ps.LastSeen.Before(longAgo) {
hsAgo = "<i>offline</i>"
} else if !ps.KeepAlive {
hsAgo = "on demand"
} else {
hsAgo = "<b>pending</b>"
}
}
var owner string
if up, ok := st.User[ps.UserID]; ok {
owner = up.LoginName
if i := strings.Index(owner, "@"); i != -1 {
owner = owner[:i]
}
}
f("<tr><td>%s</td><td>%s<div class=owner>%s</div><div class=tailaddr>%s</div></td><td>%v</td><td>%v</td><td>%v</td>",
peer.ShortString(),
osEmoji(ps.OS)+" "+html.EscapeString(simplifyHostname(ps.HostName)),
html.EscapeString(owner),
ps.TailAddr,
ps.RxBytes,
ps.TxBytes,
hsAgo,
)
f("<td>")
match := false
for _, addr := range ps.Addrs {
if addr == ps.CurAddr {
match = true
f("<b>%s</b> 🔗<br>\n", addr)
} else {
f("%s<br>\n", addr)
}
}
if ps.CurAddr != "" && !match {
f("<b>%s</b> \xf0\x9f\xa7\xb3<br>\n", ps.CurAddr)
}
f("</tr>") // end Addrs
f("</tr>\n")
}
f("</table>")
}
func osEmoji(os string) string {
switch os {
case "linux":
return "🐧"
case "macOS":
return "🍎"
case "windows":
return "🖥️"
case "iOS":
return "📱"
case "android":
return "🤖"
case "freebsd":
return "👿"
case "openbsd":
return "🐡"
}
return "👽"
}
func simplifyHostname(s string) string {
s = strings.TrimSuffix(s, ".local")
s = strings.TrimSuffix(s, ".localdomain")
return s
}
+102
View File
@@ -9,8 +9,12 @@ package ipnstate
import (
"bytes"
"fmt"
"html"
"io"
"log"
"sort"
"strings"
"sync"
"time"
@@ -66,6 +70,14 @@ type PeerStatus struct {
InEngine bool
}
// SimpleHostName returns a potentially simplified version of ps.HostName for display purposes.
func (ps *PeerStatus) SimpleHostName() string {
n := ps.HostName
n = strings.TrimSuffix(n, ".local")
n = strings.TrimSuffix(n, ".localdomain")
return n
}
type StatusBuilder struct {
mu sync.Mutex
locked bool
@@ -170,3 +182,93 @@ func (sb *StatusBuilder) AddPeer(peer key.Public, st *PeerStatus) {
type StatusUpdater interface {
UpdateStatus(*StatusBuilder)
}
func (st *Status) WriteHTML(w io.Writer) {
f := func(format string, args ...interface{}) { fmt.Fprintf(w, format, args...) }
f(`<html><head><style>
.owner { font-size: 80%%; color: #444; }
.tailaddr { font-size: 80%%; font-family: monospace: }
</style></head>`)
f("<body><h1>Tailscale State</h1>")
//f("<p><b>logid:</b> %s</p>\n", logid)
//f("<p><b>opts:</b> <code>%s</code></p>\n", html.EscapeString(fmt.Sprintf("%+v", opts)))
f("<table border=1 cellpadding=5><tr><th>Peer</th><th>Node</th><th>Rx</th><th>Tx</th><th>Handshake</th><th>Endpoints</th></tr>")
now := time.Now()
// The tailcontrol server rounds LastSeen to 10 minutes. So we
// declare that a longAgo seen time of 15 minutes means
// they're not connected.
longAgo := now.Add(-15 * time.Minute)
for _, peer := range st.Peers() {
ps := st.Peer[peer]
var hsAgo string
if !ps.LastHandshake.IsZero() {
hsAgo = now.Sub(ps.LastHandshake).Round(time.Second).String() + " ago"
} else {
if ps.LastSeen.Before(longAgo) {
hsAgo = "<i>offline</i>"
} else if !ps.KeepAlive {
hsAgo = "on demand"
} else {
hsAgo = "<b>pending</b>"
}
}
var owner string
if up, ok := st.User[ps.UserID]; ok {
owner = up.LoginName
if i := strings.Index(owner, "@"); i != -1 {
owner = owner[:i]
}
}
f("<tr><td>%s</td><td>%s<div class=owner>%s</div><div class=tailaddr>%s</div></td><td>%v</td><td>%v</td><td>%v</td>",
peer.ShortString(),
osEmoji(ps.OS)+" "+html.EscapeString(ps.SimpleHostName()),
html.EscapeString(owner),
ps.TailAddr,
ps.RxBytes,
ps.TxBytes,
hsAgo,
)
f("<td>")
match := false
for _, addr := range ps.Addrs {
if addr == ps.CurAddr {
match = true
f("<b>%s</b> 🔗<br>\n", addr)
} else {
f("%s<br>\n", addr)
}
}
if ps.CurAddr != "" && !match {
f("<b>%s</b> \xf0\x9f\xa7\xb3<br>\n", ps.CurAddr)
}
f("</tr>") // end Addrs
f("</tr>\n")
}
f("</table>")
}
func osEmoji(os string) string {
switch os {
case "linux":
return "🐧"
case "macOS":
return "🍎"
case "windows":
return "🖥️"
case "iOS":
return "📱"
case "android":
return "🤖"
case "freebsd":
return "👿"
case "openbsd":
return "🐡"
}
return "👽"
}
+11
View File
@@ -499,6 +499,7 @@ func (b *LocalBackend) loadStateLocked(key StateKey, prefs *Prefs, legacyPath st
return nil
}
// State returns the backend's state.
func (b *LocalBackend) State() State {
b.mu.Lock()
defer b.mu.Unlock()
@@ -506,6 +507,11 @@ func (b *LocalBackend) State() State {
return b.state
}
// EngineStatus returns the engine status. See also: Status, and State.
//
// TODO(bradfitz): deprecated this and merge it with the Status method
// that returns ipnstate.Status? Maybe have that take flags for what info
// the caller cares about?
func (b *LocalBackend) EngineStatus() EngineStatus {
b.mu.Lock()
defer b.mu.Unlock()
@@ -785,6 +791,11 @@ func (b *LocalBackend) RequestEngineStatus() {
b.e.RequestStatus()
}
func (b *LocalBackend) RequestStatus() {
st := b.Status()
b.notify(Notify{Status: st})
}
// TODO(apenwarr): use a channel or something to prevent re-entrancy?
// Or maybe just call the state machine from fewer places.
func (b *LocalBackend) stateMachine() {
+12
View File
@@ -43,6 +43,7 @@ type Command struct {
Logout *NoArgs
SetPrefs *SetPrefsArgs
RequestEngineStatus *NoArgs
RequestStatus *NoArgs
FakeExpireAfter *FakeExpireAfterArgs
}
@@ -115,6 +116,9 @@ func (bs *BackendServer) GotCommand(cmd *Command) error {
} else if c := cmd.RequestEngineStatus; c != nil {
bs.b.RequestEngineStatus()
return nil
} else if c := cmd.RequestStatus; c != nil {
bs.b.RequestStatus()
return nil
} else if c := cmd.FakeExpireAfter; c != nil {
bs.b.FakeExpireAfter(c.Duration)
return nil
@@ -172,6 +176,10 @@ func (bc *BackendClient) send(cmd Command) {
bc.sendCommandMsg(b)
}
func (bc *BackendClient) SetNotifyCallback(fn func(Notify)) {
bc.notify = fn
}
func (bc *BackendClient) Quit() error {
bc.send(Command{Quit: &NoArgs{}})
return nil
@@ -200,6 +208,10 @@ func (bc *BackendClient) RequestEngineStatus() {
bc.send(Command{RequestEngineStatus: &NoArgs{}})
}
func (bc *BackendClient) RequestStatus() {
bc.send(Command{RequestStatus: &NoArgs{}})
}
func (bc *BackendClient) FakeExpireAfter(x time.Duration) {
bc.send(Command{FakeExpireAfter: &FakeExpireAfterArgs{Duration: x}})
}