diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index d2b44836a..56036681c 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -86,6 +86,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de W 💣 github.com/dblohm7/wingoes/com/automation from tailscale.com/util/osdiag/internal/wsc W github.com/dblohm7/wingoes/internal from github.com/dblohm7/wingoes/com W 💣 github.com/dblohm7/wingoes/pe from tailscale.com/util/osdiag+ + LW 💣 github.com/digitalocean/go-smbios/smbios from tailscale.com/posture github.com/fxamacker/cbor/v2 from tailscale.com/tka W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+ W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet diff --git a/go.mod b/go.mod index a42928aaf..54c0885e2 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/creack/pty v1.1.18 github.com/dave/jennifer v1.7.0 github.com/dblohm7/wingoes v0.0.0-20230929194252-e994401fc077 + github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e github.com/dsnet/try v0.0.3 github.com/evanw/esbuild v0.19.4 github.com/frankban/quicktest v1.14.5 diff --git a/go.sum b/go.sum index 9fc86d99e..24b76105a 100644 --- a/go.sum +++ b/go.sum @@ -233,6 +233,8 @@ github.com/dblohm7/wingoes v0.0.0-20230929194252-e994401fc077 h1:WphxHslVftszsr0 github.com/dblohm7/wingoes v0.0.0-20230929194252-e994401fc077/go.mod h1:6NCrWM5jRefaG7iN0iMShPalLsljHWBh9v1zxM2f8Xs= github.com/denis-tingaikin/go-header v0.4.3 h1:tEaZKAlqql6SKCY++utLmkPLd6K8IBM20Ha7UVm+mtU= github.com/denis-tingaikin/go-header v0.4.3/go.mod h1:0wOCWuN71D5qIgE2nz9KrKmuYBAC2Mra5RassOIQ2/c= +github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q= +github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A= github.com/docker/cli v24.0.6+incompatible h1:fF+XCQCgJjjQNIMjzaSmiKJSCcfcXb3TWTcc7GAneOY= github.com/docker/cli v24.0.6+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= diff --git a/ipn/ipnlocal/c2n.go b/ipn/ipnlocal/c2n.go index 135ddc8c3..23853169d 100644 --- a/ipn/ipnlocal/c2n.go +++ b/ipn/ipnlocal/c2n.go @@ -232,7 +232,7 @@ func (b *LocalBackend) handleC2NPostureIdentityGet(w http.ResponseWriter, r *htt // TODO(kradalby): Use syspolicy + envknob to allow Win registry, // macOS defaults and env to override this setting. if b.Prefs().PostureChecking() { - sns, err := posture.GetSerialNumbers() + sns, err := posture.GetSerialNumbers(b.logf) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/posture/serialnumber_notmacos.go b/posture/serialnumber_notmacos.go new file mode 100644 index 000000000..2e03fac4c --- /dev/null +++ b/posture/serialnumber_notmacos.go @@ -0,0 +1,143 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +// Build on Windows, Linux and *BSD + +//go:build windows || (linux && !android) || freebsd || openbsd || dragonfly || netbsd + +package posture + +import ( + "errors" + "fmt" + "strings" + + "github.com/digitalocean/go-smbios/smbios" + "tailscale.com/types/logger" + "tailscale.com/util/multierr" +) + +// getByteFromSmbiosStructure retrieves a 8-bit unsigned integer at the given specOffset. +func getByteFromSmbiosStructure(s *smbios.Structure, specOffset int) uint8 { + // the `Formatted` byte slice is missing the first 4 bytes of the structure that are stripped out as header info. + // so we need to subtract 4 from the offset mentioned in the SMBIOS documentation to get the right value. + index := specOffset - 4 + if index >= len(s.Formatted) || index < 0 { + return 0 + } + + return s.Formatted[index] +} + +// getStringFromSmbiosStructure retrieves a string at the given specOffset. +// Returns an empty string if no string was present. +func getStringFromSmbiosStructure(s *smbios.Structure, specOffset int) (string, error) { + index := getByteFromSmbiosStructure(s, specOffset) + + if index == 0 || int(index) > len(s.Strings) { + return "", errors.New("specified offset does not exist in smbios structure") + } + + str := s.Strings[index-1] + trimmed := strings.TrimSpace(str) + + return trimmed, nil +} + +// Product Table (Type 1) structure +// https://web.archive.org/web/20220126173219/https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.1.1.pdf +// Page 34 and onwards. +const ( + // Serial is present at the same offset in all IDs + serialNumberOffset = 0x07 + + productID = 1 + baseboardID = 2 + chassisID = 3 +) + +var ( + idToTableName = map[int]string{ + 1: "product", + 2: "baseboard", + 3: "chassis", + } + validTables []string + numOfTables int +) + +func init() { + for _, table := range idToTableName { + validTables = append(validTables, table) + } + numOfTables = len(validTables) + +} + +// serialFromSmbiosStructure extracts a serial number from a product, +// baseboard or chassis SMBIOS table. +func serialFromSmbiosStructure(s *smbios.Structure) (string, error) { + id := s.Header.Type + if (id != productID) && (id != baseboardID) && (id != chassisID) { + return "", fmt.Errorf( + "cannot get serial table type %d, supported tables are %v", + id, + validTables, + ) + } + + serial, err := getStringFromSmbiosStructure(s, serialNumberOffset) + if err != nil { + return "", fmt.Errorf( + "failed to get serial from %s table: %w", + idToTableName[int(s.Header.Type)], + err, + ) + } + + return serial, nil +} + +func GetSerialNumbers(logf logger.Logf) ([]string, error) { + // Find SMBIOS data in operating system-specific location. + rc, _, err := smbios.Stream() + if err != nil { + return nil, fmt.Errorf("failed to open dmi/smbios stream: %w", err) + } + defer rc.Close() + + // Decode SMBIOS structures from the stream. + d := smbios.NewDecoder(rc) + ss, err := d.Decode() + if err != nil { + return nil, fmt.Errorf("failed to decode dmi/smbios structures: %w", err) + } + + serials := make([]string, 0, numOfTables) + errs := make([]error, 0, numOfTables) + + for _, s := range ss { + switch s.Header.Type { + case productID, baseboardID, chassisID: + serial, err := serialFromSmbiosStructure(s) + if err != nil { + errs = append(errs, err) + continue + } + + serials = append(serials, serial) + } + } + + err = multierr.New(errs...) + + // if there were no serial numbers, check if any errors were + // returned and combine them. + if len(serials) == 0 && err != nil { + return nil, err + } + + logf("got serial numbers %v (errors: %s)", serials, err) + + return serials, nil +} diff --git a/posture/serialnumber_notmacos_test.go b/posture/serialnumber_notmacos_test.go new file mode 100644 index 000000000..f2a15e037 --- /dev/null +++ b/posture/serialnumber_notmacos_test.go @@ -0,0 +1,38 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +// Build on Windows, Linux and *BSD + +//go:build windows || (linux && !android) || freebsd || openbsd || dragonfly || netbsd + +package posture + +import ( + "fmt" + "testing" + + "tailscale.com/types/logger" +) + +func TestGetSerialNumberNotMac(t *testing.T) { + // This test is intentionally skipped as it will + // require root on Linux to get access to the serials. + // The test case is intended for local testing. + // Comment out skip for local testing. + t.Skip() + + sns, err := GetSerialNumbers(logger.Discard) + if err != nil { + t.Fatalf("failed to get serial number: %s", err) + } + + if len(sns) == 0 { + t.Fatalf("expected at least one serial number, got %v", sns) + } + + if len(sns[0]) <= 0 { + t.Errorf("expected a serial number with more than zero characters, got %s", sns[0]) + } + + fmt.Printf("serials: %v\n", sns) +} diff --git a/posture/serialnumber_stub.go b/posture/serialnumber_stub.go index 56144b00d..6fa3d4a1e 100644 --- a/posture/serialnumber_stub.go +++ b/posture/serialnumber_stub.go @@ -1,11 +1,24 @@ // Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause +// darwin: not implemented +// andoird: not implemented +// js: not implemented +// plan9: not implemented +// solaris: currently unsupported by go-smbios: +// https://github.com/digitalocean/go-smbios/pull/21 + +//go:build darwin || android || js || plan9 || solaris + package posture -import "errors" +import ( + "errors" + + "tailscale.com/types/logger" +) // GetSerialNumber returns client machine serial number(s). -func GetSerialNumbers() ([]string, error) { +func GetSerialNumbers(_ logger.Logf) ([]string, error) { return nil, errors.New("not implemented") } diff --git a/posture/serialnumber_test.go b/posture/serialnumber_test.go new file mode 100644 index 000000000..fac4392fa --- /dev/null +++ b/posture/serialnumber_test.go @@ -0,0 +1,16 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package posture + +import ( + "testing" + + "tailscale.com/types/logger" +) + +func TestGetSerialNumber(t *testing.T) { + // ensure GetSerialNumbers is implemented + // or covered by a stub on a given platform. + _, _ = GetSerialNumbers(logger.Discard) +}