diff --git a/experimental/libbox/config.go b/experimental/libbox/config.go index b7731143..4af96994 100644 --- a/experimental/libbox/config.go +++ b/experimental/libbox/config.go @@ -9,6 +9,7 @@ import ( "github.com/sagernet/sing-box" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/process" + "github.com/sagernet/sing-box/experimental/libbox/platform" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/control" @@ -97,6 +98,14 @@ func (s *platformInterfaceStub) FindProcessInfo(ctx context.Context, network str return nil, os.ErrInvalid } +func (s *platformInterfaceStub) PerAppProxyList() ([]uint32, error) { + return nil, os.ErrInvalid +} + +func (s *platformInterfaceStub) PerAppProxyMode() int32 { + return platform.PerAppProxyModeDisabled +} + type interfaceMonitorStub struct{} func (s *interfaceMonitorStub) Start() error { diff --git a/experimental/libbox/platform.go b/experimental/libbox/platform.go index 4078140f..3afa839e 100644 --- a/experimental/libbox/platform.go +++ b/experimental/libbox/platform.go @@ -22,6 +22,8 @@ type PlatformInterface interface { IncludeAllNetworks() bool ReadWIFIState() *WIFIState ClearDNSCache() + PerAppProxyList() (IntegerIterator, error) + PerAppProxyMode() int32 } type TunInterface interface { @@ -54,6 +56,11 @@ type NetworkInterfaceIterator interface { HasNext() bool } +type IntegerIterator interface { + Next() int32 + HasNext() bool +} + type OnDemandRule interface { Target() int32 DNSSearchDomainMatch() StringIterator diff --git a/experimental/libbox/platform/interface.go b/experimental/libbox/platform/interface.go index 3bec13fa..75136781 100644 --- a/experimental/libbox/platform/interface.go +++ b/experimental/libbox/platform/interface.go @@ -11,6 +11,12 @@ import ( "github.com/sagernet/sing/common/logger" ) +const ( + PerAppProxyModeDisabled int32 = iota + PerAppProxyModeExclude + PerAppProxyModeInclude +) + type Interface interface { Initialize(ctx context.Context, router adapter.Router) error UsePlatformAutoDetectInterfaceControl() bool @@ -24,5 +30,7 @@ type Interface interface { IncludeAllNetworks() bool ClearDNSCache() ReadWIFIState() adapter.WIFIState + PerAppProxyList() ([]uint32, error) + PerAppProxyMode() int32 process.Searcher } diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go index 0a54d7ab..299945ba 100644 --- a/experimental/libbox/service.go +++ b/experimental/libbox/service.go @@ -229,6 +229,18 @@ func (w *platformInterfaceWrapper) ReadWIFIState() adapter.WIFIState { return (adapter.WIFIState)(*wifiState) } +func (w *platformInterfaceWrapper) PerAppProxyList() ([]uint32, error) { + uidIterator, err := w.iif.PerAppProxyList() + if err != nil { + return nil, err + } + return common.Map(iteratorToArray[int32](uidIterator), func(it int32) uint32 { return uint32(it) }), nil +} + +func (w *platformInterfaceWrapper) PerAppProxyMode() int32 { + return w.iif.PerAppProxyMode() +} + func (w *platformInterfaceWrapper) DisableColors() bool { return runtime.GOOS != "android" } diff --git a/inbound/builder.go b/inbound/builder.go index 201ab800..e1909b84 100644 --- a/inbound/builder.go +++ b/inbound/builder.go @@ -19,7 +19,7 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, o case C.TypeTun: return NewTun(ctx, router, logger, options.Tag, options.TunOptions, platformInterface) case C.TypeRedirect: - return NewRedirect(ctx, router, logger, options.Tag, options.RedirectOptions) + return NewRedirect(ctx, router, logger, options.Tag, options.RedirectOptions, platformInterface) case C.TypeTProxy: return NewTProxy(ctx, router, logger, options.Tag, options.TProxyOptions), nil case C.TypeDirect: diff --git a/inbound/redirect.go b/inbound/redirect.go index 9b79fa1c..bf0da9be 100644 --- a/inbound/redirect.go +++ b/inbound/redirect.go @@ -7,11 +7,13 @@ import ( "net/netip" "os" "os/exec" + "sort" "strings" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/redir" C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/experimental/libbox/platform" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" @@ -24,12 +26,13 @@ import ( type Redirect struct { myInboundAdapter - autoRedirect option.AutoRedirectOptions - needSu bool - suPath string + platformInterface platform.Interface + autoRedirect option.AutoRedirectOptions + needSu bool + suPath string } -func NewRedirect(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.RedirectInboundOptions) (*Redirect, error) { +func NewRedirect(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.RedirectInboundOptions, platformInterface platform.Interface) (*Redirect, error) { redirect := &Redirect{ myInboundAdapter: myInboundAdapter{ protocol: C.TypeRedirect, @@ -40,7 +43,8 @@ func NewRedirect(ctx context.Context, router adapter.Router, logger log.ContextL tag: tag, listenOptions: options.ListenOptions, }, - autoRedirect: common.PtrValueOrDefault(options.AutoRedirect), + platformInterface: platformInterface, + autoRedirect: common.PtrValueOrDefault(options.AutoRedirect), } if redirect.autoRedirect.Enabled { if !C.IsAndroid { @@ -101,21 +105,73 @@ func (r *Redirect) Close() error { } func (r *Redirect) setupRedirect() error { - myUid := os.Getuid() - tcpPort := M.AddrPortFromNet(r.tcpListener.Addr()).Port() - interfaceRules := common.FlatMap(r.router.(adapter.Router).InterfaceFinder().Interfaces(), func(it control.Interface) []string { - return common.Map(common.Filter(it.Addresses, func(it netip.Prefix) bool { return it.Addr().Is4() }), func(it netip.Prefix) string { - return "iptables -t nat -A sing-box -p tcp -j RETURN -d " + it.String() - }) - }) - return r.runAndroidShell(` + tableName := "sing-box" + rules := ` set -e -o pipefail iptables -t nat -N sing-box -` + strings.Join(interfaceRules, "\n") + ` -iptables -t nat -A sing-box -j RETURN -m owner --uid-owner ` + F.ToString(myUid) + ` -iptables -t nat -A sing-box -p tcp -j REDIRECT --to-ports ` + F.ToString(tcpPort) + ` -iptables -t nat -A OUTPUT -p tcp -j sing-box -`) +` + rules += strings.Join(common.FlatMap(r.router.(adapter.Router).InterfaceFinder().Interfaces(), func(it control.Interface) []string { + return common.Map(common.Filter(it.Addresses, func(it netip.Prefix) bool { return it.Addr().Is4() }), func(it netip.Prefix) string { + return "iptables -t nat -A " + tableName + " -p tcp -j RETURN -d " + it.String() + }) + }), "\n") + var ( + myUid = uint32(os.Getuid()) + perAppProxyList []uint32 + perAppProxyMap = make(map[uint32]bool) + perAppProxyMode int32 + err error + ) + if r.platformInterface != nil { + perAppProxyMode = r.platformInterface.PerAppProxyMode() + if perAppProxyMode != platform.PerAppProxyModeDisabled { + perAppProxyList, err = r.platformInterface.PerAppProxyList() + if err != nil { + return E.Cause(err, "read per app proxy configuration") + } + } + for _, proxyUID := range perAppProxyList { + perAppProxyMap[proxyUID] = true + } + } + excludeUser := func(userID uint32) { + if perAppProxyMode != platform.PerAppProxyModeInclude { + perAppProxyMap[userID] = false + } else { + delete(perAppProxyMap, userID) + } + } + excludeUser(myUid) + if myUid != 0 && myUid != 2000 { + excludeUser(0) + } + perAppProxyList = perAppProxyList[:0] + for uid := range perAppProxyMap { + perAppProxyList = append(perAppProxyList, uid) + } + sort.SliceStable(perAppProxyList, func(i, j int) bool { + return perAppProxyList[i] < perAppProxyList[j] + }) + redirectPortStr := F.ToString(M.AddrPortFromNet(r.tcpListener.Addr()).Port()) + if perAppProxyMode != platform.PerAppProxyModeInclude { + rules += "\n" + strings.Join(common.Map(perAppProxyList, func(it uint32) string { + return "iptables -t nat -A " + tableName + " -j RETURN -m owner --uid-owner " + F.ToString(it) + }), "\n") + rules += "\niptables -t nat -A " + tableName + " -p tcp -j REDIRECT --to-ports " + redirectPortStr + } else { + rules += "\n" + strings.Join(common.Map(perAppProxyList, func(it uint32) string { + return "iptables -t nat -A " + tableName + " -p tcp -j REDIRECT --to-ports " + redirectPortStr + " -m owner --uid-owner " + F.ToString(it) + }), "\n") + } + rules += "\niptables -t nat -A OUTPUT -p tcp -j " + tableName + for _, ruleLine := range strings.Split(rules, "\n") { + ruleLine = strings.TrimSpace(ruleLine) + if ruleLine == "" { + continue + } + r.logger.Debug("# ", ruleLine) + } + return r.runAndroidShell(rules) } func (r *Redirect) cleanupRedirect() {