mirror of https://github.com/tailscale/tailscale/
posture: add get serial support for Windows/Linux
This commit adds support for getting serial numbers from SMBIOS on Windows/Linux (and BSD) using go-smbios. Updates #5902 Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>pull/9631/head
parent
249edaa349
commit
9eedf86563
@ -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
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
@ -1,11 +1,24 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// 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
|
package posture
|
||||||
|
|
||||||
import "errors"
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"tailscale.com/types/logger"
|
||||||
|
)
|
||||||
|
|
||||||
// GetSerialNumber returns client machine serial number(s).
|
// GetSerialNumber returns client machine serial number(s).
|
||||||
func GetSerialNumbers() ([]string, error) {
|
func GetSerialNumbers(_ logger.Logf) ([]string, error) {
|
||||||
return nil, errors.New("not implemented")
|
return nil, errors.New("not implemented")
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
Loading…
Reference in New Issue