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

View File

@ -185,7 +185,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
LD github.com/prometheus/procfs from github.com/prometheus/client_golang/prometheus
LD github.com/prometheus/procfs/internal/fs from github.com/prometheus/procfs
LD github.com/prometheus/procfs/internal/util from github.com/prometheus/procfs
L 💣 github.com/safchain/ethtool from tailscale.com/doctor/ethtool+
L 💣 github.com/safchain/ethtool from tailscale.com/net/netkernelconf
github.com/spf13/pflag from k8s.io/client-go/tools/clientcmd
W 💣 github.com/tailscale/certstore from tailscale.com/control/controlclient
W 💣 github.com/tailscale/go-winio from tailscale.com/safesocket
@ -200,7 +200,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
github.com/tailscale/goupnp/soap from github.com/tailscale/goupnp+
github.com/tailscale/goupnp/ssdp from github.com/tailscale/goupnp
github.com/tailscale/hujson from tailscale.com/ipn/conffile+
L 💣 github.com/tailscale/netlink from tailscale.com/net/routetable+
L 💣 github.com/tailscale/netlink from tailscale.com/util/linuxfw+
L 💣 github.com/tailscale/netlink/nl from github.com/tailscale/netlink
github.com/tailscale/peercred from tailscale.com/ipn/ipnauth
github.com/tailscale/web-client-prebuilt from tailscale.com/client/web
@ -787,10 +787,6 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
tailscale.com/derp/derpconst from tailscale.com/derp/derphttp+
tailscale.com/derp/derphttp from tailscale.com/ipn/localapi+
tailscale.com/disco from tailscale.com/net/tstun+
tailscale.com/doctor from tailscale.com/ipn/ipnlocal
tailscale.com/doctor/ethtool from tailscale.com/ipn/ipnlocal
💣 tailscale.com/doctor/permissions from tailscale.com/ipn/ipnlocal
tailscale.com/doctor/routetable from tailscale.com/ipn/ipnlocal
tailscale.com/drive from tailscale.com/client/local+
tailscale.com/envknob from tailscale.com/client/local+
tailscale.com/envknob/featureknob from tailscale.com/client/web+
@ -868,7 +864,6 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
tailscale.com/net/portmapper from tailscale.com/feature/portmapper
tailscale.com/net/portmapper/portmappertype from tailscale.com/net/netcheck+
tailscale.com/net/proxymux from tailscale.com/tsnet
tailscale.com/net/routetable from tailscale.com/doctor/routetable
💣 tailscale.com/net/sockopts from tailscale.com/wgengine/magicsock
tailscale.com/net/socks5 from tailscale.com/tsnet
tailscale.com/net/sockstats from tailscale.com/control/controlclient+

View File

@ -37,9 +37,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
github.com/mdlayher/sdnotify from tailscale.com/util/systemd
💣 github.com/mdlayher/socket from github.com/mdlayher/netlink+
github.com/mitchellh/go-ps from tailscale.com/safesocket
💣 github.com/safchain/ethtool from tailscale.com/doctor/ethtool+
💣 github.com/safchain/ethtool from tailscale.com/net/netkernelconf
github.com/tailscale/hujson from tailscale.com/ipn/conffile
💣 github.com/tailscale/netlink from tailscale.com/net/routetable+
💣 github.com/tailscale/netlink from tailscale.com/util/linuxfw+
💣 github.com/tailscale/netlink/nl from github.com/tailscale/netlink
github.com/tailscale/peercred from tailscale.com/ipn/ipnauth
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
@ -69,10 +69,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/derp/derpconst from tailscale.com/derp/derphttp+
tailscale.com/derp/derphttp from tailscale.com/cmd/tailscaled+
tailscale.com/disco from tailscale.com/net/tstun+
tailscale.com/doctor from tailscale.com/ipn/ipnlocal
tailscale.com/doctor/ethtool from tailscale.com/ipn/ipnlocal
💣 tailscale.com/doctor/permissions from tailscale.com/ipn/ipnlocal
tailscale.com/doctor/routetable from tailscale.com/ipn/ipnlocal
tailscale.com/drive from tailscale.com/ipn+
tailscale.com/envknob from tailscale.com/cmd/tailscaled+
tailscale.com/envknob/featureknob from tailscale.com/ipn/ipnlocal
@ -127,7 +123,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/net/packet/checksum from tailscale.com/net/tstun
tailscale.com/net/ping from tailscale.com/net/netcheck+
tailscale.com/net/portmapper/portmappertype from tailscale.com/net/netcheck+
tailscale.com/net/routetable from tailscale.com/doctor/routetable
tailscale.com/net/sockopts from tailscale.com/wgengine/magicsock
tailscale.com/net/sockstats from tailscale.com/control/controlclient+
tailscale.com/net/stun from tailscale.com/ipn/localapi+
@ -242,7 +237,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
golang.org/x/crypto/poly1305 from github.com/tailscale/wireguard-go/device
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
golang.org/x/exp/constraints from tailscale.com/doctor/permissions+
golang.org/x/exp/constraints from tailscale.com/tsweb/varz+
golang.org/x/exp/maps from tailscale.com/ipn/store/mem
golang.org/x/net/bpf from github.com/mdlayher/genetlink+
golang.org/x/net/dns/dnsmessage from net+

