hostinfo,tailcfg: report TPM availability on windows/linux (#15831)

Start collecting fleet data on TPM availability via hostinfo.

Updates #15830

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
This commit is contained in:
Andrew Lytvynov 2025-05-02 11:01:13 -07:00 committed by GitHub
parent f05347a5bf
commit 3105ecd958
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 216 additions and 2 deletions

View File

@ -41,7 +41,7 @@ while [ "$#" -gt 1 ]; do
fi
shift
ldflags="$ldflags -w -s"
tags="${tags:+$tags,}ts_omit_aws,ts_omit_bird,ts_omit_tap,ts_omit_kube,ts_omit_completion,ts_omit_ssh,ts_omit_wakeonlan,ts_omit_capture,ts_omit_relayserver,ts_omit_taildrop"
tags="${tags:+$tags,}ts_omit_aws,ts_omit_bird,ts_omit_tap,ts_omit_kube,ts_omit_completion,ts_omit_ssh,ts_omit_wakeonlan,ts_omit_capture,ts_omit_relayserver,ts_omit_taildrop,ts_omit_tpm"
;;
--box)
if [ ! -z "${TAGS:-}" ]; then

View File

@ -135,6 +135,13 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
github.com/google/go-cmp/cmp/internal/flags from github.com/google/go-cmp/cmp+
github.com/google/go-cmp/cmp/internal/function from github.com/google/go-cmp/cmp
💣 github.com/google/go-cmp/cmp/internal/value from github.com/google/go-cmp/cmp
github.com/google/go-tpm/legacy/tpm2 from github.com/google/go-tpm/tpm2/transport+
github.com/google/go-tpm/tpm2 from tailscale.com/feature/tpm
github.com/google/go-tpm/tpm2/transport from github.com/google/go-tpm/tpm2/transport/linuxtpm+
L github.com/google/go-tpm/tpm2/transport/linuxtpm from tailscale.com/feature/tpm
W github.com/google/go-tpm/tpm2/transport/windowstpm from tailscale.com/feature/tpm
github.com/google/go-tpm/tpmutil from github.com/google/go-tpm/legacy/tpm2+
W 💣 github.com/google/go-tpm/tpmutil/tbs from github.com/google/go-tpm/legacy/tpm2+
github.com/google/gofuzz from k8s.io/apimachinery/pkg/apis/meta/v1+
github.com/google/gofuzz/bytesource from github.com/google/gofuzz
L github.com/google/nftables from tailscale.com/util/linuxfw
@ -813,6 +820,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
tailscale.com/feature/relayserver from tailscale.com/feature/condregister
tailscale.com/feature/taildrop from tailscale.com/feature/condregister
L tailscale.com/feature/tap from tailscale.com/feature/condregister
tailscale.com/feature/tpm from tailscale.com/feature/condregister
tailscale.com/feature/wakeonlan from tailscale.com/feature/condregister
tailscale.com/health from tailscale.com/control/controlclient+
tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal

View File

@ -109,6 +109,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns+
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/header+
github.com/google/go-tpm/legacy/tpm2 from github.com/google/go-tpm/tpm2/transport+
github.com/google/go-tpm/tpm2 from tailscale.com/feature/tpm
github.com/google/go-tpm/tpm2/transport from github.com/google/go-tpm/tpm2/transport/linuxtpm+
L github.com/google/go-tpm/tpm2/transport/linuxtpm from tailscale.com/feature/tpm
W github.com/google/go-tpm/tpm2/transport/windowstpm from tailscale.com/feature/tpm
github.com/google/go-tpm/tpmutil from github.com/google/go-tpm/legacy/tpm2+
W 💣 github.com/google/go-tpm/tpmutil/tbs from github.com/google/go-tpm/legacy/tpm2+
L github.com/google/nftables from tailscale.com/util/linuxfw
L 💣 github.com/google/nftables/alignedbuff from github.com/google/nftables/xt
L 💣 github.com/google/nftables/binaryutil from github.com/google/nftables+
@ -271,6 +278,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/feature/relayserver from tailscale.com/feature/condregister
tailscale.com/feature/taildrop from tailscale.com/feature/condregister
L tailscale.com/feature/tap from tailscale.com/feature/condregister
tailscale.com/feature/tpm from tailscale.com/feature/condregister
tailscale.com/feature/wakeonlan from tailscale.com/feature/condregister
tailscale.com/health from tailscale.com/control/controlclient+
tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal

View File

@ -0,0 +1,8 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ios && !ts_omit_tpm
package condregister
import _ "tailscale.com/feature/tpm"

83
feature/tpm/tpm.go Normal file
View File

