329 lines
8.8 KiB
Go
329 lines
8.8 KiB
Go
// Copyright (c) 2022 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 wingoes
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"sync/atomic"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/windows"
|
|
"golang.org/x/sys/windows/registry"
|
|
)
|
|
|
|
const (
|
|
_VER_MINORVERSION = 0x0000001
|
|
_VER_MAJORVERSION = 0x0000002
|
|
_VER_BUILDNUMBER = 0x0000004
|
|
_VER_PLATFORMID = 0x0000008
|
|
_VER_SERVICEPACKMINOR = 0x0000010
|
|
_VER_SERVICEPACKMAJOR = 0x0000020
|
|
_VER_SUITENAME = 0x0000040
|
|
_VER_PRODUCT_TYPE = 0x0000080
|
|
)
|
|
|
|
const (
|
|
_VER_EQUAL = 1
|
|
_VER_GREATER = 2
|
|
_VER_GREATER_EQUAL = 3
|
|
_VER_LESS = 4
|
|
_VER_LESS_EQUAL = 5
|
|
_VER_AND = 6
|
|
_VER_OR = 7
|
|
)
|
|
|
|
const (
|
|
_VER_NT_WORKSTATION = 1
|
|
_VER_NT_DOMAIN_CONTROLLER = 2
|
|
_VER_NT_SERVER = 3
|
|
)
|
|
|
|
// The definition in x/sys/windows doesn't export the OSVersionInfoSize field,
|
|
// which we need to set before making API calls.
|
|
type _OSVERSIONINFOEX struct {
|
|
OSVersionInfoSize uint32
|
|
MajorVersion uint32
|
|
MinorVersion uint32
|
|
BuildNumber uint32
|
|
PlatformId uint32
|
|
CSDVersion [128]uint16
|
|
ServicePackMajor uint16
|
|
ServicePackMinor uint16
|
|
SuiteMask uint16
|
|
ProductType byte
|
|
_ byte // Reserved
|
|
}
|
|
|
|
type osVersionInfo struct {
|
|
fallbackBuildMin atomic.Uint32
|
|
fallbackBuildMax atomic.Uint32
|
|
build uint32
|
|
str string
|
|
isDC bool
|
|
isServer bool
|
|
useFallback bool
|
|
}
|
|
|
|
var (
|
|
// We cannot use mksyscall for these because they pass uint64 by value on
|
|
// 32-bit CPU architectures.
|
|
procVerSetConditionMask = modkernel32.NewProc("VerSetConditionMask")
|
|
procVerifyVersionInfo = modkernel32.NewProc("VerifyVersionInfoW")
|
|
verOnce sync.Once
|
|
verInfo osVersionInfo // must access via getVersionInfo()
|
|
)
|
|
|
|
func getVersionInfoInternal() (*_OSVERSIONINFOEX, error) {
|
|
osv := _OSVERSIONINFOEX{
|
|
OSVersionInfoSize: uint32(unsafe.Sizeof(_OSVERSIONINFOEX{})),
|
|
}
|
|
// Using GetVersionEx to account for app manifest.
|
|
if err := getVersionEx(&osv); err != nil {
|
|
return nil, err
|
|
}
|
|
return &osv, nil
|
|
}
|
|
|
|
func (osv *osVersionInfo) initFlags() {
|
|
if isDC, err := verQueryProductType(_VER_NT_DOMAIN_CONTROLLER); err == nil {
|
|
osv.isDC = isDC
|
|
if isDC {
|
|
// Domain Controllers are also implicitly servers.
|
|
osv.isServer = true
|
|
return
|
|
}
|
|
}
|
|
|
|
if isServer, err := verQueryProductType(_VER_NT_SERVER); err == nil {
|
|
osv.isServer = isServer
|
|
}
|
|
}
|
|
|
|
func (osv *osVersionInfo) initFallback() {
|
|
osv.initFlags()
|
|
osv.useFallback = true
|
|
}
|
|
|
|
func verCheckManifest(osv *_OSVERSIONINFOEX) bool {
|
|
major, minor, build := windows.RtlGetNtVersionNumbers()
|
|
return major == osv.MajorVersion && minor == osv.MinorVersion && build == osv.BuildNumber
|
|
}
|
|
|
|
func (osv *osVersionInfo) init() {
|
|
osvx, err := getVersionInfoInternal()
|
|
if err != nil {
|
|
osv.initFallback()
|
|
return
|
|
}
|
|
|
|
// We only support 10.0.x.x
|
|
if osvx.MajorVersion != 10 || osvx.MinorVersion != 0 {
|
|
if verCheckManifest(osvx) {
|
|
panic("change in versioning scheme -- package must be updated to interpret")
|
|
} else {
|
|
panic("incoherent Windows version -- missing/outdated manifest?")
|
|
}
|
|
}
|
|
|
|
*osv = osVersionInfo{
|
|
build: osvx.BuildNumber,
|
|
str: versionString(osvx),
|
|
isDC: osvx.ProductType == _VER_NT_DOMAIN_CONTROLLER,
|
|
// Domain Controllers are also implicitly servers.
|
|
isServer: osvx.ProductType == _VER_NT_DOMAIN_CONTROLLER || osvx.ProductType == _VER_NT_SERVER,
|
|
}
|
|
}
|
|
|
|
func getVersionInfo() *osVersionInfo {
|
|
verOnce.Do(verInfo.init)
|
|
return &verInfo
|
|
}
|
|
|
|
func versionString(osv *_OSVERSIONINFOEX) string {
|
|
fmtstr := "%d.%d.%d.%d"
|
|
vers := append(make([]any, 0, 4),
|
|
uint(osv.MajorVersion),
|
|
uint(osv.MinorVersion),
|
|
uint(osv.BuildNumber),
|
|
)
|
|
|
|
if ubr, err := getUBR(); err == nil {
|
|
vers = append(vers, uint(ubr))
|
|
} else {
|
|
fmtstr = fmtstr[:len(fmtstr)-3]
|
|
}
|
|
|
|
return fmt.Sprintf(fmtstr, vers...)
|
|
}
|
|
|
|
// getUBR returns the "update build revision," ie. the fourth component of the
|
|
// version string found on Windows 10 and Windows 11 systems.
|
|
func getUBR() (uint32, error) {
|
|
key, err := registry.OpenKey(registry.LOCAL_MACHINE,
|
|
`SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE|registry.WOW64_64KEY)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer key.Close()
|
|
|
|
val, valType, err := key.GetIntegerValue("UBR")
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if valType != registry.DWORD {
|
|
return 0, registry.ErrUnexpectedType
|
|
}
|
|
|
|
return uint32(val), nil
|
|
}
|
|
|
|
// GetOSVersionString returns the Windows version of the current machine in
|
|
// dotted-decimal form.
|
|
func GetOSVersionString() string {
|
|
return getVersionInfo().String()
|
|
}
|
|
|
|
// IsWinServer returns true if and only if this computer's version of Windows is
|
|
// a server edition.
|
|
func IsWinServer() bool {
|
|
return getVersionInfo().isServer
|
|
}
|
|
|
|
// IsWinDomainController returs true if this computer's version of Windows is
|
|
// configured to act as a domain controller.
|
|
func IsWinDomainController() bool {
|
|
return getVersionInfo().isDC
|
|
}
|
|
|
|
// Win10BuildConstant encodes build numbers for the various editions of Windows 10,
|
|
// for use with IsWin10BuildOrGreater.
|
|
type Win10BuildConstant uint32
|
|
|
|
const (
|
|
Win10BuildRTM = Win10BuildConstant(10240)
|
|
Win10Build1511 = Win10BuildConstant(10586)
|
|
Win10Build1607 = Win10BuildConstant(14393)
|
|
WinServer2016 = Win10Build1607
|
|
Win10BuildAnniversary = Win10Build1607
|
|
Win10Build1703 = Win10BuildConstant(15063)
|
|
Win10BuildCreators = Win10Build1703
|
|
Win10Build1709 = Win10BuildConstant(16299)
|
|
Win10BuildFallCreators = Win10Build1709
|
|
Win10Build1803 = Win10BuildConstant(17134)
|
|
Win10Build1809 = Win10BuildConstant(17763)
|
|
WinServer2019 = Win10Build1809
|
|
Win10Build1903 = Win10BuildConstant(18362)
|
|
Win10Build1909 = Win10BuildConstant(18363)
|
|
Win10Build2004 = Win10BuildConstant(19041)
|
|
Win10Build20H2 = Win10BuildConstant(19042)
|
|
Win10Build21H1 = Win10BuildConstant(19043)
|
|
Win10Build21H2 = Win10BuildConstant(19044)
|
|
Win10Build22H2 = Win10BuildConstant(19045)
|
|
WinServer2022 = Win10BuildConstant(20348)
|
|
)
|
|
|
|
// IsWin10BuildOrGreater returns true when running on the specified Windows 10
|
|
// build, or newer.
|
|
func IsWin10BuildOrGreater(build Win10BuildConstant) bool {
|
|
return getVersionInfo().isWin10BuildOrGreater(uint32(build))
|
|
}
|
|
|
|
// Win11BuildConstant encodes build numbers for the various editions of Windows 11,
|
|
// for use with IsWin11BuildOrGreater.
|
|
type Win11BuildConstant uint32
|
|
|
|
const (
|
|
Win11BuildRTM = Win11BuildConstant(22000)
|
|
Win11Build22H2 = Win11BuildConstant(22621)
|
|
Win11Build23H2 = Win11BuildConstant(22631)
|
|
Win11Build24H2 = Win11BuildConstant(26100)
|
|
WinServer2025 = Win11Build24H2
|
|
Win11Build25H2 = Win11BuildConstant(26200)
|
|
Win11Build26H1 = Win11BuildConstant(28000)
|
|
)
|
|
|
|
// IsWin11OrGreater returns true when running on any release of Windows 11,
|
|
// or newer.
|
|
func IsWin11OrGreater() bool {
|
|
return IsWin11BuildOrGreater(Win11BuildRTM)
|
|
}
|
|
|
|
// IsWin11BuildOrGreater returns true when running on the specified Windows 11
|
|
// build, or newer.
|
|
func IsWin11BuildOrGreater(build Win11BuildConstant) bool {
|
|
// Under the hood, Windows 11 is just Windows 10 with a sufficiently advanced
|
|
// build number.
|
|
return getVersionInfo().isWin10BuildOrGreater(uint32(build))
|
|
}
|
|
|
|
func (osv *osVersionInfo) String() string {
|
|
return osv.str
|
|
}
|
|
|
|
func (osv *osVersionInfo) isWin10BuildOrGreater(build uint32) bool {
|
|
if !osv.useFallback {
|
|
return osv.build >= build
|
|
}
|
|
|
|
if osv.fallbackBuildMin.Load() >= build {
|
|
return true
|
|
}
|
|
|
|
if build >= osv.fallbackBuildMax.Load() {
|
|
return false
|
|
}
|
|
|
|
result, err := verQueryBuild(10, 0, build)
|
|
if err == nil {
|
|
if result {
|
|
osv.fallbackBuildMin.Store(build)
|
|
} else {
|
|
osv.fallbackBuildMax.Store(build)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func verQueryBuild(major, minor, build uint32) (bool, error) {
|
|
var condMask uint64
|
|
condMask = verSetConditionMask(condMask, _VER_MAJORVERSION, _VER_GREATER_EQUAL)
|
|
condMask = verSetConditionMask(condMask, _VER_MINORVERSION, _VER_GREATER_EQUAL)
|
|
condMask = verSetConditionMask(condMask, _VER_BUILDNUMBER, _VER_GREATER_EQUAL)
|
|
|
|
typeMask := uint32(_VER_MAJORVERSION | _VER_MINORVERSION | _VER_BUILDNUMBER)
|
|
|
|
osv := _OSVERSIONINFOEX{
|
|
MajorVersion: major,
|
|
MinorVersion: minor,
|
|
BuildNumber: build,
|
|
}
|
|
|
|
return verVerify(&osv, typeMask, condMask)
|
|
}
|
|
|
|
func verQueryProductType(wantProdType byte) (bool, error) {
|
|
condMask := verSetConditionMask(0, _VER_PRODUCT_TYPE, _VER_EQUAL)
|
|
osv := _OSVERSIONINFOEX{
|
|
ProductType: wantProdType,
|
|
}
|
|
return verVerify(&osv, _VER_PRODUCT_TYPE, condMask)
|
|
}
|
|
|
|
func verVerify(osv *_OSVERSIONINFOEX, typeMask uint32, condMask uint64) (bool, error) {
|
|
osv.OSVersionInfoSize = uint32(unsafe.Sizeof(_OSVERSIONINFOEX{}))
|
|
err := verifyVersionInfo(osv, typeMask, condMask)
|
|
switch err {
|
|
case nil:
|
|
return true, nil
|
|
case windows.ERROR_OLD_WIN_VERSION:
|
|
return false, nil
|
|
default:
|
|
return false, err
|
|
}
|
|
}
|