diff --git a/control/controlclient/controlclient_test.go b/control/controlclient/controlclient_test.go index 7ea6c3986..25ad8fd42 100644 --- a/control/controlclient/controlclient_test.go +++ b/control/controlclient/controlclient_test.go @@ -70,3 +70,10 @@ func TestStatusEqual(t *testing.T) { } } } + +func TestOSVersion(t *testing.T) { + if osVersion == nil { + t.Skip("not available for OS") + } + t.Logf("Got: %#q", osVersion()) +} diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go index 1cad02c54..e71882ba2 100644 --- a/control/controlclient/direct.go +++ b/control/controlclient/direct.go @@ -19,6 +19,7 @@ "net/url" "os" "reflect" + "runtime" "strconv" "strings" "sync" @@ -165,12 +166,20 @@ func NewDirect(opts Options) (*Direct, error) { return c, nil } +var osVersion func() string // non-nil on some platforms + func NewHostinfo() *tailcfg.Hostinfo { hostname, _ := os.Hostname() + var osv string + if osVersion != nil { + osv = osVersion() + } return &tailcfg.Hostinfo{ IPNVersion: version.LONG, Hostname: hostname, OS: version.OS(), + OSVersion: osv, + GoArch: runtime.GOARCH, } } diff --git a/control/controlclient/hostinfo_linux.go b/control/controlclient/hostinfo_linux.go new file mode 100644 index 000000000..227098d25 --- /dev/null +++ b/control/controlclient/hostinfo_linux.go @@ -0,0 +1,87 @@ +// 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. + +// +build linux,!android + +package controlclient + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "strings" + "syscall" + + "go4.org/mem" + "tailscale.com/util/lineread" +) + +func init() { + osVersion = osVersionLinux +} + +func osVersionLinux() string { + m := map[string]string{} + lineread.File("/etc/os-release", 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 syscall.Utsname + syscall.Uname(&un) + + var attrBuf strings.Builder + attrBuf.WriteString("; kernel=") + for _, b := range un.Release { + if b == 0 { + break + } + attrBuf.WriteByte(byte(b)) + } + if inContainer() { + attrBuf.WriteString("; container") + } + 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": + // 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) + } + } + return fmt.Sprintf("Other%s", attr) +} + +func inContainer() (ret bool) { + 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 = true + return io.EOF // arbitrary non-nil error to stop loop + } + return nil + }) + return +} diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 912005a6c..e9ca209c2 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -260,6 +260,7 @@ type Hostinfo struct { OSVersion string // operating system version, with optional distro prefix ("Debian 10.4", "Windows 10 Pro 10.0.19041") DeviceModel string // mobile phone model ("Pixel 3a", "iPhone 11 Pro") Hostname string // name of the host the client runs on + GoArch string // the host's GOARCH value (of the running binary) RoutableIPs []wgcfg.CIDR `json:",omitempty"` // set of IP ranges this client can route RequestTags []string `json:",omitempty"` // set of ACL tags this node wants to claim Services []Service `json:",omitempty"` // services advertised by this machine diff --git a/tailcfg/tailcfg_test.go b/tailcfg/tailcfg_test.go index 1ed974f98..435fd4620 100644 --- a/tailcfg/tailcfg_test.go +++ b/tailcfg/tailcfg_test.go @@ -24,7 +24,7 @@ func fieldsOf(t reflect.Type) (fields []string) { func TestHostinfoEqual(t *testing.T) { hiHandles := []string{ "IPNVersion", "FrontendLogID", "BackendLogID", "OS", "OSVersion", - "DeviceModel", "Hostname", "RoutableIPs", "RequestTags", "Services", + "DeviceModel", "Hostname", "GoArch", "RoutableIPs", "RequestTags", "Services", "NetInfo", } if have := fieldsOf(reflect.TypeOf(Hostinfo{})); !reflect.DeepEqual(have, hiHandles) {