summaryrefslogtreecommitdiffhomepage
path: root/feature
diff options
context:
space:
mode:
Diffstat (limited to 'feature')
-rw-r--r--feature/condregister/maybe_tpm.go8
-rw-r--r--feature/taildrop/integration_test.go170
-rw-r--r--feature/taildrop/localapi.go18
-rw-r--r--feature/tpm/tpm.go83
-rw-r--r--feature/tpm/tpm_linux.go18
-rw-r--r--feature/tpm/tpm_other.go12
-rw-r--r--feature/tpm/tpm_test.go19
-rw-r--r--feature/tpm/tpm_windows.go18
8 files changed, 341 insertions, 5 deletions
diff --git a/feature/condregister/maybe_tpm.go b/feature/condregister/maybe_tpm.go
new file mode 100644
index 000000000..caa57fef1
--- /dev/null
+++ b/feature/condregister/maybe_tpm.go
@@ -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"
diff --git a/feature/taildrop/integration_test.go b/feature/taildrop/integration_test.go
new file mode 100644
index 000000000..46768bb31
--- /dev/null
+++ b/feature/taildrop/integration_test.go
@@ -0,0 +1,170 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package taildrop_test
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "testing"
+ "time"
+
+ "tailscale.com/client/local"
+ "tailscale.com/client/tailscale/apitype"
+ "tailscale.com/tailcfg"
+ "tailscale.com/tstest"
+ "tailscale.com/tstest/integration"
+ "tailscale.com/tstest/integration/testcontrol"
+)
+
+// TODO(bradfitz): add test where control doesn't send tailcfg.CapabilityFileSharing
+// and verify that we get the "file sharing not enabled by Tailscale admin" error.
+
+// TODO(bradfitz): add test between different users with the peercap to permit that?
+
+func TestTaildropIntegration(t *testing.T) {
+ tstest.Parallel(t)
+ controlOpt := integration.ConfigureControl(func(s *testcontrol.Server) {
+ s.AllNodesSameUser = true // required for Taildrop
+ })
+ env := integration.NewTestEnv(t, controlOpt)
+
+ // Create two nodes:
+ n1 := integration.NewTestNode(t, env)
+ d1 := n1.StartDaemon()
+
+ n2 := integration.NewTestNode(t, env)
+ d2 := n2.StartDaemon()
+
+ n1.AwaitListening()
+ t.Logf("n1 is listening")
+ n2.AwaitListening()
+ t.Logf("n2 is listening")
+ n1.MustUp()
+ t.Logf("n1 is up")
+ n2.MustUp()
+ t.Logf("n2 is up")
+ n1.AwaitRunning()
+ t.Logf("n1 is running")
+ n2.AwaitRunning()
+ t.Logf("n2 is running")
+
+ var peerStableID tailcfg.StableNodeID
+
+ if err := tstest.WaitFor(5*time.Second, func() error {
+ st := n1.MustStatus()
+ if len(st.Peer) == 0 {
+ return errors.New("no peers")
+ }
+ if len(st.Peer) > 1 {
+ return fmt.Errorf("got %d peers; want 1", len(st.Peer))
+ }
+ peer := st.Peer[st.Peers()[0]]
+ peerStableID = peer.ID
+ if peer.ID == st.Self.ID {
+ return errors.New("peer is self")
+ }
+
+ if len(st.TailscaleIPs) == 0 {
+ return errors.New("no Tailscale IPs")
+ }
+
+ return nil
+ }); err != nil {
+ t.Fatal(err)
+ }
+
+ const timeout = 30 * time.Second
+ ctx, cancel := context.WithTimeout(context.Background(), timeout)
+ defer cancel()
+
+ c1 := n1.LocalClient()
+ c2 := n2.LocalClient()
+
+ wantNoWaitingFiles := func(c *local.Client) {
+ t.Helper()
+ files, err := c.WaitingFiles(ctx)
+ if err != nil {
+ t.Fatalf("WaitingFiles: %v", err)
+ }
+ if len(files) != 0 {
+ t.Fatalf("WaitingFiles: got %d files; want 0", len(files))
+ }
+ }
+
+ // Verify c2 has no files.
+ wantNoWaitingFiles(c2)
+
+ gotFile := make(chan bool, 1)
+ go func() {
+ v, err := c2.AwaitWaitingFiles(t.Context(), timeout)
+ if err != nil {
+ return
+ }
+ if len(v) != 0 {
+ gotFile <- true
+ }
+ }()
+
+ fileContents := []byte("hello world this is a file")
+
+ n2ID := n2.MustStatus().Self.ID
+ t.Logf("n2 self.ID = %q; n1's peer[0].ID = %q", n2ID, peerStableID)
+ t.Logf("Doing PushFile ...")
+ err := c1.PushFile(ctx, n2.MustStatus().Self.ID, int64(len(fileContents)), "test.txt", bytes.NewReader(fileContents))
+ if err != nil {
+ t.Fatalf("PushFile from n1->n2: %v", err)
+ }
+ t.Logf("PushFile done")
+
+ select {
+ case <-gotFile:
+ t.Logf("n2 saw AwaitWaitingFiles wake up")
+ case <-ctx.Done():
+ t.Fatalf("n2 timeout waiting for AwaitWaitingFiles")
+ }
+
+ files, err := c2.WaitingFiles(ctx)
+ if err != nil {
+ t.Fatalf("c2.WaitingFiles: %v", err)
+ }
+ if len(files) != 1 {
+ t.Fatalf("c2.WaitingFiles: got %d files; want 1", len(files))
+ }
+ got := files[0]
+ want := apitype.WaitingFile{
+ Name: "test.txt",
+ Size: int64(len(fileContents)),
+ }
+ if got != want {
+ t.Fatalf("c2.WaitingFiles: got %+v; want %+v", got, want)
+ }
+
+ // Download the file.
+ rc, size, err := c2.GetWaitingFile(ctx, got.Name)
+ if err != nil {
+ t.Fatalf("c2.GetWaitingFile: %v", err)
+ }
+ if size != int64(len(fileContents)) {
+ t.Fatalf("c2.GetWaitingFile: got size %d; want %d", size, len(fileContents))
+ }
+ gotBytes, err := io.ReadAll(rc)
+ if err != nil {
+ t.Fatalf("c2.GetWaitingFile: %v", err)
+ }
+ if !bytes.Equal(gotBytes, fileContents) {
+ t.Fatalf("c2.GetWaitingFile: got %q; want %q", gotBytes, fileContents)
+ }
+
+ // Now delete it.
+ if err := c2.DeleteWaitingFile(ctx, got.Name); err != nil {
+ t.Fatalf("c2.DeleteWaitingFile: %v", err)
+ }
+ wantNoWaitingFiles(c2)
+
+ d1.MustCleanShutdown(t)
+ d2.MustCleanShutdown(t)
+}
diff --git a/feature/taildrop/localapi.go b/feature/taildrop/localapi.go
index ce812514e..067a51f91 100644
--- a/feature/taildrop/localapi.go
+++ b/feature/taildrop/localapi.go
@@ -365,6 +365,7 @@ func serveFiles(h *localapi.Handler, w http.ResponseWriter, r *http.Request) {
return
}
ctx := r.Context()
+ var wfs []apitype.WaitingFile
if s := r.FormValue("waitsec"); s != "" && s != "0" {
d, err := strconv.Atoi(s)
if err != nil {
@@ -375,11 +376,18 @@ func serveFiles(h *localapi.Handler, w http.ResponseWriter, r *http.Request) {
var cancel context.CancelFunc
ctx, cancel = context.WithDeadline(ctx, deadline)
defer cancel()
- }
- wfs, err := lb.AwaitWaitingFiles(ctx)
- if err != nil && ctx.Err() == nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
+ wfs, err = lb.AwaitWaitingFiles(ctx)
+ if err != nil && ctx.Err() == nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ } else {
+ var err error
+ wfs, err = lb.WaitingFiles()
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(wfs)
diff --git a/feature/tpm/tpm.go b/feature/tpm/tpm.go
new file mode 100644
index 000000000..18e56ae89
--- /dev/null
+++ b/feature/tpm/tpm.go
@@ -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 > '~' }))
+}
diff --git a/feature/tpm/tpm_linux.go b/feature/tpm/tpm_linux.go
new file mode 100644
index 000000000..a90c0e153
--- /dev/null
+++ b/feature/tpm/tpm_linux.go
@@ -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)
+}
diff --git a/feature/tpm/tpm_other.go b/feature/tpm/tpm_other.go
new file mode 100644
index 000000000..ba7c67621
--- /dev/null
+++ b/feature/tpm/tpm_other.go
@@ -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
+}
diff --git a/feature/tpm/tpm_test.go b/feature/tpm/tpm_test.go
new file mode 100644
index 000000000..fc0fc178c
--- /dev/null
+++ b/feature/tpm/tpm_test.go
@@ -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)
+ }
+ }
+}
diff --git a/feature/tpm/tpm_windows.go b/feature/tpm/tpm_windows.go
new file mode 100644
index 000000000..578d687af
--- /dev/null
+++ b/feature/tpm/tpm_windows.go
@@ -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)
+}