diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go index db46a956f..1d2f3d84c 100644 --- a/control/controlclient/direct.go +++ b/control/controlclient/direct.go @@ -82,7 +82,8 @@ type Direct struct { debugFlags []string skipIPForwardingCheck bool pinger Pinger - popBrowser func(url string) // or nil + popBrowser func(url string) // or nil + onDisableLogTail func() // or nil; called when control sends DisableLogTail polc policyclient.Client // always non-nil c2nHandler http.Handler // or nil panicOnUse bool // if true, panic if client is used (for testing) @@ -180,6 +181,13 @@ type Options struct { // attempted. It is used to allow the client to clean up any resources or complete any // tasks that are dependent on a live client. Shutdown func() + + // OnDisableLogTail, if non-nil, is called when the control plane + // requests logging to be disabled via MapResponse.Debug.DisableLogTail. + // This is primarily used by Headscale. The callback allows the caller + // (e.g., LocalBackend) to track this state per-instance rather than + // relying on global state. + OnDisableLogTail func() } // ControlDialPlanner is the interface optionally supplied when creating a @@ -323,6 +331,7 @@ func NewDirect(opts Options) (*Direct, error) { pinger: opts.Pinger, polc: cmp.Or(opts.PolicyClient, policyclient.Client(policyclient.NoPolicyClient{})), popBrowser: opts.PopBrowserURL, + onDisableLogTail: opts.OnDisableLogTail, c2nHandler: opts.C2NHandler, dialer: opts.Dialer, dnsCache: dnsCache, @@ -1252,7 +1261,9 @@ func (c *Direct) handleDebugMessage(ctx context.Context, debug *tailcfg.Debug) e } if buildfeatures.HasLogTail && debug.DisableLogTail { logtail.Disable() - envknob.SetNoLogsNoSupport() + if c.onDisableLogTail != nil { + c.onDisableLogTail() + } } if sleep := time.Duration(debug.SleepSeconds * float64(time.Second)); sleep > 0 { if err := sleepAsRequested(ctx, c.logf, sleep, c.clock); err != nil { diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 5f694e915..6c30b6dac 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -54,6 +54,7 @@ import ( "tailscale.com/ipn/ipnstate" "tailscale.com/log/sockstatlog" "tailscale.com/logpolicy" + "tailscale.com/logtail" "tailscale.com/net/dns" "tailscale.com/net/dnscache" "tailscale.com/net/dnsfallback" @@ -214,7 +215,14 @@ type LocalBackend struct { // Tailscale on port 5252. exposeRemoteWebClientAtomicBool atomic.Bool // TODO(nickkhyl): move to nodeBackend shutdownCalled bool // if Shutdown has been called - debugSink packet.CaptureSink + // noLogsFromControl tracks whether the current control plane has + // requested logging to be disabled via MapResponse.Debug.DisableLogTail. + // This is primarily set by Headscale. It follows the control client's + // lifecycle: it is cleared when switching profiles or control servers. + // The global envknob.NoLogsNoSupport (user-set via CLI/env) always + // takes precedence over this per-instance flag. + noLogsFromControl atomic.Bool + debugSink packet.CaptureSink sockstatLogger *sockstatlog.Logger // getTCPHandlerForFunnelFlow returns a handler for an incoming TCP flow for @@ -1780,8 +1788,18 @@ func (b *LocalBackend) setControlClientStatusLocked(c controlclient.Client, st c // Now complete the lock-free parts of what we started while locked. if st.NetMap != nil { - if envknob.NoLogsNoSupport() && st.NetMap.HasCap(tailcfg.CapabilityDataPlaneAuditLogs) { - msg := "tailnet requires logging to be enabled. Remove --no-logs-no-support from tailscaled command line." + if b.NoLogsNoSupport() && st.NetMap.HasCap(tailcfg.CapabilityDataPlaneAuditLogs) { + var msg string + if envknob.NoLogsNoSupport() { + msg = "This tailnet requires logging to be enabled. " + + "Tailscale is running with logging explicitly disabled " + + "(via --no-logs-no-support or TS_NO_LOGS_NO_SUPPORT). " + + "Restart Tailscale without this setting to connect to this tailnet." + } else { + msg = "This tailnet requires logging to be enabled. " + + "Logging was disabled by the control server. " + + "Restart Tailscale to connect to this tailnet." + } b.health.SetLocalLogConfigHealth(errors.New(msg)) // Get the current prefs again, since we unlocked above. prefs := b.pm.CurrentPrefs().AsStruct() @@ -2607,6 +2625,16 @@ func (b *LocalBackend) startLocked(opts ipn.Options) error { c2nHandler = http.HandlerFunc(b.handleC2N) } + // If no-logs-no-support was set by the previous control plane + // (e.g., Headscale's DisableLogTail), clear it before connecting to + // the new control server. If the new control plane also wants logging + // disabled, it will send DisableLogTail in its first MapResponse. + // The global user-set flag (envknob) always takes precedence. + if b.noLogsFromControl.Swap(false) && !envknob.NoLogsNoSupport() { + logtail.Enable() + b.logf("re-enabled logtail; was disabled by previous control server") + } + // TODO(apenwarr): The only way to change the ServerURL is to // re-run b.Start, because this is the only place we create a // new controlclient. EditPrefs allows you to overwrite ServerURL, @@ -2625,6 +2653,7 @@ func (b *LocalBackend) startLocked(opts ipn.Options) error { PolicyClient: b.sys.PolicyClientOrDefault(), Pinger: b, PopBrowserURL: b.tellClientToBrowseToURL, + OnDisableLogTail: func() { b.noLogsFromControl.Store(true) }, Dialer: b.Dialer(), Observer: b, C2NHandler: c2nHandler, @@ -4206,6 +4235,14 @@ func (b *LocalBackend) isDefaultServerLocked() bool { return prefs.ControlURLOrDefault(b.polc) == ipn.DefaultControlURL } +// NoLogsNoSupport reports whether logging is effectively disabled for this +// LocalBackend instance, either by the user (CLI flag or environment variable) +// or by the current control plane (via MapResponse.Debug.DisableLogTail). +// The global user-set flag always takes precedence. +func (b *LocalBackend) NoLogsNoSupport() bool { + return envknob.NoLogsNoSupport() || b.noLogsFromControl.Load() +} + func (b *LocalBackend) checkExitNodePrefsLocked(p *ipn.Prefs) error { tryingToUseExitNode := p.ExitNodeIP.IsValid() || p.ExitNodeID != "" if !tryingToUseExitNode { @@ -5636,6 +5673,7 @@ func (b *LocalBackend) applyPrefsToHostinfoLocked(hi *tailcfg.Hostinfo, prefs ip hi.RequestTags = prefs.AdvertiseTags().AsSlice() hi.ShieldsUp = prefs.ShieldsUp() hi.AllowsUpdate = buildfeatures.HasClientUpdate && (envknob.AllowsRemoteUpdate() || prefs.AutoUpdate().Apply.EqualBool(true)) + hi.NoLogsNoSupport = b.NoLogsNoSupport() if buildfeatures.HasAdvertiseRoutes { b.metrics.advertisedRoutes.Set(float64(tsaddr.WithoutExitRoute(prefs.AdvertiseRoutes()).Len())) diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index 5eec66e64..0b9adb272 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -398,7 +398,7 @@ func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) { logMarker := func() string { return fmt.Sprintf("BUG-%v-%v-%v", h.backendLogID, h.clock.Now().UTC().Format("20060102150405Z"), rands.HexString(16)) } - if envknob.NoLogsNoSupport() { + if h.b.NoLogsNoSupport() { logMarker = func() string { return "BUG-NO-LOGS-NO-SUPPORT-this-node-has-had-its-logging-disabled" } } diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index b49791be6..146b33ed0 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -2326,8 +2326,11 @@ type Debug struct { // state machine. SleepSeconds float64 `json:",omitempty"` - // DisableLogTail disables the logtail package. Once disabled it can't be - // re-enabled for the lifetime of the process. + // DisableLogTail disables the logtail package. When set by the control + // plane, logging is automatically re-enabled when switching to a + // different control server. When set by the user (via CLI flag or + // environment variable), it cannot be re-enabled for the lifetime of + // the process. // // This is primarily used by Headscale. DisableLogTail bool `json:",omitempty"`