hostinfo_linux.go 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. //go:build linux && !android
  4. package hostinfo
  5. import (
  6. "bytes"
  7. "os"
  8. "strings"
  9. "golang.org/x/sys/unix"
  10. "tailscale.com/types/ptr"
  11. "tailscale.com/util/lineiter"
  12. "tailscale.com/version/distro"
  13. )
  14. func init() {
  15. osVersion = lazyOSVersion.Get
  16. packageType = packageTypeLinux
  17. distroName = distroNameLinux
  18. distroVersion = distroVersionLinux
  19. distroCodeName = distroCodeNameLinux
  20. deviceModel = deviceModelLinux
  21. }
  22. var (
  23. lazyVersionMeta = &lazyAtomicValue[versionMeta]{f: ptr.To(linuxVersionMeta)}
  24. lazyOSVersion = &lazyAtomicValue[string]{f: ptr.To(osVersionLinux)}
  25. )
  26. type versionMeta struct {
  27. DistroName string
  28. DistroVersion string
  29. DistroCodeName string // "jammy", etc (VERSION_CODENAME from /etc/os-release)
  30. }
  31. func distroNameLinux() string {
  32. return lazyVersionMeta.Get().DistroName
  33. }
  34. func distroVersionLinux() string {
  35. return lazyVersionMeta.Get().DistroVersion
  36. }
  37. func distroCodeNameLinux() string {
  38. return lazyVersionMeta.Get().DistroCodeName
  39. }
  40. func deviceModelLinux() string {
  41. for _, path := range []string{
  42. // First try the Synology-specific location.
  43. // Example: "DS916+-j"
  44. "/proc/sys/kernel/syno_hw_version",
  45. // Otherwise, try the Devicetree model, usually set on
  46. // ARM SBCs, etc.
  47. // Example: "Raspberry Pi 4 Model B Rev 1.2"
  48. // Example: "WD My Cloud Gen2: Marvell Armada 375"
  49. "/sys/firmware/devicetree/base/model", // Raspberry Pi 4 Model B Rev 1.2"
  50. } {
  51. b, _ := os.ReadFile(path)
  52. if s := strings.Trim(string(b), "\x00\r\n\t "); s != "" {
  53. return s
  54. }
  55. }
  56. return ""
  57. }
  58. func getQnapQtsVersion(versionInfo string) string {
  59. for _, field := range strings.Fields(versionInfo) {
  60. if suffix, ok := strings.CutPrefix(field, "QTSFW_"); ok {
  61. return suffix
  62. }
  63. }
  64. return ""
  65. }
  66. func osVersionLinux() string {
  67. var un unix.Utsname
  68. unix.Uname(&un)
  69. return unix.ByteSliceToString(un.Release[:])
  70. }
  71. func linuxVersionMeta() (meta versionMeta) {
  72. dist := distro.Get()
  73. meta.DistroName = string(dist)
  74. propFile := "/etc/os-release"
  75. switch dist {
  76. case distro.Synology:
  77. propFile = "/etc.defaults/VERSION"
  78. case distro.OpenWrt:
  79. propFile = "/etc/openwrt_release"
  80. case distro.Unraid:
  81. propFile = "/etc/unraid-version"
  82. case distro.WDMyCloud:
  83. slurp, _ := os.ReadFile("/etc/version")
  84. meta.DistroVersion = string(bytes.TrimSpace(slurp))
  85. return
  86. case distro.QNAP:
  87. slurp, _ := os.ReadFile("/etc/version_info")
  88. meta.DistroVersion = getQnapQtsVersion(string(slurp))
  89. return
  90. }
  91. m := map[string]string{}
  92. for lr := range lineiter.File(propFile) {
  93. line, err := lr.Value()
  94. if err != nil {
  95. break
  96. }
  97. eq := bytes.IndexByte(line, '=')
  98. if eq == -1 {
  99. continue
  100. }
  101. k, v := string(line[:eq]), strings.Trim(string(line[eq+1:]), `"'`)
  102. m[k] = v
  103. }
  104. if v := m["VERSION_CODENAME"]; v != "" {
  105. meta.DistroCodeName = v
  106. }
  107. if v := m["VERSION_ID"]; v != "" {
  108. meta.DistroVersion = v
  109. }
  110. id := m["ID"]
  111. if id != "" {
  112. meta.DistroName = id
  113. }
  114. switch id {
  115. case "debian":
  116. // Debian's VERSION_ID is just like "11". But /etc/debian_version has "11.5" normally.
  117. // Or "bookworm/sid" on sid/testing.
  118. slurp, _ := os.ReadFile("/etc/debian_version")
  119. if v := string(bytes.TrimSpace(slurp)); v != "" {
  120. if '0' <= v[0] && v[0] <= '9' {
  121. meta.DistroVersion = v
  122. } else if meta.DistroCodeName == "" {
  123. meta.DistroCodeName = v
  124. }
  125. }
  126. case "", "centos": // CentOS 6 has no /etc/os-release, so its id is ""
  127. if meta.DistroVersion == "" {
  128. if cr, _ := os.ReadFile("/etc/centos-release"); len(cr) > 0 { // "CentOS release 6.10 (Final)
  129. meta.DistroVersion = string(bytes.TrimSpace(cr))
  130. }
  131. }
  132. }
  133. if v := m["PRETTY_NAME"]; v != "" && meta.DistroVersion == "" && !strings.HasSuffix(v, "/sid") {
  134. meta.DistroVersion = v
  135. }
  136. switch dist {
  137. case distro.Synology:
  138. meta.DistroVersion = m["productversion"]
  139. case distro.OpenWrt:
  140. meta.DistroVersion = m["DISTRIB_RELEASE"]
  141. case distro.Unraid:
  142. meta.DistroVersion = m["version"]
  143. }
  144. return
  145. }
  146. // linuxBuildTagPackageType is set by packagetype_*.go
  147. // build tag guarded files.
  148. var linuxBuildTagPackageType string
  149. func packageTypeLinux() string {
  150. if v := linuxBuildTagPackageType; v != "" {
  151. return v
  152. }
  153. // Report whether this is in a snap.
  154. // See https://snapcraft.io/docs/environment-variables
  155. // We just look at two somewhat arbitrarily.
  156. if os.Getenv("SNAP_NAME") != "" && os.Getenv("SNAP") != "" {
  157. return "snap"
  158. }
  159. return ""
  160. }