wgengine/magicsock: make debug-level stuff not logged by default

And add a CLI/localapi and c2n mechanism to enable it for a fixed
amount of time.

Updates #1548

Change-Id: I71674aaf959a9c6761ff33bbf4a417ffd42195a7
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2022-10-03 20:39:45 -07:00
committed by Brad Fitzpatrick
parent 5c69961a57
commit 1841d0bf98
10 changed files with 241 additions and 21 deletions

View File

@@ -168,6 +168,11 @@ type PartialFile struct {
// LocalBackend.userID, a string like "user-$USER_ID" (used in
// server mode).
// - on Linux/etc, it's always "_daemon" (ipn.GlobalDaemonStateKey)
//
// Additionally, the StateKey can be debug setting name:
//
// - "_debug_magicsock_until" with value being a unix timestamp stringified
// - "_debug_<component>_until" with value being a unix timestamp stringified
type StateKey string
type Options struct {

View File

@@ -8,6 +8,8 @@ import (
"encoding/json"
"io"
"net/http"
"strconv"
"time"
"tailscale.com/tailcfg"
"tailscale.com/util/clientmetric"
@@ -32,6 +34,21 @@ func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
case "/debug/metrics":
w.Header().Set("Content-Type", "text/plain")
clientmetric.WritePrometheusExpositionFormat(w)
case "/debug/component-logging":
component := r.FormValue("component")
secs, _ := strconv.Atoi(r.FormValue("secs"))
if secs == 0 {
secs -= 1
}
until := time.Now().Add(time.Duration(secs) * time.Second)
err := b.SetComponentDebugLogging(component, until)
var res struct {
Error string `json:",omitempty"`
}
if err != nil {
res.Error = err.Error()
}
writeJSON(res)
case "/ssh/usernames":
var req tailcfg.C2NSSHUsernamesRequest
if r.Method == "POST" {

View File

@@ -24,6 +24,7 @@ import (
"time"
"go4.org/netipx"
"golang.org/x/exp/slices"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/control/controlclient"
"tailscale.com/doctor"
@@ -55,6 +56,7 @@ import (
"tailscale.com/types/views"
"tailscale.com/util/deephash"
"tailscale.com/util/dnsname"
"tailscale.com/util/mak"
"tailscale.com/util/multierr"
"tailscale.com/util/osshare"
"tailscale.com/util/systemd"
@@ -186,6 +188,7 @@ type LocalBackend struct {
// *.partial file to its final name on completion.
directFileRoot string
directFileDoFinalRename bool // false on macOS, true on several NAS platforms
componentLogUntil map[string]componentLogState
// statusLock must be held before calling statusChanged.Wait() or
// statusChanged.Broadcast().
@@ -267,9 +270,91 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, diale
b.logf("[unexpected] failed to wire up peer API port for engine %T", e)
}
for _, component := range debuggableComponents {
key := componentStateKey(component)
if ut, err := ipn.ReadStoreInt(store, key); err == nil {
if until := time.Unix(ut, 0); until.After(time.Now()) {
// conditional to avoid log spam at start when off
b.SetComponentDebugLogging(component, until)
}
}
}
return b, nil
}
type componentLogState struct {
until time.Time
timer *time.Timer // if non-nil, the AfterFunc to disable it
}
var debuggableComponents = []string{
"magicsock",
}
func componentStateKey(component string) ipn.StateKey {
return ipn.StateKey("_debug_" + component + "_until")
}
// SetComponentDebugLogging sets component's debug logging enabled until the until time.
// If until is in the past, the component's debug logging is disabled.
//
// The following components are recognized:
//
// - magicsock
func (b *LocalBackend) SetComponentDebugLogging(component string, until time.Time) error {
b.mu.Lock()
defer b.mu.Unlock()
var setEnabled func(bool)
switch component {
case "magicsock":
mc, err := b.magicConn()
if err != nil {
return err
}
setEnabled = mc.SetDebugLoggingEnabled
}
if setEnabled == nil || !slices.Contains(debuggableComponents, component) {
return fmt.Errorf("unknown component %q", component)
}
timeUnixOrZero := func(t time.Time) int64 {
if t.IsZero() {
return 0
}
return t.Unix()
}
ipn.PutStoreInt(b.store, componentStateKey(component), timeUnixOrZero(until))
now := time.Now()
on := now.Before(until)
setEnabled(on)
var onFor time.Duration
if on {
onFor = until.Sub(now)
b.logf("debugging logging for component %q enabled for %v (until %v)", component, onFor.Round(time.Second), until.UTC().Format(time.RFC3339))
} else {
b.logf("debugging logging for component %q disabled", component)
}
if oldSt, ok := b.componentLogUntil[component]; ok && oldSt.timer != nil {
oldSt.timer.Stop()
}
newSt := componentLogState{until: until}
if on {
newSt.timer = time.AfterFunc(onFor, func() {
// Turn off logging after the timer fires, as long as the state is
// unchanged when the timer actually fires.
b.mu.Lock()
defer b.mu.Unlock()
if ls := b.componentLogUntil[component]; ls.until == until {
setEnabled(false)
b.logf("debugging logging for component %q disabled (by timer)", component)
}
})
}
mak.Set(&b.componentLogUntil, component, newSt)
return nil
}
// Dialer returns the backend's dialer.
func (b *LocalBackend) Dialer() *tsdial.Dialer {
return b.dialer

View File

@@ -146,6 +146,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.serveMetrics(w, r)
case "/localapi/v0/debug":
h.serveDebug(w, r)
case "/localapi/v0/component-debug-logging":
h.serveComponentDebugLogging(w, r)
case "/localapi/v0/set-expiry-sooner":
h.serveSetExpirySooner(w, r)
case "/localapi/v0/dial":
@@ -329,6 +331,24 @@ func (h *Handler) serveDebug(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "done\n")
}
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, time.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)
}
// serveProfileFunc is the implementation of Handler.serveProfile, after auth,
// for platforms where we want to link it in.
var serveProfileFunc func(http.ResponseWriter, *http.Request)

View File

@@ -6,6 +6,8 @@ package ipn
import (
"errors"
"fmt"
"strconv"
)
// ErrStateNotExist is returned by StateStore.ReadState when the
@@ -35,7 +37,7 @@ const (
// StateKey "user-1234".
ServerModeStartKey = StateKey("server-mode-start-key")
// NLKeyStateKey is the key under which we store the nodes'
// NLKeyStateKey is the key under which we store the node's
// network-lock node key, in its key.NLPrivate.MarshalText representation.
NLKeyStateKey = StateKey("_nl-node-key")
)
@@ -48,3 +50,17 @@ type StateStore interface {
// WriteState saves bs as the state associated with ID.
WriteState(id StateKey, bs []byte) error
}
// ReadStoreInt reads an integer from a StateStore.
func ReadStoreInt(store StateStore, id StateKey) (int64, error) {
v, err := store.ReadState(id)
if err != nil {
return 0, err
}
return strconv.ParseInt(string(v), 10, 64)
}
// PutStoreInt puts an integer into a StateStore.
func PutStoreInt(store StateStore, id StateKey, val int64) error {
return store.WriteState(id, fmt.Appendf(nil, "%d", val))
}