diff --git a/cmd/tailscaled/tailscaled.go b/cmd/tailscaled/tailscaled.go
index 974a0ed88..6c8a1bc43 100644
--- a/cmd/tailscaled/tailscaled.go
+++ b/cmd/tailscaled/tailscaled.go
@@ -295,7 +295,6 @@ func run() error {
var debugMux *http.ServeMux
if args.debug != "" {
debugMux = newDebugMux()
- go runDebugServer(debugMux, args.debug)
}
linkMon, err := monitor.New(logf)
@@ -314,6 +313,14 @@ func run() error {
if _, ok := e.(wgengine.ResolvingEngine).GetResolver(); !ok {
panic("internal error: exit node resolver not wired up")
}
+ if debugMux != nil {
+ if ig, ok := e.(wgengine.InternalsGetter); ok {
+ if _, mc, ok := ig.GetInternals(); ok {
+ debugMux.HandleFunc("/debug/magicsock", mc.ServeHTTPDebug)
+ }
+ }
+ go runDebugServer(debugMux, args.debug)
+ }
ns, err := newNetstack(logf, dialer, e)
if err != nil {
diff --git a/ipn/ipnlocal/peerapi.go b/ipn/ipnlocal/peerapi.go
index 1fa9e2122..35b44d6f6 100644
--- a/ipn/ipnlocal/peerapi.go
+++ b/ipn/ipnlocal/peerapi.go
@@ -553,6 +553,9 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case "/v0/metrics":
h.handleServeMetrics(w, r)
return
+ case "/v0/magicsock":
+ h.handleServeMagicsock(w, r)
+ return
}
who := h.peerUser.DisplayName
fmt.Fprintf(w, `
@@ -781,6 +784,21 @@ func (h *peerAPIHandler) handleServeEnv(w http.ResponseWriter, r *http.Request)
json.NewEncoder(w).Encode(data)
}
+func (h *peerAPIHandler) handleServeMagicsock(w http.ResponseWriter, r *http.Request) {
+ if !h.isSelf {
+ http.Error(w, "not owner", http.StatusForbidden)
+ return
+ }
+ eng := h.ps.b.e
+ if ig, ok := eng.(wgengine.InternalsGetter); ok {
+ if _, mc, ok := ig.GetInternals(); ok {
+ mc.ServeHTTPDebug(w, r)
+ return
+ }
+ }
+ http.Error(w, "miswired", 500)
+}
+
func (h *peerAPIHandler) handleServeMetrics(w http.ResponseWriter, r *http.Request) {
if !h.isSelf {
http.Error(w, "not owner", http.StatusForbidden)
diff --git a/wgengine/magicsock/debughttp.go b/wgengine/magicsock/debughttp.go
new file mode 100644
index 000000000..bde862c1b
--- /dev/null
+++ b/wgengine/magicsock/debughttp.go
@@ -0,0 +1,202 @@
+// Copyright (c) 2021 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 magicsock
+
+import (
+ "fmt"
+ "html"
+ "io"
+ "net/http"
+ "sort"
+ "strings"
+ "time"
+
+ "inet.af/netaddr"
+ "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 netaddr.IPPort
+ 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.Node{}
+ if c.netMap != nil {
+ for _, p := range c.netMap.Peers {
+ 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.lastRecv.LoadAtomic()
+
+ ep.mu.Lock()
+ defer ep.mu.Unlock()
+ if ep.lastSend == 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.lastSend))
+ fmt.Fprintf(w, "lastFullPing: %v ago
\n", fmtMono(ep.lastFullPing))
+
+ eps := make([]netaddr.IPPort, 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.IPPort {
+ 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)
+ 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.Node) string {
+ if p == nil {
+ return ""
+ }
+ n := p.Name
+ if i := strings.Index(n, "."); i != -1 {
+ return n[:i]
+ }
+ return p.Hostinfo.Hostname
+}
+
+func ipPortLess(a, b netaddr.IPPort) bool {
+ if v := a.IP().Compare(b.IP()); v != 0 {
+ return v < 0
+ }
+ return a.Port() < b.Port()
+}