From 2aa8299c375b7f105e9e599501c4607b6dc22421 Mon Sep 17 00:00:00 2001 From: Aaron Klotz Date: Tue, 9 May 2023 15:51:20 -0600 Subject: [PATCH] cmd/tailscaled, util/winutil: log our registry keys during tailscaled startup In order to improve our ability to understand the state of policies and registry settings when troubleshooting, we enumerate all values in all subkeys. x/sys/windows does not already offer this, so we need to call RegEnumValue directly. For now we're just logging this during startup, however in a future PR I plan to also trigger this code during a bugreport. I also want to log more than just registry. Fixes #8141 Signed-off-by: Aaron Klotz --- cmd/tailscaled/tailscaled_windows.go | 4 + util/winutil/mksyscall.go | 2 + util/winutil/winutil_windows.go | 168 +++++++++++++++++++++++++++ util/winutil/winutil_windows_test.go | 120 +++++++++++++++++++ util/winutil/zsyscall_windows.go | 10 ++ 5 files changed, 304 insertions(+) diff --git a/cmd/tailscaled/tailscaled_windows.go b/cmd/tailscaled/tailscaled_windows.go index ae538bed6..d40622b37 100644 --- a/cmd/tailscaled/tailscaled_windows.go +++ b/cmd/tailscaled/tailscaled_windows.go @@ -126,6 +126,10 @@ var syslogf logger.Logf = logger.Discard // At this point we're still the parent process that // Windows started. func runWindowsService(pol *logpolicy.Policy) error { + go func() { + winutil.LogSupportInfo(log.Printf) + }() + if winutil.GetPolicyInteger("LogSCMInteractions", 0) != 0 { syslog, err := eventlog.Open(serviceName) if err == nil { diff --git a/util/winutil/mksyscall.go b/util/winutil/mksyscall.go index e727431ac..f54c3273d 100644 --- a/util/winutil/mksyscall.go +++ b/util/winutil/mksyscall.go @@ -4,5 +4,7 @@ package winutil //go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go mksyscall.go +//go:generate go run golang.org/x/tools/cmd/goimports -w zsyscall_windows.go //sys queryServiceConfig2(hService windows.Handle, infoLevel uint32, buf *byte, bufLen uint32, bytesNeeded *uint32) (err error) [failretval==0] = advapi32.QueryServiceConfig2W +//sys regEnumValue(key registry.Key, index uint32, valueName *uint16, valueNameLen *uint32, reserved *uint32, valueType *uint32, pData *byte, cbData *uint32) (ret error) [failretval!=0] = advapi32.RegEnumValueW diff --git a/util/winutil/winutil_windows.go b/util/winutil/winutil_windows.go index 89fc543db..1b2eff00f 100644 --- a/util/winutil/winutil_windows.go +++ b/util/winutil/winutil_windows.go @@ -4,8 +4,11 @@ package winutil import ( + "encoding/binary" + "encoding/json" "errors" "fmt" + "io" "log" "os/exec" "os/user" @@ -13,10 +16,12 @@ import ( "strings" "syscall" "time" + "unicode/utf16" "unsafe" "golang.org/x/sys/windows" "golang.org/x/sys/windows/registry" + "tailscale.com/types/logger" ) const ( @@ -551,3 +556,166 @@ func findHomeDirInRegistry(uid string) (dir string, err error) { } return dir, nil } + +const ( + maxBinaryValueLen = 128 // we'll truncate any binary values longer than this + maxRegValueNameLen = 16384 // maximum length supported by Windows + 1 + initialValueBufLen = 80 // large enough to contain a stringified GUID encoded as UTF-16 +) + +const ( + supportInfoKeyRegistry = "Registry" +) + +// LogSupportInfo obtains information useful for troubleshooting and support, +// and writes it to the log as a JSON-encoded object. +func LogSupportInfo(logf logger.Logf) { + var b strings.Builder + if err := getSupportInfo(&b); err != nil { + log.Printf("error encoding support info: %v", err) + return + } + logf("Support Info: %s", b.String()) +} + +func getSupportInfo(w io.Writer) error { + output := make(map[string]any) + + regInfo, err := getRegistrySupportInfo(registry.LOCAL_MACHINE, []string{regPolicyBase, regBase}) + if err == nil { + output[supportInfoKeyRegistry] = regInfo + } else { + output[supportInfoKeyRegistry] = err + } + + enc := json.NewEncoder(w) + return enc.Encode(output) +} + +type getRegistrySupportInfoBufs struct { + nameBuf []uint16 + valueBuf []byte +} + +func getRegistrySupportInfo(root registry.Key, subKeys []string) (map[string]any, error) { + bufs := getRegistrySupportInfoBufs{ + nameBuf: make([]uint16, maxRegValueNameLen), + valueBuf: make([]byte, initialValueBufLen), + } + + output := make(map[string]any) + + for _, subKey := range subKeys { + if err := getRegSubKey(root, subKey, 5, &bufs, output); err != nil && !errors.Is(err, registry.ErrNotExist) { + return nil, fmt.Errorf("getRegistrySupportInfo: %w", err) + } + } + + return output, nil +} + +func keyString(key registry.Key, subKey string) string { + var keyStr string + switch key { + case registry.CLASSES_ROOT: + keyStr = `HKCR\` + case registry.CURRENT_USER: + keyStr = `HKCU\` + case registry.LOCAL_MACHINE: + keyStr = `HKLM\` + case registry.USERS: + keyStr = `HKU\` + case registry.CURRENT_CONFIG: + keyStr = `HKCC\` + case registry.PERFORMANCE_DATA: + keyStr = `HKPD\` + default: + } + + return keyStr + subKey +} + +func getRegSubKey(key registry.Key, subKey string, recursionLimit int, bufs *getRegistrySupportInfoBufs, output map[string]any) error { + keyStr := keyString(key, subKey) + k, err := registry.OpenKey(key, subKey, registry.READ) + if err != nil { + return fmt.Errorf("opening %q: %w", keyStr, err) + } + defer k.Close() + + kv := make(map[string]any) + index := uint32(0) + +loopValues: + for { + nbuf := bufs.nameBuf + nameLen := uint32(len(nbuf)) + valueType := uint32(0) + vbuf := bufs.valueBuf + valueLen := uint32(len(vbuf)) + + err := regEnumValue(k, index, &nbuf[0], &nameLen, nil, &valueType, &vbuf[0], &valueLen) + switch err { + case windows.ERROR_NO_MORE_ITEMS: + break loopValues + case windows.ERROR_MORE_DATA: + bufs.valueBuf = make([]byte, valueLen) + continue + case nil: + default: + return fmt.Errorf("regEnumValue: %w", err) + } + + var value any + + switch valueType { + case registry.SZ, registry.EXPAND_SZ: + value = windows.UTF16PtrToString((*uint16)(unsafe.Pointer(&vbuf[0]))) + case registry.BINARY: + if valueLen > maxBinaryValueLen { + valueLen = maxBinaryValueLen + } + value = append([]byte{}, vbuf[:valueLen]...) + case registry.DWORD: + value = binary.LittleEndian.Uint32(vbuf[:4]) + case registry.MULTI_SZ: + // Adapted from x/sys/windows/registry/(Key).GetStringsValue + p := (*[1 << 29]uint16)(unsafe.Pointer(&vbuf[0]))[: valueLen/2 : valueLen/2] + var strs []string + if len(p) > 0 { + if p[len(p)-1] == 0 { + p = p[:len(p)-1] + } + strs = make([]string, 0, 5) + from := 0 + for i, c := range p { + if c == 0 { + strs = append(strs, string(utf16.Decode(p[from:i]))) + from = i + 1 + } + } + } + value = strs + case registry.QWORD: + value = binary.LittleEndian.Uint64(vbuf[:8]) + default: + value = fmt.Sprintf("", valueType) + } + + kv[windows.UTF16PtrToString(&nbuf[0])] = value + index++ + } + + if recursionLimit > 0 { + if sks, err := k.ReadSubKeyNames(0); err == nil { + for _, sk := range sks { + if err := getRegSubKey(k, sk, recursionLimit-1, bufs, kv); err != nil { + return err + } + } + } + } + + output[keyStr] = kv + return nil +} diff --git a/util/winutil/winutil_windows_test.go b/util/winutil/winutil_windows_test.go index bf22d26ca..e9ca08b09 100644 --- a/util/winutil/winutil_windows_test.go +++ b/util/winutil/winutil_windows_test.go @@ -4,7 +4,13 @@ package winutil import ( + "errors" + "fmt" + "strings" "testing" + + "golang.org/x/exp/maps" + "golang.org/x/sys/windows/registry" ) const ( @@ -28,3 +34,117 @@ func TestLookupPseudoUser(t *testing.T) { t.Errorf("LookupPseudoUser(%q) unexpectedly succeeded", networkSID) } } + +func makeLongBinaryValue() []byte { + buf := make([]byte, maxBinaryValueLen*2) + for i, _ := range buf { + buf[i] = byte(i % 0xFF) + } + return buf +} + +var testData = map[string]any{ + "": "I am the default", + "StringEmpty": "", + "StringShort": "Hello", + "StringLong": strings.Repeat("7", initialValueBufLen+1), + "MultiStringEmpty": []string{}, + "MultiStringSingle": []string{"Foo"}, + "MultiStringSingleEmpty": []string{""}, + "MultiString": []string{"Foo", "Bar", "Baz"}, + "MultiStringWithEmptyBeginning": []string{"", "Foo", "Bar"}, + "MultiStringWithEmptyMiddle": []string{"Foo", "", "Bar"}, + "MultiStringWithEmptyEnd": []string{"Foo", "Bar", ""}, + "DWord": uint32(0x12345678), + "QWord": uint64(0x123456789abcdef0), + "BinaryEmpty": []byte{}, + "BinaryShort": []byte{0x01, 0x02, 0x03, 0x04}, + "BinaryLong": makeLongBinaryValue(), +} + +const ( + keyNameTest = `SOFTWARE\Tailscale Test` + subKeyNameTest = "SubKey" +) + +func setValues(t *testing.T, k registry.Key) { + for vk, v := range testData { + var err error + switch tv := v.(type) { + case string: + err = k.SetStringValue(vk, tv) + case []string: + err = k.SetStringsValue(vk, tv) + case uint32: + err = k.SetDWordValue(vk, tv) + case uint64: + err = k.SetQWordValue(vk, tv) + case []byte: + err = k.SetBinaryValue(vk, tv) + default: + t.Fatalf("Unknown type") + } + + if err != nil { + t.Fatalf("Error setting %q: %v", vk, err) + } + } +} + +func TestRegistrySupportInfo(t *testing.T) { + // Make sure the key doesn't exist yet + k, err := registry.OpenKey(registry.CURRENT_USER, keyNameTest, registry.READ) + switch { + case err == nil: + k.Close() + t.Fatalf("Test key already exists") + case !errors.Is(err, registry.ErrNotExist): + t.Fatal(err) + } + + func() { + k, _, err := registry.CreateKey(registry.CURRENT_USER, keyNameTest, registry.WRITE) + if err != nil { + t.Fatalf("Error creating test key: %v", err) + } + defer k.Close() + + setValues(t, k) + + sk, _, err := registry.CreateKey(k, subKeyNameTest, registry.WRITE) + if err != nil { + t.Fatalf("Error creating test subkey: %v", err) + } + defer sk.Close() + + setValues(t, sk) + }() + + t.Cleanup(func() { + registry.DeleteKey(registry.CURRENT_USER, keyNameTest+"\\"+subKeyNameTest) + registry.DeleteKey(registry.CURRENT_USER, keyNameTest) + }) + + wantValuesData := maps.Clone(testData) + wantValuesData["BinaryLong"] = (wantValuesData["BinaryLong"].([]byte))[:maxBinaryValueLen] + + wantKeyData := make(map[string]any) + maps.Copy(wantKeyData, wantValuesData) + wantSubKeyData := make(map[string]any) + maps.Copy(wantSubKeyData, wantValuesData) + wantKeyData[subKeyNameTest] = wantSubKeyData + + wantData := map[string]any{ + "HKCU\\" + keyNameTest: wantKeyData, + } + + gotData, err := getRegistrySupportInfo(registry.CURRENT_USER, []string{keyNameTest}) + if err != nil { + t.Errorf("getRegistrySupportInfo error: %v", err) + } + + want, got := fmt.Sprintf("%#v", wantData), fmt.Sprintf("%#v", gotData) + if want != got { + t.Errorf("Compare error: want\n%s,\ngot %s", want, got) + } +} diff --git a/util/winutil/zsyscall_windows.go b/util/winutil/zsyscall_windows.go index 8c899232f..930a87522 100644 --- a/util/winutil/zsyscall_windows.go +++ b/util/winutil/zsyscall_windows.go @@ -7,6 +7,7 @@ import ( "unsafe" "golang.org/x/sys/windows" + "golang.org/x/sys/windows/registry" ) var _ unsafe.Pointer @@ -41,6 +42,7 @@ var ( modadvapi32 = windows.NewLazySystemDLL("advapi32.dll") procQueryServiceConfig2W = modadvapi32.NewProc("QueryServiceConfig2W") + procRegEnumValueW = modadvapi32.NewProc("RegEnumValueW") ) func queryServiceConfig2(hService windows.Handle, infoLevel uint32, buf *byte, bufLen uint32, bytesNeeded *uint32) (err error) { @@ -50,3 +52,11 @@ func queryServiceConfig2(hService windows.Handle, infoLevel uint32, buf *byte, b } return } + +func regEnumValue(key registry.Key, index uint32, valueName *uint16, valueNameLen *uint32, reserved *uint32, valueType *uint32, pData *byte, cbData *uint32) (ret error) { + r0, _, _ := syscall.Syscall9(procRegEnumValueW.Addr(), 8, uintptr(key), uintptr(index), uintptr(unsafe.Pointer(valueName)), uintptr(unsafe.Pointer(valueNameLen)), uintptr(unsafe.Pointer(reserved)), uintptr(unsafe.Pointer(valueType)), uintptr(unsafe.Pointer(pData)), uintptr(unsafe.Pointer(cbData)), 0) + if r0 != 0 { + ret = syscall.Errno(r0) + } + return +}