View File

@ -259,10 +259,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/derp/derpconst from tailscale.com/derp/derphttp+
tailscale.com/derp/derphttp from tailscale.com/cmd/tailscaled+
tailscale.com/disco from tailscale.com/feature/relayserver+
tailscale.com/doctor from tailscale.com/ipn/ipnlocal
tailscale.com/doctor/ethtool from tailscale.com/ipn/ipnlocal
💣 tailscale.com/doctor/permissions from tailscale.com/ipn/ipnlocal
tailscale.com/doctor/routetable from tailscale.com/ipn/ipnlocal
tailscale.com/doctor from tailscale.com/feature/doctor
tailscale.com/doctor/ethtool from tailscale.com/feature/doctor
💣 tailscale.com/doctor/permissions from tailscale.com/feature/doctor
tailscale.com/doctor/routetable from tailscale.com/feature/doctor
tailscale.com/drive from tailscale.com/client/local+
tailscale.com/drive/driveimpl from tailscale.com/cmd/tailscaled
tailscale.com/drive/driveimpl/compositedav from tailscale.com/drive/driveimpl
@ -276,6 +276,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/feature/condregister from tailscale.com/cmd/tailscaled
tailscale.com/feature/condregister/portmapper from tailscale.com/feature/condregister
tailscale.com/feature/debugportmapper from tailscale.com/feature/condregister
tailscale.com/feature/doctor from tailscale.com/feature/condregister
tailscale.com/feature/drive from tailscale.com/feature/condregister
L tailscale.com/feature/linuxdnsfight from tailscale.com/feature/condregister
tailscale.com/feature/portlist from tailscale.com/feature/condregister

View File

