diff --git a/docs/configuration/experimental/cache-file.md b/docs/configuration/experimental/cache-file.md index b30538e5..18c430d9 100644 --- a/docs/configuration/experimental/cache-file.md +++ b/docs/configuration/experimental/cache-file.md @@ -1,7 +1,3 @@ ---- -icon: material/new-box ---- - !!! question "Since sing-box 1.8.0" !!! quote "Changes in sing-box 1.9.0" diff --git a/docs/configuration/experimental/cache-file.zh.md b/docs/configuration/experimental/cache-file.zh.md index 6d86dc84..656d53c4 100644 --- a/docs/configuration/experimental/cache-file.zh.md +++ b/docs/configuration/experimental/cache-file.zh.md @@ -1,7 +1,3 @@ ---- -icon: material/new-box ---- - !!! question "自 sing-box 1.8.0 起" !!! quote "sing-box 1.9.0 中的更改" diff --git a/docs/configuration/inbound/redirect.md b/docs/configuration/inbound/redirect.md index e0c5caf2..50a5bacd 100644 --- a/docs/configuration/inbound/redirect.md +++ b/docs/configuration/inbound/redirect.md @@ -1,11 +1,3 @@ ---- -icon: material/new-box ---- - -!!! quote "Changes in sing-box 1.10.0" - - :material-plus: [auto_redirect](#auto_redirect) - !!! quote "" Only supported on Linux and macOS. @@ -17,11 +9,6 @@ icon: material/new-box "type": "redirect", "tag": "redirect-in", - "auto_redirect": { - "enabled": false, - "continue_on_no_permission": false - }, - ... // Listen Fields } ``` @@ -29,23 +16,3 @@ icon: material/new-box ### Listen Fields See [Listen Fields](/configuration/shared/listen/) for details. - -### Fields - -#### `auto_redirect` - -!!! question "Since sing-box 1.10.0" - -!!! quote "" - - Only supported on Android. - -Automatically add iptables nat rules to hijack **IPv4 TCP** connections. - -It is expected to run with the Android graphical client (it will attempt to su at runtime). - -#### `auto_redirect.continue_on_no_permission` - -!!! question "Since sing-box 1.10.0" - -Ignore errors when the Android device is not rooted or is denied root access. diff --git a/docs/configuration/inbound/redirect.zh.md b/docs/configuration/inbound/redirect.zh.md index 53572bc5..a03049e5 100644 --- a/docs/configuration/inbound/redirect.zh.md +++ b/docs/configuration/inbound/redirect.zh.md @@ -1,11 +1,3 @@ ---- -icon: material/new-box ---- - -!!! quote "sing-box 1.10.0 中的更改" - - :material-plus: [auto_redirect](#auto_redirect) - !!! quote "" 仅支持 Linux 和 macOS。 @@ -17,35 +9,9 @@ icon: material/new-box "type": "redirect", "tag": "redirect-in", - "auto_redirect": { - "enabled": false, - "continue_on_no_permission": false - }, - ... // 监听字段 } ``` - ### 监听字段 参阅 [监听字段](/zh/configuration/shared/listen/)。 - -### 字段 - -#### `auto_redirect` - -!!! question "自 sing-box 1.10.0 起" - -!!! quote "" - - 仅支持 Android。 - -自动添加 iptables nat 规则以劫持 **IPv4 TCP** 连接。 - -它预计与 Android 图形客户端一起运行(将在运行时尝试 su)。 - -#### `auto_redirect.continue_on_no_permission` - -!!! question "自 sing-box 1.10.0 起" - -当 Android 设备未获得 root 权限或 root 访问权限被拒绝时,忽略错误。 diff --git a/docs/configuration/inbound/tun.md b/docs/configuration/inbound/tun.md index 1d5d8d0f..60cd0638 100644 --- a/docs/configuration/inbound/tun.md +++ b/docs/configuration/inbound/tun.md @@ -2,6 +2,10 @@ icon: material/new-box --- +!!! quote "Changes in sing-box 1.10.0" + + :material-plus: [auto_redirect](#auto_redirect) + !!! quote "Changes in sing-box 1.9.0" :material-plus: [platform.http_proxy.bypass_domain](#platformhttp_proxybypass_domain) @@ -29,6 +33,7 @@ icon: material/new-box "gso": false, "auto_route": true, "strict_route": true, + "auto_redirect": false, "inet4_route_address": [ "0.0.0.0/1", "128.0.0.0/1" @@ -156,6 +161,35 @@ It prevents address leaks and makes DNS hijacking work on Android. It may prevent some applications (such as VirtualBox) from working properly in certain situations. +#### auto_redirect + +!!! question "Since sing-box 1.10.0" + +!!! quote "" + + Only supported on Linux. + +Automatically configure iptables to redirect TCP connections. + +*In Android*: + +* Only IPv4 is supported +* Only local connections is forwarded + +To share your VPN connection over hotspot or repeater, use [VPNHotspot](https://github.com/Mygod/VPNHotspot). + +*In Linux*: + +* iptables is required (optional ip6tables) +* `iptables_nat` module is required + +For OpenWrt 23.05, required extra packages are: + +```bash +opkg update +opkg install iptables-nft iptables-mod-nat-extra ip6tables-nft +``` + #### inet4_route_address Use custom routes instead of default when `auto_route` is enabled. diff --git a/docs/configuration/inbound/tun.zh.md b/docs/configuration/inbound/tun.zh.md index 73d31d64..f9249d4f 100644 --- a/docs/configuration/inbound/tun.zh.md +++ b/docs/configuration/inbound/tun.zh.md @@ -2,6 +2,10 @@ icon: material/new-box --- +!!! quote "sing-box 1.10.0 中的更改" + + :material-plus: [auto_redirect](#auto_redirect) + !!! quote "sing-box 1.9.0 中的更改" :material-plus: [platform.http_proxy.bypass_domain](#platformhttp_proxybypass_domain) @@ -29,6 +33,7 @@ icon: material/new-box "gso": false, "auto_route": true, "strict_route": true, + "auto_redirect": false, "inet4_route_address": [ "0.0.0.0/1", "128.0.0.0/1" @@ -157,6 +162,35 @@ tun 接口的 IPv6 前缀。 它可能会使某些应用程序(如 VirtualBox)在某些情况下无法正常工作。 +#### auto_redirect + +!!! question "自 sing-box 1.10.0 起" + +!!! quote "" + + 仅支持 Linux。 + +自动配置 iptables 以重定向 TCP 连接。 + +*在 Android 中*: + +* 仅支持 IPv4 +* 仅转发本地连接 + +要通过热点或中继共享您的 VPN 连接,请使用 [VPNHotspot](https://github.com/Mygod/VPNHotspot)。 + +*在 Linux 中*: + +* 需要 iptables(可选 ip6tables) +* 需要 `iptables_nat` 模块 + +对于 OpenWrt 23.05,所需的额外软件包是: + +```bash +opkg update +opkg install iptables-nft iptables-mod-nat-extra ip6tables-nft +``` + #### inet4_route_address 启用 `auto_route` 时使用自定义路由而不是默认路由。 diff --git a/experimental/libbox/config.go b/experimental/libbox/config.go index 4af96994..b7731143 100644 --- a/experimental/libbox/config.go +++ b/experimental/libbox/config.go @@ -9,7 +9,6 @@ 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" @@ -98,14 +97,6 @@ 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 3afa839e..4078140f 100644 --- a/experimental/libbox/platform.go +++ b/experimental/libbox/platform.go @@ -22,8 +22,6 @@ type PlatformInterface interface { IncludeAllNetworks() bool ReadWIFIState() *WIFIState ClearDNSCache() - PerAppProxyList() (IntegerIterator, error) - PerAppProxyMode() int32 } type TunInterface interface { @@ -56,11 +54,6 @@ 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 75136781..3bec13fa 100644 --- a/experimental/libbox/platform/interface.go +++ b/experimental/libbox/platform/interface.go @@ -11,12 +11,6 @@ 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 @@ -30,7 +24,5 @@ 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 299945ba..0a54d7ab 100644 --- a/experimental/libbox/service.go +++ b/experimental/libbox/service.go @@ -229,18 +229,6 @@ 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 e1909b84..513b016f 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, platformInterface) + return NewRedirect(ctx, router, logger, options.Tag, options.RedirectOptions), nil 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 bf0da9be..4c7cf1d5 100644 --- a/inbound/redirect.go +++ b/inbound/redirect.go @@ -2,39 +2,25 @@ package inbound import ( "context" - "errors" "net" - "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" - "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" - F "github.com/sagernet/sing/common/format" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) type Redirect struct { myInboundAdapter - 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, platformInterface platform.Interface) (*Redirect, error) { +func NewRedirect(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.RedirectInboundOptions) *Redirect { redirect := &Redirect{ - myInboundAdapter: myInboundAdapter{ + myInboundAdapter{ protocol: C.TypeRedirect, network: []string{N.NetworkTCP}, ctx: ctx, @@ -43,28 +29,9 @@ func NewRedirect(ctx context.Context, router adapter.Router, logger log.ContextL tag: tag, listenOptions: options.ListenOptions, }, - platformInterface: platformInterface, - autoRedirect: common.PtrValueOrDefault(options.AutoRedirect), - } - if redirect.autoRedirect.Enabled { - if !C.IsAndroid { - return nil, E.New("auto redirect is only supported on Android") - } - userId := os.Getuid() - if userId != 0 { - suPath, err := exec.LookPath("/bin/su") - if err == nil { - redirect.needSu = true - redirect.suPath = suPath - } else if redirect.autoRedirect.ContinueOnNoPermission { - redirect.autoRedirect.Enabled = false - } else { - return nil, E.Extend(E.Cause(err, "root permission is required for auto redirect"), os.Getenv("PATH")) - } - } } redirect.connHandler = redirect - return redirect, nil + return redirect } func (r *Redirect) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { @@ -75,124 +42,3 @@ func (r *Redirect) NewConnection(ctx context.Context, conn net.Conn, metadata ad metadata.Destination = M.SocksaddrFromNetIP(destination) return r.newConnection(ctx, conn, metadata) } - -func (r *Redirect) Start() error { - err := r.myInboundAdapter.Start() - if err != nil { - return err - } - if r.autoRedirect.Enabled { - r.cleanupRedirect() - err = r.setupRedirect() - if err != nil { - var exitError *exec.ExitError - if errors.As(err, &exitError) && exitError.ExitCode() == 13 && r.autoRedirect.ContinueOnNoPermission { - r.logger.Error(E.Cause(err, "setup auto redirect")) - return nil - } - r.cleanupRedirect() - return E.Cause(err, "setup auto redirect") - } - } - return nil -} - -func (r *Redirect) Close() error { - if r.autoRedirect.Enabled { - r.cleanupRedirect() - } - return r.myInboundAdapter.Close() -} - -func (r *Redirect) setupRedirect() error { - tableName := "sing-box" - rules := ` -set -e -o pipefail -iptables -t nat -N 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() { - _ = r.runAndroidShell(` -iptables -t nat -D OUTPUT -p tcp -j sing-box -iptables -t nat -F sing-box -iptables -t nat -X sing-box -`) -} - -func (r *Redirect) runAndroidShell(content string) error { - var command *exec.Cmd - if r.needSu { - command = exec.Command(r.suPath, "-c", "sh") - } else { - command = exec.Command("sh") - } - command.Stdin = strings.NewReader(content) - combinedOutput, err := command.CombinedOutput() - if err != nil { - return E.Extend(err, string(combinedOutput)) - } - return nil -} diff --git a/inbound/tun.go b/inbound/tun.go index e82ea122..530cb71e 100644 --- a/inbound/tun.go +++ b/inbound/tun.go @@ -37,6 +37,7 @@ type Tun struct { tunStack tun.Stack platformInterface platform.Interface platformOptions option.TunPlatformOptions + autoRedirect *tunAutoRedirect } func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TunInboundOptions, platformInterface platform.Interface) (*Tun, error) { @@ -50,9 +51,9 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger } else { udpTimeout = C.UDPTimeout } + var err error includeUID := uidToRange(options.IncludeUID) if len(options.IncludeUIDRange) > 0 { - var err error includeUID, err = parseRange(includeUID, options.IncludeUIDRange) if err != nil { return nil, E.Cause(err, "parse include_uid_range") @@ -60,13 +61,13 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger } excludeUID := uidToRange(options.ExcludeUID) if len(options.ExcludeUIDRange) > 0 { - var err error excludeUID, err = parseRange(excludeUID, options.ExcludeUIDRange) if err != nil { return nil, E.Cause(err, "parse exclude_uid_range") } } - return &Tun{ + + inbound := &Tun{ tag: tag, ctx: ctx, router: router, @@ -99,7 +100,17 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger stack: options.Stack, platformInterface: platformInterface, platformOptions: common.PtrValueOrDefault(options.Platform), - }, nil + } + if options.AutoRedirect { + if !options.AutoRoute { + return nil, E.New("`auto_route` is required by `auto_redirect`") + } + inbound.autoRedirect, err = newAutoRedirect(inbound) + if err != nil { + return nil, E.Cause(err, "initialize auto redirect") + } + } + return inbound, nil } func uidToRange(uidList option.Listable[uint32]) []ranges.Range[uint32] { @@ -195,6 +206,14 @@ func (t *Tun) Start() error { if err != nil { return err } + if t.autoRedirect != nil { + monitor.Start("initiating auto redirect") + err = t.autoRedirect.Start(t.tunOptions.Name) + monitor.Finish() + if err != nil { + return E.Cause(err, "auto redirect") + } + } t.logger.Info("started at ", t.tunOptions.Name) return nil } @@ -203,6 +222,7 @@ func (t *Tun) Close() error { return common.Close( t.tunStack, t.tunIf, + common.PtrOrNil(t.autoRedirect), ) } diff --git a/inbound/tun_auto_redirect.go b/inbound/tun_auto_redirect.go new file mode 100644 index 00000000..ac071271 --- /dev/null +++ b/inbound/tun_auto_redirect.go @@ -0,0 +1,321 @@ +package inbound + +import ( + "context" + "net" + "net/netip" + "os" + "os/exec" + "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/option" + "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" + N "github.com/sagernet/sing/common/network" +) + +const ( + tableNameOutput = "sing-box-output" + tableNameForward = "sing-box-forward" + tableNamePreRouteing = "sing-box-prerouting" +) + +type tunAutoRedirect struct { + myInboundAdapter + tunOptions *tun.Options + iptablesPath string + androidSu bool + suPath string + enableIPv6 bool + ip6tablesPath string +} + +func newAutoRedirect(t *Tun) (*tunAutoRedirect, error) { + if !C.IsLinux { + return nil, E.New("only supported on linux") + } + server := &tunAutoRedirect{ + myInboundAdapter: myInboundAdapter{ + protocol: C.TypeRedirect, + network: []string{N.NetworkTCP}, + ctx: t.ctx, + router: t.router, + logger: t.logger, + tag: t.tag, + listenOptions: option.ListenOptions{ + Listen: option.NewListenAddress(netip.AddrFrom4([4]byte{127, 0, 0, 1})), + }, + }, + tunOptions: &t.tunOptions, + } + server.connHandler = server + if C.IsAndroid { + server.iptablesPath = "/system/bin/iptables" + userId := os.Getuid() + if userId != 0 { + var ( + suPath string + err error + ) + if t.platformInterface != nil { + suPath, err = exec.LookPath("/bin/su") + } else { + suPath, err = exec.LookPath("su") + } + if err == nil { + server.androidSu = true + server.suPath = suPath + } else { + return nil, E.Extend(E.Cause(err, "root permission is required for auto redirect"), os.Getenv("PATH")) + } + } + } else { + iptablesPath, err := exec.LookPath("iptables") + if err != nil { + return nil, E.Cause(err, "iptables is required") + } + server.iptablesPath = iptablesPath + } + if !C.IsAndroid && len(t.tunOptions.Inet6Address) > 0 { + err := server.initializeIP6Tables() + if err != nil { + t.logger.Debug("device has no ip6tables nat support: ", err) + } + } + return server, nil +} + +func (t *tunAutoRedirect) initializeIP6Tables() error { + ip6tablesPath, err := exec.LookPath("ip6tables") + if err != nil { + return err + } + output, err := exec.Command(ip6tablesPath, "-t nat -L", tableNameOutput).CombinedOutput() + switch exitErr := err.(type) { + case nil: + case *exec.ExitError: + if exitErr.ExitCode() != 1 { + return E.Extend(err, string(output)) + } + default: + return err + } + t.ip6tablesPath = ip6tablesPath + t.enableIPv6 = true + return nil +} + +func (t *tunAutoRedirect) Start(tunName string) error { + err := t.myInboundAdapter.Start() + if err != nil { + return E.Cause(err, "start redirect server") + } + t.cleanupIPTables(t.iptablesPath) + if t.enableIPv6 { + t.cleanupIPTables(t.ip6tablesPath) + } + err = t.setupIPTables(t.iptablesPath, tunName) + if err != nil { + return err + } + if t.enableIPv6 { + err = t.setupIPTables(t.ip6tablesPath, tunName) + if err != nil { + return err + } + } + return nil +} + +func (t *tunAutoRedirect) Close() error { + t.cleanupIPTables(t.iptablesPath) + if t.enableIPv6 { + t.cleanupIPTables(t.ip6tablesPath) + } + return t.myInboundAdapter.Close() +} + +func (t *tunAutoRedirect) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + destination, err := redir.GetOriginalDestination(conn) + if err != nil { + return E.Cause(err, "get redirect destination") + } + metadata.Destination = M.SocksaddrFromNetIP(destination) + return t.newConnection(ctx, conn, metadata) +} + +func (t *tunAutoRedirect) setupIPTables(iptablesPath string, tunName string) error { + // OUTPUT + err := t.runShell(iptablesPath, "-t nat -N", tableNameOutput) + if err != nil { + return err + } + err = t.runShell(iptablesPath, "-t nat -A", tableNameOutput, + "-p tcp -o", tunName, + "-j REDIRECT --to-ports", M.AddrPortFromNet(t.tcpListener.Addr()).Port()) + if err != nil { + return err + } + err = t.runShell(iptablesPath, "-t nat -I OUTPUT -j", tableNameOutput) + if err != nil { + return err + } + if !t.androidSu { + // FORWARD + err = t.runShell(iptablesPath, "-N", tableNameForward) + if err != nil { + return err + } + err = t.runShell(iptablesPath, "-A", tableNameForward, + "-i", tunName, "-j", "ACCEPT") + if err != nil { + return err + } + err = t.runShell(iptablesPath, "-A", tableNameForward, + "-o", tunName, "-j", "ACCEPT") + if err != nil { + return err + } + err = t.runShell(iptablesPath, "-I FORWARD -j", tableNameForward) + if err != nil { + return err + } + // PREROUTING + err = t.setupIPTablesPreRouting(iptablesPath) + if err != nil { + return err + } + } + return nil +} + +func (t *tunAutoRedirect) setupIPTablesPreRouting(iptablesPath string) error { + err := t.runShell(iptablesPath, "-t nat -N", tableNamePreRouteing) + if err != nil { + return err + } + var ( + routeAddress []netip.Prefix + routeExcludeAddress []netip.Prefix + ) + if t.iptablesPath == iptablesPath { + routeAddress = t.tunOptions.Inet4RouteAddress + routeExcludeAddress = t.tunOptions.Inet4RouteExcludeAddress + } else { + routeAddress = t.tunOptions.Inet6RouteAddress + routeExcludeAddress = t.tunOptions.Inet6RouteExcludeAddress + } + if len(routeAddress) > 0 && (len(t.tunOptions.IncludeInterface) > 0 || len(t.tunOptions.IncludeUID) > 0) { + return E.New("`*_route_address` is conflict with `include_interface` or `include_uid`") + } + if len(routeExcludeAddress) > 0 { + for _, address := range routeExcludeAddress { + err = t.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing, + "-d", address.String(), "-j RETURN") + if err != nil { + return err + } + } + } + if len(t.tunOptions.ExcludeInterface) > 0 { + for _, name := range t.tunOptions.ExcludeInterface { + err = t.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing, + "-o", name, "-j RETURN") + if err != nil { + return err + } + } + } + if len(t.tunOptions.ExcludeUID) > 0 { + for _, uid := range t.tunOptions.ExcludeUID { + err = t.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing, + "-m owner --uid-owner", uid, "-j RETURN") + if err != nil { + return err + } + } + } + for _, netIf := range t.router.(adapter.Router).InterfaceFinder().Interfaces() { + for _, addr := range netIf.Addresses { + if (t.iptablesPath == iptablesPath) != addr.Addr().Is4() { + continue + } + err = t.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing, "-d", addr.String(), "-j RETURN") + if err != nil { + return err + } + } + } + if len(routeAddress) > 0 { + for _, address := range routeAddress { + err = t.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing, + "-d", address.String(), "-p tcp -j REDIRECT --to-ports", M.AddrPortFromNet(t.tcpListener.Addr()).Port()) + if err != nil { + return err + } + } + } else if len(t.tunOptions.IncludeInterface) > 0 || len(t.tunOptions.IncludeUID) > 0 { + for _, name := range t.tunOptions.IncludeInterface { + err = t.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing, + "-o", name, "-p tcp -j REDIRECT --to-ports", M.AddrPortFromNet(t.tcpListener.Addr()).Port()) + if err != nil { + return err + } + } + for _, uidRange := range t.tunOptions.IncludeUID { + for i := uidRange.Start; i <= uidRange.End; i++ { + err = t.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing, + "-m owner --uid-owner", i, "-p tcp -j REDIRECT --to-ports", M.AddrPortFromNet(t.tcpListener.Addr()).Port()) + if err != nil { + return err + } + } + } + } else { + err = t.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing, + "-p tcp -j REDIRECT --to-ports", M.AddrPortFromNet(t.tcpListener.Addr()).Port()) + if err != nil { + return err + } + } + err = t.runShell(iptablesPath, "-t nat -I PREROUTING -j", tableNamePreRouteing) + if err != nil { + return err + } + return nil +} + +func (t *tunAutoRedirect) cleanupIPTables(iptablesPath string) { + _ = t.runShell(iptablesPath, "-t nat -D OUTPUT -j", tableNameOutput) + _ = t.runShell(iptablesPath, "-t nat -F", tableNameOutput) + _ = t.runShell(iptablesPath, "-t nat -X", tableNameOutput) + if !t.androidSu { + _ = t.runShell(iptablesPath, "-D FORWARD -j", tableNameForward) + _ = t.runShell(iptablesPath, "-F", tableNameForward) + _ = t.runShell(iptablesPath, "-X", tableNameForward) + _ = t.runShell(iptablesPath, "-t nat -D PREROUTING -j", tableNamePreRouteing) + _ = t.runShell(iptablesPath, "-t nat -F", tableNamePreRouteing) + _ = t.runShell(iptablesPath, "-t nat -X", tableNamePreRouteing) + } +} + +func (t *tunAutoRedirect) runShell(commands ...any) error { + commandStr := strings.Join(F.MapToString(commands), " ") + var command *exec.Cmd + if t.androidSu { + command = exec.Command(t.suPath, "-c", commandStr) + } else { + commandArray := strings.Split(commandStr, " ") + command = exec.Command(commandArray[0], commandArray[1:]...) + } + combinedOutput, err := command.CombinedOutput() + if err != nil { + return E.Extend(err, F.ToString(commandStr, ": ", string(combinedOutput))) + } + return nil +} diff --git a/option/redir.go b/option/redir.go index a2af951c..743a6e10 100644 --- a/option/redir.go +++ b/option/redir.go @@ -2,12 +2,6 @@ package option type RedirectInboundOptions struct { ListenOptions - AutoRedirect *AutoRedirectOptions `json:"auto_redirect,omitempty"` -} - -type AutoRedirectOptions struct { - Enabled bool `json:"enabled,omitempty"` - ContinueOnNoPermission bool `json:"continue_on_no_permission,omitempty"` } type TProxyInboundOptions struct { diff --git a/option/tun.go b/option/tun.go index ac66a806..91930866 100644 --- a/option/tun.go +++ b/option/tun.go @@ -9,6 +9,7 @@ type TunInboundOptions struct { Inet4Address Listable[netip.Prefix] `json:"inet4_address,omitempty"` Inet6Address Listable[netip.Prefix] `json:"inet6_address,omitempty"` AutoRoute bool `json:"auto_route,omitempty"` + AutoRedirect bool `json:"auto_redirect,omitempty"` StrictRoute bool `json:"strict_route,omitempty"` Inet4RouteAddress Listable[netip.Prefix] `json:"inet4_route_address,omitempty"` Inet6RouteAddress Listable[netip.Prefix] `json:"inet6_route_address,omitempty"`