diff --git a/common/settings/proxy_linux.go b/common/settings/proxy_linux.go index dcfcf7dfb..5f594dcd8 100644 --- a/common/settings/proxy_linux.go +++ b/common/settings/proxy_linux.go @@ -16,15 +16,31 @@ import ( ) type LinuxSystemProxy struct { + execute func(name string, args ...string) error hasGSettings bool kWriteConfigCmd string - sudoUser string serverAddr M.Socksaddr supportSOCKS bool isEnabled bool } func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool, bypassDomain []string) (*LinuxSystemProxy, error) { + var sudoUser string + if os.Getuid() == 0 { + sudoUser = os.Getenv("SUDO_USER") + } + return NewLinuxSystemProxy(serverAddr, supportSOCKS, func(name string, args ...string) error { + if os.Getuid() != 0 { + return shell.Exec(name, args...).Attach().Run() + } else if sudoUser != "" { + return shell.Exec("su", "-", sudoUser, "-c", F.ToString(name, " ", strings.Join(args, " "))).Attach().Run() + } else { + return E.New("set system proxy: unable to set as root") + } + }) +} + +func NewLinuxSystemProxy(serverAddr M.Socksaddr, supportSOCKS bool, execute func(name string, args ...string) error) (*LinuxSystemProxy, error) { hasGSettings := common.Error(exec.LookPath("gsettings")) == nil kWriteConfigCmds := []string{ "kwriteconfig5", @@ -37,17 +53,13 @@ func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bo break } } - var sudoUser string - if os.Getuid() == 0 { - sudoUser = os.Getenv("SUDO_USER") - } if !hasGSettings && kWriteConfigCmd == "" { return nil, E.New("unsupported desktop environment") } return &LinuxSystemProxy{ + execute: execute, hasGSettings: hasGSettings, kWriteConfigCmd: kWriteConfigCmd, - sudoUser: sudoUser, serverAddr: serverAddr, supportSOCKS: supportSOCKS, }, nil @@ -59,7 +71,7 @@ func (p *LinuxSystemProxy) IsEnabled() bool { func (p *LinuxSystemProxy) Enable() error { if p.hasGSettings { - err := p.runAsUser("gsettings", "set", "org.gnome.system.proxy.http", "enabled", "true") + err := p.execute("gsettings", "set", "org.gnome.system.proxy.http", "enabled", "true") if err != nil { return err } @@ -71,17 +83,17 @@ func (p *LinuxSystemProxy) Enable() error { if err != nil { return err } - err = p.runAsUser("gsettings", "set", "org.gnome.system.proxy", "use-same-proxy", F.ToString(p.supportSOCKS)) + err = p.execute("gsettings", "set", "org.gnome.system.proxy", "use-same-proxy", F.ToString(p.supportSOCKS)) if err != nil { return err } - err = p.runAsUser("gsettings", "set", "org.gnome.system.proxy", "mode", "manual") + err = p.execute("gsettings", "set", "org.gnome.system.proxy", "mode", "manual") if err != nil { return err } } if p.kWriteConfigCmd != "" { - err := p.runAsUser(p.kWriteConfigCmd, "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "ProxyType", "1") + err := p.execute(p.kWriteConfigCmd, "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "ProxyType", "1") if err != nil { return err } @@ -93,11 +105,11 @@ func (p *LinuxSystemProxy) Enable() error { if err != nil { return err } - err = p.runAsUser(p.kWriteConfigCmd, "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "Authmode", "0") + err = p.execute(p.kWriteConfigCmd, "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "Authmode", "0") if err != nil { return err } - err = p.runAsUser("dbus-send", "--type=signal", "/KIO/Scheduler", "org.kde.KIO.Scheduler.reparseSlaveConfiguration", "string:''") + err = p.execute("dbus-send", "--type=signal", "/KIO/Scheduler", "org.kde.KIO.Scheduler.reparseSlaveConfiguration", "string:''") if err != nil { return err } @@ -108,17 +120,17 @@ func (p *LinuxSystemProxy) Enable() error { func (p *LinuxSystemProxy) Disable() error { if p.hasGSettings { - err := p.runAsUser("gsettings", "set", "org.gnome.system.proxy", "mode", "none") + err := p.execute("gsettings", "set", "org.gnome.system.proxy", "mode", "none") if err != nil { return err } } if p.kWriteConfigCmd != "" { - err := p.runAsUser(p.kWriteConfigCmd, "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "ProxyType", "0") + err := p.execute(p.kWriteConfigCmd, "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "ProxyType", "0") if err != nil { return err } - err = p.runAsUser("dbus-send", "--type=signal", "/KIO/Scheduler", "org.kde.KIO.Scheduler.reparseSlaveConfiguration", "string:''") + err = p.execute("dbus-send", "--type=signal", "/KIO/Scheduler", "org.kde.KIO.Scheduler.reparseSlaveConfiguration", "string:''") if err != nil { return err } @@ -127,23 +139,13 @@ func (p *LinuxSystemProxy) Disable() error { return nil } -func (p *LinuxSystemProxy) runAsUser(name string, args ...string) error { - if os.Getuid() != 0 { - return shell.Exec(name, args...).Attach().Run() - } else if p.sudoUser != "" { - return shell.Exec("su", "-", p.sudoUser, "-c", F.ToString(name, " ", strings.Join(args, " "))).Attach().Run() - } else { - return E.New("set system proxy: unable to set as root") - } -} - func (p *LinuxSystemProxy) setGnomeProxy(proxyTypes ...string) error { for _, proxyType := range proxyTypes { - err := p.runAsUser("gsettings", "set", "org.gnome.system.proxy."+proxyType, "host", p.serverAddr.AddrString()) + err := p.execute("gsettings", "set", "org.gnome.system.proxy."+proxyType, "host", p.serverAddr.AddrString()) if err != nil { return err } - err = p.runAsUser("gsettings", "set", "org.gnome.system.proxy."+proxyType, "port", F.ToString(p.serverAddr.Port)) + err = p.execute("gsettings", "set", "org.gnome.system.proxy."+proxyType, "port", F.ToString(p.serverAddr.Port)) if err != nil { return err } @@ -159,7 +161,7 @@ func (p *LinuxSystemProxy) setKDEProxy(proxyTypes ...string) error { } else { proxyUrl = "http://" + p.serverAddr.String() } - err := p.runAsUser( + err := p.execute( p.kWriteConfigCmd, "--file", "kioslaverc", diff --git a/experimental/boxdd/peer.go b/experimental/boxdd/peer.go index 43600ad04..cae63b9cb 100644 --- a/experimental/boxdd/peer.go +++ b/experimental/boxdd/peer.go @@ -1,3 +1,4 @@ +//nolint:unused package main import ( diff --git a/experimental/boxdd/platform_linux.go b/experimental/boxdd/platform_linux.go new file mode 100644 index 000000000..77dfedcd5 --- /dev/null +++ b/experimental/boxdd/platform_linux.go @@ -0,0 +1,394 @@ +//go:build linux && !android + +package main + +import ( + "net/netip" + "os" + "os/exec" + "os/user" + "strconv" + "strings" + "sync" + "syscall" + + "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/sagernet/sing/common/shell" +) + +type linuxPlatformInterface struct { + daemon *Daemon + access sync.Mutex + ownerUser *user.User + systemProxy *settings.LinuxSystemProxy + systemProxyEnabled bool +} + +func newPlatformInterface(daemonInstance *Daemon) (daemonPlatform, error) { + return &linuxPlatformInterface{ + daemon: daemonInstance, + systemProxyEnabled: true, + }, nil +} + +func (p *linuxPlatformInterface) Initialize(networkManager adapter.NetworkManager) error { + return nil +} + +func (p *linuxPlatformInterface) UsePlatformAutoDetectInterfaceControl() bool { + return false +} + +func (p *linuxPlatformInterface) AutoDetectInterfaceControl(fd int) error { + return os.ErrInvalid +} + +func (p *linuxPlatformInterface) UsePlatformInterface() bool { + return false +} + +func (p *linuxPlatformInterface) OpenInterface(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error) { + return nil, os.ErrInvalid +} + +func (p *linuxPlatformInterface) ProcessPlatformOptions(options option.TunPlatformOptions) error { + if options.HTTPProxy == nil || !options.HTTPProxy.Enabled { + return nil + } + httpProxyOptions := options.HTTPProxy + systemProxy, err := settings.NewLinuxSystemProxy( + M.ParseSocksaddrHostPort(httpProxyOptions.Server, httpProxyOptions.ServerPort), + false, + p.executeAsOwner, + ) + if err != nil { + p.daemon.logger.Warn("initialize system proxy: ", err) + return nil + } + 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 *linuxPlatformInterface) UsePlatformDefaultInterfaceMonitor() bool { + return false +} + +func (p *linuxPlatformInterface) CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor { + return nil +} + +func (p *linuxPlatformInterface) UsePlatformNetworkInterfaces() bool { + return false +} + +func (p *linuxPlatformInterface) NetworkInterfaces() ([]adapter.NetworkInterface, error) { + return nil, os.ErrInvalid +} + +func (p *linuxPlatformInterface) UnderNetworkExtension() bool { + return false +} + +func (p *linuxPlatformInterface) NetworkExtensionIncludeAllNetworks() bool { + return false +} + +func (p *linuxPlatformInterface) ClearDNSCache() { +} + +func (p *linuxPlatformInterface) RequestPermissionForWIFIState() error { + return nil +} + +func (p *linuxPlatformInterface) ReadWIFIState() adapter.WIFIState { + return adapter.WIFIState{} +} + +func (p *linuxPlatformInterface) UsePlatformConnectionOwnerFinder() bool { + return false +} + +func (p *linuxPlatformInterface) FindConnectionOwner(request *adapter.FindConnectionOwnerRequest) (*adapter.ConnectionOwner, error) { + return nil, os.ErrInvalid +} + +func (p *linuxPlatformInterface) UsePlatformWIFIMonitor() bool { + return false +} + +func (p *linuxPlatformInterface) UsePlatformNotification() bool { + return false +} + +func (p *linuxPlatformInterface) SendNotification(notification *adapter.Notification) error { + return nil +} + +func (p *linuxPlatformInterface) MyInterfaceAddress() []netip.Addr { + return nil +} + +func (p *linuxPlatformInterface) UsePlatformNeighborResolver() bool { + return false +} + +func (p *linuxPlatformInterface) StartNeighborMonitor(listener adapter.NeighborUpdateListener) error { + return os.ErrInvalid +} + +func (p *linuxPlatformInterface) CloseNeighborMonitor(listener adapter.NeighborUpdateListener) error { + return nil +} + +func (p *linuxPlatformInterface) UsePlatformShell() bool { + return false +} + +func (p *linuxPlatformInterface) CheckPlatformShell() error { + return nil +} + +func (p *linuxPlatformInterface) OpenShellSession(user *adapter.PlatformUser, command string, environ []string, term string, rows int32, cols int32) (adapter.ShellSession, error) { + return nil, os.ErrInvalid +} + +func (p *linuxPlatformInterface) LookupUser(username string) (*adapter.PlatformUser, error) { + return nil, os.ErrInvalid +} + +func (p *linuxPlatformInterface) LookupSFTPServer() (string, error) { + return "", os.ErrInvalid +} + +func (p *linuxPlatformInterface) ReadSystemSSHHostKey() ([]byte, error) { + return nil, os.ErrInvalid +} + +func (p *linuxPlatformInterface) TailscaleHostname() string { + return "" +} + +func (p *linuxPlatformInterface) UsePlatformBridge() bool { + return false +} + +func (p *linuxPlatformInterface) CreateBridge(options adapter.BridgeOptions) (adapter.BridgeSession, error) { + return nil, os.ErrInvalid +} + +func (p *linuxPlatformInterface) PrepareOwner(identity peerIdentity) error { + p.access.Lock() + defer p.access.Unlock() + if listenAddress != "" { + return p.applySystemProxyLocked() + } + if p.ownerUser != nil && p.ownerUser.Uid == identity.UserID { + return p.applySystemProxyLocked() + } + ownerUser, err := user.LookupId(identity.UserID) + if err != nil { + return E.Cause(err, "lookup owner user") + } + return p.replaceOwnerLocked(ownerUser) +} + +func (p *linuxPlatformInterface) RestoreOwner(state ownerState) error { + p.access.Lock() + defer p.access.Unlock() + if listenAddress != "" { + return nil + } + ownerUser, err := user.LookupId(state.UserID) + if err != nil { + return E.Cause(err, "lookup owner user") + } + p.ownerUser = ownerUser + return nil +} + +func (p *linuxPlatformInterface) ReleaseOwner() error { + p.access.Lock() + defer p.access.Unlock() + err := p.disableSystemProxyLocked() + if err != nil { + return err + } + p.ownerUser = nil + return nil +} + +func (p *linuxPlatformInterface) ResetPlatformOptions() error { + p.access.Lock() + defer p.access.Unlock() + err := p.disableSystemProxyLocked() + if err == nil { + p.systemProxy = nil + } + return err +} + +func (p *linuxPlatformInterface) SetSystemProxyPreference(enabled bool) { + p.access.Lock() + p.systemProxyEnabled = enabled + p.access.Unlock() +} + +func (p *linuxPlatformInterface) 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 *linuxPlatformInterface) 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 *linuxPlatformInterface) HandleSessionChange(eventType uint32, sessionID uint32, state ownerState) (uint32, bool, error) { + return 0, false, nil +} + +func (p *linuxPlatformInterface) Close() error { + p.access.Lock() + defer p.access.Unlock() + err := p.disableSystemProxyLocked() + p.systemProxy = nil + p.ownerUser = nil + return err +} + +func (p *linuxPlatformInterface) applySystemProxyLocked() error { + if p.systemProxy == nil { + return nil + } + if p.systemProxyEnabled { + if p.systemProxy.IsEnabled() || !p.ownerSessionActiveLocked() { + return nil + } + return p.systemProxy.Enable() + } + return p.disableSystemProxyLocked() +} + +func (p *linuxPlatformInterface) disableSystemProxyLocked() error { + if p.systemProxy == nil || !p.systemProxy.IsEnabled() || !p.ownerSessionActiveLocked() { + return nil + } + return p.systemProxy.Disable() +} + +func (p *linuxPlatformInterface) ownerSessionActiveLocked() bool { + if listenAddress != "" { + return true + } + if p.ownerUser == nil { + return false + } + _, err := os.Stat(ownerSessionBusPath(p.ownerUser)) + return err == nil +} + +func (p *linuxPlatformInterface) executeAsOwner(name string, args ...string) error { + if listenAddress != "" { + return shell.Exec(name, args...).Attach().Run() + } + ownerUser := p.ownerUser + if ownerUser == nil { + return E.New("missing owner session") + } + uid, err := strconv.ParseUint(ownerUser.Uid, 10, 32) + if err != nil { + return E.Cause(err, "parse owner user ID") + } + gid, err := strconv.ParseUint(ownerUser.Gid, 10, 32) + if err != nil { + return E.Cause(err, "parse owner group ID") + } + searchPath := os.Getenv("PATH") + if searchPath == "" { + searchPath = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + } + command := exec.Command(name, args...) + command.SysProcAttr = &syscall.SysProcAttr{ + Credential: &syscall.Credential{ + Uid: uint32(uid), + Gid: uint32(gid), + }, + } + command.Env = []string{ + "HOME=" + ownerUser.HomeDir, + "USER=" + ownerUser.Username, + "LOGNAME=" + ownerUser.Username, + "PATH=" + searchPath, + "XDG_RUNTIME_DIR=" + ownerRuntimeDirectory(ownerUser), + "DBUS_SESSION_BUS_ADDRESS=unix:path=" + ownerSessionBusPath(ownerUser), + } + output, err := command.CombinedOutput() + if err != nil { + outputText := strings.TrimSpace(string(output)) + if outputText != "" { + return E.Cause(err, "execute (", name, ") ", strings.Join(args, " "), ": ", outputText) + } + return E.Cause(err, "execute (", name, ") ", strings.Join(args, " ")) + } + return nil +} + +func (p *linuxPlatformInterface) replaceOwnerLocked(ownerUser *user.User) error { + err := p.disableSystemProxyLocked() + if err != nil { + return err + } + p.ownerUser = ownerUser + return p.applySystemProxyLocked() +} + +func ownerRuntimeDirectory(ownerUser *user.User) string { + return "/run/user/" + ownerUser.Uid +} + +func ownerSessionBusPath(ownerUser *user.User) string { + return ownerRuntimeDirectory(ownerUser) + "/bus" +} + +var _ daemonPlatform = (*linuxPlatformInterface)(nil) diff --git a/experimental/boxdd/platform_stub.go b/experimental/boxdd/platform_stub.go index 0ad7e298d..856767881 100644 --- a/experimental/boxdd/platform_stub.go +++ b/experimental/boxdd/platform_stub.go @@ -1,4 +1,4 @@ -//go:build !windows +//go:build !windows && !(linux && !android) package main