diff --git a/common/settings/proxy_android.go b/common/settings/proxy_android.go index c045cb09..d3768ced 100644 --- a/common/settings/proxy_android.go +++ b/common/settings/proxy_android.go @@ -1,43 +1,73 @@ package settings import ( + "context" "os" "strings" - "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" + E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" + M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/shell" ) -var ( - useRish bool - rishPath string -) +type AndroidSystemProxy struct { + useRish bool + rishPath string + serverAddr M.Socksaddr + supportSOCKS bool + isEnabled bool +} -func init() { +func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*AndroidSystemProxy, error) { userId := os.Getuid() + var ( + useRish bool + rishPath string + ) if userId == 0 || userId == 1000 || userId == 2000 { useRish = false } else { rishPath, useRish = C.FindPath("rish") + if !useRish { + return nil, E.Cause(os.ErrPermission, "root or system (adb) permission is required for set system proxy") + } } -} - -func runAndroidShell(name string, args ...string) error { - if !useRish { - return shell.Exec(name, args...).Attach().Run() - } else { - return shell.Exec("sh", rishPath, "-c", F.ToString(name, " ", strings.Join(args, " "))).Attach().Run() - } -} - -func SetSystemProxy(router adapter.Router, port uint16, isMixed bool) (func() error, error) { - err := runAndroidShell("settings", "put", "global", "http_proxy", F.ToString("127.0.0.1:", port)) - if err != nil { - return nil, err - } - return func() error { - return runAndroidShell("settings", "put", "global", "http_proxy", ":0") + return &AndroidSystemProxy{ + useRish: useRish, + rishPath: rishPath, + serverAddr: serverAddr, + supportSOCKS: supportSOCKS, }, nil } + +func (p *AndroidSystemProxy) IsEnabled() bool { + return p.isEnabled +} + +func (p *AndroidSystemProxy) Enable() error { + err := p.runAndroidShell("settings", "put", "global", "http_proxy", p.serverAddr.String()) + if err != nil { + return err + } + p.isEnabled = true + return nil +} + +func (p *AndroidSystemProxy) Disable() error { + err := p.runAndroidShell("settings", "put", "global", "http_proxy", ":0") + if err != nil { + return err + } + p.isEnabled = false + return nil +} + +func (p *AndroidSystemProxy) runAndroidShell(name string, args ...string) error { + if !p.useRish { + return shell.Exec(name, args...).Attach().Run() + } else { + return shell.Exec("sh", p.rishPath, "-c", F.ToString(name, " ", strings.Join(args, " "))).Attach().Run() + } +} diff --git a/common/settings/proxy_darwin.go b/common/settings/proxy_darwin.go index 101ed096..f03658a8 100644 --- a/common/settings/proxy_darwin.go +++ b/common/settings/proxy_darwin.go @@ -1,56 +1,56 @@ package settings import ( + "context" "net/netip" + "strconv" "strings" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-tun" E "github.com/sagernet/sing/common/exceptions" - F "github.com/sagernet/sing/common/format" + M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/shell" "github.com/sagernet/sing/common/x/list" ) -type systemProxy struct { +type DarwinSystemProxy struct { monitor tun.DefaultInterfaceMonitor interfaceName string element *list.Element[tun.DefaultInterfaceUpdateCallback] - port uint16 - isMixed bool + serverAddr M.Socksaddr + supportSOCKS bool + isEnabled bool } -func (p *systemProxy) update(event int) { - newInterfaceName := p.monitor.DefaultInterfaceName(netip.IPv4Unspecified()) - if p.interfaceName == newInterfaceName { - return +func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*DarwinSystemProxy, error) { + interfaceMonitor := adapter.RouterFromContext(ctx).InterfaceMonitor() + if interfaceMonitor == nil { + return nil, E.New("missing interface monitor") } - if p.interfaceName != "" { - _ = p.unset() + proxy := &DarwinSystemProxy{ + monitor: interfaceMonitor, + serverAddr: serverAddr, + supportSOCKS: supportSOCKS, } - p.interfaceName = newInterfaceName - interfaceDisplayName, err := getInterfaceDisplayName(p.interfaceName) - if err != nil { - return - } - if p.isMixed { - err = shell.Exec("networksetup", "-setsocksfirewallproxy", interfaceDisplayName, "127.0.0.1", F.ToString(p.port)).Attach().Run() - } - if err == nil { - err = shell.Exec("networksetup", "-setwebproxy", interfaceDisplayName, "127.0.0.1", F.ToString(p.port)).Attach().Run() - } - if err == nil { - _ = shell.Exec("networksetup", "-setsecurewebproxy", interfaceDisplayName, "127.0.0.1", F.ToString(p.port)).Attach().Run() - } - return + proxy.element = interfaceMonitor.RegisterCallback(proxy.update) + return proxy, nil } -func (p *systemProxy) unset() error { +func (p *DarwinSystemProxy) IsEnabled() bool { + return p.isEnabled +} + +func (p *DarwinSystemProxy) Enable() error { + return p.update0() +} + +func (p *DarwinSystemProxy) Disable() error { interfaceDisplayName, err := getInterfaceDisplayName(p.interfaceName) if err != nil { return err } - if p.isMixed { + if p.supportSOCKS { err = shell.Exec("networksetup", "-setsocksfirewallproxystate", interfaceDisplayName, "off").Attach().Run() } if err == nil { @@ -59,9 +59,53 @@ func (p *systemProxy) unset() error { if err == nil { err = shell.Exec("networksetup", "-setsecurewebproxystate", interfaceDisplayName, "off").Attach().Run() } + if err == nil { + p.isEnabled = false + } return err } +func (p *DarwinSystemProxy) update(event int) { + if event&tun.EventInterfaceUpdate == 0 { + return + } + if !p.isEnabled { + return + } + _ = p.update0() +} + +func (p *DarwinSystemProxy) update0() error { + newInterfaceName := p.monitor.DefaultInterfaceName(netip.IPv4Unspecified()) + if p.interfaceName == newInterfaceName { + return nil + } + if p.interfaceName != "" { + _ = p.Disable() + } + p.interfaceName = newInterfaceName + interfaceDisplayName, err := getInterfaceDisplayName(p.interfaceName) + if err != nil { + return err + } + if p.supportSOCKS { + err = shell.Exec("networksetup", "-setsocksfirewallproxy", interfaceDisplayName, p.serverAddr.AddrString(), strconv.Itoa(int(p.serverAddr.Port))).Attach().Run() + } + if err != nil { + return err + } + err = shell.Exec("networksetup", "-setwebproxy", interfaceDisplayName, p.serverAddr.AddrString(), strconv.Itoa(int(p.serverAddr.Port))).Attach().Run() + if err != nil { + return err + } + err = shell.Exec("networksetup", "-setsecurewebproxy", interfaceDisplayName, p.serverAddr.AddrString(), strconv.Itoa(int(p.serverAddr.Port))).Attach().Run() + if err != nil { + return err + } + p.isEnabled = true + return nil +} + func getInterfaceDisplayName(name string) (string, error) { content, err := shell.Exec("networksetup", "-listallhardwareports").ReadOutput() if err != nil { @@ -77,21 +121,3 @@ func getInterfaceDisplayName(name string) (string, error) { } return "", E.New(name, " not found in networksetup -listallhardwareports") } - -func SetSystemProxy(router adapter.Router, port uint16, isMixed bool) (func() error, error) { - interfaceMonitor := router.InterfaceMonitor() - if interfaceMonitor == nil { - return nil, E.New("missing interface monitor") - } - proxy := &systemProxy{ - monitor: interfaceMonitor, - port: port, - isMixed: isMixed, - } - proxy.update(tun.EventInterfaceUpdate) - proxy.element = interfaceMonitor.RegisterCallback(proxy.update) - return func() error { - interfaceMonitor.UnregisterCallback(proxy.element) - return proxy.unset() - }, nil -} diff --git a/common/settings/proxy_linux.go b/common/settings/proxy_linux.go index d1bb9eca..af379c6d 100644 --- a/common/settings/proxy_linux.go +++ b/common/settings/proxy_linux.go @@ -3,106 +3,137 @@ package settings import ( + "context" "os" "os/exec" "strings" - "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" + M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/shell" ) -var ( - hasGSettings bool - isKDE5 bool - sudoUser string -) +type LinuxSystemProxy struct { + hasGSettings bool + hasKWriteConfig5 bool + sudoUser string + serverAddr M.Socksaddr + supportSOCKS bool + isEnabled bool +} -func init() { - isKDE5 = common.Error(exec.LookPath("kwriteconfig5")) == nil - hasGSettings = common.Error(exec.LookPath("gsettings")) == nil +func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*LinuxSystemProxy, error) { + hasGSettings := common.Error(exec.LookPath("gsettings")) == nil + hasKWriteConfig5 := common.Error(exec.LookPath("kwriteconfig5")) == nil + var sudoUser string if os.Getuid() == 0 { sudoUser = os.Getenv("SUDO_USER") } + if !hasGSettings && !hasKWriteConfig5 { + return nil, E.New("unsupported desktop environment") + } + return &LinuxSystemProxy{ + hasGSettings: hasGSettings, + hasKWriteConfig5: hasKWriteConfig5, + sudoUser: sudoUser, + serverAddr: serverAddr, + supportSOCKS: supportSOCKS, + }, nil } -func runAsUser(name string, args ...string) error { +func (p *LinuxSystemProxy) IsEnabled() bool { + return p.isEnabled +} + +func (p *LinuxSystemProxy) Enable() error { + if p.hasGSettings { + err := p.runAsUser("gsettings", "set", "org.gnome.system.proxy.http", "enabled", "true") + if err != nil { + return err + } + if p.supportSOCKS { + err = p.setGnomeProxy("ftp", "http", "https", "socks") + } else { + err = p.setGnomeProxy("http", "https") + } + if err != nil { + return err + } + err = p.runAsUser("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") + if err != nil { + return err + } + } + if p.hasKWriteConfig5 { + err := p.runAsUser("kwriteconfig5", "--file", "kioslaverc", "--group", "'Proxy Settings'", "--key", "ProxyType", "1") + if err != nil { + return err + } + if p.supportSOCKS { + err = p.setKDEProxy("ftp", "http", "https", "socks") + } else { + err = p.setKDEProxy("http", "https") + } + if err != nil { + return err + } + err = p.runAsUser("kwriteconfig5", "--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:''") + if err != nil { + return err + } + } + p.isEnabled = true + return nil +} + +func (p *LinuxSystemProxy) Disable() error { + if p.hasGSettings { + err := p.runAsUser("gsettings", "set", "org.gnome.system.proxy", "mode", "none") + if err != nil { + return err + } + } + if p.hasKWriteConfig5 { + err := p.runAsUser("kwriteconfig5", "--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:''") + if err != nil { + return err + } + } + p.isEnabled = false + return nil +} + +func (p *LinuxSystemProxy) runAsUser(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 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 SetSystemProxy(router adapter.Router, port uint16, isMixed bool) (func() error, error) { - if hasGSettings { - err := runAsUser("gsettings", "set", "org.gnome.system.proxy.http", "enabled", "true") - if err != nil { - return nil, err - } - if isMixed { - err = setGnomeProxy(port, "ftp", "http", "https", "socks") - } else { - err = setGnomeProxy(port, "http", "https") - } - if err != nil { - return nil, err - } - err = runAsUser("gsettings", "set", "org.gnome.system.proxy", "use-same-proxy", F.ToString(isMixed)) - if err != nil { - return nil, err - } - err = runAsUser("gsettings", "set", "org.gnome.system.proxy", "mode", "manual") - if err != nil { - return nil, err - } - return func() error { - return runAsUser("gsettings", "set", "org.gnome.system.proxy", "mode", "none") - }, nil - } - if isKDE5 { - err := runAsUser("kwriteconfig5", "--file", "kioslaverc", "--group", "'Proxy Settings'", "--key", "ProxyType", "1") - if err != nil { - return nil, err - } - if isMixed { - err = setKDEProxy(port, "ftp", "http", "https", "socks") - } else { - err = setKDEProxy(port, "http", "https") - } - if err != nil { - return nil, err - } - err = runAsUser("kwriteconfig5", "--file", "kioslaverc", "--group", "'Proxy Settings'", "--key", "Authmode", "0") - if err != nil { - return nil, err - } - err = runAsUser("dbus-send", "--type=signal", "/KIO/Scheduler", "org.kde.KIO.Scheduler.reparseSlaveConfiguration", "string:''") - if err != nil { - return nil, err - } - return func() error { - err = runAsUser("kwriteconfig5", "--file", "kioslaverc", "--group", "'Proxy Settings'", "--key", "ProxyType", "0") - if err != nil { - return err - } - return runAsUser("dbus-send", "--type=signal", "/KIO/Scheduler", "org.kde.KIO.Scheduler.reparseSlaveConfiguration", "string:''") - }, nil - } - return nil, E.New("unsupported desktop environment") -} - -func setGnomeProxy(port uint16, proxyTypes ...string) error { +func (p *LinuxSystemProxy) setGnomeProxy(proxyTypes ...string) error { for _, proxyType := range proxyTypes { - err := runAsUser("gsettings", "set", "org.gnome.system.proxy."+proxyType, "host", "127.0.0.1") + err := p.runAsUser("gsettings", "set", "org.gnome.system.proxy."+proxyType, "host", p.serverAddr.AddrString()) if err != nil { return err } - err = runAsUser("gsettings", "set", "org.gnome.system.proxy."+proxyType, "port", F.ToString(port)) + err = p.runAsUser("gsettings", "set", "org.gnome.system.proxy."+proxyType, "port", F.ToString(p.serverAddr.Port)) if err != nil { return err } @@ -110,20 +141,20 @@ func setGnomeProxy(port uint16, proxyTypes ...string) error { return nil } -func setKDEProxy(port uint16, proxyTypes ...string) error { +func (p *LinuxSystemProxy) setKDEProxy(proxyTypes ...string) error { for _, proxyType := range proxyTypes { var proxyUrl string if proxyType == "socks" { - proxyUrl = "socks://127.0.0.1:" + F.ToString(port) + proxyUrl = "socks://" + p.serverAddr.String() } else { - proxyUrl = "http://127.0.0.1:" + F.ToString(port) + proxyUrl = "http://" + p.serverAddr.String() } - err := runAsUser( + err := p.runAsUser( "kwriteconfig5", "--file", "kioslaverc", "--group", - "'Proxy Settings'", + "Proxy Settings", "--key", proxyType+"Proxy", proxyUrl, ) diff --git a/common/settings/proxy_stub.go b/common/settings/proxy_stub.go index 08ed0186..56eb4b65 100644 --- a/common/settings/proxy_stub.go +++ b/common/settings/proxy_stub.go @@ -3,11 +3,12 @@ package settings import ( + "context" "os" - "github.com/sagernet/sing-box/adapter" + M "github.com/sagernet/sing/common/metadata" ) -func SetSystemProxy(router adapter.Router, port uint16, isMixed bool) (func() error, error) { +func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (SystemProxy, error) { return nil, os.ErrInvalid } diff --git a/common/settings/proxy_windows.go b/common/settings/proxy_windows.go index 4a3b070e..793ac1d1 100644 --- a/common/settings/proxy_windows.go +++ b/common/settings/proxy_windows.go @@ -1,17 +1,43 @@ package settings import ( - "github.com/sagernet/sing-box/adapter" - F "github.com/sagernet/sing/common/format" + "context" + + M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/wininet" ) -func SetSystemProxy(router adapter.Router, port uint16, isMixed bool) (func() error, error) { - err := wininet.SetSystemProxy(F.ToString("http://127.0.0.1:", port), "") - if err != nil { - return nil, err - } - return func() error { - return wininet.ClearSystemProxy() +type WindowsSystemProxy struct { + serverAddr M.Socksaddr + supportSOCKS bool + isEnabled bool +} + +func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*WindowsSystemProxy, error) { + return &WindowsSystemProxy{ + serverAddr: serverAddr, + supportSOCKS: supportSOCKS, }, nil } + +func (p *WindowsSystemProxy) IsEnabled() bool { + return p.isEnabled +} + +func (p *WindowsSystemProxy) Enable() error { + err := wininet.SetSystemProxy("http://"+p.serverAddr.String(), "") + if err != nil { + return err + } + p.isEnabled = true + return nil +} + +func (p *WindowsSystemProxy) Disable() error { + err := wininet.ClearSystemProxy() + if err != nil { + return err + } + p.isEnabled = false + return nil +} diff --git a/common/settings/system_proxy.go b/common/settings/system_proxy.go new file mode 100644 index 00000000..0635c6f6 --- /dev/null +++ b/common/settings/system_proxy.go @@ -0,0 +1,7 @@ +package settings + +type SystemProxy interface { + IsEnabled() bool + Enable() error + Disable() error +} diff --git a/inbound/default.go b/inbound/default.go index 9ddfc915..cb6f61fe 100644 --- a/inbound/default.go +++ b/inbound/default.go @@ -33,8 +33,8 @@ type myInboundAdapter struct { // http mixed - setSystemProxy bool - clearSystemProxy func() error + setSystemProxy bool + systemProxy settings.SystemProxy // internal @@ -91,7 +91,19 @@ func (a *myInboundAdapter) Start() error { } } if a.setSystemProxy { - a.clearSystemProxy, err = settings.SetSystemProxy(a.router, M.SocksaddrFromNet(a.tcpListener.Addr()).Port, a.protocol == C.TypeMixed) + listenPort := M.SocksaddrFromNet(a.tcpListener.Addr()).Port + var listenAddrString string + listenAddr := a.listenOptions.Listen.Build() + if listenAddr.IsUnspecified() { + listenAddrString = "127.0.0.1" + } else { + listenAddrString = listenAddr.String() + } + a.systemProxy, err = settings.NewSystemProxy(a.ctx, M.ParseSocksaddrHostPort(listenAddrString, listenPort), a.protocol == C.TypeMixed) + if err != nil { + return E.Cause(err, "initialize system proxy") + } + err = a.systemProxy.Enable() if err != nil { return E.Cause(err, "set system proxy") } @@ -102,8 +114,8 @@ func (a *myInboundAdapter) Start() error { func (a *myInboundAdapter) Close() error { a.inShutdown.Store(true) var err error - if a.clearSystemProxy != nil { - err = a.clearSystemProxy() + if a.systemProxy != nil && a.systemProxy.IsEnabled() { + err = a.systemProxy.Disable() } return E.Errors(err, common.Close( a.tcpListener,