Procházet zdrojové kódy

hostinfo, tailcfg: split Hostinfo.OSVersion into separate fields

Stop jamming everything into one string.

Fixes #5578

Change-Id: I7dec8d6c073bddc7dc5f653e3baf2b4bf6b68378
Signed-off-by: Brad Fitzpatrick <[email protected]>
Brad Fitzpatrick před 3 roky
rodič
revize
d5e7e3093d

+ 62 - 17
hostinfo/hostinfo.go

@@ -12,6 +12,7 @@ import (
 	"os"
 	"os"
 	"runtime"
 	"runtime"
 	"strings"
 	"strings"
+	"sync"
 	"sync/atomic"
 	"sync/atomic"
 	"time"
 	"time"
 
 
@@ -31,25 +32,68 @@ func New() *tailcfg.Hostinfo {
 	hostname, _ := os.Hostname()
 	hostname, _ := os.Hostname()
 	hostname = dnsname.FirstLabel(hostname)
 	hostname = dnsname.FirstLabel(hostname)
 	return &tailcfg.Hostinfo{
 	return &tailcfg.Hostinfo{
-		IPNVersion:  version.Long,
-		Hostname:    hostname,
-		OS:          version.OS(),
-		OSVersion:   GetOSVersion(),
-		Desktop:     desktop(),
-		Package:     packageTypeCached(),
-		GoArch:      runtime.GOARCH,
-		GoVersion:   runtime.Version(),
-		DeviceModel: deviceModel(),
-		Cloud:       string(cloudenv.Get()),
+		IPNVersion:     version.Long,
+		Hostname:       hostname,
+		OS:             version.OS(),
+		OSVersion:      GetOSVersion(),
+		Container:      lazyInContainer.Get(),
+		Distro:         condCall(distroName),
+		DistroVersion:  condCall(distroVersion),
+		DistroCodeName: condCall(distroCodeName),
+		Env:            string(GetEnvType()),
+		Desktop:        desktop(),
+		Package:        packageTypeCached(),
+		GoArch:         runtime.GOARCH,
+		GoVersion:      runtime.Version(),
+		DeviceModel:    deviceModel(),
+		Cloud:          string(cloudenv.Get()),
 	}
 	}
 }
 }
 
 
 // non-nil on some platforms
 // non-nil on some platforms
 var (
 var (
-	osVersion   func() string
-	packageType func() string
+	osVersion      func() string
+	packageType    func() string
+	distroName     func() string
+	distroVersion  func() string
+	distroCodeName func() string
 )
 )
 
 
