hostinfo.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. // Package hostinfo answers questions about the host environment that Tailscale is
  4. // running on.
  5. package hostinfo
  6. import (
  7. "bufio"
  8. "bytes"
  9. "io"
  10. "os"
  11. "os/exec"
  12. "runtime"
  13. "runtime/debug"
  14. "strings"
  15. "sync"
  16. "sync/atomic"
  17. "time"
  18. "go4.org/mem"
  19. "tailscale.com/envknob"
  20. "tailscale.com/tailcfg"
  21. "tailscale.com/types/opt"
  22. "tailscale.com/types/ptr"
  23. "tailscale.com/util/cloudenv"
  24. "tailscale.com/util/dnsname"
  25. "tailscale.com/util/lineread"
  26. "tailscale.com/version"
  27. )
  28. var started = time.Now()
  29. // New returns a partially populated Hostinfo for the current host.
  30. func New() *tailcfg.Hostinfo {
  31. hostname, _ := os.Hostname()
  32. hostname = dnsname.FirstLabel(hostname)
  33. return &tailcfg.Hostinfo{
  34. IPNVersion: version.Long(),
  35. Hostname: hostname,
  36. App: appTypeCached(),
  37. OS: version.OS(),
  38. OSVersion: GetOSVersion(),
  39. Container: lazyInContainer.Get(),
  40. Distro: condCall(distroName),
  41. DistroVersion: condCall(distroVersion),
  42. DistroCodeName: condCall(distroCodeName),
  43. Env: string(GetEnvType()),
  44. Desktop: desktop(),
  45. Package: packageTypeCached(),
  46. GoArch: runtime.GOARCH,
  47. GoArchVar: lazyGoArchVar.Get(),
  48. GoVersion: runtime.Version(),
  49. Machine: condCall(unameMachine),
  50. DeviceModel: deviceModel(),
  51. Cloud: string(cloudenv.Get()),
  52. NoLogsNoSupport: envknob.NoLogsNoSupport(),
  53. AllowsUpdate: envknob.AllowsRemoteUpdate(),
  54. WoLMACs: getWoLMACs(),
  55. }
  56. }
  57. // non-nil on some platforms
  58. var (
  59. osVersion func() string
  60. packageType func() string
  61. distroName func() string
  62. distroVersion func() string
  63. distroCodeName func() string
  64. unameMachine func() string
  65. )
  66. func condCall[T any](fn func() T) T {
  67. var zero T
  68. if fn == nil {
  69. return zero
  70. }
  71. return fn()
  72. }
  73. var (
  74. lazyInContainer = &lazyAtomicValue[opt.Bool]{f: ptr.To(inContainer)}
  75. lazyGoArchVar = &lazyAtomicValue[string]{f: ptr.To(goArchVar)}
  76. )
  77. type lazyAtomicValue[T any] struct {
  78. // f is a pointer to a fill function. If it's nil or points
  79. // to nil, then Get returns the zero value for T.
  80. f *func() T
  81. once sync.Once
  82. v T
  83. }
  84. func (v *lazyAtomicValue[T]) Get() T {
  85. v.once.Do(v.fill)
  86. return v.v
  87. }
  88. func (v *lazyAtomicValue[T]) fill() {
  89. if v.f == nil || *v.f == nil {
  90. return
  91. }
  92. v.v = (*v.f)()
  93. }
  94. // GetOSVersion returns the OSVersion of current host if available.
  95. func GetOSVersion() string {
  96. if s, _ := osVersionAtomic.Load().(string); s != "" {
  97. return s
  98. }
  99. if osVersion != nil {
  100. return osVersion()
  101. }
  102. return ""
  103. }
  104. func appTypeCached() string {
  105. if v, ok := appType.Load().(string); ok {
  106. return v
  107. }
  108. return ""
  109. }
  110. func packageTypeCached() string {
  111. if v, _ := packagingType.Load().(string); v != "" {
  112. return v
  113. }
  114. if packageType == nil {
  115. return ""
  116. }
  117. v := packageType()
  118. if v != "" {
  119. SetPackage(v)
  120. }
  121. return v
  122. }
  123. // EnvType represents a known environment type.
  124. // The empty string, the default, means unknown.
  125. type EnvType string
  126. const (
  127. KNative = EnvType("kn")
  128. AWSLambda = EnvType("lm")
  129. Heroku = EnvType("hr")
  130. AzureAppService = EnvType("az")
  131. AWSFargate = EnvType("fg")
  132. FlyDotIo = EnvType("fly")
  133. Kubernetes = EnvType("k8s")
  134. DockerDesktop = EnvType("dde")
  135. Replit = EnvType("repl")
  136. HomeAssistantAddOn = EnvType("haao")
  137. )
  138. var envType atomic.Value // of EnvType
  139. func GetEnvType() EnvType {
  140. if e, ok := envType.Load().(EnvType); ok {
  141. return e
  142. }
  143. e := getEnvType()
  144. envType.Store(e)
  145. return e
  146. }
  147. var (
  148. deviceModelAtomic atomic.Value // of string
  149. osVersionAtomic atomic.Value // of string
  150. desktopAtomic atomic.Value // of opt.Bool
  151. packagingType atomic.Value // of string
  152. appType atomic.Value // of string
  153. firewallMode atomic.Value // of string
  154. )
  155. // SetDeviceModel sets the device model for use in Hostinfo updates.
  156. func SetDeviceModel(model string) { deviceModelAtomic.Store(model) }
  157. // SetOSVersion sets the OS version.
  158. func SetOSVersion(v string) { osVersionAtomic.Store(v) }
  159. // SetFirewallMode sets the firewall mode for the app.
  160. func SetFirewallMode(v string) { firewallMode.Store(v) }
  161. // SetPackage sets the packaging type for the app.
  162. //
  163. // As of 2022-03-25, this is used by Android ("nogoogle" for the
  164. // F-Droid build) and tsnet (set to "tsnet").
  165. func SetPackage(v string) { packagingType.Store(v) }
  166. // SetApp sets the app type for the app.
  167. // It is used by tsnet to specify what app is using it such as "golinks"
  168. // and "k8s-operator".
  169. func SetApp(v string) { appType.Store(v) }
  170. func deviceModel() string {
  171. s, _ := deviceModelAtomic.Load().(string)
  172. return s
  173. }
  174. // FirewallMode returns the firewall mode for the app.
  175. // It is empty if unset.
  176. func FirewallMode() string {
  177. s, _ := firewallMode.Load().(string)
  178. return s
  179. }
  180. func desktop() (ret opt.Bool) {
  181. if runtime.GOOS != "linux" {
  182. return opt.Bool("")
  183. }
  184. if v := desktopAtomic.Load(); v != nil {
  185. v, _ := v.(opt.Bool)
  186. return v
  187. }
  188. seenDesktop := false
  189. lineread.File("/proc/net/unix", func(line []byte) error {
  190. seenDesktop = seenDesktop || mem.Contains(mem.B(line), mem.S(" @/tmp/dbus-"))
  191. seenDesktop = seenDesktop || mem.Contains(mem.B(line), mem.S(".X11-unix"))
  192. seenDesktop = seenDesktop || mem.Contains(mem.B(line), mem.S("/wayland-1"))
  193. return nil
  194. })
  195. ret.Set(seenDesktop)
  196. // Only cache after a minute - compositors might not have started yet.
  197. if time.Since(started) > time.Minute {
  198. desktopAtomic.Store(ret)
  199. }
  200. return ret
  201. }
  202. func getEnvType() EnvType {
  203. if inKnative() {
  204. return KNative
  205. }
  206. if inAWSLambda() {
  207. return AWSLambda
  208. }
  209. if inHerokuDyno() {
  210. return Heroku
  211. }
  212. if inAzureAppService() {
  213. return AzureAppService
  214. }
  215. if inAWSFargate() {
  216. return AWSFargate
  217. }
  218. if inFlyDotIo() {
  219. return FlyDotIo
  220. }
  221. if inKubernetes() {
  222. return Kubernetes
  223. }
  224. if inDockerDesktop() {
  225. return DockerDesktop
  226. }
  227. if inReplit() {
  228. return Replit
  229. }
  230. if inHomeAssistantAddOn() {
  231. return HomeAssistantAddOn
  232. }
  233. return ""
  234. }
  235. // inContainer reports whether we're running in a container.
  236. func inContainer() opt.Bool {
  237. if runtime.GOOS != "linux" {
  238. return ""
  239. }
  240. var ret opt.Bool
  241. ret.Set(false)
  242. if _, err := os.Stat("/.dockerenv"); err == nil {
  243. ret.Set(true)
  244. return ret
  245. }
  246. if _, err := os.Stat("/run/.containerenv"); err == nil {
  247. // See https://github.com/cri-o/cri-o/issues/5461
  248. ret.Set(true)
  249. return ret
  250. }
  251. lineread.File("/proc/1/cgroup", func(line []byte) error {
  252. if mem.Contains(mem.B(line), mem.S("/docker/")) ||
  253. mem.Contains(mem.B(line), mem.S("/lxc/")) {
  254. ret.Set(true)
  255. return io.EOF // arbitrary non-nil error to stop loop
  256. }
  257. return nil
  258. })
  259. lineread.File("/proc/mounts", func(line []byte) error {
  260. if mem.Contains(mem.B(line), mem.S("lxcfs /proc/cpuinfo fuse.lxcfs")) {
  261. ret.Set(true)
  262. return io.EOF
  263. }
  264. return nil
  265. })
  266. return ret
  267. }
  268. func inKnative() bool {
  269. // https://cloud.google.com/run/docs/reference/container-contract#env-vars
  270. if os.Getenv("K_REVISION") != "" && os.Getenv("K_CONFIGURATION") != "" &&
  271. os.Getenv("K_SERVICE") != "" && os.Getenv("PORT") != "" {
  272. return true
  273. }
  274. return false
  275. }
  276. func inAWSLambda() bool {
  277. // https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html
  278. if os.Getenv("AWS_LAMBDA_FUNCTION_NAME") != "" &&
  279. os.Getenv("AWS_LAMBDA_FUNCTION_VERSION") != "" &&
  280. os.Getenv("AWS_LAMBDA_INITIALIZATION_TYPE") != "" &&
  281. os.Getenv("AWS_LAMBDA_RUNTIME_API") != "" {
  282. return true
  283. }
  284. return false
  285. }
  286. func inHerokuDyno() bool {
  287. // https://devcenter.heroku.com/articles/dynos#local-environment-variables
  288. if os.Getenv("PORT") != "" && os.Getenv("DYNO") != "" {
  289. return true
  290. }
  291. return false
  292. }
  293. func inAzureAppService() bool {
  294. if os.Getenv("APPSVC_RUN_ZIP") != "" && os.Getenv("WEBSITE_STACK") != "" &&
  295. os.Getenv("WEBSITE_AUTH_AUTO_AAD") != "" {
  296. return true
  297. }
  298. return false
  299. }
  300. func inAWSFargate() bool {
  301. return os.Getenv("AWS_EXECUTION_ENV") == "AWS_ECS_FARGATE"
  302. }
  303. func inFlyDotIo() bool {
  304. if os.Getenv("FLY_APP_NAME") != "" && os.Getenv("FLY_REGION") != "" {
  305. return true
  306. }
  307. return false
  308. }
  309. func inReplit() bool {
  310. // https://docs.replit.com/programming-ide/getting-repl-metadata
  311. if os.Getenv("REPL_OWNER") != "" && os.Getenv("REPL_SLUG") != "" {
  312. return true
  313. }
  314. return false
  315. }
  316. func inKubernetes() bool {
  317. if os.Getenv("KUBERNETES_SERVICE_HOST") != "" && os.Getenv("KUBERNETES_SERVICE_PORT") != "" {
  318. return true
  319. }
  320. return false
  321. }
  322. func inDockerDesktop() bool {
  323. return os.Getenv("TS_HOST_ENV") == "dde"
  324. }
  325. func inHomeAssistantAddOn() bool {
  326. if os.Getenv("SUPERVISOR_TOKEN") != "" || os.Getenv("HASSIO_TOKEN") != "" {
  327. return true
  328. }
  329. return false
  330. }
  331. // goArchVar returns the GOARM or GOAMD64 etc value that the binary was built
  332. // with.
  333. func goArchVar() string {
  334. bi, ok := debug.ReadBuildInfo()
  335. if !ok {
  336. return ""
  337. }
  338. // Look for GOARM, GOAMD64, GO386, etc. Note that the little-endian
  339. // "le"-suffixed GOARCH values don't have their own environment variable.
  340. //
  341. // See https://pkg.go.dev/cmd/go#hdr-Environment_variables and the
  342. // "Architecture-specific environment variables" section:
  343. wantKey := "GO" + strings.ToUpper(strings.TrimSuffix(runtime.GOARCH, "le"))
  344. for _, s := range bi.Settings {
  345. if s.Key == wantKey {
  346. return s.Value
  347. }
  348. }
  349. return ""
  350. }
  351. type etcAptSrcResult struct {
  352. mod time.Time
  353. disabled bool
  354. }
  355. var etcAptSrcCache atomic.Value // of etcAptSrcResult
  356. // DisabledEtcAptSource reports whether Ubuntu (or similar) has disabled
  357. // the /etc/apt/sources.list.d/tailscale.list file contents upon upgrade
  358. // to a new release of the distro.
  359. //
  360. // See https://github.com/tailscale/tailscale/issues/3177
  361. func DisabledEtcAptSource() bool {
  362. if runtime.GOOS != "linux" {
  363. return false
  364. }
  365. const path = "/etc/apt/sources.list.d/tailscale.list"
  366. fi, err := os.Stat(path)
  367. if err != nil || !fi.Mode().IsRegular() {
  368. return false
  369. }
  370. mod := fi.ModTime()
  371. if c, ok := etcAptSrcCache.Load().(etcAptSrcResult); ok && c.mod.Equal(mod) {
  372. return c.disabled
  373. }
  374. f, err := os.Open(path)
  375. if err != nil {
  376. return false
  377. }
  378. defer f.Close()
  379. v := etcAptSourceFileIsDisabled(f)
  380. etcAptSrcCache.Store(etcAptSrcResult{mod: mod, disabled: v})
  381. return v
  382. }
  383. func etcAptSourceFileIsDisabled(r io.Reader) bool {
  384. bs := bufio.NewScanner(r)
  385. disabled := false // did we find the "disabled on upgrade" comment?
  386. for bs.Scan() {
  387. line := strings.TrimSpace(bs.Text())
  388. if strings.Contains(line, "# disabled on upgrade") {
  389. disabled = true
  390. }
  391. if line == "" || line[0] == '#' {
  392. continue
  393. }
  394. // Well, it has some contents in it at least.
  395. return false
  396. }
  397. return disabled
  398. }
  399. // IsSELinuxEnforcing reports whether SELinux is in "Enforcing" mode.
  400. func IsSELinuxEnforcing() bool {
  401. if runtime.GOOS != "linux" {
  402. return false
  403. }
  404. out, _ := exec.Command("getenforce").Output()
  405. return string(bytes.TrimSpace(out)) == "Enforcing"
  406. }