Something went wrong. Try again.
forked from tailscale/tailscale
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140// Copyright (c) Tailscale Inc & contributors// SPDX-License-Identifier: BSD-3-Clause
package feature
import ( "runtime" "slices" "strings" "sync"
"tailscale.com/envknob" "tailscale.com/util/set")
// disabledEnv is the TS_DISABLE_FEATURE environment variable: a comma// separated list of feature names to disable in this process, even if// they're compiled in. It's a mitigation and attack-surface-reduction// knob, the runtime analog of the ts_omit_<name> build tags. For// example, on a system where the tailscaled unit loads environment// from /etc/default/tailscaled://// TS_DISABLE_FEATURE=ssh,taildrop//// The value is read once, at first use, which in practice is during// package initialization; setting it later has no effect, because// features register themselves from init.var disabledEnv = envknob.RegisterString("TS_DISABLE_FEATURE")
// disabledByEnv is the set of normalized feature names listed in// TS_DISABLE_FEATURE, parsed once on first use.var disabledByEnv = sync.OnceValue(func() set.Set[string] { return set.SetOf(parseDisabledList(disabledEnv()))})
// parseDisabledList splits a TS_DISABLE_FEATURE value into normalized// feature names, skipping empty entries.func parseDisabledList(s string) []string { var names []string for _, ent := range strings.Split(s, ",") { if name := normalizeFeatureName(ent); name != "" { names = append(names, name) } } return names}
// normalizeFeatureName canonicalizes a feature name whether it came// from TS_DISABLE_FEATURE or from code. It trims spaces, lowercases,// drops an optional ts_omit_ build-tag prefix, and maps underscores to// dashes. Registered names use dashes by convention (for example// "desktop-sessions"), but their corresponding build tags use// underscores, so both spellings are accepted.func normalizeFeatureName(s string) string { s = strings.TrimSpace(s) s = strings.ToLower(s) s = strings.TrimPrefix(s, "ts_omit_") return strings.ReplaceAll(s, "_", "-")}
// Disabled reports whether the named feature has been disabled for// this process via the TS_DISABLE_FEATURE environment variable. A// disabled feature behaves as if it had not been linked in: [Register]// reports false, and hooks and extensions registered by its package// are ignored.func Disabled(name string) bool { return disabledByEnv().Contains(normalizeFeatureName(name))}
// EnvDisabled returns the sorted feature names listed in the// TS_DISABLE_FEATURE environment variable, whether or not they name// features that this build contains. It is for diagnostics, such as// the debug-features LocalAPI endpoint.func EnvDisabled() []string { names := disabledByEnv().Slice() slices.Sort(names) return names}
// featurePkgPrefix is the import path prefix of the feature package// tree. The name of a package under it is the first path element after// the prefix, so sub-packages of a feature (such as// feature/captiveportal/netcheckhook) count as their parent feature.const featurePkgPrefix = "tailscale.com/feature/"
// mechanismPkgPrefix is the prefix of symbol names belonging to the// registration machinery itself (this package), which the stack walk// in callerFeatureName steps over to find the registering feature. It// matches both plain functions (tailscale.com/feature.callerFeatureName)// and methods (tailscale.com/feature.(*Hook[...]).Set).const mechanismPkgPrefix = "tailscale.com/feature."
// callerFeatureDisabled reports whether the feature package calling// into this package has been disabled via TS_DISABLE_FEATURE. It backs// the silent skip in [Hook.Set] and [Hooks.Add] so that a feature// package which registers hooks without first consulting [Register]// still gets disabled. If the caller is not a feature package, it// reports false.func callerFeatureDisabled() bool { name, ok := callerFeatureName() return ok && Disabled(name)}
// callerFeatureName walks up the call stack, starting just above this// package's own frames, and returns the name of the first calling// package under tailscale.com/feature/. The second result is false if// no calling feature package is found, which means the caller is// ordinary code and its registration is always wanted.//// Frames are matched by symbol name prefix rather than by parsing out// package paths, because the symbol name of a Hook.Set frame includes// its type parameters, which themselves contain import paths.func callerFeatureName() (string, bool) { var pcs [16]uintptr n := runtime.Callers(1, pcs[:]) frames := runtime.CallersFrames(pcs[:n]) for { frame, more := frames.Next() fn := frame.Function switch { case fn == "": return "", false case strings.HasPrefix(fn, mechanismPkgPrefix): // A frame of the registration machinery; look above it. case strings.HasPrefix(fn, featurePkgPrefix): rest := strings.TrimPrefix(fn, featurePkgPrefix) if i := strings.IndexAny(rest, "/."); i >= 0 { rest = rest[:i] } return rest, true default: // A frame outside the feature tree; the search is over. return "", false } if !more { return "", false } }}