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