@ -0,0 +1,83 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package tpm implements support for TPM 2.0 devices.
package tpm
import (
"slices"
"sync"
"github.com/google/go-tpm/tpm2"
"github.com/google/go-tpm/tpm2/transport"
"tailscale.com/feature"
"tailscale.com/hostinfo"
"tailscale.com/tailcfg"
)
var infoOnce = sync.OnceValue(info)
func init() {
feature.Register("tpm")
hostinfo.RegisterHostinfoNewHook(func(hi *tailcfg.Hostinfo) {
hi.TPM = infoOnce()
})
}
//lint:ignore U1000 used in Linux and Windows builds only
func infoFromCapabilities(tpm transport.TPM) *tailcfg.TPMInfo {
info := new(tailcfg.TPMInfo)
toStr := func(s *string) func(*tailcfg.TPMInfo, uint32) {
return func(info *tailcfg.TPMInfo, value uint32) {
*s += propToString(value)
}
}
for _, cap := range []struct {
prop tpm2.TPMPT
apply func(info *tailcfg.TPMInfo, value uint32)
}{
{tpm2.TPMPTManufacturer, toStr(&info.Manufacturer)},
{tpm2.TPMPTVendorString1, toStr(&info.Vendor)},
{tpm2.TPMPTVendorString2, toStr(&info.Vendor)},
{tpm2.TPMPTVendorString3, toStr(&info.Vendor)},
{tpm2.TPMPTVendorString4, toStr(&info.Vendor)},
{tpm2.TPMPTRevision, func(info *tailcfg.TPMInfo, value uint32) { info.SpecRevision = int(value) }},
{tpm2.TPMPTVendorTPMType, func(info *tailcfg.TPMInfo, value uint32) { info.Model = int(value) }},
{tpm2.TPMPTFirmwareVersion1, func(info *tailcfg.TPMInfo, value uint32) { info.FirmwareVersion += uint64(value) << 32 }},
{tpm2.TPMPTFirmwareVersion2, func(info *tailcfg.TPMInfo, value uint32) { info.FirmwareVersion += uint64(value) }},
} {
resp, err := tpm2.GetCapability{
Capability: tpm2.TPMCapTPMProperties,
Property: uint32(cap.prop),
PropertyCount: 1,
}.Execute(tpm)
if err != nil {
continue
}
props, err := resp.CapabilityData.Data.TPMProperties()
if err != nil {
continue
}
if len(props.TPMProperty) == 0 {
continue
}
cap.apply(info, props.TPMProperty[0].Value)
}
return info
}
// propToString converts TPM_PT property value, which is a uint32, into a
// string of up to 4 ASCII characters. This encoding applies only to some
// properties, see
// https://trustedcomputinggroup.org/resource/tpm-library-specification/ Part
// 2, section 6.13.
func propToString(v uint32) string {
chars := []byte{
byte(v >> 24),
byte(v >> 16),
byte(v >> 8),
byte(v),
}
// Delete any non-printable ASCII characters.
return string(slices.DeleteFunc(chars, func(b byte) bool { return b < ' ' || b > '~' }))
}

18
feature/tpm/tpm_linux.go Normal file
View File

@ -0,0 +1,18 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package tpm
import (
"github.com/google/go-tpm/tpm2/transport/linuxtpm"
"tailscale.com/tailcfg"
)
func info() *tailcfg.TPMInfo {
t, err := linuxtpm.Open("/dev/tpm0")
if err != nil {
return nil
}
defer t.Close()
return infoFromCapabilities(t)
}

12
feature/tpm/tpm_other.go Normal file
View File

@ -0,0 +1,12 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !linux && !windows
package tpm
import "tailscale.com/tailcfg"
func info() *tailcfg.TPMInfo {
return nil
}

19
feature/tpm/tpm_test.go Normal file
View File

@ -0,0 +1,19 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package tpm
import "testing"
func TestPropToString(t *testing.T) {
for prop, want := range map[uint32]string{
0: "",
0x4D534654: "MSFT",
0x414D4400: "AMD",
0x414D440D: "AMD",
} {
if got := propToString(prop); got != want {
t.Errorf("propToString(0x%x): got %q, want %q", prop, got, want)
}
}
}

View File

@ -0,0 +1,18 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package tpm
import (
"github.com/google/go-tpm/tpm2/transport/windowstpm"
"tailscale.com/tailcfg"
)
func info() *tailcfg.TPMInfo {
t, err := windowstpm.Open()
if err != nil {
return nil
}
defer t.Close()
return infoFromCapabilities(t)
}

1
go.mod
View File

