diff --git a/cmd/tailscaled/tailscaled.go b/cmd/tailscaled/tailscaled.go index 69c12d9e7..2397bbe48 100644 --- a/cmd/tailscaled/tailscaled.go +++ b/cmd/tailscaled/tailscaled.go @@ -113,6 +113,7 @@ var args struct { verbose int socksAddr string // listen address for SOCKS5 server httpProxyAddr string // listen address for HTTP proxy server + disableLogs bool } var ( @@ -144,6 +145,7 @@ func main() { flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket") flag.StringVar(&args.birdSocketPath, "bird-socket", "", "path of the bird unix socket") flag.BoolVar(&printVersion, "version", false, "print version information and exit") + flag.BoolVar(&args.disableLogs, "no-logs-no-support", false, "disable log uploads; this also disables any technical support") if len(os.Args) > 0 && filepath.Base(os.Args[0]) == "tailscale" && beCLI != nil { beCLI() @@ -199,6 +201,10 @@ func main() { args.statepath = paths.DefaultTailscaledStateFile() } + if args.disableLogs { + envknob.SetNoLogsNoSupport() + } + if beWindowsSubprocess() { return } diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go index 80401f4dd..12de6d2ca 100644 --- a/control/controlclient/direct.go +++ b/control/controlclient/direct.go @@ -937,6 +937,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool } if resp.Debug.DisableLogTail { logtail.Disable() + envknob.SetNoLogsNoSupport() } if resp.Debug.LogHeapPprof { go logheap.LogHeap(resp.Debug.LogHeapURL) diff --git a/envknob/envknob.go b/envknob/envknob.go index aa11763cc..ecc731b3d 100644 --- a/envknob/envknob.go +++ b/envknob/envknob.go @@ -155,3 +155,14 @@ func SSHPolicyFile() string { return String("TS_DEBUG_SSH_POLICY_FILE") } // SSHIgnoreTailnetPolicy is whether to ignore the Tailnet SSH policy for development. func SSHIgnoreTailnetPolicy() bool { return Bool("TS_DEBUG_SSH_IGNORE_TAILNET_POLICY") } + +// NoLogsNoSupport reports whether the client's opted out of log uploads and +// technical support. +func NoLogsNoSupport() bool { + return Bool("TS_NO_LOGS_NO_SUPPORT") +} + +// SetNoLogsNoSupport enables no-logs-no-support mode. +func SetNoLogsNoSupport() { + os.Setenv("TS_NO_LOGS_NO_SUPPORT", "true") +} diff --git a/hostinfo/hostinfo.go b/hostinfo/hostinfo.go index d7f239e7c..8d626400c 100644 --- a/hostinfo/hostinfo.go +++ b/hostinfo/hostinfo.go @@ -17,6 +17,7 @@ import ( "time" "go4.org/mem" + "tailscale.com/envknob" "tailscale.com/tailcfg" "tailscale.com/types/opt" "tailscale.com/util/cloudenv" @@ -32,21 +33,22 @@ func New() *tailcfg.Hostinfo { hostname, _ := os.Hostname() hostname = dnsname.FirstLabel(hostname) return &tailcfg.Hostinfo{ - IPNVersion: version.Long, - Hostname: hostname, - OS: version.OS(), - OSVersion: GetOSVersion(), - Container: lazyInContainer.Get(), - Distro: condCall(distroName), - DistroVersion: condCall(distroVersion), - DistroCodeName: condCall(distroCodeName), - Env: string(GetEnvType()), - Desktop: desktop(), - Package: packageTypeCached(), - GoArch: runtime.GOARCH, - GoVersion: runtime.Version(), - DeviceModel: deviceModel(), - Cloud: string(cloudenv.Get()), + IPNVersion: version.Long, + Hostname: hostname, + OS: version.OS(), + OSVersion: GetOSVersion(), + Container: lazyInContainer.Get(), + Distro: condCall(distroName), + DistroVersion: condCall(distroVersion), + DistroCodeName: condCall(distroCodeName), + Env: string(GetEnvType()), + Desktop: desktop(), + Package: packageTypeCached(), + GoArch: runtime.GOARCH, + GoVersion: runtime.Version(), + DeviceModel: deviceModel(), + Cloud: string(cloudenv.Get()), + NoLogsNoSupport: envknob.NoLogsNoSupport(), } } diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index e854992e3..50e4909e9 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -25,6 +25,7 @@ import ( "time" "tailscale.com/client/tailscale/apitype" + "tailscale.com/envknob" "tailscale.com/ipn" "tailscale.com/ipn/ipnlocal" "tailscale.com/ipn/ipnstate" @@ -213,6 +214,9 @@ func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) { } logMarker := fmt.Sprintf("BUG-%v-%v-%v", h.backendLogID, time.Now().UTC().Format("20060102150405Z"), randHex(8)) + if envknob.NoLogsNoSupport() { + logMarker = "BUG-NO-LOGS-NO-SUPPORT-this-node-has-had-its-logging-disabled" + } h.logf("user bugreport: %s", logMarker) if note := r.FormValue("note"); len(note) > 0 { h.logf("user bugreport note: %s", note) diff --git a/logpolicy/logpolicy.go b/logpolicy/logpolicy.go index e201df765..638b8c8e7 100644 --- a/logpolicy/logpolicy.go +++ b/logpolicy/logpolicy.go @@ -539,7 +539,10 @@ func New(collection string) *Policy { conf.IncludeProcSequence = true } - if val := getLogTarget(); val != "" { + if envknob.NoLogsNoSupport() { + log.Println("You have disabled logging. Tailscale will not be able to provide support.") + conf.HTTPC = &http.Client{Transport: noopPretendSuccessTransport{}} + } else if val := getLogTarget(); val != "" { log.Println("You have enabled a non-default log target. Doing without being told to by Tailscale staff or your network administrator will make getting support difficult.") conf.BaseURL = val u, _ := url.Parse(val) @@ -735,3 +738,14 @@ func goVersion() string { } return v } + +type noopPretendSuccessTransport struct{} + +func (noopPretendSuccessTransport) RoundTrip(req *http.Request) (*http.Response, error) { + io.ReadAll(req.Body) + req.Body.Close() + return &http.Response{ + StatusCode: 200, + Status: "200 OK", + }, nil +} diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 514a01bdd..24f544ad9 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -495,6 +495,7 @@ type Hostinfo struct { Hostname string `json:",omitempty"` // name of the host the client runs on ShieldsUp bool `json:",omitempty"` // indicates whether the host is blocking incoming connections ShareeNode bool `json:",omitempty"` // indicates this node exists in netmap because it's owned by a shared-to user + NoLogsNoSupport bool `json:",omitempty"` // indicates that the user has opted out of sending logs and support GoArch string `json:",omitempty"` // the host's GOARCH value (of the running binary) GoVersion string `json:",omitempty"` // Go version binary was built with RoutableIPs []netip.Prefix `json:",omitempty"` // set of IP ranges this client can route diff --git a/tailcfg/tailcfg_clone.go b/tailcfg/tailcfg_clone.go index f89bc6c2f..9bbd396b5 100644 --- a/tailcfg/tailcfg_clone.go +++ b/tailcfg/tailcfg_clone.go @@ -131,6 +131,7 @@ var _HostinfoCloneNeedsRegeneration = Hostinfo(struct { Hostname string ShieldsUp bool ShareeNode bool + NoLogsNoSupport bool GoArch string GoVersion string RoutableIPs []netip.Prefix diff --git a/tailcfg/tailcfg_test.go b/tailcfg/tailcfg_test.go index ce13bef37..c391963db 100644 --- a/tailcfg/tailcfg_test.go +++ b/tailcfg/tailcfg_test.go @@ -47,6 +47,7 @@ func TestHostinfoEqual(t *testing.T) { "Hostname", "ShieldsUp", "ShareeNode", + "NoLogsNoSupport", "GoArch", "GoVersion", "RoutableIPs", diff --git a/tailcfg/tailcfg_view.go b/tailcfg/tailcfg_view.go index d06489bda..d2d96a7d3 100644 --- a/tailcfg/tailcfg_view.go +++ b/tailcfg/tailcfg_view.go @@ -266,6 +266,7 @@ func (v HostinfoView) DeviceModel() string { return v.ж.DeviceModel } func (v HostinfoView) Hostname() string { return v.ж.Hostname } func (v HostinfoView) ShieldsUp() bool { return v.ж.ShieldsUp } func (v HostinfoView) ShareeNode() bool { return v.ж.ShareeNode } +func (v HostinfoView) NoLogsNoSupport() bool { return v.ж.NoLogsNoSupport } func (v HostinfoView) GoArch() string { return v.ж.GoArch } func (v HostinfoView) GoVersion() string { return v.ж.GoVersion } func (v HostinfoView) RoutableIPs() views.IPPrefixSlice { @@ -298,6 +299,7 @@ var _HostinfoViewNeedsRegeneration = Hostinfo(struct { Hostname string ShieldsUp bool ShareeNode bool + NoLogsNoSupport bool GoArch string GoVersion string RoutableIPs []netip.Prefix