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