Compare commits
4 Commits
bm/4via6
...
knyar/post
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5ab63cfb9 | ||
|
|
160e3fcb51 | ||
|
|
fc9051c658 | ||
|
|
cd36d20fb1 |
@@ -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
19
cmd/ptest/ptest.go
Normal 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)
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
120
posture/screenlock_macos.go
Normal 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
82
posture/serial_linux.go
Normal 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
26
posture/serial_macos.go
Normal 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
43
posture/serial_windows.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user