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>main
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
|
||||
// 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") |
||||
} |
||||
|
||||
@ -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