// Copyright (c) 2020 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. //go:build linux && !android // +build linux,!android package hostinfo import ( "bytes" "fmt" "io/ioutil" "os" "strings" "golang.org/x/sys/unix" "tailscale.com/util/lineread" "tailscale.com/util/strs" "tailscale.com/version/distro" ) func init() { osVersion = osVersionLinux packageType = packageTypeLinux if v := linuxDeviceModel(); v != "" { SetDeviceModel(v) } } func linuxDeviceModel() string { for _, path := range []string{ // First try the Synology-specific location. // Example: "DS916+-j" "/proc/sys/kernel/syno_hw_version", // Otherwise, try the Devicetree model, usually set on // ARM SBCs, etc. // Example: "Raspberry Pi 4 Model B Rev 1.2" // Example: "WD My Cloud Gen2: Marvell Armada 375" "/sys/firmware/devicetree/base/model", // Raspberry Pi 4 Model B Rev 1.2" } { b, _ := os.ReadFile(path) if s := strings.Trim(string(b), "\x00\r\n\t "); s != "" { return s } } return "" } func getQnapQtsVersion(versionInfo string) string { for _, field := range strings.Fields(versionInfo) { if suffix, ok := strs.CutPrefix(field, "QTSFW_"); ok { return "QTS " + suffix } } return "" } func osVersionLinux() string { // TODO(bradfitz,dgentry): cache this, or make caller(s) cache it. dist := distro.Get() propFile := "/etc/os-release" switch dist { case distro.Synology: propFile = "/etc.defaults/VERSION" case distro.OpenWrt: propFile = "/etc/openwrt_release" case distro.WDMyCloud: slurp, _ := ioutil.ReadFile("/etc/version") return fmt.Sprintf("%s", string(bytes.TrimSpace(slurp))) case distro.QNAP: slurp, _ := ioutil.ReadFile("/etc/version_info") return getQnapQtsVersion(string(slurp)) } m := map[string]string{} lineread.File(propFile, func(line []byte) error { eq := bytes.IndexByte(line, '=') if eq == -1 { return nil } k, v := string(line[:eq]), strings.Trim(string(line[eq+1:]), `"'`) m[k] = v return nil }) var un unix.Utsname unix.Uname(&un) var attrBuf strings.Builder attrBuf.WriteString("; kernel=") attrBuf.WriteString(unix.ByteSliceToString(un.Release[:])) if inContainer() { attrBuf.WriteString("; container") } if env := GetEnvType(); env != "" { fmt.Fprintf(&attrBuf, "; env=%s", env) } attr := attrBuf.String() id := m["ID"] switch id { case "debian": slurp, _ := ioutil.ReadFile("/etc/debian_version") return fmt.Sprintf("Debian %s (%s)%s", bytes.TrimSpace(slurp), m["VERSION_CODENAME"], attr) case "ubuntu": return fmt.Sprintf("Ubuntu %s%s", m["VERSION"], attr) case "", "centos": // CentOS 6 has no /etc/os-release, so its id is "" if cr, _ := ioutil.ReadFile("/etc/centos-release"); len(cr) > 0 { // "CentOS release 6.10 (Final) return fmt.Sprintf("%s%s", bytes.TrimSpace(cr), attr) } fallthrough case "fedora", "rhel", "alpine", "nixos": // Their PRETTY_NAME is fine as-is for all versions I tested. fallthrough default: if v := m["PRETTY_NAME"]; v != "" { return fmt.Sprintf("%s%s", v, attr) } } switch dist { case distro.Synology: return fmt.Sprintf("Synology %s%s", m["productversion"], attr) case distro.OpenWrt: return fmt.Sprintf("OpenWrt %s%s", m["DISTRIB_RELEASE"], attr) case distro.Gokrazy: return fmt.Sprintf("Gokrazy%s", attr) } return fmt.Sprintf("Other%s", attr) } func packageTypeLinux() string { // Report whether this is in a snap. // See https://snapcraft.io/docs/environment-variables // We just look at two somewhat arbitrarily. if os.Getenv("SNAP_NAME") != "" && os.Getenv("SNAP") != "" { return "snap" } return "" }