From 8d83adde07f53d247d8b861bc9559ac44d799dd1 Mon Sep 17 00:00:00 2001 From: Nick Khyl Date: Sun, 7 Apr 2024 13:56:31 -0500 Subject: [PATCH] util/winutil/winenv: add package for current Windows environment details Package winenv provides information about the current Windows environment. This includes details such as whether the device is a server or workstation, and if it is AD domain-joined, MDM-registered, or neither. Updates tailscale/corp#18342 Signed-off-by: Nick Khyl --- util/winutil/winenv/mksyscall.go | 14 +++ util/winutil/winenv/winenv_windows.go | 109 ++++++++++++++++++++++++ util/winutil/winenv/zsyscall_windows.go | 77 +++++++++++++++++ 3 files changed, 200 insertions(+) create mode 100644 util/winutil/winenv/mksyscall.go create mode 100644 util/winutil/winenv/winenv_windows.go create mode 100644 util/winutil/winenv/zsyscall_windows.go diff --git a/util/winutil/winenv/mksyscall.go b/util/winutil/winenv/mksyscall.go new file mode 100644 index 000000000..4d848c8de --- /dev/null +++ b/util/winutil/winenv/mksyscall.go @@ -0,0 +1,14 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause +package winenv + +//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go mksyscall.go + +// https://web.archive.org/web/20240407040123/https://learn.microsoft.com/en-us/windows/win32/api/mdmregistration/nf-mdmregistration-isdeviceregisteredwithmanagement +//sys isDeviceRegisteredWithManagement(isMDMRegistered *bool, upnBufLen uint32, upnBuf *uint16) (hr int32, err error) = MDMRegistration.IsDeviceRegisteredWithManagement? + +// https://web.archive.org/web/20240407035921/https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-verifyversioninfow +//sys verifyVersionInfo(verInfo *osVersionInfoEx, typ verTypeMask, cond verCondMask) (res bool) = kernel32.VerifyVersionInfoW + +// https://web.archive.org/web/20240407035706/https://learn.microsoft.com/en-us/windows/win32/api/winnt/nf-winnt-versetconditionmask +//sys verSetConditionMask(condMask verCondMask, typ verTypeMask, cond verCond) (res verCondMask) = kernel32.VerSetConditionMask diff --git a/util/winutil/winenv/winenv_windows.go b/util/winutil/winenv/winenv_windows.go new file mode 100644 index 000000000..81fe42026 --- /dev/null +++ b/util/winutil/winenv/winenv_windows.go @@ -0,0 +1,109 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +// Package winenv provides information about the current Windows environment. +// This includes details such as whether the device is a server or workstation, +// if it is AD domain-joined, MDM-registered, or neither, and other characteristics. +package winenv + +import ( + "runtime" + "unsafe" + + "golang.org/x/sys/windows" +) + +// osVersionInfoEx contains operating system version information. +// See [OSVERSIONINFOEXW] for details. +// +// [OSVERSIONINFOEXW]: https://web.archive.org/web/20240407035213/https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-osversioninfoexw +type osVersionInfoEx struct { + cbSize uint32 + majorVersion uint32 + minorVersion uint32 + buildNumber uint32 + platformId uint32 + csdVersion [128]uint16 + servicePackMajor uint16 + servicePackMinor uint16 + suiteMask uint16 + productType verProductType + reserved uint8 +} + +type ( + verTypeMask uint32 + verCondMask uint64 + verCond uint8 + verProductType uint8 +) + +// See [VER_SET_CONDITION] and [VerSetConditionMask] for details. +// +// [VER_SET_CONDITION]: https://web.archive.org/web/20240407035400/https://learn.microsoft.com/en-us/windows/win32/api/winnt/nf-winnt-ver_set_condition +// [VerSetConditionMask]: https://web.archive.org/web/20240407035706/https://learn.microsoft.com/en-us/windows/win32/api/winnt/nf-winnt-versetconditionmask +const ( + _VER_MINORVERSION = verTypeMask(0x0000001) + _VER_MAJORVERSION = verTypeMask(0x0000002) + _VER_BUILDNUMBER = verTypeMask(0x0000004) + _VER_PLATFORMID = verTypeMask(0x0000008) + _VER_SERVICEPACKMINOR = verTypeMask(0x0000010) + _VER_SERVICEPACKMAJOR = verTypeMask(0x0000020) + _VER_SUITENAME = verTypeMask(0x0000040) + _VER_PRODUCT_TYPE = verTypeMask(0x0000080) + + _VER_NT_WORKSTATION = verProductType(1) + _VER_NT_DOMAIN_CONTROLLER = verProductType(2) + _VER_NT_SERVER = verProductType(3) + + _VER_EQUAL = verCond(1) + _VER_GREATER = verCond(2) + _VER_GREATER_EQUAL = verCond(3) + _VER_LESS = verCond(4) + _VER_LESS_EQUAL = verCond(5) + _VER_AND = verCond(6) + _VER_OR = verCond(7) +) + +// IsDomainJoined reports whether the device is domain-joined. +func IsDomainJoined() bool { + var domain *uint16 + var status uint32 + if err := windows.NetGetJoinInformation(nil, &domain, &status); err != nil { + return false + } + windows.NetApiBufferFree((*byte)(unsafe.Pointer(domain))) + return status == windows.NetSetupDomainName +} + +// IsMDMRegistered reports whether the device is MDM-registered. +func IsMDMRegistered() bool { + const S_OK int32 = 0 + var isMDMRegistered bool + if hr, err := isDeviceRegisteredWithManagement(&isMDMRegistered, 0, nil); err != nil || hr != S_OK { + return false + } + return isMDMRegistered +} + +// IsManaged reports whether the device is managed through AD or MDM. +func IsManaged() bool { + return IsDomainJoined() || IsMDMRegistered() +} + +// IsWindowsServer reports whether the device is running a Windows Server operating system. +func IsWindowsServer() bool { + if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" { + // TODO(nickkhyl): the Windows Server versions we support do not have 32-bit editions. + // But we should remove this check once we adopt mkwinsyscallx, as it can handle 64-bit + // long arguments such as verCondMask. + return false + } + + osvi := &osVersionInfoEx{ + cbSize: uint32(unsafe.Sizeof(osVersionInfoEx{})), + productType: _VER_NT_WORKSTATION, + } + condMask := verSetConditionMask(0, _VER_PRODUCT_TYPE, _VER_EQUAL) + return !verifyVersionInfo(osvi, _VER_PRODUCT_TYPE, condMask) +} diff --git a/util/winutil/winenv/zsyscall_windows.go b/util/winutil/winenv/zsyscall_windows.go new file mode 100644 index 000000000..2bdfdd9b1 --- /dev/null +++ b/util/winutil/winenv/zsyscall_windows.go @@ -0,0 +1,77 @@ +// Code generated by 'go generate'; DO NOT EDIT. + +package winenv + +import ( + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +var _ unsafe.Pointer + +// Do the interface allocations only once for common +// Errno values. +const ( + errnoERROR_IO_PENDING = 997 +) + +var ( + errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) + errERROR_EINVAL error = syscall.EINVAL +) + +// errnoErr returns common boxed Errno values, to prevent +// allocations at runtime. +func errnoErr(e syscall.Errno) error { + switch e { + case 0: + return errERROR_EINVAL + case errnoERROR_IO_PENDING: + return errERROR_IO_PENDING + } + // TODO: add more here, after collecting data on the common + // error values see on Windows. (perhaps when running + // all.bat?) + return e +} + +var ( + modMDMRegistration = windows.NewLazySystemDLL("MDMRegistration.dll") + modkernel32 = windows.NewLazySystemDLL("kernel32.dll") + + procIsDeviceRegisteredWithManagement = modMDMRegistration.NewProc("IsDeviceRegisteredWithManagement") + procVerSetConditionMask = modkernel32.NewProc("VerSetConditionMask") + procVerifyVersionInfoW = modkernel32.NewProc("VerifyVersionInfoW") +) + +func isDeviceRegisteredWithManagement(isMDMRegistered *bool, upnBufLen uint32, upnBuf *uint16) (hr int32, err error) { + err = procIsDeviceRegisteredWithManagement.Find() + if err != nil { + return + } + var _p0 uint32 + if *isMDMRegistered { + _p0 = 1 + } + r0, _, e1 := syscall.Syscall(procIsDeviceRegisteredWithManagement.Addr(), 3, uintptr(unsafe.Pointer(&_p0)), uintptr(upnBufLen), uintptr(unsafe.Pointer(upnBuf))) + *isMDMRegistered = _p0 != 0 + hr = int32(r0) + if hr == 0 { + err = errnoErr(e1) + } + return +} + +func verSetConditionMask(condMask verCondMask, typ verTypeMask, cond verCond) (res verCondMask) { + r0, _, _ := syscall.Syscall(procVerSetConditionMask.Addr(), 3, uintptr(condMask), uintptr(typ), uintptr(cond)) + res = verCondMask(r0) + return +} + +func verifyVersionInfo(verInfo *osVersionInfoEx, typ verTypeMask, cond verCondMask) (res bool) { + r0, _, _ := syscall.Syscall(procVerifyVersionInfoW.Addr(), 3, uintptr(unsafe.Pointer(verInfo)), uintptr(typ), uintptr(cond)) + res = r0 != 0 + return +}