Bladeren bron

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 <[email protected]>
Aaron Klotz 1 jaar geleden
bovenliggende
commit
bd2a6d5386

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

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

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

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

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

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