From bd2a6d5386520fe0507284e61bc155c4f31c5af6 Mon Sep 17 00:00:00 2001 From: Aaron Klotz Date: Wed, 5 Jun 2024 14:48:57 -0600 Subject: [PATCH] util/winutil: add UserProfile type for (un)loading user profiles S4U logons do not automatically load the associated user profile. In this PR we add UserProfile to handle that part. Windows docs indicate that we should try to resolve a remote profile path when present, so we attempt to do so when the local computer is joined to a domain. Updates #12383 Signed-off-by: Aaron Klotz --- cmd/derper/depaware.txt | 2 +- cmd/tailscale/depaware.txt | 2 +- cmd/tailscaled/depaware.txt | 2 +- util/winutil/mksyscall.go | 2 + util/winutil/userprofile_windows.go | 205 ++++++++++++++++++++++++++++ util/winutil/zsyscall_windows.go | 20 +++ 6 files changed, 230 insertions(+), 3 deletions(-) create mode 100644 util/winutil/userprofile_windows.go diff --git a/cmd/derper/depaware.txt b/cmd/derper/depaware.txt index fd2de6e8c..58cb5d3c6 100644 --- a/cmd/derper/depaware.txt +++ b/cmd/derper/depaware.txt @@ -157,7 +157,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa tailscale.com/util/syspolicy from tailscale.com/ipn tailscale.com/util/vizerror from tailscale.com/tailcfg+ W 💣 tailscale.com/util/winutil from tailscale.com/hostinfo+ - W 💣 tailscale.com/util/winutil/winenv from tailscale.com/hostinfo + W 💣 tailscale.com/util/winutil/winenv from tailscale.com/hostinfo+ tailscale.com/version from tailscale.com/derp+ tailscale.com/version/distro from tailscale.com/envknob+ tailscale.com/wgengine/filter from tailscale.com/types/netmap diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index c0b626f13..f02611530 100644 --- a/cmd/tailscale/depaware.txt +++ b/cmd/tailscale/depaware.txt @@ -164,7 +164,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep tailscale.com/util/vizerror from tailscale.com/tailcfg+ 💣 tailscale.com/util/winutil from tailscale.com/clientupdate+ W 💣 tailscale.com/util/winutil/authenticode from tailscale.com/clientupdate - W 💣 tailscale.com/util/winutil/winenv from tailscale.com/hostinfo + W 💣 tailscale.com/util/winutil/winenv from tailscale.com/hostinfo+ tailscale.com/version from tailscale.com/client/web+ tailscale.com/version/distro from tailscale.com/client/web+ tailscale.com/wgengine/capture from tailscale.com/cmd/tailscale/cli diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 24f6cb4da..c3d958e9d 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -400,7 +400,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de 💣 tailscale.com/util/winutil from tailscale.com/clientupdate+ W 💣 tailscale.com/util/winutil/authenticode from tailscale.com/clientupdate+ W tailscale.com/util/winutil/policy from tailscale.com/ipn/ipnlocal - W 💣 tailscale.com/util/winutil/winenv from tailscale.com/hostinfo + W 💣 tailscale.com/util/winutil/winenv from tailscale.com/hostinfo+ tailscale.com/util/zstdframe from tailscale.com/control/controlclient+ tailscale.com/version from tailscale.com/client/web+ tailscale.com/version/distro from tailscale.com/client/web+ diff --git a/util/winutil/mksyscall.go b/util/winutil/mksyscall.go index 17f41ddcc..1bfdffa1a 100644 --- a/util/winutil/mksyscall.go +++ b/util/winutil/mksyscall.go @@ -7,6 +7,7 @@ //go:generate go run golang.org/x/tools/cmd/goimports -w zsyscall_windows.go //sys getApplicationRestartSettings(process windows.Handle, commandLine *uint16, commandLineLen *uint32, flags *uint32) (ret wingoes.HRESULT) = kernel32.GetApplicationRestartSettings +//sys loadUserProfile(token windows.Token, profileInfo *_PROFILEINFO) (err error) [int32(failretval)==0] = userenv.LoadUserProfileW //sys queryServiceConfig2(hService windows.Handle, infoLevel uint32, buf *byte, bufLen uint32, bytesNeeded *uint32) (err error) [failretval==0] = advapi32.QueryServiceConfig2W //sys registerApplicationRestart(cmdLineExclExeName *uint16, flags uint32) (ret wingoes.HRESULT) = kernel32.RegisterApplicationRestart //sys rmEndSession(session _RMHANDLE) (ret error) = rstrtmgr.RmEndSession @@ -14,3 +15,4 @@ //sys rmJoinSession(pSession *_RMHANDLE, sessionKey *uint16) (ret error) = rstrtmgr.RmJoinSession //sys rmRegisterResources(session _RMHANDLE, nFiles uint32, rgsFileNames **uint16, nApplications uint32, rgApplications *_RM_UNIQUE_PROCESS, nServices uint32, rgsServiceNames **uint16) (ret error) = rstrtmgr.RmRegisterResources //sys rmStartSession(pSession *_RMHANDLE, flags uint32, sessionKey *uint16) (ret error) = rstrtmgr.RmStartSession +//sys unloadUserProfile(token windows.Token, profile registry.Key) (err error) [int32(failretval)==0] = userenv.UnloadUserProfile diff --git a/util/winutil/userprofile_windows.go b/util/winutil/userprofile_windows.go new file mode 100644 index 000000000..99fb99d22 --- /dev/null +++ b/util/winutil/userprofile_windows.go @@ -0,0 +1,205 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package winutil + +import ( + "os/user" + "strings" + "unsafe" + + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/registry" + "tailscale.com/types/logger" + "tailscale.com/util/winutil/winenv" +) + +type _PROFILEINFO struct { + Size uint32 + Flags uint32 + UserName *uint16 + ProfilePath *uint16 + DefaultPath *uint16 + ServerName *uint16 + PolicyPath *uint16 + Profile registry.Key +} + +// _PROFILEINFO flags +const ( + _PI_NOUI = 0x00000001 +) + +type _USER_INFO_4 struct { + Name *uint16 + Password *uint16 + PasswordAge uint32 + Priv uint32 + HomeDir *uint16 + Comment *uint16 + Flags uint32 + ScriptPath *uint16 + AuthFlags uint32 + FullName *uint16 + UsrComment *uint16 + Parms *uint16 + Workstations *uint16 + LastLogon uint32 + LastLogoff uint32 + AcctExpires uint32 + MaxStorage uint32 + UnitsPerWeek uint32 + LogonHours *byte + BadPwCount uint32 + NumLogons uint32 + LogonServer *uint16 + CountryCode uint32 + CodePage uint32 + UserSID *windows.SID + PrimaryGroupID uint32 + Profile *uint16 + HomeDirDrive *uint16 + PasswordExpired uint32 +} + +// UserProfile encapsulates a loaded Windows user profile. +type UserProfile struct { + token windows.Token + profileKey registry.Key +} + +// LoadUserProfile loads the Windows user profile associated with token and u. +// u serves simply as a hint for speeding up resolution of the username and thus +// must reference the same user as token. u may also be nil, in which case token +// is queried for the username. +func LoadUserProfile(token windows.Token, u *user.User) (up *UserProfile, err error) { + computerName, userName, err := getComputerAndUserName(token, u) + if err != nil { + return nil, err + } + + var roamingProfilePath *uint16 + if winenv.IsDomainJoined() { + roamingProfilePath, err = getRoamingProfilePath(nil, computerName, userName) + if err != nil { + return nil, err + } + } + + pi := _PROFILEINFO{ + Size: uint32(unsafe.Sizeof(_PROFILEINFO{})), + Flags: _PI_NOUI, + UserName: userName, + ProfilePath: roamingProfilePath, + ServerName: computerName, + } + if err := loadUserProfile(token, &pi); err != nil { + return nil, err + } + + // Duplicate the token so that we have a copy to use during cleanup without + // consuming the token passed into this function. + var dupToken windows.Handle + cp := windows.CurrentProcess() + if err := windows.DuplicateHandle(cp, windows.Handle(token), cp, &dupToken, 0, + false, windows.DUPLICATE_SAME_ACCESS); err != nil { + return nil, err + } + + return &UserProfile{ + token: windows.Token(dupToken), + profileKey: pi.Profile, + }, nil +} + +// RegKey returns the registry key associated with the user profile. +// The caller must not close the returned key. +func (up *UserProfile) RegKey() registry.Key { + return up.profileKey +} + +// Close unloads the user profile and cleans up any other resources held by up. +func (up *UserProfile) Close() error { + if up.profileKey != 0 { + if err := unloadUserProfile(up.token, up.profileKey); err != nil { + return err + } + up.profileKey = 0 + } + + if up.token != 0 { + up.token.Close() + up.token = 0 + } + return nil +} + +func getRoamingProfilePath(logf logger.Logf, computerName, userName *uint16) (path *uint16, err error) { + // logf is for debugging/testing. + if logf == nil { + logf = logger.Discard + } + + var pbuf *byte + if err := windows.NetUserGetInfo(computerName, userName, 4, &pbuf); err != nil { + return nil, err + } + defer windows.NetApiBufferFree(pbuf) + + ui4 := (*_USER_INFO_4)(unsafe.Pointer(pbuf)) + logf("getRoamingProfilePath: got %#v", *ui4) + profilePath := ui4.Profile + if profilePath == nil { + return nil, nil + } + + var sz int + for ptr := unsafe.Pointer(profilePath); *(*uint16)(ptr) != 0; sz++ { + ptr = unsafe.Pointer(uintptr(ptr) + unsafe.Sizeof(*profilePath)) + } + + if sz == 0 { + return nil, nil + } + + buf := unsafe.Slice(profilePath, sz+1) + cp := append([]uint16{}, buf...) + return unsafe.SliceData(cp), nil +} + +func getComputerAndUserName(token windows.Token, u *user.User) (computerName *uint16, userName *uint16, err error) { + if u == nil { + tokenUser, err := token.GetTokenUser() + if err != nil { + return nil, nil, err + } + + u, err = user.LookupId(tokenUser.User.Sid.String()) + if err != nil { + return nil, nil, err + } + } + + var strComputer, strUser string + before, after, hasBackslash := strings.Cut(u.Username, `\`) + if hasBackslash { + strComputer = before + strUser = after + } else { + strUser = before + } + + if strComputer != "" { + computerName, err = windows.UTF16PtrFromString(strComputer) + if err != nil { + return nil, nil, err + } + } + + userName, err = windows.UTF16PtrFromString(strUser) + if err != nil { + return nil, nil, err + } + + return computerName, userName, nil +} diff --git a/util/winutil/zsyscall_windows.go b/util/winutil/zsyscall_windows.go index b228ff158..8bb0091f7 100644 --- a/util/winutil/zsyscall_windows.go +++ b/util/winutil/zsyscall_windows.go @@ -8,6 +8,7 @@ "github.com/dblohm7/wingoes" "golang.org/x/sys/windows" + "golang.org/x/sys/windows/registry" ) var _ unsafe.Pointer @@ -42,6 +43,7 @@ func errnoErr(e syscall.Errno) error { modadvapi32 = windows.NewLazySystemDLL("advapi32.dll") modkernel32 = windows.NewLazySystemDLL("kernel32.dll") modrstrtmgr = windows.NewLazySystemDLL("rstrtmgr.dll") + moduserenv = windows.NewLazySystemDLL("userenv.dll") procQueryServiceConfig2W = modadvapi32.NewProc("QueryServiceConfig2W") procGetApplicationRestartSettings = modkernel32.NewProc("GetApplicationRestartSettings") @@ -51,6 +53,8 @@ func errnoErr(e syscall.Errno) error { procRmJoinSession = modrstrtmgr.NewProc("RmJoinSession") procRmRegisterResources = modrstrtmgr.NewProc("RmRegisterResources") procRmStartSession = modrstrtmgr.NewProc("RmStartSession") + procLoadUserProfileW = moduserenv.NewProc("LoadUserProfileW") + procUnloadUserProfile = moduserenv.NewProc("UnloadUserProfile") ) func queryServiceConfig2(hService windows.Handle, infoLevel uint32, buf *byte, bufLen uint32, bytesNeeded *uint32) (err error) { @@ -112,3 +116,19 @@ func rmStartSession(pSession *_RMHANDLE, flags uint32, sessionKey *uint16) (ret } return } + +func loadUserProfile(token windows.Token, profileInfo *_PROFILEINFO) (err error) { + r1, _, e1 := syscall.Syscall(procLoadUserProfileW.Addr(), 2, uintptr(token), uintptr(unsafe.Pointer(profileInfo)), 0) + if int32(r1) == 0 { + err = errnoErr(e1) + } + return +} + +func unloadUserProfile(token windows.Token, profile registry.Key) (err error) { + r1, _, e1 := syscall.Syscall(procUnloadUserProfile.Addr(), 2, uintptr(token), uintptr(profile), 0) + if int32(r1) == 0 { + err = errnoErr(e1) + } + return +}