mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-08-04 14:36:07 +00:00
tailscale: support Windows SSH user sessions
This commit is contained in:
parent
07ad6818a9
commit
7392f6974b
8 changed files with 644 additions and 89 deletions
|
|
@ -166,7 +166,7 @@ Run a Tailscale SSH server on tailnet port 22.
|
|||
Access is controlled by the SSH ACL in the Tailscale admin console, which maps each connection to a local user. How that user is resolved, and which users are allowed, depends on the platform:
|
||||
|
||||
- **Linux** and **macOS**: the user is resolved from the system user database. Switching to a user other than the one sing-box runs as requires running as root; without root, sessions are limited to the current user.
|
||||
- **Windows**: sessions run as the sing-box process identity; the mapped user is not impersonated, so a session mapped to a different local account is refused.
|
||||
- **Windows**: in the command line client, sessions run as the sing-box process identity; the mapped user is not impersonated, so a session mapped to a different local account is refused. In the graphical client, there is no such restriction.
|
||||
- **Android**: the user is resolved by the app rather than the system user database. `root` is the superuser (UID 0) and `shell` is the ADB shell user (UID 2000); every other name is resolved as the package name of an installed application, running as that application's UID with its data directory as the home directory, so the target application must be installed. `termux` is a shortcut for `com.termux`, and `sing-box` for the app's own package name; when Termux is installed, the `root` and `termux` users load the Termux environment. Running as the sing-box application itself requires no root, while any other user requires granted root access; without root, sessions are limited to the sing-box user.
|
||||
- **macOS**: the SSH server is only available in the standalone version and requires the Root Helper; the App Store version is not supported.
|
||||
- **iOS**: the SSH server is only available in the jailbreak build; the App Store and TestFlight versions are not supported.
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ UDP NAT 过期时间。
|
|||
访问控制由 Tailscale 管理控制台中的 SSH ACL 决定,它将每个连接映射到一个本地用户。该用户如何解析、以及允许哪些用户,取决于平台:
|
||||
|
||||
- **Linux** 和 **macOS**:从系统用户数据库解析用户。要切换到 sing-box 运行身份以外的用户需要以 root 运行;非 root 时,会话仅限于当前用户。
|
||||
- **Windows**:会话以 sing-box 进程的身份运行;映射的用户不会被模拟,因此映射到其他本地账户的会话将被拒绝。
|
||||
- **Windows**:在命令行客户端中,会话以 sing-box 进程的身份运行;映射的用户不会被模拟,因此映射到其他本地账户的会话将被拒绝。在图形客户端中没有此限制。
|
||||
- **Android**:用户由应用解析,而非系统用户数据库。`root` 即超级用户(UID 0),`shell` 为 ADB shell 用户(UID 2000);其他名称均作为已安装应用的包名解析,以该应用的 UID 运行,并使用其数据目录作为主目录,因此目标应用必须已安装。`termux` 是 `com.termux` 的快捷方式,`sing-box` 是应用自身包名的快捷方式;当 Termux 已安装时,`root` 和 `termux` 用户将加载 Termux 环境。以 sing-box 应用自身身份运行无需 root,其他用户则需要已授予的 root 权限;非 root 时,会话仅限于 sing-box 用户。
|
||||
- **macOS**:SSH 服务器仅在独立版本中可用,且需要 Root Helper;App Store 版本不支持。
|
||||
- **iOS**:SSH 服务器仅在越狱版本中可用;App Store 和 TestFlight 版本不支持。
|
||||
|
|
|
|||
|
|
@ -4,8 +4,11 @@ package main
|
|||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
|
@ -20,6 +23,7 @@ import (
|
|||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
|
||||
"github.com/tailscale/go-winio"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
|
|
@ -172,11 +176,11 @@ func (p *windowsPlatformInterface) CloseNeighborMonitor(listener adapter.Neighbo
|
|||
}
|
||||
|
||||
func (p *windowsPlatformInterface) UsePlatformShell() bool {
|
||||
return false
|
||||
return listenAddress == ""
|
||||
}
|
||||
|
||||
func (p *windowsPlatformInterface) CheckPlatformShell() error {
|
||||
return os.ErrInvalid
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *windowsPlatformInterface) OpenShellSession(user *adapter.PlatformUser, command string, environ []string, term string, rows int32, cols int32) (adapter.ShellSession, error) {
|
||||
|
|
@ -184,11 +188,29 @@ func (p *windowsPlatformInterface) OpenShellSession(user *adapter.PlatformUser,
|
|||
}
|
||||
|
||||
func (p *windowsPlatformInterface) LookupUser(username string) (*adapter.PlatformUser, error) {
|
||||
return nil, os.ErrInvalid
|
||||
requestedUser, err := user.Lookup(username)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "lookup Windows user")
|
||||
}
|
||||
return &adapter.PlatformUser{
|
||||
Username: requestedUser.Username,
|
||||
Uid: os.Getuid(),
|
||||
Gid: os.Getgid(),
|
||||
HomeDir: requestedUser.HomeDir,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *windowsPlatformInterface) LookupSFTPServer() (string, error) {
|
||||
return "", os.ErrInvalid
|
||||
for _, sftpPath := range []string{
|
||||
filepath.Join(os.Getenv("SystemRoot"), "System32", "OpenSSH", "sftp-server.exe"),
|
||||
filepath.Join(os.Getenv("ProgramFiles"), "OpenSSH", "sftp-server.exe"),
|
||||
} {
|
||||
_, err := os.Stat(sftpPath)
|
||||
if err == nil {
|
||||
return sftpPath, nil
|
||||
}
|
||||
}
|
||||
return "", E.New("sftp-server not found")
|
||||
}
|
||||
|
||||
func (p *windowsPlatformInterface) ReadSystemSSHHostKey() ([]byte, error) {
|
||||
|
|
@ -199,6 +221,14 @@ func (p *windowsPlatformInterface) TailscaleHostname() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func (p *windowsPlatformInterface) AcquireWindowsUserToken(localUser *adapter.PlatformUser) (windows.Token, io.Closer, error) {
|
||||
requestedUser, err := user.Lookup(localUser.Username)
|
||||
if err != nil {
|
||||
return 0, nil, E.Cause(err, "lookup Windows user")
|
||||
}
|
||||
return acquireWindowsUserSession(requestedUser)
|
||||
}
|
||||
|
||||
func (p *windowsPlatformInterface) UsePlatformBridge() bool {
|
||||
return false
|
||||
}
|
||||
|
|
@ -471,7 +501,9 @@ func runImpersonated(token windows.Token, operation func() error) error {
|
|||
|
||||
func querySessionImpersonationToken(sessionID uint32) (windows.Token, error) {
|
||||
var primaryToken windows.Token
|
||||
err := windows.WTSQueryUserToken(sessionID, &primaryToken)
|
||||
err := winio.RunWithPrivileges([]string{seTcbPrivilege}, func() error {
|
||||
return windows.WTSQueryUserToken(sessionID, &primaryToken)
|
||||
})
|
||||
if err != nil {
|
||||
return 0, E.Cause(err, "query session user token")
|
||||
}
|
||||
|
|
|
|||
376
experimental/boxdd/user_token_windows.go
Normal file
376
experimental/boxdd/user_token_windows.go
Normal file
|
|
@ -0,0 +1,376 @@
|
|||
//go:build windows
|
||||
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os/user"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/tailscale/util/winutil"
|
||||
"github.com/sagernet/tailscale/util/winutil/winenv"
|
||||
|
||||
winio "github.com/tailscale/go-winio"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const (
|
||||
windowsLogonSource = "singbox"
|
||||
kerberosPackageName = "Kerberos"
|
||||
msv1PackageName = "MICROSOFT_AUTHENTICATION_PACKAGE_V1_0"
|
||||
kerbS4ULogon int32 = 12
|
||||
msv1S4ULogonMessage int32 = 12
|
||||
s4uCheckLogonHours uint32 = 0x2
|
||||
networkLogon int32 = 3
|
||||
tokenSourceLength = 8
|
||||
seBackupPrivilege = "SeBackupPrivilege"
|
||||
seRestorePrivilege = "SeRestorePrivilege"
|
||||
)
|
||||
|
||||
type (
|
||||
lsaHandle windows.Handle
|
||||
lsaOperationalMode uint32
|
||||
)
|
||||
|
||||
type kerberosS4ULogon struct {
|
||||
MessageType int32
|
||||
Flags uint32
|
||||
ClientUPN windows.NTUnicodeString
|
||||
ClientRealm windows.NTUnicodeString
|
||||
}
|
||||
|
||||
type msv1S4ULogon struct {
|
||||
MessageType int32
|
||||
Flags uint32
|
||||
UserPrincipalName windows.NTUnicodeString
|
||||
DomainName windows.NTUnicodeString
|
||||
}
|
||||
|
||||
type tokenSource struct {
|
||||
SourceName [tokenSourceLength]byte
|
||||
SourceIdentifier windows.LUID
|
||||
}
|
||||
|
||||
type quotaLimits struct {
|
||||
PagedPoolLimit uintptr
|
||||
NonPagedPoolLimit uintptr
|
||||
MinimumWorkingSetSize uintptr
|
||||
MaximumWorkingSetSize uintptr
|
||||
PagefileLimit uintptr
|
||||
TimeLimit int64
|
||||
}
|
||||
|
||||
func acquireWindowsUserSession(requestedUser *user.User) (windows.Token, io.Closer, error) {
|
||||
var (
|
||||
primaryToken windows.Token
|
||||
profile *winutil.UserProfile
|
||||
)
|
||||
err := winio.RunWithPrivileges([]string{seTcbPrivilege, seBackupPrivilege, seRestorePrivilege}, func() error {
|
||||
impersonationToken, err := logonWindowsUserS4U(requestedUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer impersonationToken.Close()
|
||||
primaryToken, err = duplicatePrimaryToken(impersonationToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tokenUser, err := primaryToken.GetTokenUser()
|
||||
if err != nil {
|
||||
return E.Cause(err, "query S4U token user")
|
||||
}
|
||||
if !strings.EqualFold(tokenUser.User.Sid.String(), requestedUser.Uid) {
|
||||
return E.New("S4U token identity does not match requested Windows user")
|
||||
}
|
||||
profile, err = winutil.LoadUserProfile(primaryToken, requestedUser)
|
||||
if err != nil {
|
||||
return E.Cause(err, "load Windows user profile")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
if primaryToken != 0 {
|
||||
err = E.Errors(err, primaryToken.Close())
|
||||
}
|
||||
return 0, nil, err
|
||||
}
|
||||
return primaryToken, common.Closer(func() error {
|
||||
profileError := winio.RunWithPrivileges([]string{seBackupPrivilege, seRestorePrivilege}, profile.Close)
|
||||
return E.Errors(profileError, primaryToken.Close())
|
||||
}), nil
|
||||
}
|
||||
|
||||
func logonWindowsUserS4U(requestedUser *user.User) (token windows.Token, err error) {
|
||||
processName, err := windows.NewNTString(windowsLogonSource)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var (
|
||||
handle lsaHandle
|
||||
mode lsaOperationalMode
|
||||
)
|
||||
status := lsaRegisterLogonProcess(processName, &handle, &mode)
|
||||
if status != 0 {
|
||||
return 0, E.Cause(status, "register LSA logon process")
|
||||
}
|
||||
defer func() {
|
||||
closeStatus := lsaDeregisterLogonProcess(handle)
|
||||
if closeStatus != 0 {
|
||||
err = E.Errors(err, E.Cause(closeStatus, "deregister LSA logon process"))
|
||||
}
|
||||
}()
|
||||
|
||||
username, domainUser, err := classifyWindowsUser(requestedUser.Username)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var (
|
||||
packageName string
|
||||
authenticationInformation unsafe.Pointer
|
||||
authenticationInformationLength uint32
|
||||
)
|
||||
if domainUser {
|
||||
if !winenv.IsDomainJoined() {
|
||||
return 0, E.New("cannot log on as a domain user from a Windows device that is not domain joined")
|
||||
}
|
||||
packageName = kerberosPackageName
|
||||
upn, err := samAccountNameToUPN(username)
|
||||
if err != nil {
|
||||
return 0, E.Cause(err, "resolve Windows user principal name")
|
||||
}
|
||||
upn16, err := windows.UTF16FromString(upn)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
logonInfo, logonInfoLen, buffers := winutil.AllocateContiguousBuffer[kerberosS4ULogon](upn16)
|
||||
logonInfo.MessageType = kerbS4ULogon
|
||||
logonInfo.Flags = s4uCheckLogonHours
|
||||
winutil.SetNTString(&logonInfo.ClientUPN, buffers[0])
|
||||
authenticationInformation = unsafe.Pointer(logonInfo)
|
||||
authenticationInformationLength = logonInfoLen
|
||||
} else {
|
||||
packageName = msv1PackageName
|
||||
username16, err := windows.UTF16FromString(username)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
thisComputer := []uint16{'.', 0}
|
||||
logonInfo, logonInfoLen, buffers := winutil.AllocateContiguousBuffer[msv1S4ULogon](username16, thisComputer)
|
||||
logonInfo.MessageType = msv1S4ULogonMessage
|
||||
logonInfo.Flags = s4uCheckLogonHours
|
||||
winutil.SetNTString(&logonInfo.UserPrincipalName, buffers[0])
|
||||
winutil.SetNTString(&logonInfo.DomainName, buffers[1])
|
||||
authenticationInformation = unsafe.Pointer(logonInfo)
|
||||
authenticationInformationLength = logonInfoLen
|
||||
}
|
||||
|
||||
packageString, err := windows.NewNTString(packageName)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var packageID uint32
|
||||
status = lsaLookupAuthenticationPackage(handle, packageString, &packageID)
|
||||
if status != 0 {
|
||||
return 0, E.Cause(status, "lookup LSA authentication package")
|
||||
}
|
||||
|
||||
var source tokenSource
|
||||
copy(source.SourceName[:], windowsLogonSource)
|
||||
err = allocateLocallyUniqueID(&source.SourceIdentifier)
|
||||
if err != nil {
|
||||
return 0, E.Cause(err, "allocate LSA logon identifier")
|
||||
}
|
||||
originName, err := windows.NewNTString(windowsLogonSource)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var (
|
||||
profileBuffer uintptr
|
||||
profileBufferLength uint32
|
||||
logonID windows.LUID
|
||||
quotas quotaLimits
|
||||
subStatus windows.NTStatus
|
||||
)
|
||||
status = lsaLogonUser(
|
||||
handle,
|
||||
originName,
|
||||
networkLogon,
|
||||
packageID,
|
||||
authenticationInformation,
|
||||
authenticationInformationLength,
|
||||
nil,
|
||||
&source,
|
||||
&profileBuffer,
|
||||
&profileBufferLength,
|
||||
&logonID,
|
||||
&token,
|
||||
"as,
|
||||
&subStatus,
|
||||
)
|
||||
if profileBuffer != 0 {
|
||||
defer lsaFreeReturnBuffer(profileBuffer)
|
||||
}
|
||||
if status != 0 {
|
||||
return 0, E.New("S4U logon for ", requestedUser.Username, " failed: ", status, ", substatus: ", subStatus)
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func classifyWindowsUser(username string) (sanitizedUsername string, domainUser bool, err error) {
|
||||
domain, account, hasDomain := strings.Cut(username, `\`)
|
||||
if !hasDomain {
|
||||
return username, false, nil
|
||||
}
|
||||
if domain == "." {
|
||||
return account, false, nil
|
||||
}
|
||||
computerName, err := windows.ComputerName()
|
||||
if err != nil {
|
||||
return "", false, E.Cause(err, "query Windows computer name")
|
||||
}
|
||||
if strings.EqualFold(domain, computerName) {
|
||||
return account, false, nil
|
||||
}
|
||||
return username, true, nil
|
||||
}
|
||||
|
||||
func samAccountNameToUPN(samAccountName string) (string, error) {
|
||||
_, account, _ := strings.Cut(samAccountName, `\`)
|
||||
upn, err := windows.TranslateAccountName(samAccountName, windows.NameSamCompatible, windows.NameUserPrincipal, 50)
|
||||
if err == nil {
|
||||
return upn, nil
|
||||
}
|
||||
canonicalName, canonicalError := windows.TranslateAccountName(samAccountName, windows.NameSamCompatible, windows.NameCanonical, 50)
|
||||
if canonicalError != nil {
|
||||
return "", E.Errors(err, canonicalError)
|
||||
}
|
||||
domain, _, found := strings.Cut(canonicalName, "/")
|
||||
if !found || domain == "" {
|
||||
return "", E.New("invalid canonical domain name for ", samAccountName)
|
||||
}
|
||||
return account + "@" + domain, nil
|
||||
}
|
||||
|
||||
func duplicatePrimaryToken(impersonationToken windows.Token) (windows.Token, error) {
|
||||
securityDescriptor, err := windows.GetSecurityInfo(
|
||||
windows.Handle(impersonationToken),
|
||||
windows.SE_KERNEL_OBJECT,
|
||||
windows.DACL_SECURITY_INFORMATION,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, E.Cause(err, "query S4U token security")
|
||||
}
|
||||
securityAttributes := windows.SecurityAttributes{
|
||||
Length: uint32(unsafe.Sizeof(windows.SecurityAttributes{})),
|
||||
SecurityDescriptor: securityDescriptor,
|
||||
}
|
||||
var primaryToken windows.Token
|
||||
err = windows.DuplicateTokenEx(
|
||||
impersonationToken,
|
||||
0,
|
||||
&securityAttributes,
|
||||
windows.SecurityImpersonation,
|
||||
windows.TokenPrimary,
|
||||
&primaryToken,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, E.Cause(err, "duplicate S4U primary token")
|
||||
}
|
||||
return primaryToken, nil
|
||||
}
|
||||
|
||||
var (
|
||||
modAdvapi32 = windows.NewLazySystemDLL("advapi32.dll")
|
||||
modSecur32 = windows.NewLazySystemDLL("secur32.dll")
|
||||
|
||||
procAllocateLocallyUniqueID = modAdvapi32.NewProc("AllocateLocallyUniqueId")
|
||||
procLsaDeregisterLogonProcess = modSecur32.NewProc("LsaDeregisterLogonProcess")
|
||||
procLsaFreeReturnBuffer = modSecur32.NewProc("LsaFreeReturnBuffer")
|
||||
procLsaLogonUser = modSecur32.NewProc("LsaLogonUser")
|
||||
procLsaLookupAuthenticationPackage = modSecur32.NewProc("LsaLookupAuthenticationPackage")
|
||||
procLsaRegisterLogonProcess = modSecur32.NewProc("LsaRegisterLogonProcess")
|
||||
)
|
||||
|
||||
func allocateLocallyUniqueID(luid *windows.LUID) error {
|
||||
result, _, callError := syscall.SyscallN(procAllocateLocallyUniqueID.Addr(), uintptr(unsafe.Pointer(luid)))
|
||||
if result == 0 {
|
||||
if callError == 0 {
|
||||
return syscall.EINVAL
|
||||
}
|
||||
return callError
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func lsaDeregisterLogonProcess(handle lsaHandle) windows.NTStatus {
|
||||
result, _, _ := syscall.SyscallN(procLsaDeregisterLogonProcess.Addr(), uintptr(handle))
|
||||
return windows.NTStatus(result)
|
||||
}
|
||||
|
||||
func lsaFreeReturnBuffer(buffer uintptr) windows.NTStatus {
|
||||
result, _, _ := syscall.SyscallN(procLsaFreeReturnBuffer.Addr(), buffer)
|
||||
return windows.NTStatus(result)
|
||||
}
|
||||
|
||||
func lsaLookupAuthenticationPackage(handle lsaHandle, packageName *windows.NTString, packageID *uint32) windows.NTStatus {
|
||||
result, _, _ := syscall.SyscallN(
|
||||
procLsaLookupAuthenticationPackage.Addr(),
|
||||
uintptr(handle),
|
||||
uintptr(unsafe.Pointer(packageName)),
|
||||
uintptr(unsafe.Pointer(packageID)),
|
||||
)
|
||||
return windows.NTStatus(result)
|
||||
}
|
||||
|
||||
func lsaRegisterLogonProcess(processName *windows.NTString, handle *lsaHandle, mode *lsaOperationalMode) windows.NTStatus {
|
||||
result, _, _ := syscall.SyscallN(
|
||||
procLsaRegisterLogonProcess.Addr(),
|
||||
uintptr(unsafe.Pointer(processName)),
|
||||
uintptr(unsafe.Pointer(handle)),
|
||||
uintptr(unsafe.Pointer(mode)),
|
||||
)
|
||||
return windows.NTStatus(result)
|
||||
}
|
||||
|
||||
func lsaLogonUser(
|
||||
handle lsaHandle,
|
||||
originName *windows.NTString,
|
||||
logonType int32,
|
||||
authenticationPackage uint32,
|
||||
authenticationInformation unsafe.Pointer,
|
||||
authenticationInformationLength uint32,
|
||||
localGroups *windows.Tokengroups,
|
||||
sourceContext *tokenSource,
|
||||
profileBuffer *uintptr,
|
||||
profileBufferLength *uint32,
|
||||
logonID *windows.LUID,
|
||||
token *windows.Token,
|
||||
quotas *quotaLimits,
|
||||
subStatus *windows.NTStatus,
|
||||
) windows.NTStatus {
|
||||
result, _, _ := syscall.SyscallN(
|
||||
procLsaLogonUser.Addr(),
|
||||
uintptr(handle),
|
||||
uintptr(unsafe.Pointer(originName)),
|
||||
uintptr(logonType),
|
||||
uintptr(authenticationPackage),
|
||||
uintptr(authenticationInformation),
|
||||
uintptr(authenticationInformationLength),
|
||||
uintptr(unsafe.Pointer(localGroups)),
|
||||
uintptr(unsafe.Pointer(sourceContext)),
|
||||
uintptr(unsafe.Pointer(profileBuffer)),
|
||||
uintptr(unsafe.Pointer(profileBufferLength)),
|
||||
uintptr(unsafe.Pointer(logonID)),
|
||||
uintptr(unsafe.Pointer(token)),
|
||||
uintptr(unsafe.Pointer(quotas)),
|
||||
uintptr(unsafe.Pointer(subStatus)),
|
||||
)
|
||||
return windows.NTStatus(result)
|
||||
}
|
||||
|
|
@ -556,7 +556,7 @@ func (s *Server) handleSession(session gliderssh.Session) {
|
|||
session.Exit(1)
|
||||
return
|
||||
}
|
||||
err = verifyShellIdentity(localUser)
|
||||
err = verifyShellIdentity(s.platformInterface, localUser)
|
||||
if err != nil {
|
||||
s.logger.Warn("shell rejected for ", localUser.Username, ": ", err)
|
||||
fmt.Fprintf(session.Stderr(), "%s\r\n", err)
|
||||
|
|
@ -565,17 +565,13 @@ func (s *Server) handleSession(session gliderssh.Session) {
|
|||
}
|
||||
var agentSocketPath string
|
||||
if connInfo.action.AllowAgentForwarding && !s.disableForwarding && gliderssh.AgentRequested(session) {
|
||||
agentListener, listenErr := gliderssh.NewAgentListener()
|
||||
if listenErr == nil {
|
||||
agentListener, err := newAgentListener(localUser)
|
||||
if err == nil {
|
||||
defer agentListener.Close()
|
||||
agentSocketPath = agentListener.Addr().String()
|
||||
// The agent socket is created as the server identity; hand it to the
|
||||
// target user so SSH_AUTH_SOCK is reachable after privileges drop.
|
||||
prepareErr := prepareAgentSocket(agentSocketPath, localUser.Uid, localUser.Gid)
|
||||
if prepareErr != nil {
|
||||
s.logger.Warn("prepare agent socket: ", prepareErr)
|
||||
}
|
||||
go gliderssh.ForwardAgentConnections(agentListener, session)
|
||||
} else {
|
||||
s.logger.Warn("create agent listener: ", err)
|
||||
}
|
||||
}
|
||||
env := s.buildEnvironment(session, connInfo, localUser)
|
||||
|
|
@ -748,7 +744,7 @@ func (s *Server) handleSFTP(ctx context.Context, session gliderssh.Session, conn
|
|||
s.serveBuiltinSFTP(ctx, session, localUser)
|
||||
return
|
||||
}
|
||||
err = verifyShellIdentity(localUser)
|
||||
err = verifyShellIdentity(s.platformInterface, localUser)
|
||||
if err != nil {
|
||||
s.logger.Warn("sftp rejected for ", localUser.Username, ": ", err)
|
||||
fmt.Fprintf(session.Stderr(), "%s\r\n", err)
|
||||
|
|
@ -758,7 +754,7 @@ func (s *Server) handleSFTP(ctx context.Context, session gliderssh.Session, conn
|
|||
env := s.buildEnvironment(session, connInfo, localUser)
|
||||
sftpSession, err := s.backend.OpenSession(shellRequest{
|
||||
User: localUser,
|
||||
Command: sftpCommand(sftpPath),
|
||||
Command: sftpCommand(sftpPath, localUser.Shell),
|
||||
Env: env,
|
||||
})
|
||||
if err != nil {
|
||||
|
|
@ -811,8 +807,11 @@ func (s *Server) buildEnvironment(session gliderssh.Session, connInfo *sshConnIn
|
|||
"USER="+localUser.Username,
|
||||
"HOME="+localUser.HomeDir,
|
||||
"SHELL="+localUser.Shell,
|
||||
"PATH="+defaultPathEnv(),
|
||||
)
|
||||
defaultPath := defaultPathEnv(s.platformInterface)
|
||||
if defaultPath != "" {
|
||||
env = append(env, "PATH="+defaultPath)
|
||||
}
|
||||
env = append(env, platformEnvironment(localUser)...)
|
||||
remoteAddr := session.RemoteAddr()
|
||||
localAddr := session.LocalAddr()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
package tailssh
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
|
@ -22,7 +23,7 @@ func requestedUserMatchesProcess(localUser *adapter.PlatformUser) (bool, error)
|
|||
|
||||
// verifyShellIdentity is a no-op on Unix: spawned shells and sftp-server drop to the
|
||||
// requested user via setCredential, so the child already runs as that user.
|
||||
func verifyShellIdentity(_ *adapter.PlatformUser) error {
|
||||
func verifyShellIdentity(_ adapter.PlatformInterface, _ *adapter.PlatformUser) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -30,7 +31,7 @@ func systemHostKeyPath() string {
|
|||
return "/etc/ssh/ssh_host_ed25519_key"
|
||||
}
|
||||
|
||||
func defaultPathEnv() string {
|
||||
func defaultPathEnv(_ adapter.PlatformInterface) string {
|
||||
return "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
}
|
||||
|
||||
|
|
@ -38,31 +39,40 @@ func userSocketDirectories(localUser *adapter.PlatformUser) []string {
|
|||
return gliderssh.UserSocketDirectories(localUser.HomeDir, strconv.Itoa(localUser.Uid))
|
||||
}
|
||||
|
||||
// prepareAgentSocket hands the agent-forwarding socket to the target user so
|
||||
// SSH_AUTH_SOCK stays reachable after the shell drops privileges. No-op when the
|
||||
// shell runs as the server identity.
|
||||
func prepareAgentSocket(socketPath string, uid, gid int) error {
|
||||
if uid < 0 || uid == os.Getuid() {
|
||||
return nil
|
||||
}
|
||||
err := os.Chown(socketPath, uid, gid)
|
||||
func newAgentListener(localUser *adapter.PlatformUser) (net.Listener, error) {
|
||||
listener, err := gliderssh.NewAgentListener()
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
socketPath := listener.Addr().String()
|
||||
if localUser.Uid < 0 || localUser.Uid == os.Getuid() {
|
||||
return listener, nil
|
||||
}
|
||||
err = os.Chown(socketPath, localUser.Uid, localUser.Gid)
|
||||
if err != nil {
|
||||
listener.Close()
|
||||
return nil, err
|
||||
}
|
||||
err = os.Chmod(socketPath, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
listener.Close()
|
||||
return nil, err
|
||||
}
|
||||
// Make the MkdirTemp parent traversable so the dropped-privilege child can
|
||||
// reach the socket.
|
||||
return os.Chmod(filepath.Dir(socketPath), 0o755)
|
||||
err = os.Chmod(filepath.Dir(socketPath), 0o755)
|
||||
if err != nil {
|
||||
listener.Close()
|
||||
return nil, err
|
||||
}
|
||||
return listener, nil
|
||||
}
|
||||
|
||||
func platformEnvironment(_ *adapter.PlatformUser) []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func sftpCommand(sftpPath string) string {
|
||||
func sftpCommand(sftpPath, _ string) string {
|
||||
return sftpPath + " 2>/dev/null"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@
|
|||
package tailssh
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/user"
|
||||
"strings"
|
||||
|
|
@ -12,6 +15,7 @@ import (
|
|||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/tailscale/util/winutil"
|
||||
|
||||
winio "github.com/tailscale/go-winio"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
|
|
@ -19,10 +23,6 @@ func isPrivilegedUser() bool {
|
|||
return winutil.IsCurrentProcessElevated()
|
||||
}
|
||||
|
||||
// requestedUserMatchesProcess reports whether the ACL-mapped user is the same Windows
|
||||
// account the sing-box process runs as. Windows has no impersonation wired up, so a
|
||||
// session always runs with the process identity; this is the only case where the
|
||||
// identity it runs as equals the requested one.
|
||||
func requestedUserMatchesProcess(localUser *adapter.PlatformUser) (bool, error) {
|
||||
tokenUser, err := windows.GetCurrentProcessToken().GetTokenUser()
|
||||
if err != nil {
|
||||
|
|
@ -36,9 +36,13 @@ func requestedUserMatchesProcess(localUser *adapter.PlatformUser) (bool, error)
|
|||
return strings.EqualFold(tokenUser.User.Sid.String(), requested.Uid), nil
|
||||
}
|
||||
|
||||
// verifyShellIdentity refuses a spawned shell/SFTP session whose ACL-mapped user differs
|
||||
// from the process identity it would actually run as, since Windows has no impersonation.
|
||||
func verifyShellIdentity(localUser *adapter.PlatformUser) error {
|
||||
func verifyShellIdentity(platformInterface adapter.PlatformInterface, localUser *adapter.PlatformUser) error {
|
||||
if platformInterface != nil && platformInterface.UsePlatformShell() {
|
||||
_, loaded := platformInterface.(windowsUserTokenProvider)
|
||||
if loaded {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
match, err := requestedUserMatchesProcess(localUser)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -53,7 +57,10 @@ func systemHostKeyPath() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func defaultPathEnv() string {
|
||||
func defaultPathEnv(platformInterface adapter.PlatformInterface) string {
|
||||
if platformInterface != nil && platformInterface.UsePlatformShell() {
|
||||
return ""
|
||||
}
|
||||
systemRoot := os.Getenv("SystemRoot")
|
||||
return systemRoot + `\system32;` + systemRoot + `;` + systemRoot + `\System32\Wbem`
|
||||
}
|
||||
|
|
@ -62,10 +69,22 @@ func userSocketDirectories(localUser *adapter.PlatformUser) []string {
|
|||
return []string{localUser.HomeDir, os.TempDir()}
|
||||
}
|
||||
|
||||
// prepareAgentSocket is a no-op on Windows: shells run as the server identity, so
|
||||
// the agent socket needs no ownership change.
|
||||
func prepareAgentSocket(_ string, _, _ int) error {
|
||||
return nil
|
||||
func newAgentListener(localUser *adapter.PlatformUser) (net.Listener, error) {
|
||||
requestedUser, err := user.Lookup(localUser.Username)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "lookup requested user")
|
||||
}
|
||||
pipePath := `\\.\pipe\sing-box-tailssh-agent-` + rand.Text()
|
||||
securityDescriptor := fmt.Sprintf(`D:P(A;;GA;;;SY)(A;;GRGW;;;%s)`, requestedUser.Uid)
|
||||
listener, err := winio.ListenPipe(pipePath, &winio.PipeConfig{
|
||||
SecurityDescriptor: securityDescriptor,
|
||||
InputBufferSize: 64 * 1024,
|
||||
OutputBufferSize: 64 * 1024,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "listen on agent pipe")
|
||||
}
|
||||
return listener, nil
|
||||
}
|
||||
|
||||
func platformEnvironment(localUser *adapter.PlatformUser) []string {
|
||||
|
|
@ -80,8 +99,11 @@ func platformEnvironment(localUser *adapter.PlatformUser) []string {
|
|||
return env
|
||||
}
|
||||
|
||||
func sftpCommand(sftpPath string) string {
|
||||
return sftpPath
|
||||
func sftpCommand(sftpPath, shell string) string {
|
||||
if isPowerShell(shell) {
|
||||
return `& "` + sftpPath + `"`
|
||||
}
|
||||
return `"` + sftpPath + `"`
|
||||
}
|
||||
|
||||
func sshSignalToSyscall(sig gliderssh.Signal) int {
|
||||
|
|
|
|||
|
|
@ -16,18 +16,45 @@ import (
|
|||
"github.com/sagernet/tailscale/util/winutil"
|
||||
"github.com/sagernet/tailscale/util/winutil/conpty"
|
||||
|
||||
"github.com/tailscale/go-winio"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func selectShellBackend(_ adapter.PlatformInterface) shellBackend {
|
||||
return &windowsShellBackend{}
|
||||
const (
|
||||
seAssignPrimaryToken = "SeAssignPrimaryTokenPrivilege"
|
||||
seIncreaseQuota = "SeIncreaseQuotaPrivilege"
|
||||
)
|
||||
|
||||
type windowsUserTokenProvider interface {
|
||||
AcquireWindowsUserToken(user *adapter.PlatformUser) (windows.Token, io.Closer, error)
|
||||
}
|
||||
|
||||
func CheckServerSupport(_ adapter.PlatformInterface) (string, error) {
|
||||
func selectShellBackend(platformInterface adapter.PlatformInterface) shellBackend {
|
||||
backend := &windowsShellBackend{}
|
||||
if platformInterface != nil && platformInterface.UsePlatformShell() {
|
||||
backend.userTokenProvider, _ = platformInterface.(windowsUserTokenProvider)
|
||||
}
|
||||
return backend
|
||||
}
|
||||
|
||||
func CheckServerSupport(platformInterface adapter.PlatformInterface) (string, error) {
|
||||
if platformInterface != nil && platformInterface.UsePlatformShell() {
|
||||
_, loaded := platformInterface.(windowsUserTokenProvider)
|
||||
if !loaded {
|
||||
return "", E.New("platform shell does not provide Windows user tokens")
|
||||
}
|
||||
err := platformInterface.CheckPlatformShell()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func lookupSFTPServer(_ adapter.PlatformInterface) (string, error) {
|
||||
func lookupSFTPServer(platformInterface adapter.PlatformInterface) (string, error) {
|
||||
if platformInterface != nil && platformInterface.UsePlatformShell() {
|
||||
return platformInterface.LookupSFTPServer()
|
||||
}
|
||||
sftpPath, err := exec.LookPath("sftp-server")
|
||||
if err != nil {
|
||||
return "", E.New("sftp-server not found")
|
||||
|
|
@ -35,20 +62,48 @@ func lookupSFTPServer(_ adapter.PlatformInterface) (string, error) {
|
|||
return sftpPath, nil
|
||||
}
|
||||
|
||||
type windowsShellBackend struct{}
|
||||
type windowsShellBackend struct {
|
||||
userTokenProvider windowsUserTokenProvider
|
||||
}
|
||||
|
||||
func (b *windowsShellBackend) OpenSession(request shellRequest) (shellSession, error) {
|
||||
func (b *windowsShellBackend) OpenSession(request shellRequest) (session shellSession, err error) {
|
||||
var (
|
||||
userToken windows.Token
|
||||
userResource io.Closer
|
||||
)
|
||||
if b.userTokenProvider != nil {
|
||||
userToken, userResource, err = b.userTokenProvider.AcquireWindowsUserToken(request.User)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = E.Errors(err, userResource.Close())
|
||||
}
|
||||
}()
|
||||
userEnvironment, err := userToken.Environ(false)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "query user environment")
|
||||
}
|
||||
request.Env = mergeWindowsEnvironment(userEnvironment, request.Env)
|
||||
}
|
||||
shell := request.User.Shell
|
||||
if request.Term != "" {
|
||||
session, err := openConPTYSession(request, shell)
|
||||
if err == nil {
|
||||
return session, nil
|
||||
}
|
||||
if !errors.Is(err, conpty.ErrUnsupported) {
|
||||
session, err = openConPTYSession(request, shell, userToken)
|
||||
if err != nil && !errors.Is(err, conpty.ErrUnsupported) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return openPipeSession(request, shell)
|
||||
if request.Term == "" || err != nil {
|
||||
session, err = openPipeSession(request, shell, userToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if userResource != nil {
|
||||
session = &windowsUserShellSession{shellSession: session, resource: userResource}
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (b *windowsShellBackend) Close() error {
|
||||
|
|
@ -59,15 +114,51 @@ func buildCommandLine(shell, command string) string {
|
|||
if command == "" {
|
||||
return `"` + shell + `"`
|
||||
}
|
||||
base := strings.ToLower(filepath.Base(shell))
|
||||
switch base {
|
||||
case "pwsh.exe", "powershell.exe":
|
||||
if isPowerShell(shell) {
|
||||
// -NoProfile/-NonInteractive keep the invoking user's PowerShell profile from
|
||||
// writing into the (binary) SFTP/stdout stream and corrupting it.
|
||||
return `"` + shell + `" -NoLogo -NoProfile -NonInteractive -Command ` + command
|
||||
default:
|
||||
return `"` + shell + `" /c ` + command
|
||||
}
|
||||
return `"` + shell + `" /c ` + command
|
||||
}
|
||||
|
||||
func isPowerShell(shell string) bool {
|
||||
switch strings.ToLower(filepath.Base(shell)) {
|
||||
case "pwsh", "pwsh.exe", "powershell", "powershell.exe":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func mergeWindowsEnvironment(baseEnvironment, overrideEnvironment []string) []string {
|
||||
environment := make([]string, 0, len(baseEnvironment)+len(overrideEnvironment))
|
||||
variableIndex := make(map[string]int, len(baseEnvironment)+len(overrideEnvironment))
|
||||
for _, variables := range [][]string{baseEnvironment, overrideEnvironment} {
|
||||
for _, variable := range variables {
|
||||
name := windowsEnvironmentName(variable)
|
||||
index, loaded := variableIndex[name]
|
||||
if loaded {
|
||||
environment[index] = variable
|
||||
} else {
|
||||
variableIndex[name] = len(environment)
|
||||
environment = append(environment, variable)
|
||||
}
|
||||
}
|
||||
}
|
||||
return environment
|
||||
}
|
||||
|
||||
func windowsEnvironmentName(variable string) string {
|
||||
start := 0
|
||||
if strings.HasPrefix(variable, "=") {
|
||||
start = 1
|
||||
}
|
||||
separator := strings.IndexByte(variable[start:], '=')
|
||||
if separator == -1 {
|
||||
return strings.ToLower(variable)
|
||||
}
|
||||
return strings.ToLower(variable[:start+separator])
|
||||
}
|
||||
|
||||
// clampConsoleDimension keeps a client-supplied window dimension within the
|
||||
|
|
@ -83,7 +174,7 @@ func clampConsoleDimension(value uint16) int16 {
|
|||
return int16(value)
|
||||
}
|
||||
|
||||
func createShellProcess(shell string, request shellRequest, startupInfo *windows.StartupInfo, inheritHandles bool, createProcessFlags uint32) (windows.Handle, error) {
|
||||
func createShellProcess(shell string, request shellRequest, startupInfo *windows.StartupInfo, inheritHandles bool, createProcessFlags uint32, userToken windows.Token) (windows.Handle, error) {
|
||||
cmdLine := buildCommandLine(shell, request.Command)
|
||||
cmdLine16, err := windows.UTF16PtrFromString(cmdLine)
|
||||
if err != nil {
|
||||
|
|
@ -105,28 +196,40 @@ func createShellProcess(shell string, request shellRequest, startupInfo *windows
|
|||
// NewEnvBlock requires the variables sorted case-insensitively by name.
|
||||
envCopy := slices.Clone(request.Env)
|
||||
slices.SortFunc(envCopy, func(a, b string) int {
|
||||
aName, _, _ := strings.Cut(a, "=")
|
||||
bName, _, _ := strings.Cut(b, "=")
|
||||
return strings.Compare(strings.ToLower(aName), strings.ToLower(bName))
|
||||
return strings.Compare(windowsEnvironmentName(a), windowsEnvironmentName(b))
|
||||
})
|
||||
envBlock := winutil.NewEnvBlock(envCopy)
|
||||
var processInfo windows.ProcessInformation
|
||||
// request.User only sets HomeDir and Env here; the child inherits the sing-box
|
||||
// process identity because Windows impersonation is not implemented. Sessions
|
||||
// whose requested user differs from the process identity are refused before
|
||||
// reaching this point (verifyShellIdentity in handleSession/handleSFTP).
|
||||
err = windows.CreateProcess(
|
||||
exe16,
|
||||
cmdLine16,
|
||||
nil,
|
||||
nil,
|
||||
inheritHandles,
|
||||
createProcessFlags|windows.CREATE_NEW_PROCESS_GROUP,
|
||||
envBlock,
|
||||
dir16,
|
||||
startupInfo,
|
||||
&processInfo,
|
||||
)
|
||||
if userToken == 0 {
|
||||
err = windows.CreateProcess(
|
||||
exe16,
|
||||
cmdLine16,
|
||||
nil,
|
||||
nil,
|
||||
inheritHandles,
|
||||
createProcessFlags|windows.CREATE_NEW_PROCESS_GROUP,
|
||||
envBlock,
|
||||
dir16,
|
||||
startupInfo,
|
||||
&processInfo,
|
||||
)
|
||||
} else {
|
||||
err = winio.RunWithPrivileges([]string{seAssignPrimaryToken, seIncreaseQuota}, func() error {
|
||||
return windows.CreateProcessAsUser(
|
||||
userToken,
|
||||
exe16,
|
||||
cmdLine16,
|
||||
nil,
|
||||
nil,
|
||||
inheritHandles,
|
||||
createProcessFlags|windows.CREATE_NEW_PROCESS_GROUP,
|
||||
envBlock,
|
||||
dir16,
|
||||
startupInfo,
|
||||
&processInfo,
|
||||
)
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
return 0, E.Cause(err, "create process")
|
||||
}
|
||||
|
|
@ -134,6 +237,15 @@ func createShellProcess(shell string, request shellRequest, startupInfo *windows
|
|||
return processInfo.Process, nil
|
||||
}
|
||||
|
||||
type windowsUserShellSession struct {
|
||||
shellSession
|
||||
resource io.Closer
|
||||
}
|
||||
|
||||
func (s *windowsUserShellSession) Close() error {
|
||||
return E.Errors(s.shellSession.Close(), s.resource.Close())
|
||||
}
|
||||
|
||||
type conptyShellSession struct {
|
||||
console *conpty.PseudoConsole
|
||||
input io.WriteCloser
|
||||
|
|
@ -143,7 +255,7 @@ type conptyShellSession struct {
|
|||
exitCode uint32
|
||||
}
|
||||
|
||||
func openConPTYSession(request shellRequest, shell string) (shellSession, error) {
|
||||
func openConPTYSession(request shellRequest, shell string, userToken windows.Token) (shellSession, error) {
|
||||
cols := request.Cols
|
||||
rows := request.Rows
|
||||
if cols == 0 {
|
||||
|
|
@ -171,7 +283,7 @@ func openConPTYSession(request shellRequest, shell string) (shellSession, error)
|
|||
console.Close()
|
||||
return nil, E.Cause(err, "resolve startup info")
|
||||
}
|
||||
process, err := createShellProcess(shell, request, startupInfo, inheritHandles, createProcessFlags)
|
||||
process, err := createShellProcess(shell, request, startupInfo, inheritHandles, createProcessFlags, userToken)
|
||||
startupInfoBuilder.Close()
|
||||
if err != nil {
|
||||
console.Close()
|
||||
|
|
@ -202,7 +314,11 @@ func (s *conptyShellSession) waitProcess() {
|
|||
}
|
||||
|
||||
func (s *conptyShellSession) Read(p []byte) (int, error) {
|
||||
return s.output.Read(p)
|
||||
n, err := s.output.Read(p)
|
||||
if errors.Is(err, os.ErrClosed) {
|
||||
return n, io.EOF
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (s *conptyShellSession) Write(p []byte) (int, error) {
|
||||
|
|
@ -261,7 +377,7 @@ type pipeShellSession struct {
|
|||
exitCode uint32
|
||||
}
|
||||
|
||||
func openPipeSession(request shellRequest, shell string) (shellSession, error) {
|
||||
func openPipeSession(request shellRequest, shell string, userToken windows.Token) (shellSession, error) {
|
||||
var stdinR, stdinW windows.Handle
|
||||
err := windows.CreatePipe(&stdinR, &stdinW, nil, 0)
|
||||
if err != nil {
|
||||
|
|
@ -304,7 +420,7 @@ func openPipeSession(request shellRequest, shell string) (shellSession, error) {
|
|||
windows.CloseHandle(stdoutR)
|
||||
return nil, E.Cause(err, "resolve startup info")
|
||||
}
|
||||
process, err := createShellProcess(shell, request, startupInfo, inheritHandles, createProcessFlags)
|
||||
process, err := createShellProcess(shell, request, startupInfo, inheritHandles, createProcessFlags, userToken)
|
||||
startupInfoBuilder.Close()
|
||||
if err != nil {
|
||||
windows.CloseHandle(stdinW)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue