diff --git a/posture/serialnumber_macos.go b/posture/serialnumber_macos.go new file mode 100644 index 000000000..48355d313 --- /dev/null +++ b/posture/serialnumber_macos.go @@ -0,0 +1,74 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +//go:build cgo && darwin && !ios + +package posture + +// #cgo LDFLAGS: -framework CoreFoundation -framework IOKit +// #include +// #include +// +// #if __MAC_OS_X_VERSION_MIN_REQUIRED < 120000 +// #define kIOMainPortDefault kIOMasterPortDefault +// #endif +// +// const char * +// getSerialNumber() +// { +// CFMutableDictionaryRef matching = IOServiceMatching("IOPlatformExpertDevice"); +// if (!matching) { +// return "err: failed to create dictionary to match IOServices"; +// } +// +// io_service_t service = IOServiceGetMatchingService(kIOMainPortDefault, matching); +// if (!service) { +// return "err: failed to look up registered IOService objects that match a matching dictionary"; +// } +// +// CFStringRef serialNumberRef = IORegistryEntryCreateCFProperty( +// service, +// CFSTR("IOPlatformSerialNumber"), +// kCFAllocatorDefault, +// 0 +// ); +// if (!serialNumberRef) { +// return "err: failed to look up serial number in IORegistry"; +// } +// +// CFIndex length = CFStringGetLength(serialNumberRef); +// CFIndex max_size = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1; +// char *serialNumberBuf = (char *)malloc(max_size); +// +// bool result = CFStringGetCString(serialNumberRef, serialNumberBuf, max_size, kCFStringEncodingUTF8); +// +// CFRelease(serialNumberRef); +// IOObjectRelease(service); +// +// if (!result) { +// free(serialNumberBuf); +// +// return "err: failed to convert serial number reference to string"; +// } +// +// return serialNumberBuf; +// } +import "C" +import ( + "fmt" + "strings" + + "tailscale.com/types/logger" +) + +// GetSerialNumber returns the platform serial sumber as reported by IOKit. +func GetSerialNumbers(_ logger.Logf) ([]string, error) { + csn := C.getSerialNumber() + serialNumber := C.GoString(csn) + + if err, ok := strings.CutPrefix(serialNumber, "err: "); ok { + return nil, fmt.Errorf("failed to get serial number from IOKit: %s", err) + } + + return []string{serialNumber}, nil +} diff --git a/posture/serialnumber_macos_test.go b/posture/serialnumber_macos_test.go new file mode 100644 index 000000000..4ca22edf3 --- /dev/null +++ b/posture/serialnumber_macos_test.go @@ -0,0 +1,37 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +//go:build cgo && darwin && !ios + +package posture + +import ( + "fmt" + "testing" + + "tailscale.com/types/logger" + "tailscale.com/util/cibuild" +) + +func TestGetSerialNumberMac(t *testing.T) { + // Do not run this test on CI, it can only be ran on macOS + // and we currenty only use Linux runners. + if cibuild.On() { + t.Skip() + } + + sns, err := GetSerialNumbers(logger.Discard) + if err != nil { + t.Fatalf("failed to get serial number: %s", err) + } + + if len(sns) != 1 { + t.Errorf("expected list of 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 6fa3d4a1e..5ad12677e 100644 --- a/posture/serialnumber_stub.go +++ b/posture/serialnumber_stub.go @@ -1,14 +1,14 @@ // Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause -// darwin: not implemented -// andoird: not implemented +// ios: Apple does not allow getting serials on iOS +// android: 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 +//go:build ios || android || solaris || plan9 || js || wasm || (darwin && !cgo) package posture