cmd/tailscale: add tailscale status region name, last write, consistently star

There's a lot of confusion around what tailscale status shows, so make it better:
show region names, last write time, and put stars around DERP too if active.

Now stars are always present if activity, and always somewhere.
This commit is contained in:
Brad Fitzpatrick 2020-07-03 13:44:22 -07:00
parent 0ea51872c9
commit 630379a1d0
3 changed files with 89 additions and 28 deletions

View File

@ -14,6 +14,7 @@
"net" "net"
"net/http" "net/http"
"os" "os"
"time"
"github.com/peterbourgon/ff/v2/ffcli" "github.com/peterbourgon/ff/v2/ffcli"
"github.com/toqueteos/webbrowser" "github.com/toqueteos/webbrowser"
@ -127,6 +128,15 @@ func runStatus(ctx context.Context, args []string) error {
ps.TxBytes, ps.TxBytes,
ps.RxBytes, ps.RxBytes,
) )
// TODO: let server report this active bool instead
active := !ps.LastWrite.IsZero() && time.Since(ps.LastWrite) < 2*time.Minute
relay := ps.Relay
if active && relay != "" && ps.CurAddr == "" {
relay = "*" + relay + "*"
} else {
relay = " " + relay
}
f("%-6s", relay)
for i, addr := range ps.Addrs { for i, addr := range ps.Addrs {
if i != 0 { if i != 0 {
f(", ") f(", ")

View File

@ -49,10 +49,12 @@ type PeerStatus struct {
// Endpoints: // Endpoints:
Addrs []string Addrs []string
CurAddr string // one of Addrs, or unique if roaming CurAddr string // one of Addrs, or unique if roaming
Relay string // DERP region
RxBytes int64 RxBytes int64
TxBytes int64 TxBytes int64
Created time.Time // time registered with tailcontrol Created time.Time // time registered with tailcontrol
LastWrite time.Time // time last packet sent
LastSeen time.Time // last seen to tailcontrol LastSeen time.Time // last seen to tailcontrol
LastHandshake time.Time // with local wireguard LastHandshake time.Time // with local wireguard
KeepAlive bool KeepAlive bool
@ -135,6 +137,9 @@ func (sb *StatusBuilder) AddPeer(peer key.Public, st *PeerStatus) {
if v := st.HostName; v != "" { if v := st.HostName; v != "" {
e.HostName = v e.HostName = v
} }
if v := st.Relay; v != "" {
e.Relay = v
}
if v := st.UserID; v != 0 { if v := st.UserID; v != 0 {
e.UserID = v e.UserID = v
} }
@ -165,6 +170,9 @@ func (sb *StatusBuilder) AddPeer(peer key.Public, st *PeerStatus) {
if v := st.LastSeen; !v.IsZero() { if v := st.LastSeen; !v.IsZero() {
e.LastSeen = v e.LastSeen = v
} }
if v := st.LastWrite; !v.IsZero() {
e.LastWrite = v
}
if st.InNetworkMap { if st.InNetworkMap {
e.InNetworkMap = true e.InNetworkMap = true
} }
@ -211,28 +219,19 @@ func (st *Status) WriteHTML(w io.Writer) {
//f("<p><b>opts:</b> <code>%s</code></p>\n", html.EscapeString(fmt.Sprintf("%+v", opts))) //f("<p><b>opts:</b> <code>%s</code></p>\n", html.EscapeString(fmt.Sprintf("%+v", opts)))
f("<table>\n<thead>\n") f("<table>\n<thead>\n")
f("<tr><th>Peer</th><th>Node</th><th>Owner</th><th>Rx</th><th>Tx</th><th>Handshake</th><th>Endpoints</th></tr>\n") f("<tr><th>Peer</th><th>Node</th><th>Owner</th><th>Rx</th><th>Tx</th><th>Activity</th><th>Endpoints</th></tr>\n")
f("</thead>\n<tbody>\n") f("</thead>\n<tbody>\n")
now := time.Now() 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() { for _, peer := range st.Peers() {
ps := st.Peer[peer] ps := st.Peer[peer]
var hsAgo string var actAgo string
if !ps.LastHandshake.IsZero() { if !ps.LastWrite.IsZero() {
hsAgo = now.Sub(ps.LastHandshake).Round(time.Second).String() + " ago" ago := now.Sub(ps.LastWrite)
} else { actAgo = ago.Round(time.Second).String() + " ago"
if ps.LastSeen.Before(longAgo) { if ago < 5*time.Minute {
hsAgo = "<i>offline</i>" actAgo = "<b>" + actAgo + "</b>"
} else if !ps.KeepAlive {
hsAgo = "on demand"
} else {
hsAgo = "<b>pending</b>"
} }
} }
var owner string var owner string
@ -250,9 +249,20 @@ func (st *Status) WriteHTML(w io.Writer) {
html.EscapeString(owner), html.EscapeString(owner),
ps.RxBytes, ps.RxBytes,
ps.TxBytes, ps.TxBytes,
hsAgo, actAgo,
) )
f("<td class=\"aright\">") f("<td class=\"aright\">")
// TODO: let server report this active bool instead
active := !ps.LastWrite.IsZero() && time.Since(ps.LastWrite) < 2*time.Minute
relay := ps.Relay
if relay != "" {
if active && ps.CurAddr == "" {
f("🔗 <b>derp-%v</b><br>", html.EscapeString(relay))
} else {
f("derp-%v<br>", html.EscapeString(relay))
}
}
match := false match := false
for _, addr := range ps.Addrs { for _, addr := range ps.Addrs {
if addr == ps.CurAddr { if addr == ps.CurAddr {

View File

@ -686,6 +686,8 @@ func (as *AddrSet) appendDests(dsts []netaddr.IPPort, b []byte) (_ []netaddr.IPP
as.mu.Lock() as.mu.Lock()
defer as.mu.Unlock() defer as.mu.Unlock()
as.lastSend = now
// Some internal invariant checks. // Some internal invariant checks.
if len(as.addrs) != len(as.ipPorts) { if len(as.addrs) != len(as.ipPorts) {
panic(fmt.Sprintf("lena %d != leni %d", len(as.addrs), len(as.ipPorts))) panic(fmt.Sprintf("lena %d != leni %d", len(as.addrs), len(as.ipPorts)))
@ -2094,6 +2096,8 @@ type AddrSet struct {
mu sync.Mutex // guards following fields mu sync.Mutex // guards following fields
lastSend time.Time
// roamAddr is non-nil if/when we receive a correctly signed // roamAddr is non-nil if/when we receive a correctly signed
// WireGuard packet from an unexpected address. If so, we // WireGuard packet from an unexpected address. If so, we
// remember it and send responses there in the future, but // remember it and send responses there in the future, but
@ -2308,6 +2312,26 @@ func (a *AddrSet) String() string {
return buf.String() return buf.String()
} }
func (as *AddrSet) populatePeerStatus(ps *ipnstate.PeerStatus) {
as.mu.Lock()
defer as.mu.Unlock()
ps.LastWrite = as.lastSend
for i, ua := range as.addrs {
if ua.IP.Equal(derpMagicIP) {
continue
}
uaStr := ua.String()
ps.Addrs = append(ps.Addrs, uaStr)
if as.curAddr == i {
ps.CurAddr = uaStr
}
}
if as.roamAddr != nil {
ps.CurAddr = udpAddrDebugString(*as.roamAddrStd)
}
}
func (a *AddrSet) Addrs() []wgcfg.Endpoint { func (a *AddrSet) Addrs() []wgcfg.Endpoint {
var eps []wgcfg.Endpoint var eps []wgcfg.Endpoint
for _, addr := range a.addrs { for _, addr := range a.addrs {
@ -2566,6 +2590,28 @@ func sbPrintAddr(sb *strings.Builder, a net.UDPAddr) {
fmt.Fprintf(sb, ":%d", a.Port) fmt.Fprintf(sb, ":%d", a.Port)
} }
func (c *Conn) derpRegionCodeOfAddrLocked(ipPort string) string {
_, portStr, err := net.SplitHostPort(ipPort)
if err != nil {
return ""
}
regionID, err := strconv.Atoi(portStr)
if err != nil {
return ""
}
return c.derpRegionCodeOfIDLocked(regionID)
}
func (c *Conn) derpRegionCodeOfIDLocked(regionID int) string {
if c.derpMap == nil {
return ""
}
if r, ok := c.derpMap.Regions[regionID]; ok {
return r.RegionCode
}
return ""
}
func (c *Conn) UpdateStatus(sb *ipnstate.StatusBuilder) { func (c *Conn) UpdateStatus(sb *ipnstate.StatusBuilder) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
@ -2574,6 +2620,7 @@ func (c *Conn) UpdateStatus(sb *ipnstate.StatusBuilder) {
ps := &ipnstate.PeerStatus{InMagicSock: true} ps := &ipnstate.PeerStatus{InMagicSock: true}
if node, ok := c.nodeOfDisco[dk]; ok { if node, ok := c.nodeOfDisco[dk]; ok {
ps.Addrs = append(ps.Addrs, node.Endpoints...) ps.Addrs = append(ps.Addrs, node.Endpoints...)
ps.Relay = c.derpRegionCodeOfAddrLocked(node.DERP)
} }
de.populatePeerStatus(ps) de.populatePeerStatus(ps)
sb.AddPeer(de.publicKey, ps) sb.AddPeer(de.publicKey, ps)
@ -2582,17 +2629,9 @@ func (c *Conn) UpdateStatus(sb *ipnstate.StatusBuilder) {
for k, as := range c.addrsByKey { for k, as := range c.addrsByKey {
ps := &ipnstate.PeerStatus{ ps := &ipnstate.PeerStatus{
InMagicSock: true, InMagicSock: true,
Relay: c.derpRegionCodeOfIDLocked(as.derpID()),
} }
for i, ua := range as.addrs { as.populatePeerStatus(ps)
uaStr := udpAddrDebugString(ua)
ps.Addrs = append(ps.Addrs, uaStr)
if as.curAddr == i {
ps.CurAddr = uaStr
}
}
if as.roamAddr != nil {
ps.CurAddr = udpAddrDebugString(*as.roamAddrStd)
}
sb.AddPeer(k, ps) sb.AddPeer(k, ps)
} }
@ -3078,8 +3117,10 @@ func (de *discoEndpoint) populatePeerStatus(ps *ipnstate.PeerStatus) {
return return
} }
ps.LastWrite = de.lastSend
now := time.Now() now := time.Now()
if udpAddr, _ := de.addrForSendLocked(now); !udpAddr.IsZero() { if udpAddr, derpAddr := de.addrForSendLocked(now); !udpAddr.IsZero() && derpAddr.IsZero() {
ps.CurAddr = udpAddr.String() ps.CurAddr = udpAddr.String()
} }
} }