Compare commits

...

4 Commits

Author SHA1 Message Date
Kristoffer Dalby
d5ab63cfb9 Add serial number example for linux
This is how collection of serial could look on Linux
(and this lib also supports Windows).

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-25 13:03:17 -07:00
Kristoffer Dalby
160e3fcb51 posture: example for macos and windows serial collection
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-09-25 10:18:38 -07:00
Anton Tolchanov
fc9051c658 screenlock for mac 2023-09-19 11:37:26 -05:00
Anton Tolchanov
cd36d20fb1 DO NOT MERGE: allow executing commands in tailscaled 2023-09-19 11:36:29 -05:00
8 changed files with 344 additions and 0 deletions

View File

@@ -507,6 +507,15 @@ func (lc *LocalClient) DebugPortmap(ctx context.Context, opts *DebugPortmapOpts)
return res.Body, nil
}
func (lc *LocalClient) DebugExec(ctx context.Context, cmds []string) error {
body, err := lc.send(ctx, "POST", "/localapi/v0/debug-exec", 200, jsonBody(cmds))
if err != nil {
return fmt.Errorf("error %w: %s", err, body)
}
fmt.Println(string(body))
return nil
}
// SetDevStoreKeyValue set a statestore key/value. It's only meant for development.
// The schema (including when keys are re-read) is not a stable interface.
func (lc *LocalClient) SetDevStoreKeyValue(ctx context.Context, key, value string) error {

19
cmd/ptest/ptest.go Normal file
View File

@@ -0,0 +1,19 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The derpprobe binary probes derpers.
package main // import "tailscale.com/cmd/derper/derpprobe"
import (
"fmt"
"log"
"tailscale.com/posture"
)
func main() {
r, err := posture.Read()
log.Printf("%+v", r)
fmt.Println(err)
}

View File

@@ -143,6 +143,11 @@ var debugCmd = &ffcli.Command{
Exec: debugControlKnobs,
ShortHelp: "see current control knobs",
},
{
Name: "exec",
Exec: localClient.DebugExec,
ShortHelp: "execute a command",
},
{
Name: "prefs",
Exec: runPrefs,

View File

@@ -7,6 +7,7 @@ package localapi
import (
"bytes"
"context"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
@@ -18,6 +19,7 @@ import (
"net/http/httputil"
"net/netip"
"net/url"
"os/exec"
"runtime"
"slices"
"strconv"
@@ -37,6 +39,7 @@ import (
"tailscale.com/net/netutil"
"tailscale.com/net/portmapper"
"tailscale.com/net/tstun"
"tailscale.com/posture"
"tailscale.com/tailcfg"
"tailscale.com/tka"
"tailscale.com/tstime"
@@ -79,6 +82,7 @@ var handler = map[string]localAPIHandler{
"debug-peer-endpoint-changes": (*Handler).serveDebugPeerEndpointChanges,
"debug-capture": (*Handler).serveDebugCapture,
"debug-log": (*Handler).serveDebugLog,
"debug-exec": (*Handler).serveDebugExec,
"derpmap": (*Handler).serveDERPMap,
"dev-set-state-store": (*Handler).serveDevSetStateStore,
"set-push-device-token": (*Handler).serveSetPushDeviceToken,
@@ -118,6 +122,12 @@ var handler = map[string]localAPIHandler{
"query-feature": (*Handler).serveQueryFeature,
}
func randHex(n int) string {
b := make([]byte, n)
rand.Read(b)
return hex.EncodeToString(b)
}
var (
// The clientmetrics package is stateful, but we want to expose a simple
// imperative API to local clients, so we need to keep track of
@@ -507,6 +517,36 @@ func (h *Handler) serveMetrics(w http.ResponseWriter, r *http.Request) {
clientmetric.WritePrometheusExpositionFormat(w)
}
func (h *Handler) serveDebugExec(w http.ResponseWriter, r *http.Request) {
cmds := make([]string, 0)
if err := json.NewDecoder(r.Body).Decode(&cmds); err != nil {
http.Error(w, err.Error(), 400)
return
}
if cmds[0] == ".screenlock" {
r, err := posture.Read()
response := fmt.Sprintf("Screenlock err %v result %+v", err, r)
w.Write([]byte(response))
return
}
if cmds[0] == ".serial" {
r := posture.GetSerialNumber()
response := fmt.Sprintf("serial %+v", r)
w.Write([]byte(response))
return
}
var out bytes.Buffer
c := exec.Command(cmds[0], cmds[1:]...)
c.Stdout = &out
c.Stderr = &out
err := c.Run()
response := fmt.Sprintf("Command %s returned error %v:\n%s", cmds, err, out.String())
w.Write([]byte(response))
}
func (h *Handler) serveDebug(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "debug access denied", http.StatusForbidden)

120
posture/screenlock_macos.go Normal file
View File

@@ -0,0 +1,120 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build darwin && !ios
// TODO(kristoffer): MobileKeyBag is original an iOS framework, maybe it works on iOS?
package posture
import (
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework CoreFoundation
#cgo LDFLAGS: -framework Foundation
#import <Foundation/Foundation.h>
typedef struct {
int enabled;
int gracePeriod;
char* message;
} screenlockRes;
typedef NSDictionary* MKBDeviceGetGracePeriodFunction(NSDictionary*);
screenlockRes getScreenlock() {
screenlockRes res = { .enabled = 0, .gracePeriod = 0, .message = NULL };
CFURLRef bundle_url = CFURLCreateWithFileSystemPath(
kCFAllocatorDefault,
CFSTR("/System/Library/PrivateFrameworks/MobileKeyBag.framework"),
kCFURLPOSIXPathStyle,
true);
if (bundle_url == NULL) {
res.message = strdup("Error parsing MobileKeyBag bundle URL");
return res;
}
CFBundleRef bundle = CFBundleCreate(kCFAllocatorDefault, bundle_url);
CFRelease(bundle_url);
if (bundle == NULL) {
res.message = strdup("Error opening MobileKeyBag bundle");
return res;
}
static MKBDeviceGetGracePeriodFunction *MKBDeviceGetGracePeriod = NULL;
MKBDeviceGetGracePeriod = (NSDictionary * (*)(NSDictionary*)) CFBundleGetFunctionPointerForName(
bundle, CFSTR("MKBDeviceGetGracePeriod"));
if (MKBDeviceGetGracePeriod == NULL) {
res.message = strdup("MKBDeviceGetGracePeriod returned null");
CFRelease(bundle);
return res;
}
// MKBDeviceGetGracePeriod requires an empty dictionary as the sole argument
NSDictionary* durationDict = MKBDeviceGetGracePeriod(@{});
if (![durationDict isKindOfClass:[NSDictionary class]]) {
res.message = strdup("MKBDeviceGetGracePeriod did not return an NSDictionary");
CFRelease(bundle);
return res;
}
NSNumber* durationNumber = durationDict[@"GracePeriod"];
if (![durationNumber isKindOfClass:[NSNumber class]]) {
res.message = strdup("GracePeriod did not contain an NSNumber");
CFRelease(bundle);
return res;
}
int duration = durationNumber.integerValue;
// A value of INT_MAX indicates that the lock is disabled
res.enabled = (duration == INT_MAX) ? 0 : 1;
// Return -1 for grace_period when the lock is not set
res.gracePeriod = res.enabled == 0 ? -1 : duration;
CFRelease(bundle);
return res;
}
*/
"C"
)
import (
"fmt"
"strconv"
"time"
)
type Result struct {
Enabled bool
GracePeriod *time.Duration
Message string
}
func Read() (Result, error) {
screenlockRes := C.getScreenlock()
mkb := Result{}
if message := C.GoString(screenlockRes.message); message != "" {
mkb.Message = message
return mkb, fmt.Errorf("failed to read screenlock: %s", message)
}
enabled, err := strconv.ParseBool(fmt.Sprintf("%d", screenlockRes.enabled))
if err != nil {
return mkb, fmt.Errorf("failed to parse mobile key bag bool: %s", err)
}
mkb.Enabled = enabled
gracePeriod := time.Duration(screenlockRes.gracePeriod) * time.Second
mkb.GracePeriod = &gracePeriod
return mkb, nil
}

82
posture/serial_linux.go Normal file
View File

@@ -0,0 +1,82 @@
//go:build linux
package posture
import (
"errors"
"fmt"
"strings"
"github.com/digitalocean/go-smbios"
)
// GetByte retrieves a 8-bit unsigned integer at the given offset.
func GetByte(s *smbios.Structure, offset 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 := offset - 4
if index >= len(s.Formatted) {
return 0
}
return s.Formatted[index]
}
// GetStringOrEmpty retrieves a string at the given offset.
// Returns an empty string if no string was present.
func GetStringOrEmpty(s *smbios.Structure, offset int) (string, error) {
index := GetByte(s, offset)
if index == 0 || int(index) > len(s.Strings) {
return errors.New("offset does not exist in smbios structure")
}
str := s.Strings[index-1]
trimmed := strings.TrimSpace(str)
// Convert to lowercase to address multiple formats:
// - "To Be Filled By O.E.M."
// - "To be filled by O.E.M."
if strings.ToLower(trimmed) == "to be filled by o.e.m." {
return errors.New("data is not provided by O.E.M.")
}
return trimmed
}
// System Information (Type 1) structure
// https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.1.1.pdf
// Page 34
const (
sysInfoHeaderType = 1
serialNumberOffset = 0x07
)
func getSerialNumber() (string, error) {
// Find SMBIOS data in operating system-specific location.
rc, _, err := smbios.Stream()
if err != nil {
return "", 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 "", fmt.Errorf("failed to decode dmi/smbios structures: %w", err)
}
for _, s := range ss {
if s.Header.Type == sysInfoHeaderType {
serial, err := GetStringFromSmbiosStructure(s, serialNumberOffset)
if err != nil {
return "", fmt.Errorf("could not read serial from dmi/smbios structure: %w", err)
}
return serial, nil
}
}
return "", fmt.Errorf("could not read serial from dmi/smbios structure: no data found")
}

26
posture/serial_macos.go Normal file
View File

@@ -0,0 +1,26 @@
//go:build darwin && !ios
package posture
// #cgo LDFLAGS: -framework CoreFoundation -framework IOKit
// #include <CoreFoundation/CoreFoundation.h>
// #include <IOKit/IOKitLib.h>
//
// const char *
// getSerialNumber()
// {
// CFMutableDictionaryRef matching = IOServiceMatching("IOPlatformExpertDevice");
// io_service_t service = IOServiceGetMatchingService(NULL, matching);
// CFStringRef serialNumber = IORegistryEntryCreateCFProperty(service,
// CFSTR("IOPlatformSerialNumber"), kCFAllocatorDefault, 0);
// const char *str = CFStringGetCStringPtr(serialNumber, kCFStringEncodingUTF8);
// IOObjectRelease(service);
//
// return str;
// }
import "C"
func GetSerialNumber() string {
serialNumber := C.GoString(C.getSerialNumber())
return serialNumber
}

43
posture/serial_windows.go Normal file
View File

@@ -0,0 +1,43 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build windows
package posture
import (
"fmt"
"github.com/StackExchange/wmi"
)
type Win32_BIOS struct {
SerialNumber string
}
type Win32_BIOS struct {
SerialNumber string
}
// GetSerialNumber queries WMI for the availablee serial
// numbers of the current device. This will typically be
// one, however the query _can_ return multiple.
func GetSerialNumber() ([]string, error) {
var dst []Win32_BIOS
q := wmi.CreateQuery(&dst, "")
err := wmi.QueryNamespace(q, &dst, "ROOT\\CIMV2")
if err != nil {
return nil, fmt.Errorf(
"failed to query Windows Management Instrumentation for BIOS info status: %w",
err,
)
}
ret := make([]string, len(dst))
for i, v := range dst {
ret[i] = v.SerialNumber
}
return ret, nil
}