Skip to content

Commit ae319b4

Browse files
committed
wgengine/magicsock: add HTML debug handler to see magicsock state
Change-Id: Ibc46f4e9651e1c86ec6f5d139f5e9bdc7a488415 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
1 parent c7f5bc0 commit ae319b4

File tree

3 files changed

+228
-1
lines changed

3 files changed

+228
-1
lines changed

cmd/tailscaled/tailscaled.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,6 @@ func run() error {
295295
var debugMux *http.ServeMux
296296
if args.debug != "" {
297297
debugMux = newDebugMux()
298-
go runDebugServer(debugMux, args.debug)
299298
}
300299

301300
linkMon, err := monitor.New(logf)
@@ -314,6 +313,14 @@ func run() error {
314313
if _, ok := e.(wgengine.ResolvingEngine).GetResolver(); !ok {
315314
panic("internal error: exit node resolver not wired up")
316315
}
316+
if debugMux != nil {
317+
if ig, ok := e.(wgengine.InternalsGetter); ok {
318+
if _, mc, ok := ig.GetInternals(); ok {
319+
debugMux.HandleFunc("/debug/magicsock", mc.ServeHTTPDebug)
320+
}
321+
}
322+
go runDebugServer(debugMux, args.debug)
323+
}
317324

318325
ns, err := newNetstack(logf, dialer, e)
319326
if err != nil {

ipn/ipnlocal/peerapi.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,9 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
553553
case "/v0/metrics":
554554
h.handleServeMetrics(w, r)
555555
return
556+
case "/v0/magicsock":
557+
h.handleServeMagicsock(w, r)
558+
return
556559
}
557560
who := h.peerUser.DisplayName
558561
fmt.Fprintf(w, `<html>
@@ -781,6 +784,21 @@ func (h *peerAPIHandler) handleServeEnv(w http.ResponseWriter, r *http.Request)
781784
json.NewEncoder(w).Encode(data)
782785
}
783786

787+
func (h *peerAPIHandler) handleServeMagicsock(w http.ResponseWriter, r *http.Request) {
788+
if !h.isSelf {
789+
http.Error(w, "not owner", http.StatusForbidden)
790+
return
791+
}
792+
eng := h.ps.b.e
793+
if ig, ok := eng.(wgengine.InternalsGetter); ok {
794+
if _, mc, ok := ig.GetInternals(); ok {
795+
mc.ServeHTTPDebug(w, r)
796+
return
797+
}
798+
}
799+
http.Error(w, "miswired", 500)
800+
}
801+
784802
func (h *peerAPIHandler) handleServeMetrics(w http.ResponseWriter, r *http.Request) {
785803
if !h.isSelf {
786804
http.Error(w, "not owner", http.StatusForbidden)

wgengine/magicsock/debughttp.go

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package magicsock
6+
7+
import (
8+
"fmt"
9+
"html"
10+
"io"
11+
"net/http"
12+
"sort"
13+
"strings"
14+
"time"
15+
16+
"inet.af/netaddr"
17+
"tailscale.com/tailcfg"
18+
"tailscale.com/tstime/mono"
19+
"tailscale.com/types/key"
20+
)
21+
22+
// ServeHTTPDebug serves an HTML representation of the innards of c for debugging.
23+
//
24+
// It's accessible either from tailscaled's debug port (at
25+
// /debug/magicsock) or via peerapi to a peer that's owned by the same
26+
// user (so they can e.g. inspect their phones).
27+
func (c *Conn) ServeHTTPDebug(w http.ResponseWriter, r *http.Request) {
28+
c.mu.Lock()
29+
defer c.mu.Unlock()
30+
31+
now := time.Now()
32+
33+
w.Header().Set("Content-Type", "text/html; charset=utf-8")
34+
fmt.Fprintf(w, "<h1>magicsock</h1>")
35+
36+
fmt.Fprintf(w, "<h2 id=derp><a href=#derp>#</a> DERP</h2><ul>")
37+
if c.derpMap != nil {
38+
type D struct {
39+
regionID int
40+
lastWrite time.Time
41+
createTime time.Time
42+
}
43+
ent := make([]D, 0, len(c.activeDerp))
44+
for rid, ad := range c.activeDerp {
45+
ent = append(ent, D{
46+
regionID: rid,
47+
lastWrite: *ad.lastWrite,
48+
createTime: ad.createTime,
49+
})
50+
}
51+
sort.Slice(ent, func(i, j int) bool {
52+
return ent[i].regionID < ent[j].regionID
53+
})
54+
for _, e := range ent {
55+
r, ok := c.derpMap.Regions[e.regionID]
56+
if !ok {
57+
continue
58+
}
59+
home := ""
60+
if e.regionID == c.myDerp {
61+
home = "🏠"
62+
}
63+
fmt.Fprintf(w, "<li>%s %d - %v: created %v ago, write %v ago</li>\n",
64+
home, e.regionID, html.EscapeString(r.RegionCode),
65+
now.Sub(e.createTime).Round(time.Second),
66+
now.Sub(e.lastWrite).Round(time.Second),
67+
)
68+
}
69+
70+
}
71+
fmt.Fprintf(w, "</ul>\n")
72+
73+
fmt.Fprintf(w, "<h2 id=ipport><a href=#ipport>#</a> ip:port to endpoint</h2><ul>")
74+
{
75+
type kv struct {
76+
ipp netaddr.IPPort
77+
pi *peerInfo
78+
}
79+
ent := make([]kv, 0, len(c.peerMap.byIPPort))
80+
for k, v := range c.peerMap.byIPPort {
81+
ent = append(ent, kv{k, v})
82+
}
83+
sort.Slice(ent, func(i, j int) bool { return ipPortLess(ent[i].ipp, ent[j].ipp) })
84+
for _, e := range ent {
85+
ep := e.pi.ep
86+
shortStr := ep.publicKey.ShortString()
87+
fmt.Fprintf(w, "<li>%v: <a href='#%v'>%v</a></li>\n", e.ipp, strings.Trim(shortStr, "[]"), shortStr)
88+
}
89+
90+
}
91+
fmt.Fprintf(w, "</ul>\n")
92+
93+
fmt.Fprintf(w, "<h2 id=bykey><a href=#bykey>#</a> endpoints by key</h2>")
94+
{
95+
type kv struct {
96+
pub key.NodePublic
97+
pi *peerInfo
98+
}
99+
ent := make([]kv, 0, len(c.peerMap.byNodeKey))
100+
for k, v := range c.peerMap.byNodeKey {
101+
ent = append(ent, kv{k, v})
102+
}
103+
sort.Slice(ent, func(i, j int) bool { return ent[i].pub.Less(ent[j].pub) })
104+
105+
peers := map[key.NodePublic]*tailcfg.Node{}
106+
if c.netMap != nil {
107+
for _, p := range c.netMap.Peers {
108+
peers[p.Key] = p
109+
}
110+
}
111+
112+
for _, e := range ent {
113+
ep := e.pi.ep
114+
shortStr := e.pub.ShortString()
115+
name := peerDebugName(peers[e.pub])
116+
fmt.Fprintf(w, "<h3 id=%v><a href='#%v'>%v</a> - %s</h3>\n",
117+
strings.Trim(shortStr, "[]"),
118+
strings.Trim(shortStr, "[]"),
119+
shortStr,
120+
html.EscapeString(name))
121+
printEndpointHTML(w, ep)
122+
}
123+
124+
}
125+
}
126+
127+
func printEndpointHTML(w io.Writer, ep *endpoint) {
128+
lastRecv := ep.lastRecv.LoadAtomic()
129+
130+
ep.mu.Lock()
131+
defer ep.mu.Unlock()
132+
if ep.lastSend == 0 && lastRecv == 0 {
133+
return // no activity ever
134+
}
135+
136+
now := time.Now()
137+
mnow := mono.Now()
138+
fmtMono := func(m mono.Time) string {
139+
if m == 0 {
140+
return "-"
141+
}
142+
return mnow.Sub(m).Round(time.Millisecond).String()
143+
}
144+
145+
fmt.Fprintf(w, "<p>Best: <b>%+v</b>, %v ago (for %v)</p>\n", ep.bestAddr, fmtMono(ep.bestAddrAt), ep.trustBestAddrUntil.Sub(mnow).Round(time.Millisecond))
146+
fmt.Fprintf(w, "<p>heartbeating: %v</p>\n", ep.heartBeatTimer != nil)
147+
fmt.Fprintf(w, "<p>lastSend: %v ago</p>\n", fmtMono(ep.lastSend))
148+
fmt.Fprintf(w, "<p>lastFullPing: %v ago</p>\n", fmtMono(ep.lastFullPing))
149+
150+
eps := make([]netaddr.IPPort, 0, len(ep.endpointState))
151+
for ipp := range ep.endpointState {
152+
eps = append(eps, ipp)
153+
}
154+
sort.Slice(eps, func(i, j int) bool { return ipPortLess(eps[i], eps[j]) })
155+
io.WriteString(w, "<p>Endpoints:</p><ul>")
156+
for _, ipp := range eps {
157+
s := ep.endpointState[ipp]
158+
if ipp == ep.bestAddr.IPPort {
159+
fmt.Fprintf(w, "<li><b>%s</b>: (best)<ul>", ipp)
160+
} else {
161+
fmt.Fprintf(w, "<li>%s: ...<ul>", ipp)
162+
}
163+
fmt.Fprintf(w, "<li>lastPing: %v ago</li>\n", fmtMono(s.lastPing))
164+
if s.lastGotPing.IsZero() {
165+
fmt.Fprintf(w, "<li>disco-learned-at: -</li>\n")
166+
} else {
167+
fmt.Fprintf(w, "<li>disco-learned-at: %v ago</li>\n", now.Sub(s.lastGotPing).Round(time.Second))
168+
}
169+
fmt.Fprintf(w, "<li>callMeMaybeTime: %v</li>\n", s.callMeMaybeTime)
170+
for i := range s.recentPongs {
171+
if i == 5 {
172+
break
173+
}
174+
pos := (int(s.recentPong) - i) % len(s.recentPongs)
175+
pr := s.recentPongs[pos]
176+
fmt.Fprintf(w, "<li>pong %v ago: in %v, from %v src %v</li>\n",
177+
fmtMono(pr.pongAt), pr.latency.Round(time.Millisecond/10),
178+
pr.from, pr.pongSrc)
179+
}
180+
fmt.Fprintf(w, "</ul></li>\n")
181+
}
182+
io.WriteString(w, "</ul>")
183+
184+
}
185+
186+
func peerDebugName(p *tailcfg.Node) string {
187+
if p == nil {
188+
return ""
189+
}
190+
n := p.Name
191+
if i := strings.Index(n, "."); i != -1 {
192+
return n[:i]
193+
}
194+
return p.Hostinfo.Hostname
195+
}
196+
197+
func ipPortLess(a, b netaddr.IPPort) bool {
198+
if v := a.IP().Compare(b.IP()); v != 0 {
199+
return v < 0
200+
}
201+
return a.Port() < b.Port()
202+
}

0 commit comments

Comments
 (0)