doctor: add ts_omit_doctor support

Updates #12614

Change-Id: I84c166c4b99ca75d70abe4087e5ff3f7d90d4bcc
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2025-09-26 13:33:08 -07:00
committed by Brad Fitzpatrick
parent 87ee0f4e98
commit 832e94607e
14 changed files with 154 additions and 109 deletions
@@ -0,0 +1,13 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by gen.go; DO NOT EDIT.
//go:build ts_omit_doctor
package buildfeatures
// HasDoctor is whether the binary was built with support for modular feature "Diagnose possible issues with Tailscale and its host environment".
// Specifically, it's whether the binary was NOT built with the "ts_omit_doctor" build tag.
// It's a const so it can be used for dead code elimination.
const HasDoctor = false
@@ -0,0 +1,13 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by gen.go; DO NOT EDIT.
//go:build !ts_omit_doctor
package buildfeatures
// HasDoctor is whether the binary was built with support for modular feature "Diagnose possible issues with Tailscale and its host environment".
// Specifically, it's whether the binary was NOT built with the "ts_omit_doctor" build tag.
// It's a const so it can be used for dead code elimination.
const HasDoctor = true
+8
View File
@@ -0,0 +1,8 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_doctor
package condregister
import _ "tailscale.com/feature/doctor"
+95
View File
@@ -0,0 +1,95 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// The doctor package registers the "doctor" problem diagnosis support into the
// rest of Tailscale.
package doctor
import (
"context"
"fmt"
"html"
"net/http"
"time"
"tailscale.com/doctor"
"tailscale.com/doctor/ethtool"
"tailscale.com/doctor/permissions"
"tailscale.com/doctor/routetable"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/net/tsaddr"
"tailscale.com/types/logger"
)
func init() {
ipnlocal.HookDoctor.Set(visitDoctor)
ipnlocal.RegisterPeerAPIHandler("/v0/doctor", handleServeDoctor)
}
func handleServeDoctor(h ipnlocal.PeerAPIHandler, w http.ResponseWriter, r *http.Request) {
if !h.CanDebug() {
http.Error(w, "denied; no debug access", http.StatusForbidden)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprintln(w, "<h1>Doctor Output</h1>")
fmt.Fprintln(w, "<pre>")
b := h.LocalBackend()
visitDoctor(r.Context(), b, func(format string, args ...any) {
line := fmt.Sprintf(format, args...)
fmt.Fprintln(w, html.EscapeString(line))
})
fmt.Fprintln(w, "</pre>")
}
func visitDoctor(ctx context.Context, b *ipnlocal.LocalBackend, logf logger.Logf) {
// We can write logs too fast for logtail to handle, even when
// opting-out of rate limits. Limit ourselves to at most one message
// per 20ms and a burst of 60 log lines, which should be fast enough to
// not block for too long but slow enough that we can upload all lines.
logf = logger.SlowLoggerWithClock(ctx, logf, 20*time.Millisecond, 60, b.Clock().Now)
var checks []doctor.Check
checks = append(checks,
permissions.Check{},
routetable.Check{},
ethtool.Check{},
)
// Print a log message if any of the global DNS resolvers are Tailscale
// IPs; this can interfere with our ability to connect to the Tailscale
// controlplane.
checks = append(checks, doctor.CheckFunc("dns-resolvers", func(_ context.Context, logf logger.Logf) error {
nm := b.NetMap()
if nm == nil {
return nil
}
for i, resolver := range nm.DNS.Resolvers {
ipp, ok := resolver.IPPort()
if ok && tsaddr.IsTailscaleIP(ipp.Addr()) {
logf("resolver %d is a Tailscale address: %v", i, resolver)
}
}
for i, resolver := range nm.DNS.FallbackResolvers {
ipp, ok := resolver.IPPort()
if ok && tsaddr.IsTailscaleIP(ipp.Addr()) {
logf("fallback resolver %d is a Tailscale address: %v", i, resolver)
}
}
return nil
}))
// TODO(andrew): more
numChecks := len(checks)
checks = append(checks, doctor.CheckFunc("numchecks", func(_ context.Context, log logger.Logf) error {
log("%d checks", numChecks)
return nil
}))
doctor.RunChecks(ctx, logf, checks...)
}
+1
View File
@@ -105,6 +105,7 @@ var Features = map[FeatureTag]FeatureMeta{
Deps: []FeatureTag{"portmapper"},
},
"desktop_sessions": {"DesktopSessions", "Desktop sessions support", nil},
"doctor": {"Doctor", "Diagnose possible issues with Tailscale and its host environment", nil},
"drive": {"Drive", "Tailscale Drive (file server) support", nil},
"gro": {
Sym: "GRO",
+2
View File
@@ -33,11 +33,13 @@ type peerAPIHandler struct {
isSelf bool // whether peerNode is owned by same user as this node
selfNode tailcfg.NodeView // this node; always non-nil
peerNode tailcfg.NodeView // peerNode is who's making the request
canDebug bool // whether peerNode can debug this node (goroutines, metrics, magicsock internal state, etc)
}
func (h *peerAPIHandler) IsSelfUntagged() bool {
return !h.selfNode.IsTagged() && !h.peerNode.IsTagged() && h.isSelf
}
func (h *peerAPIHandler) CanDebug() bool { return h.canDebug }
func (h *peerAPIHandler) Peer() tailcfg.NodeView { return h.peerNode }
func (h *peerAPIHandler) Self() tailcfg.NodeView { return h.selfNode }
func (h *peerAPIHandler) RemoteAddr() netip.AddrPort { return h.remoteAddr }