mirror of
https://github.com/tailscale/tailscale.git
synced 2024-12-02 14:35:39 +00:00
8b47322acc
This commit implements probing of UDP path lifetime on the tail end of an active direct connection. Probing configuration has two parts - Cliffs, which are various timeout cliffs of interest, and CycleCanStartEvery, which limits how often a probing cycle can start, per-endpoint. Initially a statically defined default configuration will be used. The default configuration has cliffs of 10s, 30s, and 60s, with a CycleCanStartEvery of 24h. Probing results are communicated via clientmetric counters. Probing is off by default, and can be enabled via control knob. Probing is purely informational and does not yet drive any magicsock behaviors. Updates #540 Signed-off-by: Jordan Whited <jordan@tailscale.com>
206 lines
5.3 KiB
Go
206 lines
5.3 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/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, "<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 {
|
|
ipp netip.AddrPort
|
|
pi *peerInfo
|
|
}
|
|
ent := make([]kv, 0, len(c.peerMap.byIPPort))
|
|
for k, v := range c.peerMap.byIPPort {
|
|
ent = append(ent, kv{k, v})
|
|
}
|
|
sort.Slice(ent, func(i, j int) bool { return ipPortLess(ent[i].ipp, ent[j].ipp) })
|
|
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.ipp, 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 i := range c.peers.LenIter() {
|
|
p := c.peers.At(i)
|
|
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 ipPortLess(eps[i], eps[j]) })
|
|
io.WriteString(w, "<p>Endpoints:</p><ul>")
|
|
for _, ipp := range eps {
|
|
s := ep.endpointState[ipp]
|
|
if ipp == ep.bestAddr.AddrPort {
|
|
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 ipPortLess(a, b netip.AddrPort) bool {
|
|
if v := a.Addr().Compare(b.Addr()); v != 0 {
|
|
return v < 0
|
|
}
|
|
return a.Port() < b.Port()
|
|
}
|