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 @@ package winutil //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 @@ package winutil //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 @@ import ( "github.com/dblohm7/wingoes" "golang.org/x/sys/windows" + "golang.org/x/sys/windows/registry" ) var _ unsafe.Pointer @@ -42,6 +43,7 @@ var ( 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 @@ var ( 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 +}