From ae319b463612a6b484215195cfa04a0e5c36ee53 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 21 Dec 2021 10:26:13 -0800 Subject: [PATCH] wgengine/magicsock: add HTML debug handler to see magicsock state Change-Id: Ibc46f4e9651e1c86ec6f5d139f5e9bdc7a488415 Signed-off-by: Brad Fitzpatrick --- cmd/tailscaled/tailscaled.go | 9 +- ipn/ipnlocal/peerapi.go | 18 +++ wgengine/magicsock/debughttp.go | 202 ++++++++++++++++++++++++++++++++ 3 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 wgengine/magicsock/debughttp.go 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

\n") + + fmt.Fprintf(w, "

# ip:port to endpoint

\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: