tailscale/posture/serialnumber_notmacos.go
Kristoffer Dalby 9eedf86563 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>
2023-10-09 13:50:34 +02:00

144 lines
3.5 KiB
Go

// 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
}