mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 00:01:40 +01:00 
			
		
		
		
	Saves 168 KB. Updates #12614 Change-Id: Iaab3ae3efc6ddc7da39629ef13e5ec44976952ba Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
		
			
				
	
	
		
			222 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			222 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // 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/feature"
 | |
| 	"tailscale.com/feature/buildfeatures"
 | |
| 	"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) {
 | |
| 	if !buildfeatures.HasDebug {
 | |
| 		http.Error(w, feature.ErrUnavailable.Error(), http.StatusNotImplemented)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	c.mu.Lock()
 | |
| 	defer c.mu.Unlock()
 | |
| 
 | |
| 	now := time.Now()
 | |
| 
 | |
| 	w.Header().Set("Content-Type", "text/html; charset=utf-8")
 | |
| 	fmt.Fprintf(w, "<h1>magicsock</h1>")
 | |
| 
 | |
| 	fmt.Fprintf(w, "<h2 id=derp><a href=#derp>#</a> DERP</h2><ul>")
 | |
| 	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, "<li>%s %d - %v: created %v ago, write %v ago</li>\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, "</ul>\n")
 | |
| 
 | |
| 	fmt.Fprintf(w, "<h2 id=ipport><a href=#ipport>#</a> ip:port to endpoint</h2><ul>")
 | |
| 	{
 | |
| 		type kv struct {
 | |
| 			addr epAddr
 | |
| 			pi   *peerInfo
 | |
| 		}
 | |
| 		ent := make([]kv, 0, len(c.peerMap.byEpAddr))
 | |
| 		for k, v := range c.peerMap.byEpAddr {
 | |
| 			ent = append(ent, kv{k, v})
 | |
| 		}
 | |
| 		sort.Slice(ent, func(i, j int) bool { return epAddrLess(ent[i].addr, ent[j].addr) })
 | |
| 		for _, e := range ent {
 | |
| 			ep := e.pi.ep
 | |
| 			shortStr := ep.publicKey.ShortString()
 | |
| 			fmt.Fprintf(w, "<li>%v: <a href='#%v'>%v</a></li>\n", e.addr, strings.Trim(shortStr, "[]"), shortStr)
 | |
| 		}
 | |
| 
 | |
| 	}
 | |
| 	fmt.Fprintf(w, "</ul>\n")
 | |
| 
 | |
| 	fmt.Fprintf(w, "<h2 id=bykey><a href=#bykey>#</a> endpoints by key</h2>")
 | |
| 	{
 | |
| 		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 _, p := range c.peers.All() {
 | |
| 			peers[p.Key()] = p
 | |
| 		}
 | |
| 
 | |
| 		for _, e := range ent {
 | |
| 			ep := e.pi.ep
 | |
| 			shortStr := e.pub.ShortString()
 | |
| 			name := peerDebugName(peers[e.pub])
 | |
| 			fmt.Fprintf(w, "<h3 id=%v><a href='#%v'>%v</a> - %s</h3>\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, "<p>Best: <b>%+v</b>, %v ago (for %v)</p>\n", ep.bestAddr, fmtMono(ep.bestAddrAt), ep.trustBestAddrUntil.Sub(mnow).Round(time.Millisecond))
 | |
| 	fmt.Fprintf(w, "<p>heartbeating: %v</p>\n", ep.heartBeatTimer != nil)
 | |
| 	fmt.Fprintf(w, "<p>lastSend: %v ago</p>\n", fmtMono(ep.lastSendExt))
 | |
| 	fmt.Fprintf(w, "<p>lastFullPing: %v ago</p>\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 addrPortLess(eps[i], eps[j]) })
 | |
| 	io.WriteString(w, "<p>Endpoints:</p><ul>")
 | |
| 	for _, ipp := range eps {
 | |
| 		s := ep.endpointState[ipp]
 | |
| 		if ipp == ep.bestAddr.ap && !ep.bestAddr.vni.IsSet() {
 | |
| 			fmt.Fprintf(w, "<li><b>%s</b>: (best)<ul>", ipp)
 | |
| 		} else {
 | |
| 			fmt.Fprintf(w, "<li>%s: ...<ul>", ipp)
 | |
| 		}
 | |
| 		fmt.Fprintf(w, "<li>lastPing: %v ago</li>\n", fmtMono(s.lastPing))
 | |
| 		if s.lastGotPing.IsZero() {
 | |
| 			fmt.Fprintf(w, "<li>disco-learned-at: -</li>\n")
 | |
| 		} else {
 | |
| 			fmt.Fprintf(w, "<li>disco-learned-at: %v ago</li>\n", now.Sub(s.lastGotPing).Round(time.Second))
 | |
| 		}
 | |
| 		fmt.Fprintf(w, "<li>callMeMaybeTime: %v</li>\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, "<li>pong %v ago: in %v, from %v src %v</li>\n",
 | |
| 				fmtMono(pr.pongAt), pr.latency.Round(time.Millisecond/10),
 | |
| 				pr.from, pr.pongSrc)
 | |
| 		}
 | |
| 		fmt.Fprintf(w, "</ul></li>\n")
 | |
| 	}
 | |
| 	io.WriteString(w, "</ul>")
 | |
| 
 | |
| }
 | |
| 
 | |
| 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 addrPortLess(a, b netip.AddrPort) bool {
 | |
| 	if v := a.Addr().Compare(b.Addr()); v != 0 {
 | |
| 		return v < 0
 | |
| 	}
 | |
| 	return a.Port() < b.Port()
 | |
| }
 | |
| 
 | |
| func epAddrLess(a, b epAddr) bool {
 | |
| 	if v := a.ap.Addr().Compare(b.ap.Addr()); v != 0 {
 | |
| 		return v < 0
 | |
| 	}
 | |
| 	if a.ap.Port() == b.ap.Port() {
 | |
| 		return a.vni.Get() < b.vni.Get()
 | |
| 	}
 | |
| 	return a.ap.Port() < b.ap.Port()
 | |
| }
 |