mirror of
https://github.com/tailscale/tailscale.git
synced 2026-05-04 19:56:35 +02:00
Replace Conn.peers (sorted views.Slice) with peersByID, a map[tailcfg.NodeID]tailcfg.NodeView. The only caller that needed the sorted slice (the disco message receive path's binary search) becomes a single map lookup. Drop nodesEqual. Add Conn.UpsertPeer / Conn.RemovePeer for O(1) single-peer endpoint work. RemovePeer also performs a targeted single-disco-key cleanup (previously that scan was O(discoInfo)). Extract the shared per-peer upsert body as upsertPeerLocked; still used by SetNetworkMap's bulk path. SetNetworkMap is documented as the bulk / initial / self-change path; UpsertPeer and RemovePeer are preferred for single-peer changes. Make the relay server set update O(1) per peer: add serverUpsertCh / serverRemoveCh to relayManager with matching run-loop handlers. UpsertPeer / RemovePeer evaluate the per-peer relay predicate locally and dispatch upsert or remove. The full-rebuild updateRelayServersSet stays for the initial netmap, filter changes, and fallback. Move the hasPeerRelayServers atomic from Conn onto relayManager, next to the serversByNodeKey map it summarizes. The run loop is now the single writer and needs no back-pointer to Conn; endpoint's two hot-path readers take one extra hop to de.c.relayManager.hasPeerRelayServers but the cost is the same atomic load. No callers use UpsertPeer/RemovePeer yet; a subsequent change will plumb per-peer add/remove through the incremental map update path. Updates #12542 Change-Id: If6a3442fe29ccbd77890ea61b754a4d1ad6ef225 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
222 lines
5.7 KiB
Go
222 lines
5.7 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// 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 := make(map[key.NodePublic]tailcfg.NodeView, len(c.peersByID))
|
|
for _, p := range c.peersByID {
|
|
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()
|
|
}
|