From 81bfee7ef7d08865b90bf8ac1f5a5544fa590008 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 13 Jul 2026 19:12:05 +0800 Subject: [PATCH] platform: Add http proxy support for Windows --- adapter/platform.go | 1 + common/dialer/default.go | 2 +- common/listener/listener.go | 2 +- common/settings/proxy_android.go | 2 +- common/settings/proxy_darwin.go | 2 +- common/settings/proxy_linux.go | 2 +- common/settings/proxy_stub.go | 2 +- common/settings/proxy_windows.go | 7 +- experimental/boxdd/cmd_run_windows.go | 9 +- experimental/boxdd/cmd_service_windows.go | 2 +- experimental/boxdd/desktop_service.go | 52 ++- experimental/boxdd/managed.go | 33 +- experimental/boxdd/peer_windows.go | 26 ++ experimental/boxdd/platform.go | 26 ++ experimental/boxdd/platform_stub.go | 7 + experimental/boxdd/platform_windows.go | 530 ++++++++++++++++++++++ experimental/boxdd/security_windows.go | 86 +++- experimental/boxdd/server.go | 69 ++- experimental/boxdd/snapshot.go | 38 +- experimental/libbox/config.go | 4 + experimental/libbox/service.go | 4 + protocol/tailscale/endpoint.go | 6 +- protocol/tun/inbound.go | 18 +- route/network.go | 4 +- 24 files changed, 862 insertions(+), 72 deletions(-) create mode 100644 experimental/boxdd/platform.go create mode 100644 experimental/boxdd/platform_stub.go create mode 100644 experimental/boxdd/platform_windows.go diff --git a/adapter/platform.go b/adapter/platform.go index ce9bd9dca..6362a19c4 100644 --- a/adapter/platform.go +++ b/adapter/platform.go @@ -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 diff --git a/common/dialer/default.go b/common/dialer/default.go index 2310fe97e..cf22ba0b3 100644 --- a/common/dialer/default.go +++ b/common/dialer/default.go @@ -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) diff --git a/common/listener/listener.go b/common/listener/listener.go index e04ae466a..7b657c985 100644 --- a/common/listener/listener.go +++ b/common/listener/listener.go @@ -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") } diff --git a/common/settings/proxy_android.go b/common/settings/proxy_android.go index d3768cedd..df89c32e5 100644 --- a/common/settings/proxy_android.go +++ b/common/settings/proxy_android.go @@ -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 diff --git a/common/settings/proxy_darwin.go b/common/settings/proxy_darwin.go index baaf6ceda..51103d9ce 100644 --- a/common/settings/proxy_darwin.go +++ b/common/settings/proxy_darwin.go @@ -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") diff --git a/common/settings/proxy_linux.go b/common/settings/proxy_linux.go index 84e27b123..dcfcf7dfb 100644 --- a/common/settings/proxy_linux.go +++ b/common/settings/proxy_linux.go @@ -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", diff --git a/common/settings/proxy_stub.go b/common/settings/proxy_stub.go index 56eb4b651..ffbf07221 100644 --- a/common/settings/proxy_stub.go +++ b/common/settings/proxy_stub.go @@ -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 } diff --git a/common/settings/proxy_windows.go b/common/settings/proxy_windows.go index 793ac1d1c..bd6a5dd2c 100644 --- a/common/settings/proxy_windows.go +++ b/common/settings/proxy_windows.go @@ -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 } diff --git a/experimental/boxdd/cmd_run_windows.go b/experimental/boxdd/cmd_run_windows.go index d0e61aa23..52d1ce346 100644 --- a/experimental/boxdd/cmd_run_windows.go +++ b/experimental/boxdd/cmd_run_windows.go @@ -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 } diff --git a/experimental/boxdd/cmd_service_windows.go b/experimental/boxdd/cmd_service_windows.go index 80d6ddd37..e14a9986d 100644 --- a/experimental/boxdd/cmd_service_windows.go +++ b/experimental/boxdd/cmd_service_windows.go @@ -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) diff --git a/experimental/boxdd/desktop_service.go b/experimental/boxdd/desktop_service.go index afd59ffa6..f4c0cf9d3 100644 --- a/experimental/boxdd/desktop_service.go +++ b/experimental/boxdd/desktop_service.go @@ -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) { diff --git a/experimental/boxdd/managed.go b/experimental/boxdd/managed.go index 1aa395dde..cce6f15cd 100644 --- a/experimental/boxdd/managed.go +++ b/experimental/boxdd/managed.go @@ -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 { diff --git a/experimental/boxdd/peer_windows.go b/experimental/boxdd/peer_windows.go index eefff9724..1aad09008 100644 --- a/experimental/boxdd/peer_windows.go +++ b/experimental/boxdd/peer_windows.go @@ -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() diff --git a/experimental/boxdd/platform.go b/experimental/boxdd/platform.go new file mode 100644 index 000000000..d7681420d --- /dev/null +++ b/experimental/boxdd/platform.go @@ -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) +} diff --git a/experimental/boxdd/platform_stub.go b/experimental/boxdd/platform_stub.go new file mode 100644 index 000000000..0ad7e298d --- /dev/null +++ b/experimental/boxdd/platform_stub.go @@ -0,0 +1,7 @@ +//go:build !windows + +package main + +func newPlatformInterface(daemon *Daemon) (daemonPlatform, error) { + return nil, nil +} diff --git a/experimental/boxdd/platform_windows.go b/experimental/boxdd/platform_windows.go new file mode 100644 index 000000000..5d129894b --- /dev/null +++ b/experimental/boxdd/platform_windows.go @@ -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) diff --git a/experimental/boxdd/security_windows.go b/experimental/boxdd/security_windows.go index 40cd92e2f..7fea1d175 100644 --- a/experimental/boxdd/security_windows.go +++ b/experimental/boxdd/security_windows.go @@ -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) } diff --git a/experimental/boxdd/server.go b/experimental/boxdd/server.go index f97a349c2..87871b31a 100644 --- a/experimental/boxdd/server.go +++ b/experimental/boxdd/server.go @@ -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() } diff --git a/experimental/boxdd/snapshot.go b/experimental/boxdd/snapshot.go index 09eb65b5d..e5d20d88a 100644 --- a/experimental/boxdd/snapshot.go +++ b/experimental/boxdd/snapshot.go @@ -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 { diff --git a/experimental/libbox/config.go b/experimental/libbox/config.go index f2d379dea..8d5a7d6b1 100644 --- a/experimental/libbox/config.go +++ b/experimental/libbox/config.go @@ -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 } diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go index 46af69482..52b0b01c8 100644 --- a/experimental/libbox/service.go +++ b/experimental/libbox/service.go @@ -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 { diff --git a/protocol/tailscale/endpoint.go b/protocol/tailscale/endpoint.go index 17da2613e..bfbe3a8b7 100644 --- a/protocol/tailscale/endpoint.go +++ b/protocol/tailscale/endpoint.go @@ -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", diff --git a/protocol/tun/inbound.go b/protocol/tun/inbound.go index 814a711ba..730f68509 100644 --- a/protocol/tun/inbound.go +++ b/protocol/tun/inbound.go @@ -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, }) diff --git a/route/network.go b/route/network.go index 41352435f..4fbcf22e5 100644 --- a/route/network.go +++ b/route/network.go @@ -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 })