Browse Source

envknob: add new package for all the strconv.ParseBool(os.Getenv(..))

A new package can also later record/report which knobs are checked and
set. It also makes the code cleaner & easier to grep for env knobs.

Change-Id: Id8a123ab7539f1fadbd27e0cbeac79c2e4f09751
Signed-off-by: Brad Fitzpatrick <[email protected]>
Brad Fitzpatrick 4 years ago
parent
commit
41fd4eab5c

+ 2 - 2
cmd/tailscale/cli/file.go

@@ -16,7 +16,6 @@ import (
 	"net/http"
 	"os"
 	"path/filepath"
-	"strconv"
 	"strings"
 	"time"
 	"unicode/utf8"
@@ -26,6 +25,7 @@ import (
 	"inet.af/netaddr"
 	"tailscale.com/client/tailscale"
 	"tailscale.com/client/tailscale/apitype"
+	"tailscale.com/envknob"
 	"tailscale.com/ipn"
 	"tailscale.com/net/tsaddr"
 	"tailscale.com/tailcfg"
@@ -148,7 +148,7 @@ func runCp(ctx context.Context, args []string) error {
 				name = filepath.Base(fileArg)
 			}
 
-			if slow, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_SLOW_PUSH")); slow {
+			if envknob.Bool("TS_DEBUG_SLOW_PUSH") {
 				fileContents = &slowReader{r: fileContents}
 			}
 		}

+ 2 - 2
cmd/tailscale/cli/netcheck.go

@@ -13,13 +13,13 @@ import (
 	"io/ioutil"
 	"log"
 	"net/http"
-	"os"
 	"sort"
 	"strings"
 	"time"
 
 	"github.com/peterbourgon/ff/v3/ffcli"
 	"tailscale.com/client/tailscale"
+	"tailscale.com/envknob"
 	"tailscale.com/ipn"
 	"tailscale.com/net/netcheck"
 	"tailscale.com/net/portmapper"
@@ -49,7 +49,7 @@ var netcheckArgs struct {
 
 func runNetcheck(ctx context.Context, args []string) error {
 	c := &netcheck.Client{
-		UDPBindAddr: os.Getenv("TS_DEBUG_NETCHECK_UDP_BIND"),
+		UDPBindAddr: envknob.String("TS_DEBUG_NETCHECK_UDP_BIND"),
 		PortMapper:  portmapper.NewClient(logger.WithPrefix(log.Printf, "portmap: "), nil),
 	}
 	if netcheckArgs.verbose {

+ 1 - 0
cmd/tailscale/depaware.txt

@@ -38,6 +38,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
         tailscale.com/derp/derphttp                                  from tailscale.com/net/netcheck
    L    tailscale.com/derp/wsconn                                    from tailscale.com/derp/derphttp
         tailscale.com/disco                                          from tailscale.com/derp
+        tailscale.com/envknob                                        from tailscale.com/cmd/tailscale/cli+
         tailscale.com/hostinfo                                       from tailscale.com/net/interfaces
         tailscale.com/ipn                                            from tailscale.com/cmd/tailscale/cli+
         tailscale.com/ipn/ipnstate                                   from tailscale.com/cmd/tailscale/cli+

+ 2 - 1
cmd/tailscaled/debug.go

@@ -24,6 +24,7 @@ import (
 
 	"inet.af/netaddr"
 	"tailscale.com/derp/derphttp"
+	"tailscale.com/envknob"
 	"tailscale.com/ipn"
 	"tailscale.com/net/interfaces"
 	"tailscale.com/net/portmapper"
@@ -224,7 +225,7 @@ func debugPortmap(ctx context.Context) error {
 	defer cancel()
 
 	portmapper.VerboseLogs = true
-	switch os.Getenv("TS_DEBUG_PORTMAP_TYPE") {
+	switch envknob.String("TS_DEBUG_PORTMAP_TYPE") {
 	case "":
 	case "pmp":
 		portmapper.DisablePCP = true

+ 1 - 0
cmd/tailscaled/depaware.txt

@@ -171,6 +171,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
         tailscale.com/derp/derphttp                                  from tailscale.com/cmd/tailscaled+
    L    tailscale.com/derp/wsconn                                    from tailscale.com/derp/derphttp
         tailscale.com/disco                                          from tailscale.com/derp+
+        tailscale.com/envknob                                        from tailscale.com/cmd/tailscaled+
         tailscale.com/health                                         from tailscale.com/control/controlclient+
         tailscale.com/hostinfo                                       from tailscale.com/control/controlclient+
         tailscale.com/ipn                                            from tailscale.com/client/tailscale+

+ 5 - 9
cmd/tailscaled/tailscaled.go

@@ -23,12 +23,12 @@ import (
 	"path/filepath"
 	"runtime"
 	"runtime/debug"
-	"strconv"
 	"strings"
 	"syscall"
 	"time"
 
 	"inet.af/netaddr"
+	"tailscale.com/envknob"
 	"tailscale.com/ipn"
 	"tailscale.com/ipn/ipnserver"
 	"tailscale.com/logpolicy"
@@ -224,7 +224,7 @@ func statePathOrDefault() string {
 func ipnServerOpts() (o ipnserver.Options) {
 	// Allow changing the OS-specific IPN behavior for tests
 	// so we can e.g. test Windows-specific behaviors on Linux.
-	goos := os.Getenv("TS_DEBUG_TAILSCALED_IPN_GOOS")
+	goos := envknob.String("TS_DEBUG_TAILSCALED_IPN_GOOS")
 	if goos == "" {
 		goos = runtime.GOOS
 	}
@@ -272,13 +272,13 @@ func run() error {
 	}
 
 	var logf logger.Logf = log.Printf
-	if v, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_MEMORY")); v {
+	if envknob.Bool("TS_DEBUG_MEMORY") {
 		logf = logger.RusagePrefixLog(logf)
 	}
 	logf = logger.RateLimitedFn(logf, 5*time.Second, 5, 100)
 
 	if args.cleanup {
-		if os.Getenv("TS_PLEASE_PANIC") != "" {
+		if envknob.Bool("TS_PLEASE_PANIC") {
 			panic("TS_PLEASE_PANIC asked us to panic")
 		}
 		dns.Cleanup(logf, args.tunname)
@@ -431,11 +431,7 @@ func createEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer)
 var wrapNetstack = shouldWrapNetstack()
 
 func shouldWrapNetstack() bool {
-	if e := os.Getenv("TS_DEBUG_WRAP_NETSTACK"); e != "" {
-		v, err := strconv.ParseBool(e)
-		if err != nil {
-			log.Fatalf("invalid TS_DEBUG_WRAP_NETSTACK value: %v", err)
-		}
+	if v, ok := envknob.LookupBool("TS_DEBUG_WRAP_NETSTACK"); ok {
 		return v
 	}
 	if distro.Get() == distro.Synology {

+ 2 - 1
cmd/tailscaled/tailscaled_windows.go

@@ -29,6 +29,7 @@ import (
 	"golang.org/x/sys/windows/svc"
 	"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
 	"inet.af/netaddr"
+	"tailscale.com/envknob"
 	"tailscale.com/ipn/ipnserver"
 	"tailscale.com/logpolicy"
 	"tailscale.com/net/dns"
@@ -314,7 +315,7 @@ func startIPNServer(ctx context.Context, logid string) error {
 	// not called concurrently and is not called again once it
 	// successfully returns an engine.
 	getEngine := func() (wgengine.Engine, error) {
-		if msg := os.Getenv("TS_DEBUG_WIN_FAIL"); msg != "" {
+		if msg := envknob.String("TS_DEBUG_WIN_FAIL"); msg != "" {
 			return nil, fmt.Errorf("pretending to be a service failure: %v", msg)
 		}
 		for {

+ 8 - 20
control/controlclient/direct.go

@@ -21,7 +21,6 @@ import (
 	"os/exec"
 	"reflect"
 	"runtime"
-	"strconv"
 	"strings"
 	"sync"
 	"sync/atomic"
@@ -30,6 +29,7 @@ import (
 	"go4.org/mem"
 	"inet.af/netaddr"
 	"tailscale.com/control/controlknobs"
+	"tailscale.com/envknob"
 	"tailscale.com/health"
 	"tailscale.com/hostinfo"
 	"tailscale.com/ipn/ipnstate"
@@ -874,8 +874,8 @@ func decode(res *http.Response, v interface{}, serverKey key.MachinePublic, mkey
 }
 
 var (
-	debugMap, _      = strconv.ParseBool(os.Getenv("TS_DEBUG_MAP"))
-	debugRegister, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_REGISTER"))
+	debugMap      = envknob.Bool("TS_DEBUG_MAP")
+	debugRegister = envknob.Bool("TS_DEBUG_REGISTER")
 )
 
 var jsonEscapedZero = []byte(`\u0000`)
@@ -985,26 +985,14 @@ type debug struct {
 
 func initDebug() debug {
 	return debug{
-		NetMap:         envBool("TS_DEBUG_NETMAP"),
-		ProxyDNS:       envBool("TS_DEBUG_PROXY_DNS"),
-		StripEndpoints: envBool("TS_DEBUG_STRIP_ENDPOINTS"),
-		StripCaps:      envBool("TS_DEBUG_STRIP_CAPS"),
-		Disco:          os.Getenv("TS_DEBUG_USE_DISCO") == "" || envBool("TS_DEBUG_USE_DISCO"),
+		NetMap:         envknob.Bool("TS_DEBUG_NETMAP"),
+		ProxyDNS:       envknob.Bool("TS_DEBUG_PROXY_DNS"),
+		StripEndpoints: envknob.Bool("TS_DEBUG_STRIP_ENDPOINTS"),
+		StripCaps:      envknob.Bool("TS_DEBUG_STRIP_CAPS"),
+		Disco:          envknob.BoolDefaultTrue("TS_DEBUG_USE_DISCO"),
 	}
 }
 
-func envBool(k string) bool {
-	e := os.Getenv(k)
-	if e == "" {
-		return false
-	}
-	v, err := strconv.ParseBool(e)
-	if err != nil {
-		panic(fmt.Sprintf("invalid non-bool %q for env var %q", e, k))
-	}
-	return v
-}
-
 var clockNow = time.Now
 
 // opt.Bool configs from control.

+ 2 - 3
control/controlclient/map.go

@@ -6,11 +6,10 @@ package controlclient
 
 import (
 	"log"
-	"os"
 	"sort"
-	"strconv"
 
 	"inet.af/netaddr"
+	"tailscale.com/envknob"
 	"tailscale.com/tailcfg"
 	"tailscale.com/types/key"
 	"tailscale.com/types/logger"
@@ -289,7 +288,7 @@ func cloneNodes(v1 []*tailcfg.Node) []*tailcfg.Node {
 	return v2
 }
 
-var debugSelfIPv6Only, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_SELF_V6_ONLY"))
+var debugSelfIPv6Only = envknob.Bool("TS_DEBUG_SELF_V6_ONLY")
 
 func filterSelfAddresses(in []netaddr.IPPrefix) (ret []netaddr.IPPrefix) {
 	switch {

+ 2 - 5
control/controlknobs/controlknobs.go

@@ -7,9 +7,7 @@
 package controlknobs
 
 import (
-	"os"
-	"strconv"
-
+	"tailscale.com/envknob"
 	"tailscale.com/syncs"
 )
 
@@ -17,8 +15,7 @@ import (
 var disableUPnP syncs.AtomicBool
 
 func init() {
-	v, _ := strconv.ParseBool(os.Getenv("TS_DISABLE_UPNP"))
-	SetDisableUPnP(v)
+	SetDisableUPnP(envknob.Bool("TS_DISABLE_UPNP"))
 }
 
 // DisableUPnP reports the last reported value from control

+ 3 - 3
derp/derp_server.go

@@ -25,7 +25,6 @@ import (
 	"math/rand"
 	"net"
 	"net/http"
-	"os"
 	"os/exec"
 	"runtime"
 	"strconv"
@@ -40,6 +39,7 @@ import (
 	"inet.af/netaddr"
 	"tailscale.com/client/tailscale"
 	"tailscale.com/disco"
+	"tailscale.com/envknob"
 	"tailscale.com/metrics"
 	"tailscale.com/syncs"
 	"tailscale.com/types/key"
@@ -48,14 +48,14 @@ import (
 	"tailscale.com/version"
 )
 
-var debug, _ = strconv.ParseBool(os.Getenv("DERP_DEBUG_LOGS"))
+var debug = envknob.Bool("DERP_DEBUG_LOGS")
 
 // verboseDropKeys is the set of destination public keys that should
 // verbosely log whenever DERP drops a packet.
 var verboseDropKeys = map[key.NodePublic]bool{}
 
 func init() {
-	keys := os.Getenv("TS_DEBUG_VERBOSE_DROPS")
+	keys := envknob.String("TS_DEBUG_VERBOSE_DROPS")
 	if keys == "" {
 		return
 	}

+ 2 - 4
derp/derphttp/derphttp_client.go

@@ -23,9 +23,7 @@ import (
 	"net"
 	"net/http"
 	"net/url"
-	"os"
 	"runtime"
-	"strconv"
 	"strings"
 	"sync"
 	"time"
@@ -33,6 +31,7 @@ import (
 	"go4.org/mem"
 	"inet.af/netaddr"
 	"tailscale.com/derp"
+	"tailscale.com/envknob"
 	"tailscale.com/net/dnscache"
 	"tailscale.com/net/netns"
 	"tailscale.com/net/tlsdial"
@@ -190,8 +189,7 @@ func useWebsockets() bool {
 		return true
 	}
 	if dialWebsocketFunc != nil {
-		v, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_DERP_WS_CLIENT"))
-		return v
+		return envknob.Bool("TS_DEBUG_DERP_WS_CLIENT")
 	}
 	return false
 }

+ 102 - 0
envknob/envknob.go

@@ -0,0 +1,102 @@
+// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package envknob provides access to environment-variable tweakable
+// debug settings.
+//
+// These are primarily knobs used by Tailscale developers during
+// development or by users when instructed to by Tailscale developers
+// when debugging something. They are not a stable interface and may
+// be removed or any time.
+//
+// A related package, control/controlknobs, are knobs that can be
+// changed at runtime by the control plane. Sometimes both are used:
+// an envknob for the default/explicit value, else falling back
+// to the controlknob value.
+package envknob
+
+import (
+	"log"
+	"os"
+	"strconv"
+
+	"tailscale.com/types/opt"
+)
+
+// String returns the named environment variable, using os.Getenv.
+//
+// In the future it will also track usage for reporting on debug pages.
+func String(envVar string) string {
+	return os.Getenv(envVar)
+}
+
+// Bool returns the boolean value of the named environment variable.
+// If the variable is not set, it returns false.
+// An invalid value exits the binary with a failure.
+func Bool(envVar string) bool {
+	return boolOr(envVar, false)
+}
+
+// BoolDefaultTrue is like Bool, but returns true by default if the
+// environment variable isn't present.
+func BoolDefaultTrue(envVar string) bool {
+	return boolOr(envVar, true)
+}
+
+func boolOr(envVar string, implicitValue bool) bool {
+	val := os.Getenv(envVar)
+	if val == "" {
+		return implicitValue
+	}
+	b, err := strconv.ParseBool(val)
+	if err == nil {
+		return b
+	}
+	log.Fatalf("invalid environment variable %s value %q: %v", envVar, val, err)
+	panic("unreachable")
+}
+
+// LookupBool returns the boolean value of the named environment value.
+// The ok result is whether a value was set.
+// If the value isn't a valid int, it exits the program with a failure.
+func LookupBool(envVar string) (v bool, ok bool) {
+	val := os.Getenv(envVar)
+	if val == "" {
+		return false, false
+	}
+	b, err := strconv.ParseBool(val)
+	if err == nil {
+		return b, true
+	}
+	log.Fatalf("invalid environment variable %s value %q: %v", envVar, val, err)
+	panic("unreachable")
+}
+
+// OptBool is like Bool, but returns an opt.Bool, so the caller can
+// distinguish between implicitly and explicitly false.
+func OptBool(envVar string) opt.Bool {
+	b, ok := LookupBool(envVar)
+	if !ok {
+		return ""
+	}
+	var ret opt.Bool
+	ret.Set(b)
+	return ret
+}
+
+// LookupInt returns the integer value of the named environment value.
+// The ok result is whether a value was set.
+// If the value isn't a valid int, it exits the program with a failure.
+func LookupInt(envVar string) (v int, ok bool) {
+	val := os.Getenv(envVar)
+	if val == "" {
+		return 0, false
+	}
+	v, err := strconv.Atoi(val)
+	if err == nil {
+		return v, true
+	}
+	log.Fatalf("invalid environment variable %s value %q: %v", envVar, val, err)
+	panic("unreachable")
+}

+ 2 - 2
health/health.go

@@ -10,13 +10,13 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
-	"os"
 	"runtime"
 	"sort"
 	"sync"
 	"sync/atomic"
 	"time"
 
+	"tailscale.com/envknob"
 	"tailscale.com/tailcfg"
 	"tailscale.com/util/multierr"
 )
@@ -308,7 +308,7 @@ func OverallError() error {
 	return overallErrorLocked()
 }
 
-var fakeErrForTesting = os.Getenv("TS_DEBUG_FAKE_HEALTH_ERROR")
+var fakeErrForTesting = envknob.String("TS_DEBUG_FAKE_HEALTH_ERROR")
 
 func overallErrorLocked() error {
 	if !anyInterfaceUp {

+ 3 - 2
ipn/ipnlocal/local.go

@@ -27,6 +27,7 @@ import (
 	"inet.af/netaddr"
 	"tailscale.com/client/tailscale/apitype"
 	"tailscale.com/control/controlclient"
+	"tailscale.com/envknob"
 	"tailscale.com/health"
 	"tailscale.com/hostinfo"
 	"tailscale.com/ipn"
@@ -64,7 +65,7 @@ import (
 var controlDebugFlags = getControlDebugFlags()
 
 func getControlDebugFlags() []string {
-	if e := os.Getenv("TS_DEBUG_CONTROL_FLAGS"); e != "" {
+	if e := envknob.String("TS_DEBUG_CONTROL_FLAGS"); e != "" {
 		return strings.Split(e, ",")
 	}
 	return nil
@@ -1349,7 +1350,7 @@ func (b *LocalBackend) popBrowserAuthNow() {
 }
 
 // For testing lazy machine key generation.
-var panicOnMachineKeyGeneration, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_PANIC_MACHINE_KEY"))
+var panicOnMachineKeyGeneration = envknob.Bool("TS_DEBUG_PANIC_MACHINE_KEY")
 
 func (b *LocalBackend) createGetMachinePrivateKeyFunc() func() (key.MachinePrivate, error) {
 	var cache atomic.Value

+ 2 - 2
ipn/localapi/cert.go

@@ -29,12 +29,12 @@ import (
 	"net/http"
 	"os"
 	"path/filepath"
-	"strconv"
 	"strings"
 	"sync"
 	"time"
 
 	"golang.org/x/crypto/acme"
+	"tailscale.com/envknob"
 	"tailscale.com/ipn/ipnstate"
 	"tailscale.com/types/logger"
 )
@@ -63,7 +63,7 @@ func (h *Handler) certDir() (string, error) {
 	return full, nil
 }
 
-var acmeDebug, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_ACME"))
+var acmeDebug = envknob.Bool("TS_DEBUG_ACME")
 
 func (h *Handler) serveCert(w http.ResponseWriter, r *http.Request) {
 	if !h.PermitWrite {

+ 4 - 4
logpolicy/logpolicy.go

@@ -25,13 +25,13 @@ import (
 	"os/exec"
 	"path/filepath"
 	"runtime"
-	"strconv"
 	"strings"
 	"sync"
 	"time"
 
 	"golang.org/x/term"
 	"tailscale.com/atomicfile"
+	"tailscale.com/envknob"
 	"tailscale.com/log/filelogger"
 	"tailscale.com/logtail"
 	"tailscale.com/logtail/filch"
@@ -227,7 +227,7 @@ func runningUnderSystemd() bool {
 }
 
 func redirectStderrToLogPanics() bool {
-	return runningUnderSystemd() || os.Getenv("TS_PLEASE_PANIC") != ""
+	return runningUnderSystemd() || envknob.Bool("TS_PLEASE_PANIC")
 }
 
 // winProgramDataAccessible reports whether the directory (assumed to
@@ -405,7 +405,7 @@ func New(collection string) *Policy {
 	} else {
 		lflags = log.LstdFlags
 	}
-	if v, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_LOG_TIME")); v {
+	if envknob.Bool("TS_DEBUG_LOG_TIME") {
 		lflags = log.LstdFlags | log.Lmicroseconds
 	}
 	if runningUnderSystemd() {
@@ -670,7 +670,7 @@ func NewLogtailTransport(host string) *http.Transport {
 	// TODO(bradfitz): remove this debug knob once we've decided
 	// to upload via HTTP/1 or HTTP/2 (probably HTTP/1). Or we might just enforce
 	// it server-side.
-	if h1, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_FORCE_H1_LOGS")); h1 {
+	if envknob.Bool("TS_DEBUG_FORCE_H1_LOGS") {
 		tr.TLSClientConfig = nil // DefaultTransport's was already initialized w/ h2
 		tr.ForceAttemptHTTP2 = false
 		tr.TLSNextProto = map[string]func(authority string, c *tls.Conn) http.RoundTripper{}

+ 2 - 3
net/dns/manager_windows.go

@@ -7,10 +7,8 @@ package dns
 import (
 	"errors"
 	"fmt"
-	"os"
 	"os/exec"
 	"sort"
-	"strconv"
 	"strings"
 	"syscall"
 	"time"
@@ -19,6 +17,7 @@ import (
 	"golang.org/x/sys/windows/registry"
 	"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
 	"inet.af/netaddr"
+	"tailscale.com/envknob"
 	"tailscale.com/types/logger"
 	"tailscale.com/util/dnsname"
 )
@@ -36,7 +35,7 @@ const (
 	versionKey = `SOFTWARE\Microsoft\Windows NT\CurrentVersion`
 )
 
-var configureWSL, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_CONFIGURE_WSL"))
+var configureWSL = envknob.Bool("TS_DEBUG_CONFIGURE_WSL")
 
 type windowsManager struct {
 	logf       logger.Logf

+ 2 - 3
net/dnscache/dnscache.go

@@ -15,14 +15,13 @@ import (
 	"fmt"
 	"log"
 	"net"
-	"os"
 	"runtime"
-	"strconv"
 	"sync"
 	"time"
 
 	"golang.org/x/sync/singleflight"
 	"inet.af/netaddr"
+	"tailscale.com/envknob"
 )
 
 var single = &Resolver{
@@ -100,7 +99,7 @@ func (r *Resolver) ttl() time.Duration {
 	return 10 * time.Minute
 }
 
-var debug, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_DNS_CACHE"))
+var debug = envknob.Bool("TS_DEBUG_DNS_CACHE")
 
 // LookupIP returns the host's primary IP address (either IPv4 or
 // IPv6, but preferring IPv4) and optionally its IPv6 address, if

+ 2 - 3
net/netcheck/netcheck.go

@@ -16,16 +16,15 @@ import (
 	"log"
 	"net"
 	"net/http"
-	"os"
 	"runtime"
 	"sort"
-	"strconv"
 	"sync"
 	"time"
 
 	"github.com/tcnksm/go-httpstat"
 	"inet.af/netaddr"
 	"tailscale.com/derp/derphttp"
+	"tailscale.com/envknob"
 	"tailscale.com/net/interfaces"
 	"tailscale.com/net/neterror"
 	"tailscale.com/net/netns"
@@ -40,7 +39,7 @@ import (
 
 // Debugging and experimentation tweakables.
 var (
-	debugNetcheck, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_NETCHECK"))
+	debugNetcheck = envknob.Bool("TS_DEBUG_NETCHECK")
 )
 
 // The various default timeouts for things.

+ 3 - 2
net/tlsdial/tlsdial.go

@@ -17,10 +17,11 @@ import (
 	"errors"
 	"log"
 	"os"
-	"strconv"
 	"sync"
 	"sync/atomic"
 	"time"
+
+	"tailscale.com/envknob"
 )
 
 var counterFallbackOK int32 // atomic
@@ -31,7 +32,7 @@ var counterFallbackOK int32 // atomic
 // See https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/Key_Log_Format
 var sslKeyLogFile = os.Getenv("SSLKEYLOGFILE")
 
-var debug, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_TLS_DIAL"))
+var debug = envknob.Bool("TS_DEBUG_TLS_DIAL")
 
 // Config returns a tls.Config for connecting to a server.
 // If base is non-nil, it's cloned as the base config before

+ 2 - 3
net/tstun/tun.go

@@ -11,13 +11,12 @@ package tstun
 
 import (
 	"errors"
-	"os"
 	"runtime"
-	"strconv"
 	"strings"
 	"time"
 
 	"golang.zx2c4.com/wireguard/tun"
+	"tailscale.com/envknob"
 	"tailscale.com/types/logger"
 )
 
@@ -32,7 +31,7 @@ import (
 var tunMTU = 1280
 
 func init() {
-	if mtu, _ := strconv.Atoi(os.Getenv("TS_DEBUG_MTU")); mtu != 0 {
+	if mtu, ok := envknob.LookupInt("TS_DEBUG_MTU"); ok {
 		tunMTU = mtu
 	}
 }

+ 3 - 3
portlist/portlist.go

@@ -6,10 +6,10 @@ package portlist
 
 import (
 	"fmt"
-	"os"
 	"sort"
-	"strconv"
 	"strings"
+
+	"tailscale.com/envknob"
 )
 
 // Port is a listening port on the machine.
@@ -74,7 +74,7 @@ func (pl List) String() string {
 	return strings.TrimRight(sb.String(), "\n")
 }
 
-var debugDisablePortlist, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_DISABLE_PORTLIST"))
+var debugDisablePortlist = envknob.Bool("TS_DEBUG_DISABLE_PORTLIST")
 
 func GetList(prev List) (List, error) {
 	if debugDisablePortlist {

+ 1 - 1
scripts/check_license_headers.sh

@@ -10,7 +10,7 @@
 check_file() {
     got=$1
 
-    for year in `seq 2019 2021`; do
+    for year in `seq 2019 2022`; do
         want=$(cat <<EOF
 // Copyright (c) $year Tailscale Inc & AUTHORS All rights reserved.
 // Use of this source code is governed by a BSD-style

+ 1 - 0
tstest/integration/tailscaled_deps_test_darwin.go

@@ -14,6 +14,7 @@ import (
 	_ "inet.af/netaddr"
 	_ "tailscale.com/chirp"
 	_ "tailscale.com/derp/derphttp"
+	_ "tailscale.com/envknob"
 	_ "tailscale.com/ipn"
 	_ "tailscale.com/ipn/ipnserver"
 	_ "tailscale.com/logpolicy"

+ 1 - 0
tstest/integration/tailscaled_deps_test_freebsd.go

@@ -14,6 +14,7 @@ import (
 	_ "inet.af/netaddr"
 	_ "tailscale.com/chirp"
 	_ "tailscale.com/derp/derphttp"
+	_ "tailscale.com/envknob"
 	_ "tailscale.com/ipn"
 	_ "tailscale.com/ipn/ipnserver"
 	_ "tailscale.com/logpolicy"

+ 1 - 0
tstest/integration/tailscaled_deps_test_linux.go

@@ -14,6 +14,7 @@ import (
 	_ "inet.af/netaddr"
 	_ "tailscale.com/chirp"
 	_ "tailscale.com/derp/derphttp"
+	_ "tailscale.com/envknob"
 	_ "tailscale.com/ipn"
 	_ "tailscale.com/ipn/ipnserver"
 	_ "tailscale.com/logpolicy"

+ 1 - 0
tstest/integration/tailscaled_deps_test_openbsd.go

@@ -14,6 +14,7 @@ import (
 	_ "inet.af/netaddr"
 	_ "tailscale.com/chirp"
 	_ "tailscale.com/derp/derphttp"
+	_ "tailscale.com/envknob"
 	_ "tailscale.com/ipn"
 	_ "tailscale.com/ipn/ipnserver"
 	_ "tailscale.com/logpolicy"

+ 1 - 0
tstest/integration/tailscaled_deps_test_windows.go

@@ -17,6 +17,7 @@ import (
 	_ "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
 	_ "inet.af/netaddr"
 	_ "tailscale.com/derp/derphttp"
+	_ "tailscale.com/envknob"
 	_ "tailscale.com/ipn"
 	_ "tailscale.com/ipn/ipnserver"
 	_ "tailscale.com/logpolicy"

+ 3 - 2
tsweb/tsweb.go

@@ -25,6 +25,7 @@ import (
 	"time"
 
 	"inet.af/netaddr"
+	"tailscale.com/envknob"
 	"tailscale.com/metrics"
 	"tailscale.com/net/tsaddr"
 	"tailscale.com/types/logger"
@@ -70,12 +71,12 @@ func AllowDebugAccess(r *http.Request) bool {
 	if err != nil {
 		return false
 	}
-	if tsaddr.IsTailscaleIP(ip) || ip.IsLoopback() || ipStr == os.Getenv("TS_ALLOW_DEBUG_IP") {
+	if tsaddr.IsTailscaleIP(ip) || ip.IsLoopback() || ipStr == envknob.String("TS_ALLOW_DEBUG_IP") {
 		return true
 	}
 	if r.Method == "GET" {
 		urlKey := r.FormValue("debugkey")
-		keyPath := os.Getenv("TS_DEBUG_KEY_PATH")
+		keyPath := envknob.String("TS_DEBUG_KEY_PATH")
 		if urlKey != "" && keyPath != "" {
 			slurp, err := ioutil.ReadFile(keyPath)
 			if err == nil && string(bytes.TrimSpace(slurp)) == urlKey {

+ 3 - 2
types/logger/logger.go

@@ -14,12 +14,13 @@ import (
 	"io"
 	"io/ioutil"
 	"log"
-	"os"
 	"strings"
 	"sync"
 	"time"
 
 	"context"
+
+	"tailscale.com/envknob"
 )
 
 // Logf is the basic Tailscale logger type: a printf-like func.
@@ -83,7 +84,7 @@ type limitData struct {
 	ele      *list.Element // list element used to access this string in the cache
 }
 
-var disableRateLimit = os.Getenv("TS_DEBUG_LOG_RATE") == "all"
+var disableRateLimit = envknob.String("TS_DEBUG_LOG_RATE") == "all"
 
 // rateFree are format string substrings that are exempt from rate limiting.
 // Things should not be added to this unless they're already limited otherwise

+ 2 - 2
wgengine/filter/filter.go

@@ -7,11 +7,11 @@ package filter
 
 import (
 	"fmt"
-	"os"
 	"sync"
 	"time"
 
 	"inet.af/netaddr"
+	"tailscale.com/envknob"
 	"tailscale.com/net/flowtrack"
 	"tailscale.com/net/packet"
 	"tailscale.com/tstime/rate"
@@ -225,7 +225,7 @@ var dropBucket = rate.NewLimiter(rate.Every(5*time.Second), 10)
 //   effectively disable the limits on the log rate by setting the limit
 //   to 1 millisecond. This should capture everything.
 func init() {
-	if os.Getenv("TS_DEBUG_FILTER_RATE_LIMIT_LOGS") != "all" {
+	if envknob.String("TS_DEBUG_FILTER_RATE_LIMIT_LOGS") != "all" {
 		return
 	}
 

+ 9 - 15
wgengine/magicsock/debugknobs.go

@@ -8,8 +8,7 @@
 package magicsock
 
 import (
-	"os"
-	"strconv"
+	"tailscale.com/envknob"
 )
 
 // Various debugging and experimental tweakables, set by environment
@@ -17,25 +16,23 @@ import (
 var (
 	// debugDisco prints verbose logs of active discovery events as
 	// they happen.
-	debugDisco, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_DISCO"))
+	debugDisco = envknob.Bool("TS_DEBUG_DISCO")
 	// debugOmitLocalAddresses removes all local interface addresses
 	// from magicsock's discovered local endpoints. Used in some tests.
-	debugOmitLocalAddresses, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_OMIT_LOCAL_ADDRS"))
+	debugOmitLocalAddresses = envknob.Bool("TS_DEBUG_OMIT_LOCAL_ADDRS")
 	// debugUseDerpRoute temporarily (2020-03-22) controls whether DERP
-	// reverse routing is enabled (Issue 150). It will become always true
-	// later.
-	debugUseDerpRouteEnv = os.Getenv("TS_DEBUG_ENABLE_DERP_ROUTE")
-	debugUseDerpRoute, _ = strconv.ParseBool(debugUseDerpRouteEnv)
+	// reverse routing is enabled (Issue 150).
+	debugUseDerpRoute = envknob.OptBool("TS_DEBUG_ENABLE_DERP_ROUTE")
 	// logDerpVerbose logs all received DERP packets, including their
 	// full payload.
-	logDerpVerbose, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_DERP"))
+	logDerpVerbose = envknob.Bool("TS_DEBUG_DERP")
 	// debugReSTUNStopOnIdle unconditionally enables the "shut down
 	// STUN if magicsock is idle" behavior that normally only triggers
 	// on mobile devices, lowers the shutdown interval, and logs more
 	// verbosely about idle measurements.
-	debugReSTUNStopOnIdle, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_RESTUN_STOP_ON_IDLE"))
+	debugReSTUNStopOnIdle = envknob.Bool("TS_DEBUG_RESTUN_STOP_ON_IDLE")
 	// debugAlwaysDERP disables the use of UDP, forcing all peer communication over DERP.
-	debugAlwaysDERP, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_ALWAYS_USE_DERP"))
+	debugAlwaysDERP = envknob.Bool("TS_DEBUG_ALWAYS_USE_DERP")
 )
 
 // inTest reports whether the running program is a test that set the
@@ -44,7 +41,4 @@ var (
 // Unlike the other debug tweakables above, this one needs to be
 // checked every time at runtime, because tests set this after program
 // startup.
-func inTest() bool {
-	inTest, _ := strconv.ParseBool(os.Getenv("IN_TS_TEST"))
-	return inTest
-}
+func inTest() bool { return envknob.Bool("IN_TS_TEST") }

+ 2 - 2
wgengine/magicsock/magicsock.go

@@ -61,8 +61,8 @@ import (
 // useDerpRoute reports whether magicsock should enable the DERP
 // return path optimization (Issue 150).
 func useDerpRoute() bool {
-	if debugUseDerpRouteEnv != "" {
-		return debugUseDerpRoute
+	if b, ok := debugUseDerpRoute.Get(); ok {
+		return b
 	}
 	ob := controlclient.DERPRouteFlag()
 	if v, ok := ob.Get(); ok {

+ 2 - 1
wgengine/netstack/netstack.go

@@ -34,6 +34,7 @@ import (
 	"inet.af/netstack/tcpip/transport/tcp"
 	"inet.af/netstack/tcpip/transport/udp"
 	"inet.af/netstack/waiter"
+	"tailscale.com/envknob"
 	"tailscale.com/net/packet"
 	"tailscale.com/net/tsaddr"
 	"tailscale.com/net/tsdial"
@@ -49,7 +50,7 @@ import (
 
 const debugPackets = false
 
-var debugNetstack, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_NETSTACK"))
+var debugNetstack = envknob.Bool("TS_DEBUG_NETSTACK")
 
 // Impl contains the state for the netstack implementation,
 // and implements wgengine.FakeImpl to act as a userspace network

+ 0 - 13
wgengine/pendopen.go

@@ -6,9 +6,7 @@ package wgengine
 
 import (
 	"fmt"
-	"os"
 	"runtime"
-	"strconv"
 	"time"
 
 	"tailscale.com/ipn/ipnstate"
@@ -22,17 +20,6 @@ import (
 
 const tcpTimeoutBeforeDebug = 5 * time.Second
 
-// debugConnectFailures reports whether the local node should track
-// outgoing TCP connections and log which ones fail and why.
-func debugConnectFailures() bool {
-	s := os.Getenv("TS_DEBUG_CONNECT_FAILURES")
-	if s == "" {
-		return true
-	}
-	v, _ := strconv.ParseBool(s)
-	return v
-}
-
 type pendingOpenFlow struct {
 	timer *time.Timer // until giving up on the flow
 

+ 2 - 1
wgengine/router/router_linux.go

@@ -22,6 +22,7 @@ import (
 	"golang.org/x/time/rate"
 	"golang.zx2c4.com/wireguard/tun"
 	"inet.af/netaddr"
+	"tailscale.com/envknob"
 	"tailscale.com/net/tsaddr"
 	"tailscale.com/syncs"
 	"tailscale.com/types/logger"
@@ -188,7 +189,7 @@ func useAmbientCaps() bool {
 	return v >= 7
 }
 
-var forceIPCommand, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_USE_IP_COMMAND"))
+var forceIPCommand = envknob.Bool("TS_DEBUG_USE_IP_COMMAND")
 
 // useIPCommand reports whether r should use the "ip" command (or its
 // fake commandRunner for tests) instead of netlink.

+ 5 - 9
wgengine/userspace.go

@@ -11,10 +11,8 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"os"
 	"reflect"
 	"runtime"
-	"strconv"
 	"strings"
 	"sync"
 	"sync/atomic"
@@ -25,6 +23,7 @@ import (
 	"golang.zx2c4.com/wireguard/tun"
 	"inet.af/netaddr"
 	"tailscale.com/control/controlclient"
+	"tailscale.com/envknob"
 	"tailscale.com/health"
 	"tailscale.com/ipn/ipnstate"
 	"tailscale.com/net/dns"
@@ -366,7 +365,7 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error)
 	}
 	e.tundev.PreFilterOut = e.handleLocalPackets
 
-	if debugConnectFailures() {
+	if envknob.BoolDefaultTrue("TS_DEBUG_CONNECT_FAILURES") {
 		if e.tundev.PreFilterIn != nil {
 			return nil, errors.New("unexpected PreFilterIn already set")
 		}
@@ -550,10 +549,7 @@ func (e *userspaceEngine) pollResolver() {
 	}
 }
 
-var (
-	debugTrimWireguardEnv = os.Getenv("TS_DEBUG_TRIM_WIREGUARD")
-	debugTrimWireguard, _ = strconv.ParseBool(debugTrimWireguardEnv)
-)
+var debugTrimWireguard = envknob.OptBool("TS_DEBUG_TRIM_WIREGUARD")
 
 // forceFullWireguardConfig reports whether we should give wireguard
 // our full network map, even for inactive peers
@@ -563,8 +559,8 @@ var (
 // and we haven't got enough time testing it.
 func forceFullWireguardConfig(numPeers int) bool {
 	// Did the user explicitly enable trimmming via the environment variable knob?
-	if debugTrimWireguardEnv != "" {
-		return !debugTrimWireguard
+	if b, ok := debugTrimWireguard.Get(); ok {
+		return !b
 	}
 	if opt := controlclient.TrimWGConfig(); opt != "" {
 		return !opt.EqualBool(true)

+ 2 - 3
wgengine/watchdog.go

@@ -6,13 +6,12 @@ package wgengine
 
 import (
 	"log"
-	"os"
 	"runtime/pprof"
-	"strconv"
 	"strings"
 	"time"
 
 	"inet.af/netaddr"
+	"tailscale.com/envknob"
 	"tailscale.com/ipn/ipnstate"
 	"tailscale.com/net/dns"
 	"tailscale.com/net/dns/resolver"
@@ -32,7 +31,7 @@ import (
 //
 // If they do not, the watchdog crashes the process.
 func NewWatchdog(e Engine) Engine {
-	if v, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_DISABLE_WATCHDOG")); v {
+	if envknob.Bool("TS_DEBUG_DISABLE_WATCHDOG") {
 		return e
 	}
 	return &watchdogEngine{