// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package magicsock
import (
"fmt"
"html"
"io"
"net/http"
"net/netip"
"sort"
"strings"
"time"
"tailscale.com/tailcfg"
"tailscale.com/tstime/mono"
"tailscale.com/types/key"
)
// ServeHTTPDebug serves an HTML representation of the innards of c for debugging.
//
// It's accessible either from tailscaled's debug port (at
// /debug/magicsock) or via peerapi to a peer that's owned by the same
// user (so they can e.g. inspect their phones).
func (c *Conn) ServeHTTPDebug(w http.ResponseWriter, r *http.Request) {
c.mu.Lock()
defer c.mu.Unlock()
now := time.Now()
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprintf(w, "
magicsock
")
fmt.Fprintf(w, "# DERP
")
if c.derpMap != nil {
type D struct {
regionID int
lastWrite time.Time
createTime time.Time
}
ent := make([]D, 0, len(c.activeDerp))
for rid, ad := range c.activeDerp {
ent = append(ent, D{
regionID: rid,
lastWrite: *ad.lastWrite,
createTime: ad.createTime,
})
}
sort.Slice(ent, func(i, j int) bool {
return ent[i].regionID < ent[j].regionID
})
for _, e := range ent {
r, ok := c.derpMap.Regions[e.regionID]
if !ok {
continue
}
home := ""
if e.regionID == c.myDerp {
home = "🏠"
}
fmt.Fprintf(w, "- %s %d - %v: created %v ago, write %v ago
\n",
home, e.regionID, html.EscapeString(r.RegionCode),
now.Sub(e.createTime).Round(time.Second),
now.Sub(e.lastWrite).Round(time.Second),
)
}
}
fmt.Fprintf(w, "
\n")
fmt.Fprintf(w, "# ip:port to endpoint
")
{
type kv struct {
ipp netip.AddrPort
pi *peerInfo
}
ent := make([]kv, 0, len(c.peerMap.byIPPort))
for k, v := range c.peerMap.byIPPort {
ent = append(ent, kv{k, v})
}
sort.Slice(ent, func(i, j int) bool { return ipPortLess(ent[i].ipp, ent[j].ipp) })
for _, e := range ent {
ep := e.pi.ep
shortStr := ep.publicKey.ShortString()
fmt.Fprintf(w, "- %v: %v
\n", e.ipp, strings.Trim(shortStr, "[]"), shortStr)
}
}
fmt.Fprintf(w, "
\n")
fmt.Fprintf(w, "# endpoints by key
")
{
type kv struct {
pub key.NodePublic
pi *peerInfo
}
ent := make([]kv, 0, len(c.peerMap.byNodeKey))
for k, v := range c.peerMap.byNodeKey {
ent = append(ent, kv{k, v})
}
sort.Slice(ent, func(i, j int) bool { return ent[i].pub.Less(ent[j].pub) })
peers := map[key.NodePublic]tailcfg.NodeView{}
for i := range c.peers.Len() {
p := c.peers.At(i)
peers[p.Key()] = p
}
for _, e := range ent {
ep := e.pi.ep
shortStr := e.pub.ShortString()
name := peerDebugName(peers[e.pub])
fmt.Fprintf(w, "%v - %s
\n",
strings.Trim(shortStr, "[]"),
strings.Trim(shortStr, "[]"),
shortStr,
html.EscapeString(name))
printEndpointHTML(w, ep)
}
}
}
func printEndpointHTML(w io.Writer, ep *endpoint) {
lastRecv := ep.lastRecvWG.LoadAtomic()
ep.mu.Lock()
defer ep.mu.Unlock()
if ep.lastSendExt == 0 && lastRecv == 0 {
return // no activity ever
}
now := time.Now()
mnow := mono.Now()
fmtMono := func(m mono.Time) string {
if m == 0 {
return "-"
}
return mnow.Sub(m).Round(time.Millisecond).String()
}
fmt.Fprintf(w, "Best: %+v, %v ago (for %v)
\n", ep.bestAddr, fmtMono(ep.bestAddrAt), ep.trustBestAddrUntil.Sub(mnow).Round(time.Millisecond))
fmt.Fprintf(w, "heartbeating: %v
\n", ep.heartBeatTimer != nil)
fmt.Fprintf(w, "lastSend: %v ago
\n", fmtMono(ep.lastSendExt))
fmt.Fprintf(w, "lastFullPing: %v ago
\n", fmtMono(ep.lastFullPing))
eps := make([]netip.AddrPort, 0, len(ep.endpointState))
for ipp := range ep.endpointState {
eps = append(eps, ipp)
}
sort.Slice(eps, func(i, j int) bool { return ipPortLess(eps[i], eps[j]) })
io.WriteString(w, "Endpoints:
")
for _, ipp := range eps {
s := ep.endpointState[ipp]
if ipp == ep.bestAddr.AddrPort {
fmt.Fprintf(w, "- %s: (best)
", ipp)
} else {
fmt.Fprintf(w, "- %s: ...
", ipp)
}
fmt.Fprintf(w, "- lastPing: %v ago
\n", fmtMono(s.lastPing))
if s.lastGotPing.IsZero() {
fmt.Fprintf(w, "- disco-learned-at: -
\n")
} else {
fmt.Fprintf(w, "- disco-learned-at: %v ago
\n", now.Sub(s.lastGotPing).Round(time.Second))
}
fmt.Fprintf(w, "- callMeMaybeTime: %v
\n", s.callMeMaybeTime)
for i := range s.recentPongs {
if i == 5 {
break
}
pos := (int(s.recentPong) - i) % len(s.recentPongs)
// If s.recentPongs wraps around pos will be negative, so start
// again from the end of the slice.
if pos < 0 {
pos += len(s.recentPongs)
}
pr := s.recentPongs[pos]
fmt.Fprintf(w, "- pong %v ago: in %v, from %v src %v
\n",
fmtMono(pr.pongAt), pr.latency.Round(time.Millisecond/10),
pr.from, pr.pongSrc)
}
fmt.Fprintf(w, "
\n")
}
io.WriteString(w, "
")
}
func peerDebugName(p tailcfg.NodeView) string {
if !p.Valid() {
return ""
}
n := p.Name()
if base, _, ok := strings.Cut(n, "."); ok {
return base
}
return p.Hostinfo().Hostname()
}
func ipPortLess(a, b netip.AddrPort) bool {
if v := a.Addr().Compare(b.Addr()); v != 0 {
return v < 0
}
return a.Port() < b.Port()
}