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 <aaron@tailscale.com>
This commit is contained in:
Aaron Klotz 2024-06-05 14:48:57 -06:00
parent 9189fe007b
commit bd2a6d5386
6 changed files with 230 additions and 3 deletions

View File

@ -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

View File

@ -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

View File

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

View File

@ -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

View File

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

View File

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