platform: Add http proxy support for Windows

This commit is contained in:
世界
2026-07-13 19:12:05 +08:00
parent f505a47bca
commit 81bfee7ef7
24 changed files with 862 additions and 72 deletions
+1
View File
@@ -16,6 +16,7 @@ type PlatformInterface interface {
UsePlatformInterface() bool
OpenInterface(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error)
ProcessPlatformOptions(options option.TunPlatformOptions) error
UsePlatformDefaultInterfaceMonitor() bool
CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor
+1 -1
View File
@@ -97,7 +97,7 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc)
} else if networkManager.AutoDetectInterface() && !disableDefaultBind {
if platformInterface != nil {
if platformInterface != nil && platformInterface.UsePlatformNetworkInterfaces() {
networkStrategy = (*C.NetworkStrategy)(options.NetworkStrategy)
networkType = common.Map(options.NetworkType, option.InterfaceType.Build)
fallbackNetworkType = common.Map(options.FallbackNetworkType, option.InterfaceType.Build)
+1 -1
View File
@@ -107,7 +107,7 @@ func (l *Listener) Start() error {
} else {
listenAddrString = listenAddr.String()
}
systemProxy, err := settings.NewSystemProxy(l.ctx, M.ParseSocksaddrHostPort(listenAddrString, listenPort), l.systemProxySOCKS)
systemProxy, err := settings.NewSystemProxy(l.ctx, M.ParseSocksaddrHostPort(listenAddrString, listenPort), l.systemProxySOCKS, nil)
if err != nil {
return E.Cause(err, "initialize system proxy")
}
+1 -1
View File
@@ -20,7 +20,7 @@ type AndroidSystemProxy struct {
isEnabled bool
}
func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*AndroidSystemProxy, error) {
func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool, bypassDomain []string) (*AndroidSystemProxy, error) {
userId := os.Getuid()
var (
useRish bool
+1 -1
View File
@@ -24,7 +24,7 @@ type DarwinSystemProxy struct {
isEnabled bool
}
func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*DarwinSystemProxy, error) {
func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool, bypassDomain []string) (*DarwinSystemProxy, error) {
interfaceMonitor := service.FromContext[adapter.NetworkManager](ctx).InterfaceMonitor()
if interfaceMonitor == nil {
return nil, E.New("missing interface monitor")
+1 -1
View File
@@ -24,7 +24,7 @@ type LinuxSystemProxy struct {
isEnabled bool
}
func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*LinuxSystemProxy, error) {
func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool, bypassDomain []string) (*LinuxSystemProxy, error) {
hasGSettings := common.Error(exec.LookPath("gsettings")) == nil
kWriteConfigCmds := []string{
"kwriteconfig5",
+1 -1
View File
@@ -9,6 +9,6 @@ import (
M "github.com/sagernet/sing/common/metadata"
)
func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (SystemProxy, error) {
func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool, bypassDomain []string) (SystemProxy, error) {
return nil, os.ErrInvalid
}
+5 -2
View File
@@ -2,6 +2,7 @@ package settings
import (
"context"
"strings"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/wininet"
@@ -10,13 +11,15 @@ import (
type WindowsSystemProxy struct {
serverAddr M.Socksaddr
supportSOCKS bool
bypassDomain []string
isEnabled bool
}
func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*WindowsSystemProxy, error) {
func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool, bypassDomain []string) (*WindowsSystemProxy, error) {
return &WindowsSystemProxy{
serverAddr: serverAddr,
supportSOCKS: supportSOCKS,
bypassDomain: bypassDomain,
}, nil
}
@@ -25,7 +28,7 @@ func (p *WindowsSystemProxy) IsEnabled() bool {
}
func (p *WindowsSystemProxy) Enable() error {
err := wininet.SetSystemProxy("http://"+p.serverAddr.String(), "")
err := wininet.SetSystemProxy("http://"+p.serverAddr.String(), strings.Join(p.bypassDomain, ";"))
if err != nil {
return err
}
+8 -1
View File
@@ -68,13 +68,20 @@ func (s *windowsService) Execute(arguments []string, requests <-chan svc.ChangeR
serviceLogError(err)
return
}
statuses <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown}
statuses <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown | svc.AcceptSessionChange}
runtime.GC()
for request := range requests {
if request.Cmd == svc.Interrogate {
statuses <- request.CurrentStatus
continue
}
if request.Cmd == svc.SessionChange {
err = d.handlePlatformSessionChange(request.EventType, uint32(request.EventData))
if err != nil {
serviceLogError(E.Cause(err, "handle session change"))
}
continue
}
if request.Cmd == svc.Stop || request.Cmd == svc.Shutdown {
break
}
+1 -1
View File
@@ -54,7 +54,7 @@ func addPlatformServiceCommands() {
&commandServiceFlagAllowUnsafeInstallation,
"allow-unsafe-installation-directory-permissions",
false,
"skip installation path ancestor permission validation",
"skip installation path security validation and permission hardening",
)
commandService.AddCommand(commandServiceInstall)
commandService.AddCommand(commandServiceUninstall)
+44 -8
View File
@@ -60,11 +60,13 @@ func (s *desktopService) StartService(ctx context.Context, request *StartService
if ownerUserID != "" && ownerUserID != identity.UserID {
return nil, status.Error(codes.PermissionDenied, "the service is owned by another user")
}
if ownerUserID == "" {
err = saveOwner(identity.UserID)
if err != nil {
return nil, err
}
err = s.daemon.preparePlatformOwnerLocked(identity)
if err != nil {
return nil, err
}
err = saveOwner(identity.UserID, identity.SessionID)
if err != nil {
return nil, err
}
currentOptions, err := loadStartOptions(identity.UserID)
if err != nil && !os.IsNotExist(err) {
@@ -105,6 +107,14 @@ func (s *desktopService) ClaimService(ctx context.Context, empty *emptypb.Empty)
return nil, err
}
if ownerUserID == identity.UserID {
err = s.daemon.preparePlatformOwnerLocked(identity)
if err != nil {
return nil, err
}
err = saveOwner(identity.UserID, identity.SessionID)
if err != nil {
return nil, err
}
return &emptypb.Empty{}, nil
}
if ownerUserID != "" {
@@ -114,7 +124,11 @@ func (s *desktopService) ClaimService(ctx context.Context, empty *emptypb.Empty)
if err != nil {
return nil, err
}
err = saveOwner(identity.UserID)
err = s.daemon.preparePlatformOwnerLocked(identity)
if err != nil {
return nil, err
}
err = saveOwner(identity.UserID, identity.SessionID)
if err != nil {
return nil, err
}
@@ -136,6 +150,14 @@ func (s *desktopService) TakeOverService(ctx context.Context, empty *emptypb.Emp
return nil, err
}
if ownerUserID == identity.UserID {
err = s.daemon.preparePlatformOwnerLocked(identity)
if err != nil {
return nil, err
}
err = saveOwner(identity.UserID, identity.SessionID)
if err != nil {
return nil, err
}
return &emptypb.Empty{}, nil
}
if ownerUserID != "" {
@@ -143,12 +165,22 @@ func (s *desktopService) TakeOverService(ctx context.Context, empty *emptypb.Emp
if err != nil {
return nil, err
}
if s.daemon.platform != nil {
err = s.daemon.platform.ReleaseOwner()
if err != nil {
return nil, err
}
}
}
err = s.daemon.configureWorkingDirectoryLocked(userWorkingDirectory(identity.UserID))
if err != nil {
return nil, err
}
err = saveOwner(identity.UserID)
err = s.daemon.preparePlatformOwnerLocked(identity)
if err != nil {
return nil, err
}
err = saveOwner(identity.UserID, identity.SessionID)
if err != nil {
return nil, err
}
@@ -157,13 +189,17 @@ func (s *desktopService) TakeOverService(ctx context.Context, empty *emptypb.Emp
}
func (d *Daemon) cleanFailedStartLocked(ownerUserID string, options startOptions, startError error) error {
var platformError error
if d.platform != nil {
platformError = d.platform.ResetPlatformOptions()
}
closeError := d.startedService.CloseService()
directory := userWorkingDirectory(ownerUserID)
crashReportError := tagUnownedReports(filepath.Join(directory, crashReportsDirectoryName), ownerUserID)
oomReportError := tagUnownedReports(filepath.Join(directory, oomReportsDirectoryName), ownerUserID)
options.WasRunning = false
snapshotError := saveStartOptions(ownerUserID, options)
return E.Errors(startError, closeError, crashReportError, oomReportError, snapshotError)
return E.Errors(startError, platformError, closeError, crashReportError, oomReportError, snapshotError)
}
func (s *desktopService) GetWorkingDirectory(ctx context.Context, empty *emptypb.Empty) (*WorkingDirectoryInfo, error) {
+29 -4
View File
@@ -52,14 +52,39 @@ func (h *managedHandler) ServiceReload() error {
}
func (h *managedHandler) SystemProxyStatus() (*daemon.SystemProxyStatus, error) {
return &daemon.SystemProxyStatus{}, nil
if h.daemon.platform == nil {
return &daemon.SystemProxyStatus{}, nil
}
return h.daemon.platform.SystemProxyStatus()
}
func (h *managedHandler) SetSystemProxyEnabled(enabled bool) error {
if !enabled {
return nil
if h.daemon.platform == nil {
if !enabled {
return nil
}
return status.Error(codes.FailedPrecondition, "the system proxy is not available")
}
return status.Error(codes.FailedPrecondition, "the system proxy is not available")
ownerUserID, err := loadOwner()
if err != nil {
return err
}
options, err := loadStartOptions(ownerUserID)
if err != nil {
return err
}
previousEnabled := options.systemProxyEnabled()
err = h.daemon.platform.SetSystemProxyEnabled(enabled)
if err != nil {
return err
}
options.SystemProxyEnabled = &enabled
err = saveStartOptions(ownerUserID, options)
if err != nil {
rollbackError := h.daemon.platform.SetSystemProxyEnabled(previousEnabled)
return E.Errors(err, rollbackError)
}
return nil
}
func (h *managedHandler) TriggerNativeCrash() error {
+26
View File
@@ -520,6 +520,32 @@ func (c *windowsAuthenticatedConnection) peerConnectionIdentity() peerIdentity {
return c.identity
}
func (c *windowsAuthenticatedConnection) duplicateImpersonationToken() (windows.Token, error) {
var processToken windows.Token
err := windows.OpenProcessToken(c.parentProcess, windows.TOKEN_QUERY|windows.TOKEN_DUPLICATE, &processToken)
if err != nil {
return 0, E.Cause(err, "open application token")
}
defer processToken.Close()
return duplicateImpersonationToken(processToken)
}
func (d *Daemon) duplicatePeerImpersonationToken(identity peerIdentity) (windows.Token, error) {
d.peerAccess.Lock()
defer d.peerAccess.Unlock()
for connection, connectionIdentity := range d.peerConnections {
if connectionIdentity != identity {
continue
}
windowsConnection, loaded := connection.(*windowsAuthenticatedConnection)
if !loaded {
continue
}
return windowsConnection.duplicateImpersonationToken()
}
return 0, E.New("authenticated application connection is no longer available")
}
func (d *Daemon) registerPeerConnection(connection peerConnection) {
d.peerAccess.Lock()
defer d.peerAccess.Unlock()
+26
View File
@@ -0,0 +1,26 @@
package main
import (
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/daemon"
)
type daemonPlatform interface {
adapter.PlatformInterface
PrepareOwner(identity peerIdentity) error
RestoreOwner(state ownerState) error
ReleaseOwner() error
ResetPlatformOptions() error
SetSystemProxyPreference(enabled bool)
SystemProxyStatus() (*daemon.SystemProxyStatus, error)
SetSystemProxyEnabled(enabled bool) error
HandleSessionChange(eventType uint32, sessionID uint32, state ownerState) (uint32, bool, error)
Close() error
}
func (d *Daemon) preparePlatformOwnerLocked(identity peerIdentity) error {
if d.platform == nil {
return nil
}
return d.platform.PrepareOwner(identity)
}
+7
View File
@@ -0,0 +1,7 @@
//go:build !windows
package main
func newPlatformInterface(daemon *Daemon) (daemonPlatform, error) {
return nil, nil
}
+530
View File
@@ -0,0 +1,530 @@
//go:build windows
package main
import (
"context"
"net/netip"
"os"
"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"
"golang.org/x/sys/windows"
)
var regDisablePredefinedCacheEx = windows.NewLazySystemDLL("advapi32.dll").NewProc("RegDisablePredefinedCacheEx")
type windowsPlatformInterface struct {
daemon *Daemon
access sync.Mutex
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 false
}
func (p *windowsPlatformInterface) CheckPlatformShell() error {
return os.ErrInvalid
}
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) {
return nil, os.ErrInvalid
}
func (p *windowsPlatformInterface) LookupSFTPServer() (string, error) {
return "", os.ErrInvalid
}
func (p *windowsPlatformInterface) ReadSystemSSHHostKey() ([]byte, error) {
return nil, os.ErrInvalid
}
func (p *windowsPlatformInterface) TailscaleHostname() string {
return ""
}
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 := 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)
+63 -23
View File
@@ -60,11 +60,14 @@ func secureWindowsInstallation(executablePath string, allowUnsafeInstallation bo
if !bytes.Equal(daemonSigner, applicationSigner) {
return "", E.New("installed application and daemon have different signing certificates")
}
if allowUnsafeInstallation {
return daemonPath, nil
}
volumeRoot, err := validateFixedNTFSVolume(installationDirectory)
if err != nil {
return "", err
}
err = validateInstallationAncestors(filepath.Dir(installationDirectory), volumeRoot, !allowUnsafeInstallation)
err = validateInstallationAncestors(filepath.Dir(installationDirectory), volumeRoot, true)
if err != nil {
return "", err
}
@@ -95,18 +98,10 @@ func installedApplicationPath(daemonPath string) (string, string, error) {
return installationDirectory, filepath.Join(installationDirectory, applicationExecutableName), nil
}
func secureWindowsWorkingDirectory(path string) error {
serviceUserID, err := windowsServiceSID()
if err != nil {
return E.Cause(err, "create daemon service SID")
}
func windowsWorkingDirectorySecurityDescriptors(serviceUserID *windows.SID) (string, string, error) {
serviceUserIDString := serviceUserID.String()
if serviceUserIDString == "" {
return E.New("daemon service has an invalid SID")
}
err = validateTreeHasNoReparsePoints(path)
if err != nil {
return err
return "", "", E.New("daemon service has an invalid SID")
}
directoryDescriptor := fmt.Sprintf(
"O:SYG:SYD:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FA;;;%s)",
@@ -116,9 +111,35 @@ func secureWindowsWorkingDirectory(path string) error {
"O:SYG:SYD:P(A;;FA;;;SY)(A;;FA;;;BA)(A;;FA;;;%s)",
serviceUserIDString,
)
return directoryDescriptor, fileDescriptor, nil
}
func secureWindowsWorkingDirectory(path string, serviceUserID *windows.SID) error {
directoryDescriptor, fileDescriptor, err := windowsWorkingDirectorySecurityDescriptors(serviceUserID)
if err != nil {
return err
}
err = validateTreeHasNoReparsePoints(path)
if err != nil {
return err
}
return applyProtectedTree(path, directoryDescriptor, fileDescriptor)
}
func secureWindowsWorkingDirectoryRoot(path string, serviceUserID *windows.SID) error {
directorySecurityDescriptor, _, err := windowsWorkingDirectorySecurityDescriptors(serviceUserID)
if err != nil {
return err
}
directoryDescriptor, err := windows.SecurityDescriptorFromString(directorySecurityDescriptor)
if err != nil {
return err
}
return winio.RunWithPrivilege(winio.SeRestorePrivilege, func() error {
return applyProtectedFileSecurity(path, directoryDescriptor)
})
}
func windowsServiceSID() (*windows.SID, error) {
serviceNameUTF16 := utf16.Encode([]rune(strings.ToUpper(serviceName)))
serviceNameContent := make([]byte, len(serviceNameUTF16)*2)
@@ -137,6 +158,14 @@ func windowsServiceSID() (*windows.SID, error) {
}
func validateProtectedWindowsWorkingDirectory(path string, serviceUserID *windows.SID) error {
return validateWindowsWorkingDirectory(path, serviceUserID, false)
}
func validateRepairableWindowsWorkingDirectory(path string, serviceUserID *windows.SID) error {
return validateWindowsWorkingDirectory(path, serviceUserID, true)
}
func validateWindowsWorkingDirectory(path string, serviceUserID *windows.SID, allowAdditionalAccessControlEntries bool) error {
attributes, err := windowsFileAttributes(path)
if err != nil {
return err
@@ -177,7 +206,9 @@ func validateProtectedWindowsWorkingDirectory(path string, serviceUserID *window
if err != nil {
return err
}
if discretionaryAccessControlList == nil || discretionaryAccessControlList.AceCount != 3 {
if discretionaryAccessControlList == nil ||
(!allowAdditionalAccessControlEntries && discretionaryAccessControlList.AceCount != 3) ||
(allowAdditionalAccessControlEntries && discretionaryAccessControlList.AceCount < 3) {
return E.New("daemon working directory has unexpected access control entries")
}
administratorsUserID, err := windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid)
@@ -195,16 +226,24 @@ func validateProtectedWindowsWorkingDirectory(path string, serviceUserID *window
if err != nil {
return err
}
if accessControlEntry.Header.AceType != windows.ACCESS_ALLOWED_ACE_TYPE ||
accessControlEntry.Header.AceFlags != windows.OBJECT_INHERIT_ACE|windows.CONTAINER_INHERIT_ACE ||
uint32(accessControlEntry.Mask) != 0x001F01FF {
if accessControlEntry.Header.AceType != windows.ACCESS_ALLOWED_ACE_TYPE {
return E.New("daemon working directory has an unsafe access control entry")
}
userID := (*windows.SID)(unsafe.Pointer(&accessControlEntry.SidStart)).String()
seen, exists := expectedUsers[userID]
if !exists || seen {
if !exists {
if allowAdditionalAccessControlEntries {
continue
}
return E.New("daemon working directory grants access to an unexpected principal")
}
if accessControlEntry.Header.AceFlags != windows.OBJECT_INHERIT_ACE|windows.CONTAINER_INHERIT_ACE ||
uint32(accessControlEntry.Mask) != 0x001F01FF {
return E.New("daemon working directory has an unsafe access control entry")
}
if seen {
return E.New("daemon working directory has a duplicate access control entry")
}
expectedUsers[userID] = true
}
for _, seen := range expectedUsers {
@@ -498,18 +537,19 @@ func ensureWindowsWorkingDirectory(path string) error {
} else if err != nil {
return err
}
if created {
err = secureWindowsWorkingDirectory(path)
if !created {
err = validateRepairableWindowsWorkingDirectory(path, serviceUserID)
if err != nil {
return err
}
err = secureWindowsWorkingDirectoryRoot(path, serviceUserID)
if err != nil {
return err
}
}
err = validateProtectedWindowsWorkingDirectory(path, serviceUserID)
err = secureWindowsWorkingDirectory(path, serviceUserID)
if err != nil {
return err
}
if created {
return nil
}
return secureWindowsWorkingDirectory(path)
return validateProtectedWindowsWorkingDirectory(path, serviceUserID)
}
+66 -3
View File
@@ -9,11 +9,13 @@ import (
"strings"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/daemon"
"github.com/sagernet/sing-box/experimental/libbox"
"github.com/sagernet/sing-box/include"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/service/oomkiller"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/service"
"google.golang.org/grpc"
@@ -33,6 +35,7 @@ type Daemon struct {
closed bool
peerAccess sync.Mutex
peerConnections map[peerConnection]peerIdentity
platform daemonPlatform
}
func newDaemon() (*Daemon, error) {
@@ -41,6 +44,14 @@ func newDaemon() (*Daemon, error) {
logger: log.StdLogger(),
runtimeWorkingDirectory: workingDirectory,
}
platformInterface, err := newPlatformInterface(d)
if err != nil {
return nil, err
}
d.platform = platformInterface
if platformInterface != nil {
service.MustRegister[adapter.PlatformInterface](ctx, platformInterface)
}
d.startedService = daemon.NewStartedService(daemon.ServiceOptions{
Context: ctx,
LogMaxLines: 3000,
@@ -107,13 +118,14 @@ func (d *Daemon) restore() {
if d.closed {
return
}
ownerUserID, err := loadOwner()
ownerState, err := loadOwnerState()
if err != nil {
if !os.IsNotExist(err) {
d.logger.Warn("load owner: ", err)
}
return
}
ownerUserID := ownerState.UserID
ownerWorkingDirectory := userWorkingDirectory(ownerUserID)
err = d.configureWorkingDirectoryLocked(ownerWorkingDirectory)
if err != nil {
@@ -138,6 +150,13 @@ func (d *Daemon) restore() {
if !options.WasRunning {
return
}
if d.platform != nil {
d.platform.SetSystemProxyPreference(options.systemProxyEnabled())
err = d.platform.RestoreOwner(ownerState)
if err != nil {
d.logger.Warn("restore owner session: ", err)
}
}
configContent, err := loadServiceConfig(ownerUserID)
if err != nil {
d.logger.Error("restore service: ", err)
@@ -189,7 +208,18 @@ func (d *Daemon) startServiceLocked(ownerUserID string, configContent string, op
OomMemoryLimit: options.OOMMemoryLimit,
})
d.startedService.SetOOMKillerOptions(options.OOMKillerEnabled, options.OOMKillerDisabled, uint64(options.OOMMemoryLimit))
return d.startedService.StartOrReloadService(configContent, nil)
if d.platform != nil {
d.platform.SetSystemProxyPreference(options.systemProxyEnabled())
err = d.platform.ResetPlatformOptions()
if err != nil {
return err
}
}
err = d.startedService.StartOrReloadService(configContent, nil)
if err != nil && d.platform != nil {
return E.Errors(err, d.platform.ResetPlatformOptions())
}
return err
}
func (d *Daemon) stopServiceLocked(ownerUserID string) error {
@@ -197,6 +227,12 @@ func (d *Daemon) stopServiceLocked(ownerUserID string) error {
if err != nil && !os.IsNotExist(err) {
return err
}
if d.platform != nil {
err = d.platform.ResetPlatformOptions()
if err != nil {
return err
}
}
if d.startedService.Instance() != nil {
err = d.startedService.CloseService()
if err != nil {
@@ -222,8 +258,14 @@ func (d *Daemon) Close() {
d.lifecycleAccess.Unlock()
d.server.Stop()
d.lifecycleAccess.Lock()
if d.platform != nil {
_ = d.platform.ResetPlatformOptions()
}
_ = d.startedService.CloseService()
d.startedService.Close()
if d.platform != nil {
_ = d.platform.Close()
}
d.lifecycleAccess.Unlock()
}
@@ -285,7 +327,18 @@ func (a *daemonAuthorizer) Authorize(ctx context.Context, method string) error {
if ownerProtectedMethod(method) {
a.daemon.lifecycleAccess.Lock()
defer a.daemon.lifecycleAccess.Unlock()
return a.daemon.authorizeOwnerLocked(identity.UserID)
err = a.daemon.authorizeOwnerLocked(identity.UserID)
if err != nil {
return err
}
err = a.daemon.preparePlatformOwnerLocked(identity)
if err != nil {
return err
}
if a.daemon.platform != nil {
return saveOwner(identity.UserID, identity.SessionID)
}
return nil
}
return status.Error(codes.PermissionDenied, "the service is not available")
}
@@ -308,6 +361,16 @@ func (a *daemonAuthorizer) InvokeUnary(ctx context.Context, method string, handl
if err != nil {
return nil, err
}
err = a.daemon.preparePlatformOwnerLocked(identity)
if err != nil {
return nil, err
}
if a.daemon.platform != nil {
err = saveOwner(identity.UserID, identity.SessionID)
if err != nil {
return nil, err
}
}
return handler()
}
+26 -12
View File
@@ -18,14 +18,16 @@ const (
)
type startOptions struct {
WasRunning bool `json:"was_running"`
OOMKillerEnabled bool `json:"oom_killer_enabled"`
OOMKillerDisabled bool `json:"oom_killer_disabled"`
OOMMemoryLimit int64 `json:"oom_memory_limit"`
WasRunning bool `json:"was_running"`
OOMKillerEnabled bool `json:"oom_killer_enabled"`
OOMKillerDisabled bool `json:"oom_killer_disabled"`
OOMMemoryLimit int64 `json:"oom_memory_limit"`
SystemProxyEnabled *bool `json:"system_proxy_enabled,omitempty"`
}
type ownerState struct {
UserID string `json:"user_id"`
UserID string `json:"user_id"`
SessionID uint32 `json:"session_id,omitempty"`
}
func userWorkingDirectory(userID string) string {
@@ -34,25 +36,37 @@ func userWorkingDirectory(userID string) string {
}
func loadOwner() (string, error) {
content, err := os.ReadFile(filepath.Join(workingDirectory, ownerFileName))
if err != nil {
return "", err
}
state, err := json.UnmarshalExtended[ownerState](content)
state, err := loadOwnerState()
if err != nil {
return "", err
}
return state.UserID, nil
}
func saveOwner(userID string) error {
content, err := json.Marshal(ownerState{UserID: userID})
func loadOwnerState() (ownerState, error) {
content, err := os.ReadFile(filepath.Join(workingDirectory, ownerFileName))
if err != nil {
return ownerState{}, err
}
state, err := json.UnmarshalExtended[ownerState](content)
if err != nil {
return ownerState{}, err
}
return state, nil
}
func saveOwner(userID string, sessionID uint32) error {
content, err := json.Marshal(ownerState{UserID: userID, SessionID: sessionID})
if err != nil {
return err
}
return atomicfile.WriteFile(filepath.Join(workingDirectory, ownerFileName), content, 0o600)
}
func (o startOptions) systemProxyEnabled() bool {
return o.SystemProxyEnabled == nil || *o.SystemProxyEnabled
}
func loadServiceConfig(userID string) (string, error) {
content, err := os.ReadFile(filepath.Join(userWorkingDirectory(userID), serviceConfigFileName))
if err != nil {
+4
View File
@@ -86,6 +86,10 @@ func (s *platformInterfaceStub) OpenInterface(options *tun.Options, platformOpti
return nil, os.ErrInvalid
}
func (s *platformInterfaceStub) ProcessPlatformOptions(options option.TunPlatformOptions) error {
return nil
}
func (s *platformInterfaceStub) UsePlatformDefaultInterfaceMonitor() bool {
return true
}
+4
View File
@@ -84,6 +84,10 @@ func (w *platformInterfaceWrapper) OpenInterface(options *tun.Options, platformO
return tun.New(*options)
}
func (w *platformInterfaceWrapper) ProcessPlatformOptions(options option.TunPlatformOptions) error {
return nil
}
func myTunAddress(options *tun.Options) []netip.Addr {
addresses := make([]netip.Addr, 0, len(options.Inet4Address)+len(options.Inet6Address))
for _, prefix := range options.Inet4Address {
+3 -3
View File
@@ -259,7 +259,7 @@ func (t *Endpoint) Start(stage adapter.StartStage) error {
}
func (t *Endpoint) start() error {
if t.platformInterface != nil {
if t.platformInterface != nil && t.platformInterface.UsePlatformNetworkInterfaces() {
err := t.network.UpdateInterfaces()
if err != nil {
return err
@@ -335,7 +335,7 @@ func (t *Endpoint) start() error {
controlFunc = control.Append(controlFunc, bindFunc)
}
netns.SetControlFunc(controlFunc)
} else if runtime.GOOS == "android" && t.platformInterface != nil {
} else if runtime.GOOS == "android" && t.platformInterface != nil && t.platformInterface.UsePlatformAutoDetectInterfaceControl() {
netns.SetControlFunc(func(network, address string, c syscall.RawConn) error {
return control.Raw(c, func(fd uintptr) error {
return t.platformInterface.AutoDetectInterfaceControl(int(fd))
@@ -471,7 +471,7 @@ func (t *Endpoint) watchState() {
}
reportedAuthURL = authURL
t.logger.Info("Waiting for authentication: ", authURL)
if t.platformInterface != nil {
if t.platformInterface != nil && t.platformInterface.UsePlatformNotification() {
err := t.platformInterface.SendNotification(&adapter.Notification{
Identifier: "tailscale-authentication",
TypeName: "Tailscale Authentication Notifications",
+11 -7
View File
@@ -367,7 +367,7 @@ func (t *Inbound) Start(stage adapter.StartStage) error {
t.tunOptions.NetNs = manager.ResolvePath(t.tunOptions.NetNs)
}
}
if t.platformInterface == nil {
if t.platformInterface == nil || C.IsWindows {
t.routeAddressSet = common.FlatMap(t.routeRuleSet, adapter.RuleSet.ExtractIPSet)
for _, routeRuleSet := range t.routeRuleSet {
ipSets := routeRuleSet.ExtractIPSet()
@@ -432,12 +432,16 @@ func (t *Inbound) Start(stage adapter.StartStage) error {
}
t.logger.Trace("creating stack")
t.tunIf = tunInterface
var (
forwarderBindInterface bool
includeAllNetworks bool
)
if t.platformInterface != nil {
forwarderBindInterface = true
err = t.platformInterface.ProcessPlatformOptions(t.platformOptions)
if err != nil {
closeError := t.tunIf.Close()
t.tunIf = nil
return E.Errors(E.Cause(err, "process platform options"), closeError)
}
}
var includeAllNetworks bool
if t.platformInterface != nil && t.platformInterface.UnderNetworkExtension() {
includeAllNetworks = t.platformInterface.NetworkExtensionIncludeAllNetworks()
}
tunStack, err := tun.NewStack(t.stack, tun.StackOptions{
@@ -448,7 +452,7 @@ func (t *Inbound) Start(stage adapter.StartStage) error {
ICMPTimeout: C.ICMPTimeout,
Handler: t,
Logger: t.logger,
ForwarderBindInterface: forwarderBindInterface,
ForwarderBindInterface: C.IsDarwin,
InterfaceFinder: t.networkManager.InterfaceFinder(),
IncludeAllNetworks: includeAllNetworks,
})
+2 -2
View File
@@ -108,7 +108,7 @@ func NewNetworkManager(ctx context.Context, logger logger.ContextLogger, options
return nil, E.New("`auto_detect_interface` is required by `default_network_strategy`")
}
}
usePlatformDefaultInterfaceMonitor := nm.platformInterface != nil
usePlatformDefaultInterfaceMonitor := nm.platformInterface != nil && nm.platformInterface.UsePlatformDefaultInterfaceMonitor()
enforceInterfaceMonitor := options.AutoDetectInterface
if !usePlatformDefaultInterfaceMonitor {
networkMonitor, err := tun.NewNetworkUpdateMonitor(logger)
@@ -503,7 +503,7 @@ func (r *NetworkManager) notifyInterfaceUpdate(defaultInterface *control.Interfa
vpnStatus = "disabled"
}
options = append(options, "vpn "+vpnStatus)
} else if r.platformInterface != nil {
} else if r.platformInterface != nil && r.platformInterface.UsePlatformNetworkInterfaces() {
networkInterface := common.Find(r.networkInterfaces.Load(), func(it adapter.NetworkInterface) bool {
return it.Interface.Index == defaultInterface.Index
})