@ -44,6 +44,7 @@ require (
github.com/golangci/golangci-lint v1.57.1
github.com/google/go-cmp v0.6.0
github.com/google/go-containerregistry v0.20.2
github.com/google/go-tpm v0.9.4
github.com/google/gopacket v1.1.19
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806
github.com/google/uuid v1.6.0

4
go.sum
View File

@ -486,6 +486,10 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo=
github.com/google/go-containerregistry v0.20.2/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8=
github.com/google/go-tpm v0.9.4 h1:awZRf9FwOeTunQmHoDYSHJps3ie6f1UlhS1fOdPEt1I=
github.com/google/go-tpm v0.9.4/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba h1:qJEJcuLzH5KDR0gKc0zcktin6KSAwL7+jWKBYceddTc=
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba/go.mod h1:EFYHy8/1y2KfgTAsx7Luu7NGhoxtuVHnNo8jE7FikKc=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=

View File

@ -875,10 +875,37 @@ type Hostinfo struct {
// explicitly declared by a node.
Location *Location `json:",omitempty"`
TPM *TPMInfo `json:",omitempty"` // TPM device metadata, if available
// NOTE: any new fields containing pointers in this type
// require changes to Hostinfo.Equal.
}
// TPMInfo contains information about a TPM 2.0 device present on a node.
// All fields are read from TPM_CAP_TPM_PROPERTIES, see Part 2, section 6.13 of
// https://trustedcomputinggroup.org/resource/tpm-library-specification/.
type TPMInfo struct {
// Manufacturer is a 4-letter code from section 4.1 of
// https://trustedcomputinggroup.org/resource/vendor-id-registry/,
// for example "MSFT" for Microsoft.
// Read from TPM_PT_MANUFACTURER.
Manufacturer string `json:",omitempty"`
// Vendor is a vendor ID string, up to 16 characters.
// Read from TPM_PT_VENDOR_STRING_*.
Vendor string `json:",omitempty"`
// Model is a vendor-defined TPM model.
// Read from TPM_PT_VENDOR_TPM_TYPE.
Model int `json:",omitempty"`
// FirmwareVersion is the version number of the firmware.
// Read from TPM_PT_FIRMWARE_VERSION_*.
FirmwareVersion uint64 `json:",omitempty"`
// SpecRevision is the TPM 2.0 spec revision encoded as a single number. All
// revisions can be found at
// https://trustedcomputinggroup.org/resource/tpm-library-specification/.
// Before revision 184, TCG used the "01.83" format for revision 183.
SpecRevision int `json:",omitempty"`
}
// ServiceName is the name of a service, of the form `svc:dns-label`. Services
// represent some kind of application provided for users of the tailnet with a
// MagicDNS name and possibly dedicated IP addresses. Currently (2024-01-21),

View File

@ -141,6 +141,9 @@ func (src *Hostinfo) Clone() *Hostinfo {
if dst.Location != nil {
dst.Location = ptr.To(*src.Location)
}
if dst.TPM != nil {
dst.TPM = ptr.To(*src.TPM)
}
return dst
}
@ -184,6 +187,7 @@ var _HostinfoCloneNeedsRegeneration = Hostinfo(struct {
AppConnector opt.Bool
ServicesHash string
Location *Location
TPM *TPMInfo
}{})
// Clone makes a deep copy of NetInfo.

View File

@ -68,6 +68,7 @@ func TestHostinfoEqual(t *testing.T) {
"AppConnector",
"ServicesHash",
"Location",
"TPM",
}
if have := fieldsOf(reflect.TypeFor[Hostinfo]()); !reflect.DeepEqual(have, hiHandles) {
t.Errorf("Hostinfo.Equal check might be out of sync\nfields: %q\nhandled: %q\n",

View File

@ -301,7 +301,9 @@ func (v HostinfoView) UserspaceRouter() opt.Bool { return v.ж.User
func (v HostinfoView) AppConnector() opt.Bool { return v.ж.AppConnector }
func (v HostinfoView) ServicesHash() string { return v.ж.ServicesHash }
func (v HostinfoView) Location() LocationView { return v.ж.Location.View() }
func (v HostinfoView) Equal(v2 HostinfoView) bool { return v.ж.Equal(v2.ж) }
func (v HostinfoView) TPM() views.ValuePointer[TPMInfo] { return views.ValuePointerOf(v.ж.TPM) }
func (v HostinfoView) Equal(v2 HostinfoView) bool { return v.ж.Equal(v2.ж) }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _HostinfoViewNeedsRegeneration = Hostinfo(struct {
@ -343,6 +345,7 @@ var _HostinfoViewNeedsRegeneration = Hostinfo(struct {
AppConnector opt.Bool
ServicesHash string
Location *Location
TPM *TPMInfo
}{})
// View returns a read-only view of NetInfo.