Add per app proxy support for auto redirect

This commit is contained in:
世界 2024-05-28 12:25:54 +08:00
parent 4b0264142c
commit f3bfd1562b
No known key found for this signature in database
GPG Key ID: CD109927C34A63C4
7 changed files with 112 additions and 20 deletions

View File

@ -2,7 +2,7 @@
icon: material/new-box icon: material/new-box
--- ---
!!! quote "Changes in sing-box 1.9.0" !!! quote "Changes in sing-box 1.10.0"
:material-plus: [auto_redirect](#auto_redirect) :material-plus: [auto_redirect](#auto_redirect)

View File

@ -9,6 +9,7 @@ import (
"github.com/sagernet/sing-box" "github.com/sagernet/sing-box"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/process" "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-box/option"
"github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/control" "github.com/sagernet/sing/common/control"
@ -97,6 +98,14 @@ func (s *platformInterfaceStub) FindProcessInfo(ctx context.Context, network str
return nil, os.ErrInvalid 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{} type interfaceMonitorStub struct{}
func (s *interfaceMonitorStub) Start() error { func (s *interfaceMonitorStub) Start() error {

View File

@ -22,6 +22,8 @@ type PlatformInterface interface {
IncludeAllNetworks() bool IncludeAllNetworks() bool
ReadWIFIState() *WIFIState ReadWIFIState() *WIFIState
ClearDNSCache() ClearDNSCache()
PerAppProxyList() (IntegerIterator, error)
PerAppProxyMode() int32
} }
type TunInterface interface { type TunInterface interface {
@ -54,6 +56,11 @@ type NetworkInterfaceIterator interface {
HasNext() bool HasNext() bool
} }
type IntegerIterator interface {
Next() int32
HasNext() bool
}
type OnDemandRule interface { type OnDemandRule interface {
Target() int32 Target() int32
DNSSearchDomainMatch() StringIterator DNSSearchDomainMatch() StringIterator

View File

@ -11,6 +11,12 @@ import (
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
) )
const (
PerAppProxyModeDisabled int32 = iota
PerAppProxyModeExclude
PerAppProxyModeInclude
)
type Interface interface { type Interface interface {
Initialize(ctx context.Context, router adapter.Router) error Initialize(ctx context.Context, router adapter.Router) error
UsePlatformAutoDetectInterfaceControl() bool UsePlatformAutoDetectInterfaceControl() bool
@ -24,5 +30,7 @@ type Interface interface {
IncludeAllNetworks() bool IncludeAllNetworks() bool
ClearDNSCache() ClearDNSCache()
ReadWIFIState() adapter.WIFIState ReadWIFIState() adapter.WIFIState
PerAppProxyList() ([]uint32, error)
PerAppProxyMode() int32
process.Searcher process.Searcher
} }

View File

@ -229,6 +229,18 @@ func (w *platformInterfaceWrapper) ReadWIFIState() adapter.WIFIState {
return (adapter.WIFIState)(*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 { func (w *platformInterfaceWrapper) DisableColors() bool {
return runtime.GOOS != "android" return runtime.GOOS != "android"
} }

View File

@ -19,7 +19,7 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, o
case C.TypeTun: case C.TypeTun:
return NewTun(ctx, router, logger, options.Tag, options.TunOptions, platformInterface) return NewTun(ctx, router, logger, options.Tag, options.TunOptions, platformInterface)
case C.TypeRedirect: 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: case C.TypeTProxy:
return NewTProxy(ctx, router, logger, options.Tag, options.TProxyOptions), nil return NewTProxy(ctx, router, logger, options.Tag, options.TProxyOptions), nil
case C.TypeDirect: case C.TypeDirect:

View File

@ -7,11 +7,13 @@ import (
"net/netip" "net/netip"
"os" "os"
"os/exec" "os/exec"
"sort"
"strings" "strings"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/redir" "github.com/sagernet/sing-box/common/redir"
C "github.com/sagernet/sing-box/constant" 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/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
@ -24,12 +26,13 @@ import (
type Redirect struct { type Redirect struct {
myInboundAdapter myInboundAdapter
platformInterface platform.Interface
autoRedirect option.AutoRedirectOptions autoRedirect option.AutoRedirectOptions
needSu bool needSu bool
suPath string 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{ redirect := &Redirect{
myInboundAdapter: myInboundAdapter{ myInboundAdapter: myInboundAdapter{
protocol: C.TypeRedirect, protocol: C.TypeRedirect,
@ -40,6 +43,7 @@ func NewRedirect(ctx context.Context, router adapter.Router, logger log.ContextL
tag: tag, tag: tag,
listenOptions: options.ListenOptions, listenOptions: options.ListenOptions,
}, },
platformInterface: platformInterface,
autoRedirect: common.PtrValueOrDefault(options.AutoRedirect), autoRedirect: common.PtrValueOrDefault(options.AutoRedirect),
} }
if redirect.autoRedirect.Enabled { if redirect.autoRedirect.Enabled {
@ -101,21 +105,73 @@ func (r *Redirect) Close() error {
} }
func (r *Redirect) setupRedirect() error { func (r *Redirect) setupRedirect() error {
myUid := os.Getuid() tableName := "sing-box"
tcpPort := M.AddrPortFromNet(r.tcpListener.Addr()).Port() rules := `
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(`
set -e -o pipefail set -e -o pipefail
iptables -t nat -N sing-box 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) + ` rules += strings.Join(common.FlatMap(r.router.(adapter.Router).InterfaceFinder().Interfaces(), func(it control.Interface) []string {
iptables -t nat -A sing-box -p tcp -j REDIRECT --to-ports ` + F.ToString(tcpPort) + ` return common.Map(common.Filter(it.Addresses, func(it netip.Prefix) bool { return it.Addr().Is4() }), func(it netip.Prefix) string {
iptables -t nat -A OUTPUT -p tcp -j sing-box 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() { func (r *Redirect) cleanupRedirect() {