// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause // Package distro reports which distro we're running on. package distro import ( "bytes" "os" "runtime" "strconv" "strings" "tailscale.com/types/lazy" "tailscale.com/util/lineiter" ) type Distro string const ( Debian = Distro("debian") Arch = Distro("arch") Synology = Distro("synology") OpenWrt = Distro("openwrt") NixOS = Distro("nixos") QNAP = Distro("qnap") Pfsense = Distro("pfsense") OPNsense = Distro("opnsense") TrueNAS = Distro("truenas") Gokrazy = Distro("gokrazy") WDMyCloud = Distro("wdmycloud") Unraid = Distro("unraid") Alpine = Distro("alpine") UDMPro = Distro("udmpro") ) var distro lazy.SyncValue[Distro] var isWSL lazy.SyncValue[bool] // Get returns the current distro, or the empty string if unknown. func Get() Distro { return distro.Get(func() Distro { switch runtime.GOOS { case "linux": return linuxDistro() case "freebsd": return freebsdDistro() default: return Distro("") } }) } // IsWSL reports whether we're running in the Windows Subsystem for Linux. func IsWSL() bool { return runtime.GOOS == "linux" && isWSL.Get(func() bool { // We could look for $WSL_INTEROP instead, however that may be missing if // the user has started to use systemd in WSL2. return have("/proc/sys/fs/binfmt_misc/WSLInterop") || have("/mnt/wsl") }) } func have(file string) bool { _, err := os.Stat(file) return err == nil } func haveDir(file string) bool { fi, err := os.Stat(file) return err == nil && fi.IsDir() } func linuxDistro() Distro { switch { case haveDir("/usr/syno"): return Synology case have("/usr/local/bin/freenas-debug"): // TrueNAS Scale runs on debian return TrueNAS case isUDMPro(): // UDM-Pro runs on debian return UDMPro case have("/etc/debian_version"): return Debian case have("/etc/arch-release"): return Arch case have("/etc/openwrt_version"): return OpenWrt case have("/run/current-system/sw/bin/nixos-version"): return NixOS case have("/etc/config/uLinux.conf"): return QNAP case haveDir("/gokrazy"): return Gokrazy case have("/usr/local/wdmcserver/bin/wdmc.xml"): // Western Digital MyCloud OS3 return WDMyCloud case have("/usr/sbin/wd_crontab.sh"): // Western Digital MyCloud OS5 return WDMyCloud case have("/etc/unraid-version"): return Unraid case have("/etc/alpine-release"): return Alpine } return "" } func freebsdDistro() Distro { switch { case have("/etc/pfSense-rc"): return Pfsense case have("/usr/local/sbin/opnsense-shell"): return OPNsense case have("/usr/local/bin/freenas-debug"): // TrueNAS Core runs on FreeBSD return TrueNAS } return "" } var dsmVersion lazy.SyncValue[int] // DSMVersion reports the Synology DSM major version. // // If not Synology, it reports 0. func DSMVersion() int { if runtime.GOOS != "linux" { return 0 } return dsmVersion.Get(func() int { if Get() != Synology { return 0 } // This is set when running as a package: v, _ := strconv.Atoi(os.Getenv("SYNOPKG_DSM_VERSION_MAJOR")) if v != 0 { return v } // But when run from the command line, we have to read it from the file: for lr := range lineiter.File("/etc/VERSION") { line, err := lr.Value() if err != nil { break // but otherwise ignore } line = bytes.TrimSpace(line) if string(line) == `majorversion="7"` { return 7 } if string(line) == `majorversion="6"` { return 6 } } return 0 }) } // isUDMPro checks a couple of files known to exist on a UDM-Pro and returns // true if the expected content exists in the files. func isUDMPro() bool { // This is a performance guardrail against trying to load both // /etc/board.info and /sys/firmware/devicetree/base/soc/board-cfg/id when // not running on Debian so we don't make unnecessary calls in situations // where we definitely are NOT on a UDM Pro. In other words, the have() call // is much cheaper than the two os.ReadFile() in fileContainsAnyString(). // That said, on Debian systems we will still be making the two // os.ReadFile() in fileContainsAnyString(). if !have("/etc/debian_version") { return false } if exists, err := fileContainsAnyString("/etc/board.info", "UDMPRO", "Dream Machine PRO"); err == nil && exists { return true } if exists, err := fileContainsAnyString("/sys/firmware/devicetree/base/soc/board-cfg/id", "udm pro"); err == nil && exists { return true } return false } // fileContainsAnyString is used to determine if one or more of the provided // strings exists in a file. This is not efficient for larger files. If you want // to use this function to parse large files, please refactor to use // `io.LimitedReader`. func fileContainsAnyString(filePath string, searchStrings ...string) (bool, error) { data, err := os.ReadFile(filePath) if err != nil { return false, err } content := string(data) for _, searchString := range searchStrings { if strings.Contains(content, searchString) { return true, nil } } return false, nil }