+func condCall[T any](fn func() T) T {
+	var zero T
+	if fn == nil {
+		return zero
+	}
+	return fn()
+}
+
+var (
+	lazyInContainer = &lazyAtomicValue[opt.Bool]{f: ptrTo(inContainer)}
+)
+
+func ptrTo[T any](v T) *T { return &v }
+
+type lazyAtomicValue[T any] struct {
+	// f is a pointer to a fill function. If it's nil or points
+	// to nil, then Get returns the zero value for T.
+	f *func() T
+
+	once sync.Once
+	v    T
+}
+
+func (v *lazyAtomicValue[T]) Get() T {
+	v.once.Do(v.fill)
+	return v.v
+}
+
+func (v *lazyAtomicValue[T]) fill() {
+	if v.f == nil || *v.f == nil {
+		return
+	}
+	v.v = (*v.f)()
+}
+
 // GetOSVersion returns the OSVersion of current host if available.
 // GetOSVersion returns the OSVersion of current host if available.
 func GetOSVersion() string {
 func GetOSVersion() string {
 	if s, _ := osVersionAtomic.Load().(string); s != "" {
 	if s, _ := osVersionAtomic.Load().(string); s != "" {
@@ -179,22 +223,23 @@ func getEnvType() EnvType {
 }
 }
 
 
 // inContainer reports whether we're running in a container.
 // inContainer reports whether we're running in a container.
-func inContainer() bool {
+func inContainer() opt.Bool {
 	if runtime.GOOS != "linux" {
 	if runtime.GOOS != "linux" {
-		return false
+		return ""
 	}
 	}
-	var ret bool
+	var ret opt.Bool
+	ret.Set(false)
 	lineread.File("/proc/1/cgroup", func(line []byte) error {
 	lineread.File("/proc/1/cgroup", func(line []byte) error {
 		if mem.Contains(mem.B(line), mem.S("/docker/")) ||
 		if mem.Contains(mem.B(line), mem.S("/docker/")) ||
 			mem.Contains(mem.B(line), mem.S("/lxc/")) {
 			mem.Contains(mem.B(line), mem.S("/lxc/")) {
-			ret = true
+			ret.Set(true)
 			return io.EOF // arbitrary non-nil error to stop loop
 			return io.EOF // arbitrary non-nil error to stop loop
 		}
 		}
 		return nil
 		return nil
 	})
 	})
 	lineread.File("/proc/mounts", func(line []byte) error {
 	lineread.File("/proc/mounts", func(line []byte) error {
 		if mem.Contains(mem.B(line), mem.S("fuse.lxcfs")) {
 		if mem.Contains(mem.B(line), mem.S("fuse.lxcfs")) {
-			ret = true
+			ret.Set(true)
 			return io.EOF
 			return io.EOF
 		}
 		}
 		return nil
 		return nil

+ 37 - 27
hostinfo/hostinfo_freebsd.go

@@ -8,48 +8,58 @@
 package hostinfo
 package hostinfo
 
 
 import (
 import (
-	"fmt"
+	"bytes"
 	"os"
 	"os"
 	"os/exec"
 	"os/exec"
-	"strings"
 
 
 	"golang.org/x/sys/unix"
 	"golang.org/x/sys/unix"
 	"tailscale.com/version/distro"
 	"tailscale.com/version/distro"
 )
 )
 
 
 func init() {
 func init() {
-	osVersion = osVersionFreebsd
+	osVersion = lazyOSVersion.Get
+	distroName = distroNameFreeBSD
+	distroVersion = distroVersionFreeBSD
 }
 }
 
 
-func osVersionFreebsd() string {
-	un := unix.Utsname{}
-	unix.Uname(&un)
+var (
+	lazyVersionMeta = &lazyAtomicValue[versionMeta]{f: ptrTo(freebsdVersionMeta)}
+	lazyOSVersion   = &lazyAtomicValue[string]{f: ptrTo(osVersionFreeBSD)}
+)
+
+func distroNameFreeBSD() string {
+	return lazyVersionMeta.Get().DistroName
+}
 
 
-	var attrBuf strings.Builder
-	attrBuf.WriteString("; version=")
-	attrBuf.WriteString(unix.ByteSliceToString(un.Release[:]))
-	attr := attrBuf.String()
+func distroVersionFreeBSD() string {
+	return lazyVersionMeta.Get().DistroVersion
+}
 
 
-	version := "FreeBSD"
-	switch distro.Get() {
+type versionMeta struct {
+	DistroName     string
+	DistroVersion  string
+	DistroCodeName string
+}
+
+func osVersionFreeBSD() string {
+	var un unix.Utsname
+	unix.Uname(&un)
+	return unix.ByteSliceToString(un.Release[:])
+}
+
+func freebsdVersionMeta() (meta versionMeta) {
+	d := distro.Get()
+	meta.DistroName = string(d)
+	switch d {
 	case distro.Pfsense:
 	case distro.Pfsense:
 		b, _ := os.ReadFile("/etc/version")
 		b, _ := os.ReadFile("/etc/version")
-		version = fmt.Sprintf("pfSense %s", b)
+		meta.DistroVersion = string(bytes.TrimSpace(b))
 	case distro.OPNsense:
 	case distro.OPNsense:
-		b, err := exec.Command("opnsense-version").Output()
-		if err == nil {
-			version = string(b)
-		} else {
-			version = "OPNsense"
-		}
+		b, _ := exec.Command("opnsense-version").Output()
+		meta.DistroVersion = string(bytes.TrimSpace(b))
 	case distro.TrueNAS:
 	case distro.TrueNAS:
-		b, err := os.ReadFile("/etc/version")
-		if err == nil {
-			version = string(b)
-		} else {
-			version = "TrueNAS"
-		}
+		b, _ := os.ReadFile("/etc/version")
+		meta.DistroVersion = string(bytes.TrimSpace(b))
 	}
 	}
-	// the /etc/version files end in a newline
-	return fmt.Sprintf("%s%s", strings.TrimSuffix(version, "\n"), attr)
+	return
 }
 }

+ 66 - 38
hostinfo/hostinfo_linux.go

@@ -9,7 +9,6 @@ package hostinfo
 
 
 import (
 import (
 	"bytes"
 	"bytes"
-	"fmt"
 	"io/ioutil"
 	"io/ioutil"
 	"os"
 	"os"
 	"strings"
 	"strings"
@@ -21,14 +20,39 @@ import (
 )
 )
 
 
 func init() {
 func init() {
-	osVersion = osVersionLinux
+	osVersion = lazyOSVersion.Get
 	packageType = packageTypeLinux
 	packageType = packageTypeLinux
-
+	distroName = distroNameLinux
+	distroVersion = distroVersionLinux
+	distroCodeName = distroCodeNameLinux
 	if v := linuxDeviceModel(); v != "" {
 	if v := linuxDeviceModel(); v != "" {
 		SetDeviceModel(v)
 		SetDeviceModel(v)
 	}
 	}
 }
 }
 
 
+var (
+	lazyVersionMeta = &lazyAtomicValue[versionMeta]{f: ptrTo(linuxVersionMeta)}
+	lazyOSVersion   = &lazyAtomicValue[string]{f: ptrTo(osVersionLinux)}
+)
+
+type versionMeta struct {
+	DistroName     string
+	DistroVersion  string
+	DistroCodeName string // "jammy", etc (VERSION_CODENAME from /etc/os-release)
+}
+
+func distroNameLinux() string {
+	return lazyVersionMeta.Get().DistroName
+}
+
+func distroVersionLinux() string {
+	return lazyVersionMeta.Get().DistroVersion
+}
+
+func distroCodeNameLinux() string {
+	return lazyVersionMeta.Get().DistroCodeName
+}
+
 func linuxDeviceModel() string {
 func linuxDeviceModel() string {
 	for _, path := range []string{
 	for _, path := range []string{
 		// First try the Synology-specific location.
 		// First try the Synology-specific location.
@@ -52,15 +76,22 @@ func linuxDeviceModel() string {
 func getQnapQtsVersion(versionInfo string) string {
 func getQnapQtsVersion(versionInfo string) string {
 	for _, field := range strings.Fields(versionInfo) {
 	for _, field := range strings.Fields(versionInfo) {
 		if suffix, ok := strs.CutPrefix(field, "QTSFW_"); ok {
 		if suffix, ok := strs.CutPrefix(field, "QTSFW_"); ok {
-			return "QTS " + suffix
+			return suffix
 		}
 		}
 	}
 	}
 	return ""
 	return ""
 }
 }
 
 
 func osVersionLinux() string {
 func osVersionLinux() string {
-	// TODO(bradfitz,dgentry): cache this, or make caller(s) cache it.
+	var un unix.Utsname
+	unix.Uname(&un)
+	return unix.ByteSliceToString(un.Release[:])
+}
+
+func linuxVersionMeta() (meta versionMeta) {
 	dist := distro.Get()
 	dist := distro.Get()
+	meta.DistroName = string(dist)
+
 	propFile := "/etc/os-release"
 	propFile := "/etc/os-release"
 	switch dist {
 	switch dist {
 	case distro.Synology:
 	case distro.Synology:
@@ -69,10 +100,12 @@ func osVersionLinux() string {
 		propFile = "/etc/openwrt_release"
 		propFile = "/etc/openwrt_release"
 	case distro.WDMyCloud:
 	case distro.WDMyCloud:
 		slurp, _ := ioutil.ReadFile("/etc/version")
 		slurp, _ := ioutil.ReadFile("/etc/version")
-		return fmt.Sprintf("%s", string(bytes.TrimSpace(slurp)))
+		meta.DistroVersion = string(bytes.TrimSpace(slurp))
+		return
 	case distro.QNAP:
 	case distro.QNAP:
 		slurp, _ := ioutil.ReadFile("/etc/version_info")
 		slurp, _ := ioutil.ReadFile("/etc/version_info")
-		return getQnapQtsVersion(string(slurp))
+		meta.DistroVersion = getQnapQtsVersion(string(slurp))
+		return
 	}
 	}
 
 
 	m := map[string]string{}
 	m := map[string]string{}
@@ -86,50 +119,45 @@ func osVersionLinux() string {
 		return nil
 		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 v := m["VERSION_CODENAME"]; v != "" {
+		meta.DistroCodeName = v
 	}
 	}
-	if env := GetEnvType(); env != "" {
-		fmt.Fprintf(&attrBuf, "; env=%s", env)
+	if v := m["VERSION_ID"]; v != "" {
+		meta.DistroVersion = v
 	}
 	}
-	attr := attrBuf.String()
-
 	id := m["ID"]
 	id := m["ID"]
-
+	if id != "" {
+		meta.DistroName = id
+	}
 	switch id {
 	switch id {
 	case "debian":
 	case "debian":
+		// Debian's VERSION_ID is just like "11". But /etc/debian_version has "11.5" normally.
+		// Or "bookworm/sid" on sid/testing.
 		slurp, _ := ioutil.ReadFile("/etc/debian_version")
 		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)
+		if v := string(bytes.TrimSpace(slurp)); v != "" {
+			if '0' <= v[0] && v[0] <= '9' {
+				meta.DistroVersion = v
+			} else if meta.DistroCodeName == "" {
+				meta.DistroCodeName = v
+			}
 		}
 		}
-		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)
+	case "", "centos": // CentOS 6 has no /etc/os-release, so its id is ""
+		if meta.DistroVersion == "" {
+			if cr, _ := ioutil.ReadFile("/etc/centos-release"); len(cr) > 0 { // "CentOS release 6.10 (Final)
+				meta.DistroVersion = string(bytes.TrimSpace(cr))
+			}
 		}
 		}
 	}
 	}
+	if v := m["PRETTY_NAME"]; v != "" && meta.DistroVersion == "" && !strings.HasSuffix(v, "/sid") {
+		meta.DistroVersion = v
+	}
 	switch dist {
 	switch dist {
 	case distro.Synology:
 	case distro.Synology:
-		return fmt.Sprintf("Synology %s%s", m["productversion"], attr)
+		meta.DistroVersion = m["productversion"]
 	case distro.OpenWrt:
 	case distro.OpenWrt:
-		return fmt.Sprintf("OpenWrt %s%s", m["DISTRIB_RELEASE"], attr)
-	case distro.Gokrazy:
-		return fmt.Sprintf("Gokrazy%s", attr)
+		meta.DistroVersion = m["DISTRIB_RELEASE"]
 	}
 	}
-	return fmt.Sprintf("Other%s", attr)
+	return
 }
 }
 
 
 func packageTypeLinux() string {
 func packageTypeLinux() string {

+ 1 - 1
hostinfo/hostinfo_linux_test.go

@@ -19,7 +19,7 @@ Date:   2022-05-30 16:08:45 +0800
 remotes/origin/QTSFW_5.0.0`
 remotes/origin/QTSFW_5.0.0`
 
 
 	got := getQnapQtsVersion(version_info)
 	got := getQnapQtsVersion(version_info)
-	want := "QTS 5.0.0"
+	want := "5.0.0"
 	if got != want {
 	if got != want {
 		t.Errorf("got %q; want %q", got, want)
 		t.Errorf("got %q; want %q", got, want)
 	}
 	}

+ 6 - 10
hostinfo/hostinfo_windows.go

@@ -11,21 +11,20 @@ import (
 
 
 	"golang.org/x/sys/windows"
 	"golang.org/x/sys/windows"
 	"golang.org/x/sys/windows/registry"
 	"golang.org/x/sys/windows/registry"
-	"tailscale.com/syncs"
 	"tailscale.com/util/winutil"
 	"tailscale.com/util/winutil"
 )
 )
 
 
 func init() {
 func init() {
-	osVersion = osVersionWindows
-	packageType = packageTypeWindows
+	osVersion = lazyOSVersion.Get
+	packageType = lazyPackageType.Get
 }
 }
 
 
-var winVerCache syncs.AtomicValue[string]
+var (
+	lazyOSVersion   = &lazyAtomicValue[string]{f: ptrTo(osVersionWindows)}
+	lazyPackageType = &lazyAtomicValue[string]{f: ptrTo(packageTypeWindows)}
+)
 
 
 func osVersionWindows() string {
 func osVersionWindows() string {
-	if s, ok := winVerCache.LoadOk(); ok {
-		return s
-	}
 	major, minor, build := windows.RtlGetNtVersionNumbers()
 	major, minor, build := windows.RtlGetNtVersionNumbers()
 	s := fmt.Sprintf("%d.%d.%d", major, minor, build)
 	s := fmt.Sprintf("%d.%d.%d", major, minor, build)
 	// Windows 11 still uses 10 as its major number internally
 	// Windows 11 still uses 10 as its major number internally
@@ -34,9 +33,6 @@ func osVersionWindows() string {
 			s += fmt.Sprintf(".%d", ubr)
 			s += fmt.Sprintf(".%d", ubr)
 		}
 		}
 	}
 	}
-	if s != "" {
-		winVerCache.Store(s)
-	}
 	return s // "10.0.19041.388", ideally
 	return s // "10.0.19041.388", ideally
 }
 }
 
 

+ 23 - 5
tailcfg/tailcfg.go

@@ -466,11 +466,29 @@ type Service struct {
 // Because it contains pointers (slices), this type should not be used
 // Because it contains pointers (slices), this type should not be used
 // as a value type.
 // as a value type.
 type Hostinfo struct {
 type Hostinfo struct {
-	IPNVersion      string         `json:",omitempty"` // version of this code
-	FrontendLogID   string         `json:",omitempty"` // logtail ID of frontend instance
-	BackendLogID    string         `json:",omitempty"` // logtail ID of backend instance
-	OS              string         `json:",omitempty"` // operating system the client runs on (a version.OS value)
-	OSVersion       string         `json:",omitempty"` // operating system version, with optional distro prefix ("Debian 10.4", "Windows 10 Pro 10.0.19041")
+	IPNVersion    string `json:",omitempty"` // version of this code (in version.Long format)
+	FrontendLogID string `json:",omitempty"` // logtail ID of frontend instance
+	BackendLogID  string `json:",omitempty"` // logtail ID of backend instance
+	OS            string `json:",omitempty"` // operating system the client runs on (a version.OS value)
+
+	// OSVersion is the version of the OS, if available.
+	//
+	// For Android, it's like "10", "11", "12", etc. For iOS and macOS it's like
+	// "15.6.1" or "12.4.0". For Windows it's like "10.0.19044.1889". For
+	// FreeBSD it's like "12.3-STABLE".
+	//
+	// For Linux, prior to Tailscale 1.32, we jammed a bunch of fields into this
+	// string on Linux, like "Debian 10.4; kernel=xxx; container; env=kn" and so
+	// on. As of Tailscale 1.32, this is simply the kernel version on Linux, like
+	// "5.10.0-17-amd64".
+	OSVersion string `json:",omitempty"`
+
+	Container      opt.Bool `json:",omitempty"` // whether the client is running in a container
+	Env            string   `json:",omitempty"` // a hostinfo.EnvType in string form
+	Distro         string   `json:",omitempty"` // "debian", "ubuntu", "nixos", ...
+	DistroVersion  string   `json:",omitempty"` // "20.04", ...
+	DistroCodeName string   `json:",omitempty"` // "jammy", "bullseye", ...
+
 	Desktop         opt.Bool       `json:",omitempty"` // if a desktop was detected on Linux
 	Desktop         opt.Bool       `json:",omitempty"` // if a desktop was detected on Linux
 	Package         string         `json:",omitempty"` // Tailscale package to disambiguate ("choco", "appstore", etc; "" for unknown)
 	Package         string         `json:",omitempty"` // Tailscale package to disambiguate ("choco", "appstore", etc; "" for unknown)
 	DeviceModel     string         `json:",omitempty"` // mobile phone model ("Pixel 3a", "iPhone12,3")
 	DeviceModel     string         `json:",omitempty"` // mobile phone model ("Pixel 3a", "iPhone12,3")

+ 5 - 0
tailcfg/tailcfg_clone.go

@@ -120,6 +120,11 @@ var _HostinfoCloneNeedsRegeneration = Hostinfo(struct {
 	BackendLogID    string
 	BackendLogID    string
 	OS              string
 	OS              string
 	OSVersion       string
 	OSVersion       string
+	Container       opt.Bool
+	Env             string
+	Distro          string
+	DistroVersion   string
+	DistroCodeName  string
 	Desktop         opt.Bool
 	Desktop         opt.Bool
 	Package         string
 	Package         string
 	DeviceModel     string
 	DeviceModel     string

+ 26 - 7
tailcfg/tailcfg_test.go

@@ -31,13 +31,32 @@ func fieldsOf(t reflect.Type) (fields []string) {
 
 
 func TestHostinfoEqual(t *testing.T) {
 func TestHostinfoEqual(t *testing.T) {
 	hiHandles := []string{
 	hiHandles := []string{
-		"IPNVersion", "FrontendLogID", "BackendLogID",
-		"OS", "OSVersion", "Desktop", "Package", "DeviceModel", "Hostname",
-		"ShieldsUp", "ShareeNode",
-		"GoArch", "GoVersion",
-		"RoutableIPs", "RequestTags",
-		"Services", "NetInfo", "SSH_HostKeys", "Cloud",
-		"Userspace", "UserspaceRouter",
+		"IPNVersion",
+		"FrontendLogID",
+		"BackendLogID",
+		"OS",
+		"OSVersion",
+		"Container",
+		"Env",
+		"Distro",
+		"DistroVersion",
+		"DistroCodeName",
+		"Desktop",
+		"Package",
+		"DeviceModel",
+		"Hostname",
+		"ShieldsUp",
+		"ShareeNode",
+		"GoArch",
+		"GoVersion",
+		"RoutableIPs",
+		"RequestTags",
+		"Services",
+		"NetInfo",
+		"SSH_HostKeys",
+		"Cloud",
+		"Userspace",
+		"UserspaceRouter",
 	}
 	}
 	if have := fieldsOf(reflect.TypeOf(Hostinfo{})); !reflect.DeepEqual(have, hiHandles) {
 	if have := fieldsOf(reflect.TypeOf(Hostinfo{})); !reflect.DeepEqual(have, hiHandles) {
 		t.Errorf("Hostinfo.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
 		t.Errorf("Hostinfo.Equal check might be out of sync\nfields: %q\nhandled: %q\n",

+ 23 - 13
tailcfg/tailcfg_view.go

@@ -250,19 +250,24 @@ func (v *HostinfoView) UnmarshalJSON(b []byte) error {
 	return nil
 	return nil
 }
 }
 
 
-func (v HostinfoView) IPNVersion() string    { return v.ж.IPNVersion }
-func (v HostinfoView) FrontendLogID() string { return v.ж.FrontendLogID }
-func (v HostinfoView) BackendLogID() string  { return v.ж.BackendLogID }
-func (v HostinfoView) OS() string            { return v.ж.OS }
-func (v HostinfoView) OSVersion() string     { return v.ж.OSVersion }
-func (v HostinfoView) Desktop() opt.Bool     { return v.ж.Desktop }
-func (v HostinfoView) Package() string       { return v.ж.Package }
-func (v HostinfoView) DeviceModel() string   { return v.ж.DeviceModel }
-func (v HostinfoView) Hostname() string      { return v.ж.Hostname }
-func (v HostinfoView) ShieldsUp() bool       { return v.ж.ShieldsUp }
-func (v HostinfoView) ShareeNode() bool      { return v.ж.ShareeNode }
-func (v HostinfoView) GoArch() string        { return v.ж.GoArch }
-func (v HostinfoView) GoVersion() string     { return v.ж.GoVersion }
+func (v HostinfoView) IPNVersion() string     { return v.ж.IPNVersion }
+func (v HostinfoView) FrontendLogID() string  { return v.ж.FrontendLogID }
+func (v HostinfoView) BackendLogID() string   { return v.ж.BackendLogID }
+func (v HostinfoView) OS() string             { return v.ж.OS }
+func (v HostinfoView) OSVersion() string      { return v.ж.OSVersion }
+func (v HostinfoView) Container() opt.Bool    { return v.ж.Container }
+func (v HostinfoView) Env() string            { return v.ж.Env }
+func (v HostinfoView) Distro() string         { return v.ж.Distro }
+func (v HostinfoView) DistroVersion() string  { return v.ж.DistroVersion }
+func (v HostinfoView) DistroCodeName() string { return v.ж.DistroCodeName }
+func (v HostinfoView) Desktop() opt.Bool      { return v.ж.Desktop }
+func (v HostinfoView) Package() string        { return v.ж.Package }
+func (v HostinfoView) DeviceModel() string    { return v.ж.DeviceModel }
+func (v HostinfoView) Hostname() string       { return v.ж.Hostname }
+func (v HostinfoView) ShieldsUp() bool        { return v.ж.ShieldsUp }
+func (v HostinfoView) ShareeNode() bool       { return v.ж.ShareeNode }
+func (v HostinfoView) GoArch() string         { return v.ж.GoArch }
+func (v HostinfoView) GoVersion() string      { return v.ж.GoVersion }
 func (v HostinfoView) RoutableIPs() views.IPPrefixSlice {
 func (v HostinfoView) RoutableIPs() views.IPPrefixSlice {
 	return views.IPPrefixSliceOf(v.ж.RoutableIPs)
 	return views.IPPrefixSliceOf(v.ж.RoutableIPs)
 }
 }
@@ -282,6 +287,11 @@ var _HostinfoViewNeedsRegeneration = Hostinfo(struct {
 	BackendLogID    string
 	BackendLogID    string
 	OS              string
 	OS              string
 	OSVersion       string
 	OSVersion       string
+	Container       opt.Bool
+	Env             string
+	Distro          string
+	DistroVersion   string
+	DistroCodeName  string
 	Desktop         opt.Bool
 	Desktop         opt.Bool
 	Package         string
 	Package         string
 	DeviceModel     string
 	DeviceModel     string