Browse Source

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 <[email protected]>
Nick Khyl 1 year ago
parent
commit
8d83adde07

+ 14 - 0
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

+ 109 - 0
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)
+}

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