Files
2026-07-16 00:38:30 +08:00

566 lines
14 KiB
Go

//go:build windows
package main
import (
"context"
"io"
"net/netip"
"os"
"os/user"
"path/filepath"
"runtime"
"sync"
"syscall"
"unsafe"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/settings"
"github.com/sagernet/sing-box/daemon"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
"github.com/tailscale/go-winio"
"golang.org/x/sys/windows"
)
var regDisablePredefinedCacheEx = windows.NewLazySystemDLL("advapi32.dll").NewProc("RegDisablePredefinedCacheEx")
type windowsPlatformInterface struct {
daemon *Daemon
access sync.Mutex
updateAccess sync.Mutex
updateInProgress bool
daemonSigner []byte
ownerUserID string
sessionID uint32
token windows.Token
systemProxy *settings.WindowsSystemProxy
systemProxyEnabled bool
}
func newPlatformInterface(daemonInstance *Daemon) (daemonPlatform, error) {
result, _, _ := regDisablePredefinedCacheEx.Call()
if result != 0 {
return nil, E.Cause(syscall.Errno(result), "disable predefined registry handle cache")
}
return &windowsPlatformInterface{
daemon: daemonInstance,
systemProxyEnabled: true,
}, nil
}
func (p *windowsPlatformInterface) Initialize(networkManager adapter.NetworkManager) error {
return nil
}
func (p *windowsPlatformInterface) UsePlatformAutoDetectInterfaceControl() bool {
return false
}
func (p *windowsPlatformInterface) AutoDetectInterfaceControl(fd int) error {
return os.ErrInvalid
}
func (p *windowsPlatformInterface) UsePlatformInterface() bool {
return false
}
func (p *windowsPlatformInterface) OpenInterface(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error) {
return nil, os.ErrInvalid
}
func (p *windowsPlatformInterface) ProcessPlatformOptions(options option.TunPlatformOptions) error {
if options.HTTPProxy == nil || !options.HTTPProxy.Enabled {
return nil
}
httpProxyOptions := options.HTTPProxy
systemProxy, err := settings.NewSystemProxy(
context.Background(),
M.ParseSocksaddrHostPort(httpProxyOptions.Server, httpProxyOptions.ServerPort),
false,
[]string(httpProxyOptions.BypassDomain),
)
if err != nil {
return E.Cause(err, "initialize system proxy")
}
p.access.Lock()
if p.systemProxy != nil {
p.access.Unlock()
return E.New("only one enabled `tun.platform.http_proxy` is supported")
}
p.systemProxy = systemProxy
err = p.applySystemProxyLocked()
if err != nil {
rollbackError := p.disableSystemProxyLocked()
p.systemProxy = nil
p.access.Unlock()
return E.Errors(E.Cause(err, "set system proxy"), rollbackError)
}
p.access.Unlock()
return nil
}
func (p *windowsPlatformInterface) UsePlatformDefaultInterfaceMonitor() bool {
return false
}
func (p *windowsPlatformInterface) CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor {
return nil
}
func (p *windowsPlatformInterface) UsePlatformNetworkInterfaces() bool {
return false
}
func (p *windowsPlatformInterface) NetworkInterfaces() ([]adapter.NetworkInterface, error) {
return nil, os.ErrInvalid
}
func (p *windowsPlatformInterface) UnderNetworkExtension() bool {
return false
}
func (p *windowsPlatformInterface) NetworkExtensionIncludeAllNetworks() bool {
return false
}
func (p *windowsPlatformInterface) ClearDNSCache() {
}
func (p *windowsPlatformInterface) RequestPermissionForWIFIState() error {
return nil
}
func (p *windowsPlatformInterface) ReadWIFIState() adapter.WIFIState {
return adapter.WIFIState{}
}
func (p *windowsPlatformInterface) UsePlatformConnectionOwnerFinder() bool {
return false
}
func (p *windowsPlatformInterface) FindConnectionOwner(request *adapter.FindConnectionOwnerRequest) (*adapter.ConnectionOwner, error) {
return nil, os.ErrInvalid
}
func (p *windowsPlatformInterface) UsePlatformWIFIMonitor() bool {
return false
}
func (p *windowsPlatformInterface) UsePlatformNotification() bool {
return false
}
func (p *windowsPlatformInterface) SendNotification(notification *adapter.Notification) error {
return nil
}
func (p *windowsPlatformInterface) MyInterfaceAddress() []netip.Addr {
return nil
}
func (p *windowsPlatformInterface) UsePlatformNeighborResolver() bool {
return false
}
func (p *windowsPlatformInterface) StartNeighborMonitor(listener adapter.NeighborUpdateListener) error {
return os.ErrInvalid
}
func (p *windowsPlatformInterface) CloseNeighborMonitor(listener adapter.NeighborUpdateListener) error {
return nil
}
func (p *windowsPlatformInterface) UsePlatformShell() bool {
return listenAddress == ""
}
func (p *windowsPlatformInterface) CheckPlatformShell() error {
return nil
}
func (p *windowsPlatformInterface) OpenShellSession(user *adapter.PlatformUser, command string, environ []string, term string, rows int32, cols int32) (adapter.ShellSession, error) {
return nil, os.ErrInvalid
}
func (p *windowsPlatformInterface) LookupUser(username string) (*adapter.PlatformUser, error) {
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) {
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) {
return nil, os.ErrInvalid
}
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
}
func (p *windowsPlatformInterface) CreateBridge(options adapter.BridgeOptions) (adapter.BridgeSession, error) {
return nil, os.ErrInvalid
}
func (p *windowsPlatformInterface) PrepareOwner(identity peerIdentity) error {
p.access.Lock()
defer p.access.Unlock()
if listenAddress != "" {
p.ownerUserID = identity.UserID
p.sessionID = identity.SessionID
return p.applySystemProxyLocked()
}
if p.token != 0 && p.ownerUserID == identity.UserID && p.sessionID == identity.SessionID {
return p.applySystemProxyLocked()
}
token, err := p.daemon.duplicatePeerImpersonationToken(identity)
if err != nil {
return err
}
err = validateImpersonationToken(token, identity.UserID, identity.SessionID)
if err != nil {
token.Close()
return err
}
err = p.replaceOwnerTokenLocked(identity.UserID, identity.SessionID, token)
if err != nil {
return err
}
return nil
}
func (p *windowsPlatformInterface) RestoreOwner(state ownerState) error {
p.access.Lock()
defer p.access.Unlock()
p.ownerUserID = state.UserID
p.sessionID = state.SessionID
if listenAddress != "" {
return nil
}
if state.SessionID == 0 {
return E.New("missing owner session")
}
token, err := querySessionImpersonationToken(state.SessionID)
if err != nil {
return err
}
err = validateImpersonationToken(token, state.UserID, state.SessionID)
if err != nil {
token.Close()
return err
}
p.token = token
return nil
}
func (p *windowsPlatformInterface) ReleaseOwner() error {
p.access.Lock()
defer p.access.Unlock()
return p.releaseOwnerLocked()
}
func (p *windowsPlatformInterface) ResetPlatformOptions() error {
p.access.Lock()
defer p.access.Unlock()
err := p.disableSystemProxyLocked()
if err == nil {
p.systemProxy = nil
}
return err
}
func (p *windowsPlatformInterface) SetSystemProxyPreference(enabled bool) {
p.access.Lock()
p.systemProxyEnabled = enabled
p.access.Unlock()
}
func (p *windowsPlatformInterface) SystemProxyStatus() (*daemon.SystemProxyStatus, error) {
p.access.Lock()
defer p.access.Unlock()
available := p.systemProxy != nil
return &daemon.SystemProxyStatus{
Available: available,
Enabled: available && p.systemProxyEnabled,
}, nil
}
func (p *windowsPlatformInterface) SetSystemProxyEnabled(enabled bool) error {
p.access.Lock()
defer p.access.Unlock()
if p.systemProxy == nil {
if !enabled {
p.systemProxyEnabled = false
return nil
}
return E.New("the system proxy is not available")
}
previousEnabled := p.systemProxyEnabled
p.systemProxyEnabled = enabled
err := p.applySystemProxyLocked()
if err != nil {
p.systemProxyEnabled = previousEnabled
rollbackError := p.applySystemProxyLocked()
return E.Errors(err, rollbackError)
}
return nil
}
func (p *windowsPlatformInterface) HandleSessionChange(eventType uint32, sessionID uint32, state ownerState) (uint32, bool, error) {
p.access.Lock()
defer p.access.Unlock()
if eventType == windows.WTS_SESSION_LOGOFF {
if p.sessionID != sessionID {
return 0, false, nil
}
return 0, false, p.releaseOwnerLocked()
}
if eventType != windows.WTS_SESSION_LOGON &&
eventType != windows.WTS_CONSOLE_CONNECT &&
eventType != windows.WTS_REMOTE_CONNECT &&
eventType != windows.WTS_SESSION_UNLOCK {
return 0, false, nil
}
token, err := querySessionImpersonationToken(sessionID)
if err != nil {
return 0, false, err
}
userID, tokenSessionID, err := impersonationTokenIdentity(token)
if err != nil {
token.Close()
return 0, false, err
}
if userID != state.UserID || tokenSessionID != sessionID {
token.Close()
return 0, false, nil
}
err = p.replaceOwnerTokenLocked(userID, sessionID, token)
if err != nil {
return 0, false, err
}
return sessionID, true, nil
}
func (d *Daemon) handlePlatformSessionChange(eventType uint32, sessionID uint32) error {
d.lifecycleAccess.Lock()
defer d.lifecycleAccess.Unlock()
if d.closed || d.platform == nil {
return nil
}
state, err := loadOwnerState()
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
newSessionID, changed, err := d.platform.HandleSessionChange(eventType, sessionID, state)
if err != nil {
return err
}
if !changed {
return nil
}
return saveOwner(state.UserID, newSessionID)
}
func (p *windowsPlatformInterface) Close() error {
p.access.Lock()
defer p.access.Unlock()
systemProxyError := p.disableSystemProxyLocked()
p.systemProxy = nil
ownerError := p.closeOwnerTokenLocked()
return E.Errors(systemProxyError, ownerError)
}
func (p *windowsPlatformInterface) applySystemProxyLocked() error {
if p.systemProxy == nil {
return nil
}
if p.systemProxyEnabled {
if p.systemProxy.IsEnabled() {
return nil
}
return p.runUserOperationLocked(p.systemProxy.Enable)
}
return p.disableSystemProxyLocked()
}
func (p *windowsPlatformInterface) disableSystemProxyLocked() error {
if p.systemProxy == nil || !p.systemProxy.IsEnabled() {
return nil
}
return p.runUserOperationLocked(p.systemProxy.Disable)
}
func (p *windowsPlatformInterface) runUserOperationLocked(operation func() error) error {
if listenAddress != "" {
return operation()
}
if p.token == 0 {
return nil
}
return runImpersonated(p.token, operation)
}
func (p *windowsPlatformInterface) replaceOwnerTokenLocked(userID string, sessionID uint32, token windows.Token) error {
err := p.disableSystemProxyLocked()
if err != nil {
return E.Errors(err, token.Close())
}
err = p.closeOwnerTokenLocked()
if err != nil {
return E.Errors(err, token.Close())
}
p.ownerUserID = userID
p.sessionID = sessionID
p.token = token
err = p.applySystemProxyLocked()
if err != nil {
return E.Errors(err, p.closeOwnerTokenLocked())
}
return nil
}
func (p *windowsPlatformInterface) releaseOwnerLocked() error {
err := p.disableSystemProxyLocked()
if err != nil {
return err
}
err = p.closeOwnerTokenLocked()
p.ownerUserID = ""
p.sessionID = 0
return err
}
func (p *windowsPlatformInterface) closeOwnerTokenLocked() error {
if p.token == 0 {
return nil
}
err := p.token.Close()
p.token = 0
return err
}
func runImpersonated(token windows.Token, operation func() error) error {
result := make(chan error, 1)
go func() {
runtime.LockOSThread()
err := windows.SetThreadToken(nil, token)
if err != nil {
runtime.UnlockOSThread()
result <- E.Cause(err, "impersonate owner")
return
}
operationError := operation()
revertError := windows.RevertToSelf()
if revertError == nil {
runtime.UnlockOSThread()
} else {
revertError = E.Cause(revertError, "revert owner impersonation")
}
result <- E.Errors(operationError, revertError)
}()
return <-result
}
func querySessionImpersonationToken(sessionID uint32) (windows.Token, error) {
var primaryToken windows.Token
err := winio.RunWithPrivileges([]string{seTcbPrivilege}, func() error {
return windows.WTSQueryUserToken(sessionID, &primaryToken)
})
if err != nil {
return 0, E.Cause(err, "query session user token")
}
defer primaryToken.Close()
return duplicateImpersonationToken(primaryToken)
}
func duplicateImpersonationToken(token windows.Token) (windows.Token, error) {
var duplicatedToken windows.Token
err := windows.DuplicateTokenEx(
token,
windows.TOKEN_QUERY|windows.TOKEN_IMPERSONATE,
nil,
windows.SecurityImpersonation,
windows.TokenImpersonation,
&duplicatedToken,
)
if err != nil {
return 0, E.Cause(err, "duplicate owner impersonation token")
}
return duplicatedToken, nil
}
func validateImpersonationToken(token windows.Token, expectedUserID string, expectedSessionID uint32) error {
userID, sessionID, err := impersonationTokenIdentity(token)
if err != nil {
return err
}
if userID != expectedUserID || sessionID != expectedSessionID {
return E.New("owner token identity does not match authenticated application")
}
return nil
}
func impersonationTokenIdentity(token windows.Token) (string, uint32, error) {
user, err := token.GetTokenUser()
if err != nil {
return "", 0, E.Cause(err, "query owner token user")
}
userID := user.User.Sid.String()
if userID == "" {
return "", 0, E.New("owner token has an invalid user SID")
}
var sessionID uint32
var returnLength uint32
err = windows.GetTokenInformation(
token,
windows.TokenSessionId,
(*byte)(unsafe.Pointer(&sessionID)),
uint32(unsafe.Sizeof(sessionID)),
&returnLength,
)
if err != nil {
return "", 0, E.Cause(err, "query owner token session")
}
return userID, sessionID, nil
}
var _ daemonPlatform = (*windowsPlatformInterface)(nil)