mirror of
https://github.com/tailscale/tailscale.git
synced 2025-10-04 20:12:16 +02:00
feature/featuretags: add a catch-all "Debug" feature flag
Saves 168 KB. Updates #12614 Change-Id: Iaab3ae3efc6ddc7da39629ef13e5ec44976952ba Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
bbb16e4e72
commit
ee034d48fc
@ -31,6 +31,8 @@ import (
|
|||||||
"tailscale.com/client/tailscale/apitype"
|
"tailscale.com/client/tailscale/apitype"
|
||||||
"tailscale.com/drive"
|
"tailscale.com/drive"
|
||||||
"tailscale.com/envknob"
|
"tailscale.com/envknob"
|
||||||
|
"tailscale.com/feature"
|
||||||
|
"tailscale.com/feature/buildfeatures"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
"tailscale.com/ipn/ipnstate"
|
"tailscale.com/ipn/ipnstate"
|
||||||
"tailscale.com/net/netutil"
|
"tailscale.com/net/netutil"
|
||||||
@ -608,6 +610,9 @@ func (lc *Client) SetDevStoreKeyValue(ctx context.Context, key, value string) er
|
|||||||
// the provided duration. If the duration is in the past, the debug logging
|
// the provided duration. If the duration is in the past, the debug logging
|
||||||
// is disabled.
|
// is disabled.
|
||||||
func (lc *Client) SetComponentDebugLogging(ctx context.Context, component string, d time.Duration) error {
|
func (lc *Client) SetComponentDebugLogging(ctx context.Context, component string, d time.Duration) error {
|
||||||
|
if !buildfeatures.HasDebug {
|
||||||
|
return feature.ErrUnavailable
|
||||||
|
}
|
||||||
body, err := lc.send(ctx, "POST",
|
body, err := lc.send(ctx, "POST",
|
||||||
fmt.Sprintf("/localapi/v0/component-debug-logging?component=%s&secs=%d",
|
fmt.Sprintf("/localapi/v0/component-debug-logging?component=%s&secs=%d",
|
||||||
url.QueryEscape(component), int64(d.Seconds())), 200, nil)
|
url.QueryEscape(component), int64(d.Seconds())), 200, nil)
|
||||||
@ -862,6 +867,9 @@ func (lc *Client) EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Pref
|
|||||||
// GetDNSOSConfig returns the system DNS configuration for the current device.
|
// GetDNSOSConfig returns the system DNS configuration for the current device.
|
||||||
// That is, it returns the DNS configuration that the system would use if Tailscale weren't being used.
|
// That is, it returns the DNS configuration that the system would use if Tailscale weren't being used.
|
||||||
func (lc *Client) GetDNSOSConfig(ctx context.Context) (*apitype.DNSOSConfig, error) {
|
func (lc *Client) GetDNSOSConfig(ctx context.Context) (*apitype.DNSOSConfig, error) {
|
||||||
|
if !buildfeatures.HasDNS {
|
||||||
|
return nil, feature.ErrUnavailable
|
||||||
|
}
|
||||||
body, err := lc.get200(ctx, "/localapi/v0/dns-osconfig")
|
body, err := lc.get200(ctx, "/localapi/v0/dns-osconfig")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -877,6 +885,9 @@ func (lc *Client) GetDNSOSConfig(ctx context.Context) (*apitype.DNSOSConfig, err
|
|||||||
// It returns the raw DNS response bytes and the resolvers that were used to answer the query
|
// It returns the raw DNS response bytes and the resolvers that were used to answer the query
|
||||||
// (often just one, but can be more if we raced multiple resolvers).
|
// (often just one, but can be more if we raced multiple resolvers).
|
||||||
func (lc *Client) QueryDNS(ctx context.Context, name string, queryType string) (bytes []byte, resolvers []*dnstype.Resolver, err error) {
|
func (lc *Client) QueryDNS(ctx context.Context, name string, queryType string) (bytes []byte, resolvers []*dnstype.Resolver, err error) {
|
||||||
|
if !buildfeatures.HasDNS {
|
||||||
|
return nil, nil, feature.ErrUnavailable
|
||||||
|
}
|
||||||
body, err := lc.get200(ctx, fmt.Sprintf("/localapi/v0/dns-query?name=%s&type=%s", url.QueryEscape(name), queryType))
|
body, err := lc.get200(ctx, fmt.Sprintf("/localapi/v0/dns-query?name=%s&type=%s", url.QueryEscape(name), queryType))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
|
@ -106,7 +106,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||||||
tailscale.com/net/portmapper/portmappertype from tailscale.com/net/netcheck+
|
tailscale.com/net/portmapper/portmappertype from tailscale.com/net/netcheck+
|
||||||
tailscale.com/net/sockopts from tailscale.com/wgengine/magicsock
|
tailscale.com/net/sockopts from tailscale.com/wgengine/magicsock
|
||||||
tailscale.com/net/sockstats from tailscale.com/control/controlclient+
|
tailscale.com/net/sockstats from tailscale.com/control/controlclient+
|
||||||
tailscale.com/net/stun from tailscale.com/ipn/localapi+
|
tailscale.com/net/stun from tailscale.com/net/netcheck+
|
||||||
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
||||||
tailscale.com/net/tlsdial/blockblame from tailscale.com/net/tlsdial
|
tailscale.com/net/tlsdial/blockblame from tailscale.com/net/tlsdial
|
||||||
tailscale.com/net/tsaddr from tailscale.com/ipn+
|
tailscale.com/net/tsaddr from tailscale.com/ipn+
|
||||||
@ -141,7 +141,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||||||
tailscale.com/types/mapx from tailscale.com/ipn/ipnext
|
tailscale.com/types/mapx from tailscale.com/ipn/ipnext
|
||||||
tailscale.com/types/netlogtype from tailscale.com/net/connstats
|
tailscale.com/types/netlogtype from tailscale.com/net/connstats
|
||||||
tailscale.com/types/netmap from tailscale.com/control/controlclient+
|
tailscale.com/types/netmap from tailscale.com/control/controlclient+
|
||||||
tailscale.com/types/nettype from tailscale.com/ipn/localapi+
|
tailscale.com/types/nettype from tailscale.com/net/batching+
|
||||||
tailscale.com/types/opt from tailscale.com/control/controlknobs+
|
tailscale.com/types/opt from tailscale.com/control/controlknobs+
|
||||||
tailscale.com/types/persist from tailscale.com/control/controlclient+
|
tailscale.com/types/persist from tailscale.com/control/controlclient+
|
||||||
tailscale.com/types/preftype from tailscale.com/ipn+
|
tailscale.com/types/preftype from tailscale.com/ipn+
|
||||||
|
@ -129,7 +129,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||||||
tailscale.com/net/portmapper/portmappertype from tailscale.com/net/netcheck+
|
tailscale.com/net/portmapper/portmappertype from tailscale.com/net/netcheck+
|
||||||
tailscale.com/net/sockopts from tailscale.com/wgengine/magicsock
|
tailscale.com/net/sockopts from tailscale.com/wgengine/magicsock
|
||||||
tailscale.com/net/sockstats from tailscale.com/control/controlclient+
|
tailscale.com/net/sockstats from tailscale.com/control/controlclient+
|
||||||
tailscale.com/net/stun from tailscale.com/ipn/localapi+
|
tailscale.com/net/stun from tailscale.com/net/netcheck+
|
||||||
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
||||||
tailscale.com/net/tlsdial/blockblame from tailscale.com/net/tlsdial
|
tailscale.com/net/tlsdial/blockblame from tailscale.com/net/tlsdial
|
||||||
tailscale.com/net/tsaddr from tailscale.com/ipn+
|
tailscale.com/net/tsaddr from tailscale.com/ipn+
|
||||||
@ -166,7 +166,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||||||
tailscale.com/types/mapx from tailscale.com/ipn/ipnext
|
tailscale.com/types/mapx from tailscale.com/ipn/ipnext
|
||||||
tailscale.com/types/netlogtype from tailscale.com/net/connstats
|
tailscale.com/types/netlogtype from tailscale.com/net/connstats
|
||||||
tailscale.com/types/netmap from tailscale.com/control/controlclient+
|
tailscale.com/types/netmap from tailscale.com/control/controlclient+
|
||||||
tailscale.com/types/nettype from tailscale.com/ipn/localapi+
|
tailscale.com/types/nettype from tailscale.com/net/batching+
|
||||||
tailscale.com/types/opt from tailscale.com/control/controlknobs+
|
tailscale.com/types/opt from tailscale.com/control/controlknobs+
|
||||||
tailscale.com/types/persist from tailscale.com/control/controlclient+
|
tailscale.com/types/persist from tailscale.com/control/controlclient+
|
||||||
tailscale.com/types/preftype from tailscale.com/ipn+
|
tailscale.com/types/preftype from tailscale.com/ipn+
|
||||||
|
@ -1193,7 +1193,7 @@ func (c *Direct) handleDebugMessage(ctx context.Context, debug *tailcfg.Debug) e
|
|||||||
c.logf("exiting process with status %v per controlplane", *code)
|
c.logf("exiting process with status %v per controlplane", *code)
|
||||||
os.Exit(*code)
|
os.Exit(*code)
|
||||||
}
|
}
|
||||||
if debug.DisableLogTail {
|
if buildfeatures.HasLogTail && debug.DisableLogTail {
|
||||||
logtail.Disable()
|
logtail.Disable()
|
||||||
envknob.SetNoLogsNoSupport()
|
envknob.SetNoLogsNoSupport()
|
||||||
}
|
}
|
||||||
|
13
feature/buildfeatures/feature_debug_disabled.go
Normal file
13
feature/buildfeatures/feature_debug_disabled.go
Normal 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_debug
|
||||||
|
|
||||||
|
package buildfeatures
|
||||||
|
|
||||||
|
// HasDebug is whether the binary was built with support for modular feature "various debug support, for things that don't have or need their own more specific feature".
|
||||||
|
// Specifically, it's whether the binary was NOT built with the "ts_omit_debug" build tag.
|
||||||
|
// It's a const so it can be used for dead code elimination.
|
||||||
|
const HasDebug = false
|
13
feature/buildfeatures/feature_debug_enabled.go
Normal file
13
feature/buildfeatures/feature_debug_enabled.go
Normal 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_debug
|
||||||
|
|
||||||
|
package buildfeatures
|
||||||
|
|
||||||
|
// HasDebug is whether the binary was built with support for modular feature "various debug support, for things that don't have or need their own more specific feature".
|
||||||
|
// Specifically, it's whether the binary was NOT built with the "ts_omit_debug" build tag.
|
||||||
|
// It's a const so it can be used for dead code elimination.
|
||||||
|
const HasDebug = true
|
@ -97,6 +97,7 @@ var Features = map[FeatureTag]FeatureMeta{
|
|||||||
"clientupdate": {"ClientUpdate", "Client auto-update support", nil},
|
"clientupdate": {"ClientUpdate", "Client auto-update support", nil},
|
||||||
"completion": {"Completion", "CLI shell completion", nil},
|
"completion": {"Completion", "CLI shell completion", nil},
|
||||||
"dbus": {"DBus", "Linux DBus support", nil},
|
"dbus": {"DBus", "Linux DBus support", nil},
|
||||||
|
"debug": {"Debug", "various debug support, for things that don't have or need their own more specific feature", nil},
|
||||||
"debugeventbus": {"DebugEventBus", "eventbus debug support", nil},
|
"debugeventbus": {"DebugEventBus", "eventbus debug support", nil},
|
||||||
"debugportmapper": {
|
"debugportmapper": {
|
||||||
Sym: "DebugPortMapper",
|
Sym: "DebugPortMapper",
|
||||||
|
@ -15,6 +15,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"tailscale.com/control/controlclient"
|
"tailscale.com/control/controlclient"
|
||||||
|
"tailscale.com/feature"
|
||||||
|
"tailscale.com/feature/buildfeatures"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
"tailscale.com/net/sockstats"
|
"tailscale.com/net/sockstats"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
@ -130,6 +132,10 @@ func handleC2NLogtailFlush(b *LocalBackend, w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleC2NDebugNetMap(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
|
func handleC2NDebugNetMap(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !buildfeatures.HasDebug {
|
||||||
|
http.Error(w, feature.ErrUnavailable.Error(), http.StatusNotImplemented)
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
if r.Method != httpm.POST && r.Method != httpm.GET {
|
if r.Method != httpm.POST && r.Method != httpm.GET {
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
@ -190,20 +196,36 @@ func handleC2NDebugNetMap(b *LocalBackend, w http.ResponseWriter, r *http.Reques
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleC2NDebugGoroutines(_ *LocalBackend, w http.ResponseWriter, r *http.Request) {
|
func handleC2NDebugGoroutines(_ *LocalBackend, w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !buildfeatures.HasDebug {
|
||||||
|
http.Error(w, feature.ErrUnavailable.Error(), http.StatusNotImplemented)
|
||||||
|
return
|
||||||
|
}
|
||||||
w.Header().Set("Content-Type", "text/plain")
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
w.Write(goroutines.ScrubbedGoroutineDump(true))
|
w.Write(goroutines.ScrubbedGoroutineDump(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleC2NDebugPrefs(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
|
func handleC2NDebugPrefs(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !buildfeatures.HasDebug {
|
||||||
|
http.Error(w, feature.ErrUnavailable.Error(), http.StatusNotImplemented)
|
||||||
|
return
|
||||||
|
}
|
||||||
writeJSON(w, b.Prefs())
|
writeJSON(w, b.Prefs())
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleC2NDebugMetrics(_ *LocalBackend, w http.ResponseWriter, r *http.Request) {
|
func handleC2NDebugMetrics(_ *LocalBackend, w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !buildfeatures.HasDebug {
|
||||||
|
http.Error(w, feature.ErrUnavailable.Error(), http.StatusNotImplemented)
|
||||||
|
return
|
||||||
|
}
|
||||||
w.Header().Set("Content-Type", "text/plain")
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
clientmetric.WritePrometheusExpositionFormat(w)
|
clientmetric.WritePrometheusExpositionFormat(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleC2NDebugComponentLogging(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
|
func handleC2NDebugComponentLogging(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !buildfeatures.HasDebug {
|
||||||
|
http.Error(w, feature.ErrUnavailable.Error(), http.StatusNotImplemented)
|
||||||
|
return
|
||||||
|
}
|
||||||
component := r.FormValue("component")
|
component := r.FormValue("component")
|
||||||
secs, _ := strconv.Atoi(r.FormValue("secs"))
|
secs, _ := strconv.Atoi(r.FormValue("secs"))
|
||||||
if secs == 0 {
|
if secs == 0 {
|
||||||
|
@ -557,12 +557,14 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
|
|||||||
b.logf("[unexpected] failed to wire up PeerAPI port for engine %T", e)
|
b.logf("[unexpected] failed to wire up PeerAPI port for engine %T", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, component := range ipn.DebuggableComponents {
|
if buildfeatures.HasDebug {
|
||||||
key := componentStateKey(component)
|
for _, component := range ipn.DebuggableComponents {
|
||||||
if ut, err := ipn.ReadStoreInt(pm.Store(), key); err == nil {
|
key := componentStateKey(component)
|
||||||
if until := time.Unix(ut, 0); until.After(b.clock.Now()) {
|
if ut, err := ipn.ReadStoreInt(pm.Store(), key); err == nil {
|
||||||
// conditional to avoid log spam at start when off
|
if until := time.Unix(ut, 0); until.After(b.clock.Now()) {
|
||||||
b.SetComponentDebugLogging(component, until)
|
// conditional to avoid log spam at start when off
|
||||||
|
b.SetComponentDebugLogging(component, until)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -666,6 +668,9 @@ func componentStateKey(component string) ipn.StateKey {
|
|||||||
// - magicsock
|
// - magicsock
|
||||||
// - sockstats
|
// - sockstats
|
||||||
func (b *LocalBackend) SetComponentDebugLogging(component string, until time.Time) error {
|
func (b *LocalBackend) SetComponentDebugLogging(component string, until time.Time) error {
|
||||||
|
if !buildfeatures.HasDebug {
|
||||||
|
return feature.ErrUnavailable
|
||||||
|
}
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
defer b.mu.Unlock()
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
@ -790,6 +795,9 @@ func (b *LocalBackend) QueryDNS(name string, queryType dnsmessage.Type) (res []b
|
|||||||
// enabled until, or the zero time if component's time is not currently
|
// enabled until, or the zero time if component's time is not currently
|
||||||
// enabled.
|
// enabled.
|
||||||
func (b *LocalBackend) GetComponentDebugLogging(component string) time.Time {
|
func (b *LocalBackend) GetComponentDebugLogging(component string) time.Time {
|
||||||
|
if !buildfeatures.HasDebug {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
defer b.mu.Unlock()
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
465
ipn/localapi/debug.go
Normal file
465
ipn/localapi/debug.go
Normal file
@ -0,0 +1,465 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build !ts_omit_debug
|
||||||
|
|
||||||
|
package localapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"reflect"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"tailscale.com/feature"
|
||||||
|
"tailscale.com/feature/buildfeatures"
|
||||||
|
"tailscale.com/ipn"
|
||||||
|
"tailscale.com/types/logger"
|
||||||
|
"tailscale.com/util/eventbus"
|
||||||
|
"tailscale.com/util/httpm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register("component-debug-logging", (*Handler).serveComponentDebugLogging)
|
||||||
|
Register("debug", (*Handler).serveDebug)
|
||||||
|
Register("dev-set-state-store", (*Handler).serveDevSetStateStore)
|
||||||
|
Register("debug-bus-events", (*Handler).serveDebugBusEvents)
|
||||||
|
Register("debug-bus-graph", (*Handler).serveEventBusGraph)
|
||||||
|
Register("debug-derp-region", (*Handler).serveDebugDERPRegion)
|
||||||
|
Register("debug-dial-types", (*Handler).serveDebugDialTypes)
|
||||||
|
Register("debug-log", (*Handler).serveDebugLog)
|
||||||
|
Register("debug-packet-filter-matches", (*Handler).serveDebugPacketFilterMatches)
|
||||||
|
Register("debug-packet-filter-rules", (*Handler).serveDebugPacketFilterRules)
|
||||||
|
Register("debug-peer-endpoint-changes", (*Handler).serveDebugPeerEndpointChanges)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) serveDebugPeerEndpointChanges(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !h.PermitRead {
|
||||||
|
http.Error(w, "status access denied", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ipStr := r.FormValue("ip")
|
||||||
|
if ipStr == "" {
|
||||||
|
http.Error(w, "missing 'ip' parameter", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ip, err := netip.ParseAddr(ipStr)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "invalid IP", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
chs, err := h.b.GetPeerEndpointChanges(r.Context(), ip)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e := json.NewEncoder(w)
|
||||||
|
e.SetIndent("", "\t")
|
||||||
|
e.Encode(chs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) serveComponentDebugLogging(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !h.PermitWrite {
|
||||||
|
http.Error(w, "debug access denied", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component := r.FormValue("component")
|
||||||
|
secs, _ := strconv.Atoi(r.FormValue("secs"))
|
||||||
|
err := h.b.SetComponentDebugLogging(component, h.clock.Now().Add(time.Duration(secs)*time.Second))
|
||||||
|
var res struct {
|
||||||
|
Error string
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
res.Error = err.Error()
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) serveDebugDialTypes(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !h.PermitWrite {
|
||||||
|
http.Error(w, "debug-dial-types access denied", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method != httpm.POST {
|
||||||
|
http.Error(w, "only POST allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ip := r.FormValue("ip")
|
||||||
|
port := r.FormValue("port")
|
||||||
|
network := r.FormValue("network")
|
||||||
|
|
||||||
|
addr := ip + ":" + port
|
||||||
|
if _, err := netip.ParseAddrPort(addr); err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
fmt.Fprintf(w, "invalid address %q: %v", addr, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var bareDialer net.Dialer
|
||||||
|
|
||||||
|
dialer := h.b.Dialer()
|
||||||
|
|
||||||
|
var peerDialer net.Dialer
|
||||||
|
peerDialer.Control = dialer.PeerDialControlFunc()
|
||||||
|
|
||||||
|
// Kick off a dial with each available dialer in parallel.
|
||||||
|
dialers := []struct {
|
||||||
|
name string
|
||||||
|
dial func(context.Context, string, string) (net.Conn, error)
|
||||||
|
}{
|
||||||
|
{"SystemDial", dialer.SystemDial},
|
||||||
|
{"UserDial", dialer.UserDial},
|
||||||
|
{"PeerDial", peerDialer.DialContext},
|
||||||
|
{"BareDial", bareDialer.DialContext},
|
||||||
|
}
|
||||||
|
type result struct {
|
||||||
|
name string
|
||||||
|
conn net.Conn
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
results := make(chan result, len(dialers))
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for _, dialer := range dialers {
|
||||||
|
dialer := dialer // loop capture
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
conn, err := dialer.dial(ctx, network, addr)
|
||||||
|
results <- result{dialer.name, conn, err}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
for range len(dialers) {
|
||||||
|
res := <-results
|
||||||
|
fmt.Fprintf(w, "[%s] connected=%v err=%v\n", res.name, res.conn != nil, res.err)
|
||||||
|
if res.conn != nil {
|
||||||
|
res.conn.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) serveDebug(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !buildfeatures.HasDebug {
|
||||||
|
http.Error(w, "debug not supported in this build", http.StatusNotImplemented)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !h.PermitWrite {
|
||||||
|
http.Error(w, "debug access denied", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method != httpm.POST {
|
||||||
|
http.Error(w, "POST required", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// The action is normally in a POST form parameter, but
|
||||||
|
// some actions (like "notify") want a full JSON body, so
|
||||||
|
// permit some to have their action in a header.
|
||||||
|
var action string
|
||||||
|
switch v := r.Header.Get("Debug-Action"); v {
|
||||||
|
case "notify":
|
||||||
|
action = v
|
||||||
|
default:
|
||||||
|
action = r.FormValue("action")
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
switch action {
|
||||||
|
case "derp-set-homeless":
|
||||||
|
h.b.MagicConn().SetHomeless(true)
|
||||||
|
case "derp-unset-homeless":
|
||||||
|
h.b.MagicConn().SetHomeless(false)
|
||||||
|
case "rebind":
|
||||||
|
err = h.b.DebugRebind()
|
||||||
|
case "restun":
|
||||||
|
err = h.b.DebugReSTUN()
|
||||||
|
case "notify":
|
||||||
|
var n ipn.Notify
|
||||||
|
err = json.NewDecoder(r.Body).Decode(&n)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
h.b.DebugNotify(n)
|
||||||
|
case "notify-last-netmap":
|
||||||
|
h.b.DebugNotifyLastNetMap()
|
||||||
|
case "break-tcp-conns":
|
||||||
|
err = h.b.DebugBreakTCPConns()
|
||||||
|
case "break-derp-conns":
|
||||||
|
err = h.b.DebugBreakDERPConns()
|
||||||
|
case "force-netmap-update":
|
||||||
|
h.b.DebugForceNetmapUpdate()
|
||||||
|
case "control-knobs":
|
||||||
|
k := h.b.ControlKnobs()
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
err = json.NewEncoder(w).Encode(k.AsDebugJSON())
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "pick-new-derp":
|
||||||
|
err = h.b.DebugPickNewDERP()
|
||||||
|
case "force-prefer-derp":
|
||||||
|
var n int
|
||||||
|
err = json.NewDecoder(r.Body).Decode(&n)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
h.b.DebugForcePreferDERP(n)
|
||||||
|
case "peer-relay-servers":
|
||||||
|
servers := h.b.DebugPeerRelayServers().Slice()
|
||||||
|
slices.SortFunc(servers, func(a, b netip.Addr) int {
|
||||||
|
return a.Compare(b)
|
||||||
|
})
|
||||||
|
err = json.NewEncoder(w).Encode(servers)
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "":
|
||||||
|
err = fmt.Errorf("missing parameter 'action'")
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("unknown action %q", action)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
io.WriteString(w, "done\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) serveDevSetStateStore(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !h.PermitWrite {
|
||||||
|
http.Error(w, "debug access denied", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method != httpm.POST {
|
||||||
|
http.Error(w, "POST required", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.b.SetDevStateStore(r.FormValue("key"), r.FormValue("value")); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
io.WriteString(w, "done\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) serveDebugPacketFilterRules(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !h.PermitWrite {
|
||||||
|
http.Error(w, "debug access denied", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nm := h.b.NetMap()
|
||||||
|
if nm == nil {
|
||||||
|
http.Error(w, "no netmap", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
enc := json.NewEncoder(w)
|
||||||
|
enc.SetIndent("", "\t")
|
||||||
|
enc.Encode(nm.PacketFilterRules)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) serveDebugPacketFilterMatches(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !h.PermitWrite {
|
||||||
|
http.Error(w, "debug access denied", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nm := h.b.NetMap()
|
||||||
|
if nm == nil {
|
||||||
|
http.Error(w, "no netmap", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
enc := json.NewEncoder(w)
|
||||||
|
enc.SetIndent("", "\t")
|
||||||
|
enc.Encode(nm.PacketFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// debugEventError provides the JSON encoding of internal errors from event processing.
|
||||||
|
type debugEventError struct {
|
||||||
|
Error string
|
||||||
|
}
|
||||||
|
|
||||||
|
// serveDebugBusEvents taps into the tailscaled/utils/eventbus and streams
|
||||||
|
// events to the client.
|
||||||
|
func (h *Handler) serveDebugBusEvents(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Require write access (~root) as the logs could contain something
|
||||||
|
// sensitive.
|
||||||
|
if !h.PermitWrite {
|
||||||
|
http.Error(w, "event bus access denied", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method != httpm.GET {
|
||||||
|
http.Error(w, "GET required", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bus, ok := h.LocalBackend().Sys().Bus.GetOK()
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "event bus not running", http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
f, ok := w.(http.Flusher)
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "streaming unsupported", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
io.WriteString(w, `{"Event":"[event listener connected]\n"}`+"\n")
|
||||||
|
f.Flush()
|
||||||
|
|
||||||
|
mon := bus.Debugger().WatchBus()
|
||||||
|
defer mon.Close()
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-r.Context().Done():
|
||||||
|
fmt.Fprintf(w, `{"Event":"[event listener closed]\n"}`)
|
||||||
|
return
|
||||||
|
case <-mon.Done():
|
||||||
|
return
|
||||||
|
case event := <-mon.Events():
|
||||||
|
data := eventbus.DebugEvent{
|
||||||
|
Count: i,
|
||||||
|
Type: reflect.TypeOf(event.Event).String(),
|
||||||
|
Event: event.Event,
|
||||||
|
From: event.From.Name(),
|
||||||
|
}
|
||||||
|
for _, client := range event.To {
|
||||||
|
data.To = append(data.To, client.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg, err := json.Marshal(data); err != nil {
|
||||||
|
data.Event = debugEventError{Error: fmt.Sprintf(
|
||||||
|
"failed to marshal JSON for %T", event.Event,
|
||||||
|
)}
|
||||||
|
if errMsg, err := json.Marshal(data); err != nil {
|
||||||
|
fmt.Fprintf(w,
|
||||||
|
`{"Count": %d, "Event":"[ERROR] failed to marshal JSON for %T\n"}`,
|
||||||
|
i, event.Event)
|
||||||
|
} else {
|
||||||
|
w.Write(errMsg)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
w.Write(msg)
|
||||||
|
}
|
||||||
|
f.Flush()
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// serveEventBusGraph taps into the event bus and dumps out the active graph of
|
||||||
|
// publishers and subscribers. It does not represent anything about the messages
|
||||||
|
// exchanged.
|
||||||
|
func (h *Handler) serveEventBusGraph(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != httpm.GET {
|
||||||
|
http.Error(w, "GET required", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bus, ok := h.LocalBackend().Sys().Bus.GetOK()
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "event bus not running", http.StatusPreconditionFailed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
debugger := bus.Debugger()
|
||||||
|
clients := debugger.Clients()
|
||||||
|
|
||||||
|
graph := map[string]eventbus.DebugTopic{}
|
||||||
|
|
||||||
|
for _, client := range clients {
|
||||||
|
for _, pub := range debugger.PublishTypes(client) {
|
||||||
|
topic, ok := graph[pub.Name()]
|
||||||
|
if !ok {
|
||||||
|
topic = eventbus.DebugTopic{Name: pub.Name()}
|
||||||
|
}
|
||||||
|
topic.Publisher = client.Name()
|
||||||
|
graph[pub.Name()] = topic
|
||||||
|
}
|
||||||
|
for _, sub := range debugger.SubscribeTypes(client) {
|
||||||
|
topic, ok := graph[sub.Name()]
|
||||||
|
if !ok {
|
||||||
|
topic = eventbus.DebugTopic{Name: sub.Name()}
|
||||||
|
}
|
||||||
|
topic.Subscribers = append(topic.Subscribers, client.Name())
|
||||||
|
graph[sub.Name()] = topic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The top level map is not really needed for the client, convert to a list.
|
||||||
|
topics := eventbus.DebugTopics{}
|
||||||
|
for _, v := range graph {
|
||||||
|
topics.Topics = append(topics.Topics, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(topics)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) serveDebugLog(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !buildfeatures.HasLogTail {
|
||||||
|
http.Error(w, feature.ErrUnavailable.Error(), http.StatusNotImplemented)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !h.PermitRead {
|
||||||
|
http.Error(w, "debug-log access denied", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method != httpm.POST {
|
||||||
|
http.Error(w, "only POST allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer h.b.TryFlushLogs() // kick off upload after we're done logging
|
||||||
|
|
||||||
|
type logRequestJSON struct {
|
||||||
|
Lines []string
|
||||||
|
Prefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
var logRequest logRequestJSON
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&logRequest); err != nil {
|
||||||
|
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix := logRequest.Prefix
|
||||||
|
if prefix == "" {
|
||||||
|
prefix = "debug-log"
|
||||||
|
}
|
||||||
|
logf := logger.WithPrefix(h.logf, prefix+": ")
|
||||||
|
|
||||||
|
// 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(r.Context(), logf, 20*time.Millisecond, 60, h.clock.Now)
|
||||||
|
|
||||||
|
for _, line := range logRequest.Lines {
|
||||||
|
logf("%s", line)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build !ts_omit_debug
|
||||||
|
|
||||||
package localapi
|
package localapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -7,7 +7,6 @@ package localapi
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"cmp"
|
"cmp"
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -16,7 +15,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
"reflect"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -80,18 +78,7 @@ var handler = map[string]LocalAPIHandler{
|
|||||||
"check-prefs": (*Handler).serveCheckPrefs,
|
"check-prefs": (*Handler).serveCheckPrefs,
|
||||||
"check-reverse-path-filtering": (*Handler).serveCheckReversePathFiltering,
|
"check-reverse-path-filtering": (*Handler).serveCheckReversePathFiltering,
|
||||||
"check-udp-gro-forwarding": (*Handler).serveCheckUDPGROForwarding,
|
"check-udp-gro-forwarding": (*Handler).serveCheckUDPGROForwarding,
|
||||||
"component-debug-logging": (*Handler).serveComponentDebugLogging,
|
|
||||||
"debug": (*Handler).serveDebug,
|
|
||||||
"debug-bus-events": (*Handler).serveDebugBusEvents,
|
|
||||||
"debug-bus-graph": (*Handler).serveEventBusGraph,
|
|
||||||
"debug-derp-region": (*Handler).serveDebugDERPRegion,
|
|
||||||
"debug-dial-types": (*Handler).serveDebugDialTypes,
|
|
||||||
"debug-log": (*Handler).serveDebugLog,
|
|
||||||
"debug-packet-filter-matches": (*Handler).serveDebugPacketFilterMatches,
|
|
||||||
"debug-packet-filter-rules": (*Handler).serveDebugPacketFilterRules,
|
|
||||||
"debug-peer-endpoint-changes": (*Handler).serveDebugPeerEndpointChanges,
|
|
||||||
"derpmap": (*Handler).serveDERPMap,
|
"derpmap": (*Handler).serveDERPMap,
|
||||||
"dev-set-state-store": (*Handler).serveDevSetStateStore,
|
|
||||||
"dial": (*Handler).serveDial,
|
"dial": (*Handler).serveDial,
|
||||||
"disconnect-control": (*Handler).disconnectControl,
|
"disconnect-control": (*Handler).disconnectControl,
|
||||||
"dns-osconfig": (*Handler).serveDNSOSConfig,
|
"dns-osconfig": (*Handler).serveDNSOSConfig,
|
||||||
@ -638,352 +625,6 @@ func (h *Handler) serveUserMetrics(w http.ResponseWriter, r *http.Request) {
|
|||||||
h.b.UserMetricsRegistry().Handler(w, r)
|
h.b.UserMetricsRegistry().Handler(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) serveDebug(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !h.PermitWrite {
|
|
||||||
http.Error(w, "debug access denied", http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if r.Method != httpm.POST {
|
|
||||||
http.Error(w, "POST required", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// The action is normally in a POST form parameter, but
|
|
||||||
// some actions (like "notify") want a full JSON body, so
|
|
||||||
// permit some to have their action in a header.
|
|
||||||
var action string
|
|
||||||
switch v := r.Header.Get("Debug-Action"); v {
|
|
||||||
case "notify":
|
|
||||||
action = v
|
|
||||||
default:
|
|
||||||
action = r.FormValue("action")
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
switch action {
|
|
||||||
case "derp-set-homeless":
|
|
||||||
h.b.MagicConn().SetHomeless(true)
|
|
||||||
case "derp-unset-homeless":
|
|
||||||
h.b.MagicConn().SetHomeless(false)
|
|
||||||
case "rebind":
|
|
||||||
err = h.b.DebugRebind()
|
|
||||||
case "restun":
|
|
||||||
err = h.b.DebugReSTUN()
|
|
||||||
case "notify":
|
|
||||||
var n ipn.Notify
|
|
||||||
err = json.NewDecoder(r.Body).Decode(&n)
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
h.b.DebugNotify(n)
|
|
||||||
case "notify-last-netmap":
|
|
||||||
h.b.DebugNotifyLastNetMap()
|
|
||||||
case "break-tcp-conns":
|
|
||||||
err = h.b.DebugBreakTCPConns()
|
|
||||||
case "break-derp-conns":
|
|
||||||
err = h.b.DebugBreakDERPConns()
|
|
||||||
case "force-netmap-update":
|
|
||||||
h.b.DebugForceNetmapUpdate()
|
|
||||||
case "control-knobs":
|
|
||||||
k := h.b.ControlKnobs()
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
err = json.NewEncoder(w).Encode(k.AsDebugJSON())
|
|
||||||
if err == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case "pick-new-derp":
|
|
||||||
err = h.b.DebugPickNewDERP()
|
|
||||||
case "force-prefer-derp":
|
|
||||||
var n int
|
|
||||||
err = json.NewDecoder(r.Body).Decode(&n)
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
h.b.DebugForcePreferDERP(n)
|
|
||||||
case "peer-relay-servers":
|
|
||||||
servers := h.b.DebugPeerRelayServers().Slice()
|
|
||||||
slices.SortFunc(servers, func(a, b netip.Addr) int {
|
|
||||||
return a.Compare(b)
|
|
||||||
})
|
|
||||||
err = json.NewEncoder(w).Encode(servers)
|
|
||||||
if err == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case "":
|
|
||||||
err = fmt.Errorf("missing parameter 'action'")
|
|
||||||
default:
|
|
||||||
err = fmt.Errorf("unknown action %q", action)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "text/plain")
|
|
||||||
io.WriteString(w, "done\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) serveDevSetStateStore(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !h.PermitWrite {
|
|
||||||
http.Error(w, "debug access denied", http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if r.Method != httpm.POST {
|
|
||||||
http.Error(w, "POST required", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := h.b.SetDevStateStore(r.FormValue("key"), r.FormValue("value")); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "text/plain")
|
|
||||||
io.WriteString(w, "done\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) serveDebugPacketFilterRules(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !h.PermitWrite {
|
|
||||||
http.Error(w, "debug access denied", http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
nm := h.b.NetMap()
|
|
||||||
if nm == nil {
|
|
||||||
http.Error(w, "no netmap", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
enc := json.NewEncoder(w)
|
|
||||||
enc.SetIndent("", "\t")
|
|
||||||
enc.Encode(nm.PacketFilterRules)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) serveDebugPacketFilterMatches(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !h.PermitWrite {
|
|
||||||
http.Error(w, "debug access denied", http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
nm := h.b.NetMap()
|
|
||||||
if nm == nil {
|
|
||||||
http.Error(w, "no netmap", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
enc := json.NewEncoder(w)
|
|
||||||
enc.SetIndent("", "\t")
|
|
||||||
enc.Encode(nm.PacketFilter)
|
|
||||||
}
|
|
||||||
|
|
||||||
// EventError provides the JSON encoding of internal errors from event processing.
|
|
||||||
type EventError struct {
|
|
||||||
Error string
|
|
||||||
}
|
|
||||||
|
|
||||||
// serveDebugBusEvents taps into the tailscaled/utils/eventbus and streams
|
|
||||||
// events to the client.
|
|
||||||
func (h *Handler) serveDebugBusEvents(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// Require write access (~root) as the logs could contain something
|
|
||||||
// sensitive.
|
|
||||||
if !h.PermitWrite {
|
|
||||||
http.Error(w, "event bus access denied", http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if r.Method != httpm.GET {
|
|
||||||
http.Error(w, "GET required", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
bus, ok := h.LocalBackend().Sys().Bus.GetOK()
|
|
||||||
if !ok {
|
|
||||||
http.Error(w, "event bus not running", http.StatusNoContent)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
f, ok := w.(http.Flusher)
|
|
||||||
if !ok {
|
|
||||||
http.Error(w, "streaming unsupported", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
io.WriteString(w, `{"Event":"[event listener connected]\n"}`+"\n")
|
|
||||||
f.Flush()
|
|
||||||
|
|
||||||
mon := bus.Debugger().WatchBus()
|
|
||||||
defer mon.Close()
|
|
||||||
|
|
||||||
i := 0
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-r.Context().Done():
|
|
||||||
fmt.Fprintf(w, `{"Event":"[event listener closed]\n"}`)
|
|
||||||
return
|
|
||||||
case <-mon.Done():
|
|
||||||
return
|
|
||||||
case event := <-mon.Events():
|
|
||||||
data := eventbus.DebugEvent{
|
|
||||||
Count: i,
|
|
||||||
Type: reflect.TypeOf(event.Event).String(),
|
|
||||||
Event: event.Event,
|
|
||||||
From: event.From.Name(),
|
|
||||||
}
|
|
||||||
for _, client := range event.To {
|
|
||||||
data.To = append(data.To, client.Name())
|
|
||||||
}
|
|
||||||
|
|
||||||
if msg, err := json.Marshal(data); err != nil {
|
|
||||||
data.Event = EventError{Error: fmt.Sprintf(
|
|
||||||
"failed to marshal JSON for %T", event.Event,
|
|
||||||
)}
|
|
||||||
if errMsg, err := json.Marshal(data); err != nil {
|
|
||||||
fmt.Fprintf(w,
|
|
||||||
`{"Count": %d, "Event":"[ERROR] failed to marshal JSON for %T\n"}`,
|
|
||||||
i, event.Event)
|
|
||||||
} else {
|
|
||||||
w.Write(errMsg)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
w.Write(msg)
|
|
||||||
}
|
|
||||||
f.Flush()
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// serveEventBusGraph taps into the event bus and dumps out the active graph of
|
|
||||||
// publishers and subscribers. It does not represent anything about the messages
|
|
||||||
// exchanged.
|
|
||||||
func (h *Handler) serveEventBusGraph(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != httpm.GET {
|
|
||||||
http.Error(w, "GET required", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
bus, ok := h.LocalBackend().Sys().Bus.GetOK()
|
|
||||||
if !ok {
|
|
||||||
http.Error(w, "event bus not running", http.StatusPreconditionFailed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
debugger := bus.Debugger()
|
|
||||||
clients := debugger.Clients()
|
|
||||||
|
|
||||||
graph := map[string]eventbus.DebugTopic{}
|
|
||||||
|
|
||||||
for _, client := range clients {
|
|
||||||
for _, pub := range debugger.PublishTypes(client) {
|
|
||||||
topic, ok := graph[pub.Name()]
|
|
||||||
if !ok {
|
|
||||||
topic = eventbus.DebugTopic{Name: pub.Name()}
|
|
||||||
}
|
|
||||||
topic.Publisher = client.Name()
|
|
||||||
graph[pub.Name()] = topic
|
|
||||||
}
|
|
||||||
for _, sub := range debugger.SubscribeTypes(client) {
|
|
||||||
topic, ok := graph[sub.Name()]
|
|
||||||
if !ok {
|
|
||||||
topic = eventbus.DebugTopic{Name: sub.Name()}
|
|
||||||
}
|
|
||||||
topic.Subscribers = append(topic.Subscribers, client.Name())
|
|
||||||
graph[sub.Name()] = topic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The top level map is not really needed for the client, convert to a list.
|
|
||||||
topics := eventbus.DebugTopics{}
|
|
||||||
for _, v := range graph {
|
|
||||||
topics.Topics = append(topics.Topics, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
json.NewEncoder(w).Encode(topics)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) serveComponentDebugLogging(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !h.PermitWrite {
|
|
||||||
http.Error(w, "debug access denied", http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
component := r.FormValue("component")
|
|
||||||
secs, _ := strconv.Atoi(r.FormValue("secs"))
|
|
||||||
err := h.b.SetComponentDebugLogging(component, h.clock.Now().Add(time.Duration(secs)*time.Second))
|
|
||||||
var res struct {
|
|
||||||
Error string
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
res.Error = err.Error()
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
json.NewEncoder(w).Encode(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) serveDebugDialTypes(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !h.PermitWrite {
|
|
||||||
http.Error(w, "debug-dial-types access denied", http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if r.Method != httpm.POST {
|
|
||||||
http.Error(w, "only POST allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ip := r.FormValue("ip")
|
|
||||||
port := r.FormValue("port")
|
|
||||||
network := r.FormValue("network")
|
|
||||||
|
|
||||||
addr := ip + ":" + port
|
|
||||||
if _, err := netip.ParseAddrPort(addr); err != nil {
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
fmt.Fprintf(w, "invalid address %q: %v", addr, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
var bareDialer net.Dialer
|
|
||||||
|
|
||||||
dialer := h.b.Dialer()
|
|
||||||
|
|
||||||
var peerDialer net.Dialer
|
|
||||||
peerDialer.Control = dialer.PeerDialControlFunc()
|
|
||||||
|
|
||||||
// Kick off a dial with each available dialer in parallel.
|
|
||||||
dialers := []struct {
|
|
||||||
name string
|
|
||||||
dial func(context.Context, string, string) (net.Conn, error)
|
|
||||||
}{
|
|
||||||
{"SystemDial", dialer.SystemDial},
|
|
||||||
{"UserDial", dialer.UserDial},
|
|
||||||
{"PeerDial", peerDialer.DialContext},
|
|
||||||
{"BareDial", bareDialer.DialContext},
|
|
||||||
}
|
|
||||||
type result struct {
|
|
||||||
name string
|
|
||||||
conn net.Conn
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
results := make(chan result, len(dialers))
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
for _, dialer := range dialers {
|
|
||||||
dialer := dialer // loop capture
|
|
||||||
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
conn, err := dialer.dial(ctx, network, addr)
|
|
||||||
results <- result{dialer.name, conn, err}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
for range len(dialers) {
|
|
||||||
res := <-results
|
|
||||||
fmt.Fprintf(w, "[%s] connected=%v err=%v\n", res.name, res.conn != nil, res.err)
|
|
||||||
if res.conn != nil {
|
|
||||||
res.conn.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// servePprofFunc is the implementation of Handler.servePprof, after auth,
|
// servePprofFunc is the implementation of Handler.servePprof, after auth,
|
||||||
// for platforms where we want to link it in.
|
// for platforms where we want to link it in.
|
||||||
var servePprofFunc func(http.ResponseWriter, *http.Request)
|
var servePprofFunc func(http.ResponseWriter, *http.Request)
|
||||||
@ -1116,6 +757,10 @@ func (h *Handler) serveCheckUDPGROForwarding(w http.ResponseWriter, r *http.Requ
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) serveSetUDPGROForwarding(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) serveSetUDPGROForwarding(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !buildfeatures.HasGRO {
|
||||||
|
http.Error(w, feature.ErrUnavailable.Error(), http.StatusNotImplemented)
|
||||||
|
return
|
||||||
|
}
|
||||||
if !h.PermitWrite {
|
if !h.PermitWrite {
|
||||||
http.Error(w, "UDP GRO forwarding set access denied", http.StatusForbidden)
|
http.Error(w, "UDP GRO forwarding set access denied", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
@ -1149,34 +794,6 @@ func (h *Handler) serveStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
e.Encode(st)
|
e.Encode(st)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) serveDebugPeerEndpointChanges(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !h.PermitRead {
|
|
||||||
http.Error(w, "status access denied", http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ipStr := r.FormValue("ip")
|
|
||||||
if ipStr == "" {
|
|
||||||
http.Error(w, "missing 'ip' parameter", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ip, err := netip.ParseAddr(ipStr)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "invalid IP", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
chs, err := h.b.GetPeerEndpointChanges(r.Context(), ip)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
e := json.NewEncoder(w)
|
|
||||||
e.SetIndent("", "\t")
|
|
||||||
e.Encode(chs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InUseOtherUserIPNStream reports whether r is a request for the watch-ipn-bus
|
// InUseOtherUserIPNStream reports whether r is a request for the watch-ipn-bus
|
||||||
// handler. If so, it writes an ipn.Notify InUseOtherUser message to the user
|
// handler. If so, it writes an ipn.Notify InUseOtherUser message to the user
|
||||||
// and returns true. Otherwise it returns false, in which case it doesn't write
|
// and returns true. Otherwise it returns false, in which case it doesn't write
|
||||||
@ -1842,47 +1459,6 @@ func defBool(a string, def bool) bool {
|
|||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) serveDebugLog(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !h.PermitRead {
|
|
||||||
http.Error(w, "debug-log access denied", http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if r.Method != httpm.POST {
|
|
||||||
http.Error(w, "only POST allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer h.b.TryFlushLogs() // kick off upload after we're done logging
|
|
||||||
|
|
||||||
type logRequestJSON struct {
|
|
||||||
Lines []string
|
|
||||||
Prefix string
|
|
||||||
}
|
|
||||||
|
|
||||||
var logRequest logRequestJSON
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&logRequest); err != nil {
|
|
||||||
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
prefix := logRequest.Prefix
|
|
||||||
if prefix == "" {
|
|
||||||
prefix = "debug-log"
|
|
||||||
}
|
|
||||||
logf := logger.WithPrefix(h.logf, prefix+": ")
|
|
||||||
|
|
||||||
// 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(r.Context(), logf, 20*time.Millisecond, 60, h.clock.Now)
|
|
||||||
|
|
||||||
for _, line := range logRequest.Lines {
|
|
||||||
logf("%s", line)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
// serveUpdateCheck returns the ClientVersion from Status, which contains
|
// serveUpdateCheck returns the ClientVersion from Status, which contains
|
||||||
// information on whether an update is available, and if so, what version,
|
// information on whether an update is available, and if so, what version,
|
||||||
// *if* we support auto-updates on this platform. If we don't, this endpoint
|
// *if* we support auto-updates on this platform. If we don't, this endpoint
|
||||||
@ -1917,7 +1493,7 @@ func (h *Handler) serveUpdateCheck(w http.ResponseWriter, r *http.Request) {
|
|||||||
// supported by the OS.
|
// supported by the OS.
|
||||||
func (h *Handler) serveDNSOSConfig(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) serveDNSOSConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
if !buildfeatures.HasDNS {
|
if !buildfeatures.HasDNS {
|
||||||
http.NotFound(w, r)
|
http.Error(w, feature.ErrUnavailable.Error(), http.StatusNotImplemented)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if r.Method != httpm.GET {
|
if r.Method != httpm.GET {
|
||||||
@ -1964,7 +1540,7 @@ func (h *Handler) serveDNSOSConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
// The response if successful is a DNSQueryResponse JSON object.
|
// The response if successful is a DNSQueryResponse JSON object.
|
||||||
func (h *Handler) serveDNSQuery(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) serveDNSQuery(w http.ResponseWriter, r *http.Request) {
|
||||||
if !buildfeatures.HasDNS {
|
if !buildfeatures.HasDNS {
|
||||||
http.NotFound(w, r)
|
http.Error(w, feature.ErrUnavailable.Error(), http.StatusNotImplemented)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if r.Method != httpm.GET {
|
if r.Method != httpm.GET {
|
||||||
|
@ -17,7 +17,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
handler["policy/"] = (*Handler).servePolicy
|
Register("policy/", (*Handler).servePolicy)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) servePolicy(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) servePolicy(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -18,19 +18,19 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
handler["tka/affected-sigs"] = (*Handler).serveTKAAffectedSigs
|
Register("tka/affected-sigs", (*Handler).serveTKAAffectedSigs)
|
||||||
handler["tka/cosign-recovery-aum"] = (*Handler).serveTKACosignRecoveryAUM
|
Register("tka/cosign-recovery-aum", (*Handler).serveTKACosignRecoveryAUM)
|
||||||
handler["tka/disable"] = (*Handler).serveTKADisable
|
Register("tka/disable", (*Handler).serveTKADisable)
|
||||||
handler["tka/force-local-disable"] = (*Handler).serveTKALocalDisable
|
Register("tka/force-local-disable", (*Handler).serveTKALocalDisable)
|
||||||
handler["tka/generate-recovery-aum"] = (*Handler).serveTKAGenerateRecoveryAUM
|
Register("tka/generate-recovery-aum", (*Handler).serveTKAGenerateRecoveryAUM)
|
||||||
handler["tka/init"] = (*Handler).serveTKAInit
|
Register("tka/init", (*Handler).serveTKAInit)
|
||||||
handler["tka/log"] = (*Handler).serveTKALog
|
Register("tka/log", (*Handler).serveTKALog)
|
||||||
handler["tka/modify"] = (*Handler).serveTKAModify
|
Register("tka/modify", (*Handler).serveTKAModify)
|
||||||
handler["tka/sign"] = (*Handler).serveTKASign
|
Register("tka/sign", (*Handler).serveTKASign)
|
||||||
handler["tka/status"] = (*Handler).serveTKAStatus
|
Register("tka/status", (*Handler).serveTKAStatus)
|
||||||
handler["tka/submit-recovery-aum"] = (*Handler).serveTKASubmitRecoveryAUM
|
Register("tka/submit-recovery-aum", (*Handler).serveTKASubmitRecoveryAUM)
|
||||||
handler["tka/verify-deeplink"] = (*Handler).serveTKAVerifySigningDeeplink
|
Register("tka/verify-deeplink", (*Handler).serveTKAVerifySigningDeeplink)
|
||||||
handler["tka/wrap-preauth-key"] = (*Handler).serveTKAWrapPreauthKey
|
Register("tka/wrap-preauth-key", (*Handler).serveTKAWrapPreauthKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) serveTKAStatus(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) serveTKAStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -13,6 +13,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"tailscale.com/feature"
|
||||||
|
"tailscale.com/feature/buildfeatures"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/tstime/mono"
|
"tailscale.com/tstime/mono"
|
||||||
"tailscale.com/types/key"
|
"tailscale.com/types/key"
|
||||||
@ -24,6 +26,11 @@ import (
|
|||||||
// /debug/magicsock) or via peerapi to a peer that's owned by the same
|
// /debug/magicsock) or via peerapi to a peer that's owned by the same
|
||||||
// user (so they can e.g. inspect their phones).
|
// user (so they can e.g. inspect their phones).
|
||||||
func (c *Conn) ServeHTTPDebug(w http.ResponseWriter, r *http.Request) {
|
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()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user