| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454 |
- // Copyright (c) Tailscale Inc & AUTHORS
- // SPDX-License-Identifier: BSD-3-Clause
- // Package hostinfo answers questions about the host environment that Tailscale is
- // running on.
- package hostinfo
- import (
- "bufio"
- "bytes"
- "io"
- "os"
- "os/exec"
- "runtime"
- "runtime/debug"
- "strings"
- "sync"
- "sync/atomic"
- "time"
- "go4.org/mem"
- "tailscale.com/envknob"
- "tailscale.com/tailcfg"
- "tailscale.com/types/opt"
- "tailscale.com/types/ptr"
- "tailscale.com/util/cloudenv"
- "tailscale.com/util/dnsname"
- "tailscale.com/util/lineread"
- "tailscale.com/version"
- )
- var started = time.Now()
- // New returns a partially populated Hostinfo for the current host.
- func New() *tailcfg.Hostinfo {
- hostname, _ := os.Hostname()
- hostname = dnsname.FirstLabel(hostname)
- return &tailcfg.Hostinfo{
- IPNVersion: version.Long(),
- Hostname: hostname,
- App: appTypeCached(),
- 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,
- GoArchVar: lazyGoArchVar.Get(),
- GoVersion: runtime.Version(),
- Machine: condCall(unameMachine),
- DeviceModel: deviceModel(),
- Cloud: string(cloudenv.Get()),
- NoLogsNoSupport: envknob.NoLogsNoSupport(),
- AllowsUpdate: envknob.AllowsRemoteUpdate(),
- WoLMACs: getWoLMACs(),
- }
- }
- // non-nil on some platforms
- var (
- osVersion func() string
- packageType func() string
- distroName func() string
- distroVersion func() string
- distroCodeName func() string
- unameMachine func() string
- )
- func condCall[T any](fn func() T) T {
- var zero T
- if fn == nil {
- return zero
- }
- return fn()
- }
- var (
- lazyInContainer = &lazyAtomicValue[opt.Bool]{f: ptr.To(inContainer)}
- lazyGoArchVar = &lazyAtomicValue[string]{f: ptr.To(goArchVar)}
- )
- type lazyAtomicValue[T any] struct {
- // f is a pointer to a fill function. If it's nil or points
- // to nil, then Get returns the zero value for T.
- f *func() T
- once sync.Once
- v T
- }
- func (v *lazyAtomicValue[T]) Get() T {
- v.once.Do(v.fill)
- return v.v
- }
- func (v *lazyAtomicValue[T]) fill() {
- if v.f == nil || *v.f == nil {
- return
- }
- v.v = (*v.f)()
- }
- // GetOSVersion returns the OSVersion of current host if available.
- func GetOSVersion() string {
- if s, _ := osVersionAtomic.Load().(string); s != "" {
- return s
- }
- if osVersion != nil {
- return osVersion()
- }
- return ""
- }
- func appTypeCached() string {
- if v, ok := appType.Load().(string); ok {
- return v
- }
- return ""
- }
- func packageTypeCached() string {
- if v, _ := packagingType.Load().(string); v != "" {
- return v
- }
- if packageType == nil {
- return ""
- }
- v := packageType()
- if v != "" {
- SetPackage(v)
- }
- return v
- }
- // EnvType represents a known environment type.
- // The empty string, the default, means unknown.
- type EnvType string
- const (
- KNative = EnvType("kn")
- AWSLambda = EnvType("lm")
- Heroku = EnvType("hr")
- AzureAppService = EnvType("az")
- AWSFargate = EnvType("fg")
- FlyDotIo = EnvType("fly")
- Kubernetes = EnvType("k8s")
- DockerDesktop = EnvType("dde")
- Replit = EnvType("repl")
- HomeAssistantAddOn = EnvType("haao")
- )
- var envType atomic.Value // of EnvType
- func GetEnvType() EnvType {
- if e, ok := envType.Load().(EnvType); ok {
- return e
- }
- e := getEnvType()
- envType.Store(e)
- return e
- }
- var (
- deviceModelAtomic atomic.Value // of string
- osVersionAtomic atomic.Value // of string
- desktopAtomic atomic.Value // of opt.Bool
- packagingType atomic.Value // of string
- appType atomic.Value // of string
- firewallMode atomic.Value // of string
- )
- // SetDeviceModel sets the device model for use in Hostinfo updates.
- func SetDeviceModel(model string) { deviceModelAtomic.Store(model) }
- // SetOSVersion sets the OS version.
- func SetOSVersion(v string) { osVersionAtomic.Store(v) }
- // SetFirewallMode sets the firewall mode for the app.
- func SetFirewallMode(v string) { firewallMode.Store(v) }
- // SetPackage sets the packaging type for the app.
- //
- // As of 2022-03-25, this is used by Android ("nogoogle" for the
- // F-Droid build) and tsnet (set to "tsnet").
- func SetPackage(v string) { packagingType.Store(v) }
- // SetApp sets the app type for the app.
- // It is used by tsnet to specify what app is using it such as "golinks"
- // and "k8s-operator".
- func SetApp(v string) { appType.Store(v) }
- func deviceModel() string {
- s, _ := deviceModelAtomic.Load().(string)
- return s
- }
- // FirewallMode returns the firewall mode for the app.
- // It is empty if unset.
- func FirewallMode() string {
- s, _ := firewallMode.Load().(string)
- return s
- }
- func desktop() (ret opt.Bool) {
- if runtime.GOOS != "linux" {
- return opt.Bool("")
- }
- if v := desktopAtomic.Load(); v != nil {
- v, _ := v.(opt.Bool)
- return v
- }
- seenDesktop := false
- lineread.File("/proc/net/unix", func(line []byte) error {
- seenDesktop = seenDesktop || mem.Contains(mem.B(line), mem.S(" @/tmp/dbus-"))
- seenDesktop = seenDesktop || mem.Contains(mem.B(line), mem.S(".X11-unix"))
- seenDesktop = seenDesktop || mem.Contains(mem.B(line), mem.S("/wayland-1"))
- return nil
- })
- ret.Set(seenDesktop)
- // Only cache after a minute - compositors might not have started yet.
- if time.Since(started) > time.Minute {
- desktopAtomic.Store(ret)
- }
- return ret
- }
- func getEnvType() EnvType {
- if inKnative() {
- return KNative
- }
- if inAWSLambda() {
- return AWSLambda
- }
- if inHerokuDyno() {
- return Heroku
- }
- if inAzureAppService() {
- return AzureAppService
- }
- if inAWSFargate() {
- return AWSFargate
- }
- if inFlyDotIo() {
- return FlyDotIo
- }
- if inKubernetes() {
- return Kubernetes
- }
- if inDockerDesktop() {
- return DockerDesktop
- }
- if inReplit() {
- return Replit
- }
- if inHomeAssistantAddOn() {
- return HomeAssistantAddOn
- }
- return ""
- }
- // inContainer reports whether we're running in a container.
- func inContainer() opt.Bool {
- if runtime.GOOS != "linux" {
- return ""
- }
- var ret opt.Bool
- ret.Set(false)
- if _, err := os.Stat("/.dockerenv"); err == nil {
- ret.Set(true)
- return ret
- }
- if _, err := os.Stat("/run/.containerenv"); err == nil {
- // See https://github.com/cri-o/cri-o/issues/5461
- ret.Set(true)
- return ret
- }
- lineread.File("/proc/1/cgroup", func(line []byte) error {
- if mem.Contains(mem.B(line), mem.S("/docker/")) ||
- mem.Contains(mem.B(line), mem.S("/lxc/")) {
- ret.Set(true)
- return io.EOF // arbitrary non-nil error to stop loop
- }
- return nil
- })
- lineread.File("/proc/mounts", func(line []byte) error {
- if mem.Contains(mem.B(line), mem.S("lxcfs /proc/cpuinfo fuse.lxcfs")) {
- ret.Set(true)
- return io.EOF
- }
- return nil
- })
- return ret
- }
- func inKnative() bool {
- // https://cloud.google.com/run/docs/reference/container-contract#env-vars
- if os.Getenv("K_REVISION") != "" && os.Getenv("K_CONFIGURATION") != "" &&
- os.Getenv("K_SERVICE") != "" && os.Getenv("PORT") != "" {
- return true
- }
- return false
- }
- func inAWSLambda() bool {
- // https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html
- if os.Getenv("AWS_LAMBDA_FUNCTION_NAME") != "" &&
- os.Getenv("AWS_LAMBDA_FUNCTION_VERSION") != "" &&
- os.Getenv("AWS_LAMBDA_INITIALIZATION_TYPE") != "" &&
- os.Getenv("AWS_LAMBDA_RUNTIME_API") != "" {
- return true
- }
- return false
- }
- func inHerokuDyno() bool {
- // https://devcenter.heroku.com/articles/dynos#local-environment-variables
- if os.Getenv("PORT") != "" && os.Getenv("DYNO") != "" {
- return true
- }
- return false
- }
- func inAzureAppService() bool {
- if os.Getenv("APPSVC_RUN_ZIP") != "" && os.Getenv("WEBSITE_STACK") != "" &&
- os.Getenv("WEBSITE_AUTH_AUTO_AAD") != "" {
- return true
- }
- return false
- }
- func inAWSFargate() bool {
- return os.Getenv("AWS_EXECUTION_ENV") == "AWS_ECS_FARGATE"
- }
- func inFlyDotIo() bool {
- if os.Getenv("FLY_APP_NAME") != "" && os.Getenv("FLY_REGION") != "" {
- return true
- }
- return false
- }
- func inReplit() bool {
- // https://docs.replit.com/programming-ide/getting-repl-metadata
- if os.Getenv("REPL_OWNER") != "" && os.Getenv("REPL_SLUG") != "" {
- return true
- }
- return false
- }
- func inKubernetes() bool {
- if os.Getenv("KUBERNETES_SERVICE_HOST") != "" && os.Getenv("KUBERNETES_SERVICE_PORT") != "" {
- return true
- }
- return false
- }
- func inDockerDesktop() bool {
- return os.Getenv("TS_HOST_ENV") == "dde"
- }
- func inHomeAssistantAddOn() bool {
- if os.Getenv("SUPERVISOR_TOKEN") != "" || os.Getenv("HASSIO_TOKEN") != "" {
- return true
- }
- return false
- }
- // goArchVar returns the GOARM or GOAMD64 etc value that the binary was built
- // with.
- func goArchVar() string {
- bi, ok := debug.ReadBuildInfo()
- if !ok {
- return ""
- }
- // Look for GOARM, GOAMD64, GO386, etc. Note that the little-endian
- // "le"-suffixed GOARCH values don't have their own environment variable.
- //
- // See https://pkg.go.dev/cmd/go#hdr-Environment_variables and the
- // "Architecture-specific environment variables" section:
- wantKey := "GO" + strings.ToUpper(strings.TrimSuffix(runtime.GOARCH, "le"))
- for _, s := range bi.Settings {
- if s.Key == wantKey {
- return s.Value
- }
- }
- return ""
- }
- type etcAptSrcResult struct {
- mod time.Time
- disabled bool
- }
- var etcAptSrcCache atomic.Value // of etcAptSrcResult
- // DisabledEtcAptSource reports whether Ubuntu (or similar) has disabled
- // the /etc/apt/sources.list.d/tailscale.list file contents upon upgrade
- // to a new release of the distro.
- //
- // See https://github.com/tailscale/tailscale/issues/3177
- func DisabledEtcAptSource() bool {
- if runtime.GOOS != "linux" {
- return false
- }
- const path = "/etc/apt/sources.list.d/tailscale.list"
- fi, err := os.Stat(path)
- if err != nil || !fi.Mode().IsRegular() {
- return false
- }
- mod := fi.ModTime()
- if c, ok := etcAptSrcCache.Load().(etcAptSrcResult); ok && c.mod.Equal(mod) {
- return c.disabled
- }
- f, err := os.Open(path)
- if err != nil {
- return false
- }
- defer f.Close()
- v := etcAptSourceFileIsDisabled(f)
- etcAptSrcCache.Store(etcAptSrcResult{mod: mod, disabled: v})
- return v
- }
- func etcAptSourceFileIsDisabled(r io.Reader) bool {
- bs := bufio.NewScanner(r)
- disabled := false // did we find the "disabled on upgrade" comment?
- for bs.Scan() {
- line := strings.TrimSpace(bs.Text())
- if strings.Contains(line, "# disabled on upgrade") {
- disabled = true
- }
- if line == "" || line[0] == '#' {
- continue
- }
- // Well, it has some contents in it at least.
- return false
- }
- return disabled
- }
- // IsSELinuxEnforcing reports whether SELinux is in "Enforcing" mode.
- func IsSELinuxEnforcing() bool {
- if runtime.GOOS != "linux" {
- return false
- }
- out, _ := exec.Command("getenforce").Output()
- return string(bytes.TrimSpace(out)) == "Enforcing"
- }
|