From 630379a1d06b9dd140a8ebe8d3668e74a5227fa4 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Fri, 3 Jul 2020 13:44:22 -0700 Subject: [PATCH] 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. --- cmd/tailscale/status.go | 10 ++++++ ipn/ipnstate/ipnstate.go | 44 ++++++++++++++--------- wgengine/magicsock/magicsock.go | 63 +++++++++++++++++++++++++++------ 3 files changed, 89 insertions(+), 28 deletions(-) diff --git a/cmd/tailscale/status.go b/cmd/tailscale/status.go index 9dc641064..237ff8a40 100644 --- a/cmd/tailscale/status.go +++ b/cmd/tailscale/status.go @@ -14,6 +14,7 @@ "net" "net/http" "os" + "time" "github.com/peterbourgon/ff/v2/ffcli" "github.com/toqueteos/webbrowser" @@ -127,6 +128,15 @@ func runStatus(ctx context.Context, args []string) error { ps.TxBytes, 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 { if i != 0 { f(", ") diff --git a/ipn/ipnstate/ipnstate.go b/ipn/ipnstate/ipnstate.go index ac754e28f..e8f41c9ca 100644 --- a/ipn/ipnstate/ipnstate.go +++ b/ipn/ipnstate/ipnstate.go @@ -49,10 +49,12 @@ type PeerStatus struct { // Endpoints: Addrs []string CurAddr string // one of Addrs, or unique if roaming + Relay string // DERP region RxBytes int64 TxBytes int64 Created time.Time // time registered with tailcontrol + LastWrite time.Time // time last packet sent LastSeen time.Time // last seen to tailcontrol LastHandshake time.Time // with local wireguard KeepAlive bool @@ -135,6 +137,9 @@ func (sb *StatusBuilder) AddPeer(peer key.Public, st *PeerStatus) { if v := st.HostName; v != "" { e.HostName = v } + if v := st.Relay; v != "" { + e.Relay = v + } if v := st.UserID; v != 0 { e.UserID = v } @@ -165,6 +170,9 @@ func (sb *StatusBuilder) AddPeer(peer key.Public, st *PeerStatus) { if v := st.LastSeen; !v.IsZero() { e.LastSeen = v } + if v := st.LastWrite; !v.IsZero() { + e.LastWrite = v + } if st.InNetworkMap { e.InNetworkMap = true } @@ -211,28 +219,19 @@ func (st *Status) WriteHTML(w io.Writer) { //f("

opts: %s

\n", html.EscapeString(fmt.Sprintf("%+v", opts))) f("\n\n") - f("\n") + f("\n") f("\n\n") 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 = "offline" - } else if !ps.KeepAlive { - hsAgo = "on demand" - } else { - hsAgo = "pending" + var actAgo string + if !ps.LastWrite.IsZero() { + ago := now.Sub(ps.LastWrite) + actAgo = ago.Round(time.Second).String() + " ago" + if ago < 5*time.Minute { + actAgo = "" + actAgo + "" } } var owner string @@ -250,9 +249,20 @@ func (st *Status) WriteHTML(w io.Writer) { html.EscapeString(owner), ps.RxBytes, ps.TxBytes, - hsAgo, + actAgo, ) f("
PeerNodeOwnerRxTxHandshakeEndpoints
PeerNodeOwnerRxTxActivityEndpoints
") + // 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("🔗 derp-%v
", html.EscapeString(relay)) + } else { + f("derp-%v
", html.EscapeString(relay)) + } + } + match := false for _, addr := range ps.Addrs { if addr == ps.CurAddr { diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go index 04ec092bc..0655a36d0 100644 --- a/wgengine/magicsock/magicsock.go +++ b/wgengine/magicsock/magicsock.go @@ -686,6 +686,8 @@ func (as *AddrSet) appendDests(dsts []netaddr.IPPort, b []byte) (_ []netaddr.IPP as.mu.Lock() defer as.mu.Unlock() + as.lastSend = now + // Some internal invariant checks. if 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 + lastSend time.Time + // roamAddr is non-nil if/when we receive a correctly signed // WireGuard packet from an unexpected address. If so, we // remember it and send responses there in the future, but @@ -2308,6 +2312,26 @@ func (a *AddrSet) String() 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 { var eps []wgcfg.Endpoint for _, addr := range a.addrs { @@ -2566,6 +2590,28 @@ func sbPrintAddr(sb *strings.Builder, a net.UDPAddr) { 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) { c.mu.Lock() defer c.mu.Unlock() @@ -2574,6 +2620,7 @@ func (c *Conn) UpdateStatus(sb *ipnstate.StatusBuilder) { ps := &ipnstate.PeerStatus{InMagicSock: true} if node, ok := c.nodeOfDisco[dk]; ok { ps.Addrs = append(ps.Addrs, node.Endpoints...) + ps.Relay = c.derpRegionCodeOfAddrLocked(node.DERP) } de.populatePeerStatus(ps) sb.AddPeer(de.publicKey, ps) @@ -2582,17 +2629,9 @@ func (c *Conn) UpdateStatus(sb *ipnstate.StatusBuilder) { for k, as := range c.addrsByKey { ps := &ipnstate.PeerStatus{ InMagicSock: true, + Relay: c.derpRegionCodeOfIDLocked(as.derpID()), } - for i, ua := range as.addrs { - 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) - } + as.populatePeerStatus(ps) sb.AddPeer(k, ps) } @@ -3078,8 +3117,10 @@ func (de *discoEndpoint) populatePeerStatus(ps *ipnstate.PeerStatus) { return } + ps.LastWrite = de.lastSend + now := time.Now() - if udpAddr, _ := de.addrForSendLocked(now); !udpAddr.IsZero() { + if udpAddr, derpAddr := de.addrForSendLocked(now); !udpAddr.IsZero() && derpAddr.IsZero() { ps.CurAddr = udpAddr.String() } }