@ -132,7 +132,7 @@ tailscale.com/cmd/tsidp dependencies: (generated by github.com/tailscale/depawar
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink+
💣 github.com/mitchellh/go-ps from tailscale.com/safesocket
D github.com/prometheus-community/pro-bing from tailscale.com/wgengine/netstack
L 💣 github.com/safchain/ethtool from tailscale.com/doctor/ethtool+
L 💣 github.com/safchain/ethtool from tailscale.com/net/netkernelconf
W 💣 github.com/tailscale/certstore from tailscale.com/control/controlclient
W 💣 github.com/tailscale/go-winio from tailscale.com/safesocket
W 💣 github.com/tailscale/go-winio/internal/fs from github.com/tailscale/go-winio
@ -146,7 +146,7 @@ tailscale.com/cmd/tsidp dependencies: (generated by github.com/tailscale/depawar
github.com/tailscale/goupnp/soap from github.com/tailscale/goupnp+
github.com/tailscale/goupnp/ssdp from github.com/tailscale/goupnp
github.com/tailscale/hujson from tailscale.com/ipn/conffile
L 💣 github.com/tailscale/netlink from tailscale.com/net/routetable+
L 💣 github.com/tailscale/netlink from tailscale.com/util/linuxfw+
L 💣 github.com/tailscale/netlink/nl from github.com/tailscale/netlink
github.com/tailscale/peercred from tailscale.com/ipn/ipnauth
github.com/tailscale/web-client-prebuilt from tailscale.com/client/web
@ -229,10 +229,6 @@ tailscale.com/cmd/tsidp dependencies: (generated by github.com/tailscale/depawar
tailscale.com/derp/derpconst from tailscale.com/derp/derphttp+
tailscale.com/derp/derphttp from tailscale.com/ipn/localapi+
tailscale.com/disco from tailscale.com/net/tstun+
tailscale.com/doctor from tailscale.com/ipn/ipnlocal
tailscale.com/doctor/ethtool from tailscale.com/ipn/ipnlocal
💣 tailscale.com/doctor/permissions from tailscale.com/ipn/ipnlocal
tailscale.com/doctor/routetable from tailscale.com/ipn/ipnlocal
tailscale.com/drive from tailscale.com/client/local+
tailscale.com/envknob from tailscale.com/client/local+
tailscale.com/envknob/featureknob from tailscale.com/client/web+
@ -299,7 +295,6 @@ tailscale.com/cmd/tsidp dependencies: (generated by github.com/tailscale/depawar
tailscale.com/net/portmapper from tailscale.com/feature/portmapper
tailscale.com/net/portmapper/portmappertype from tailscale.com/net/netcheck+
tailscale.com/net/proxymux from tailscale.com/tsnet
tailscale.com/net/routetable from tailscale.com/doctor/routetable
💣 tailscale.com/net/sockopts from tailscale.com/wgengine/magicsock
tailscale.com/net/socks5 from tailscale.com/tsnet
tailscale.com/net/sockstats from tailscale.com/control/controlclient+

View File

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

View File

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

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
feature/doctor/doctor.go Normal file
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...)
}

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

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 }

View File

@ -43,10 +43,6 @@ import (
"tailscale.com/clientupdate"
"tailscale.com/control/controlclient"
"tailscale.com/control/controlknobs"
"tailscale.com/doctor"
"tailscale.com/doctor/ethtool"
"tailscale.com/doctor/permissions"
"tailscale.com/doctor/routetable"
"tailscale.com/drive"
"tailscale.com/envknob"
"tailscale.com/envknob/featureknob"
@ -6706,56 +6702,8 @@ func (b *LocalBackend) handleQuad100Port80Conn(w http.ResponseWriter, r *http.Re
io.WriteString(w, "</ul>\n")
}
func (b *LocalBackend) Doctor(ctx context.Context, 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 {
b.mu.Lock()
nm := b.NetMap()
b.mu.Unlock()
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...)
}
// HookDoctor is an optional hook for the "doctor" problem diagnosis feature.
var HookDoctor feature.Hook[func(context.Context, *LocalBackend, logger.Logf)]
// SetDevStateStore updates the LocalBackend's state storage to the provided values.
//

View File

@ -217,6 +217,7 @@ type peerAPIHandler struct {
type PeerAPIHandler interface {
Peer() tailcfg.NodeView
PeerCaps() tailcfg.PeerCapMap
CanDebug() bool // can remote node can debug this node (internal state, etc)
Self() tailcfg.NodeView
LocalBackend() *LocalBackend
IsSelfUntagged() bool // whether the peer is untagged and the same as this user
@ -380,9 +381,6 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case "/v0/interfaces":
h.handleServeInterfaces(w, r)
return
case "/v0/doctor":
h.handleServeDoctor(w, r)
return
case "/v0/sockstats":
h.handleServeSockStats(w, r)
return
@ -455,24 +453,6 @@ func (h *peerAPIHandler) handleServeInterfaces(w http.ResponseWriter, r *http.Re
fmt.Fprintln(w, "</table>")
}
func (h *peerAPIHandler) handleServeDoctor(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>")
h.ps.b.Doctor(r.Context(), func(format string, args ...any) {
line := fmt.Sprintf(format, args...)
fmt.Fprintln(w, html.EscapeString(line))
})
fmt.Fprintln(w, "</pre>")
}
func (h *peerAPIHandler) handleServeSockStats(w http.ResponseWriter, r *http.Request) {
if !h.canDebug() {
http.Error(w, "denied; no debug access", http.StatusForbidden)
@ -571,6 +551,8 @@ func (h *peerAPIHandler) handleServeSockStats(w http.ResponseWriter, r *http.Req
fmt.Fprintln(w, "</pre>")
}
func (h *peerAPIHandler) CanDebug() bool { return h.canDebug() }
// canDebug reports whether h can debug this node (goroutines, metrics,
// magicsock internal state, etc).
func (h *peerAPIHandler) canDebug() bool {

View File

@ -402,7 +402,9 @@ func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) {
}
if defBool(r.URL.Query().Get("diagnose"), false) {
h.b.Doctor(r.Context(), logger.WithPrefix(h.logf, "diag: "))
if f, ok := ipnlocal.HookDoctor.GetOk(); ok {
f(r.Context(), h.b, logger.WithPrefix(h.logf, "diag: "))
}
}
w.Header().Set("Content-Type", "text/plain")
fmt.Fprintln(w, startMarker)

View File

@ -132,7 +132,7 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
LA 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink+
LDW 💣 github.com/mitchellh/go-ps from tailscale.com/safesocket
DI github.com/prometheus-community/pro-bing from tailscale.com/wgengine/netstack
L 💣 github.com/safchain/ethtool from tailscale.com/doctor/ethtool+
L 💣 github.com/safchain/ethtool from tailscale.com/net/netkernelconf
W 💣 github.com/tailscale/certstore from tailscale.com/control/controlclient
W 💣 github.com/tailscale/go-winio from tailscale.com/safesocket
W 💣 github.com/tailscale/go-winio/internal/fs from github.com/tailscale/go-winio
@ -146,7 +146,7 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
github.com/tailscale/goupnp/soap from github.com/tailscale/goupnp+
github.com/tailscale/goupnp/ssdp from github.com/tailscale/goupnp
LDW github.com/tailscale/hujson from tailscale.com/ipn/conffile
L 💣 github.com/tailscale/netlink from tailscale.com/net/routetable+
L 💣 github.com/tailscale/netlink from tailscale.com/util/linuxfw+
L 💣 github.com/tailscale/netlink/nl from github.com/tailscale/netlink
github.com/tailscale/peercred from tailscale.com/ipn/ipnauth
LDW github.com/tailscale/web-client-prebuilt from tailscale.com/client/web
@ -225,10 +225,6 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
tailscale.com/derp/derpconst from tailscale.com/derp/derphttp+
tailscale.com/derp/derphttp from tailscale.com/ipn/localapi+
tailscale.com/disco from tailscale.com/net/tstun+
tailscale.com/doctor from tailscale.com/ipn/ipnlocal
tailscale.com/doctor/ethtool from tailscale.com/ipn/ipnlocal
💣 tailscale.com/doctor/permissions from tailscale.com/ipn/ipnlocal
tailscale.com/doctor/routetable from tailscale.com/ipn/ipnlocal
tailscale.com/drive from tailscale.com/client/local+
tailscale.com/envknob from tailscale.com/client/local+
tailscale.com/envknob/featureknob from tailscale.com/client/web+
@ -295,7 +291,6 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
tailscale.com/net/portmapper from tailscale.com/feature/portmapper
tailscale.com/net/portmapper/portmappertype from tailscale.com/net/netcheck+
tailscale.com/net/proxymux from tailscale.com/tsnet
tailscale.com/net/routetable from tailscale.com/doctor/routetable
💣 tailscale.com/net/sockopts from tailscale.com/wgengine/magicsock
tailscale.com/net/socks5 from tailscale.com/tsnet
tailscale.com/net/sockstats from tailscale.com/control/controlclient+