mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-06-13 21:54:13 +08:00
Add simple auto redirect for Android
This commit is contained in:
parent
5735227174
commit
2d8d9a1959
@ -1,3 +1,11 @@
|
|||||||
|
---
|
||||||
|
icon: material/new-box
|
||||||
|
---
|
||||||
|
|
||||||
|
!!! quote "Changes in sing-box 1.9.0"
|
||||||
|
|
||||||
|
:material-plus: [auto_redirect](#auto_redirect)
|
||||||
|
|
||||||
!!! quote ""
|
!!! quote ""
|
||||||
|
|
||||||
Only supported on Linux and macOS.
|
Only supported on Linux and macOS.
|
||||||
@ -9,6 +17,11 @@
|
|||||||
"type": "redirect",
|
"type": "redirect",
|
||||||
"tag": "redirect-in",
|
"tag": "redirect-in",
|
||||||
|
|
||||||
|
"auto_redirect": {
|
||||||
|
"enabled": false,
|
||||||
|
"continue_on_no_permission": false
|
||||||
|
},
|
||||||
|
|
||||||
... // Listen Fields
|
... // Listen Fields
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -16,3 +29,23 @@
|
|||||||
### Listen Fields
|
### Listen Fields
|
||||||
|
|
||||||
See [Listen Fields](/configuration/shared/listen/) for details.
|
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.
|
||||||
|
@ -1,3 +1,11 @@
|
|||||||
|
---
|
||||||
|
icon: material/new-box
|
||||||
|
---
|
||||||
|
|
||||||
|
!!! quote "sing-box 1.10.0 中的更改"
|
||||||
|
|
||||||
|
:material-plus: [auto_redirect](#auto_redirect)
|
||||||
|
|
||||||
!!! quote ""
|
!!! quote ""
|
||||||
|
|
||||||
仅支持 Linux 和 macOS。
|
仅支持 Linux 和 macOS。
|
||||||
@ -9,9 +17,35 @@
|
|||||||
"type": "redirect",
|
"type": "redirect",
|
||||||
"tag": "redirect-in",
|
"tag": "redirect-in",
|
||||||
|
|
||||||
|
"auto_redirect": {
|
||||||
|
"enabled": false,
|
||||||
|
"continue_on_no_permission": false
|
||||||
|
},
|
||||||
|
|
||||||
... // 监听字段
|
... // 监听字段
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 监听字段
|
### 监听字段
|
||||||
|
|
||||||
参阅 [监听字段](/zh/configuration/shared/listen/)。
|
参阅 [监听字段](/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 访问权限被拒绝时,忽略错误。
|
||||||
|
@ -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), nil
|
return NewRedirect(ctx, router, logger, options.Tag, options.RedirectOptions)
|
||||||
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:
|
||||||
|
@ -2,25 +2,36 @@ package inbound
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"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/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/control"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
F "github.com/sagernet/sing/common/format"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Redirect struct {
|
type Redirect struct {
|
||||||
myInboundAdapter
|
myInboundAdapter
|
||||||
|
autoRedirect option.AutoRedirectOptions
|
||||||
|
needSu bool
|
||||||
|
suPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRedirect(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.RedirectInboundOptions) *Redirect {
|
func NewRedirect(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.RedirectInboundOptions) (*Redirect, error) {
|
||||||
redirect := &Redirect{
|
redirect := &Redirect{
|
||||||
myInboundAdapter{
|
myInboundAdapter: myInboundAdapter{
|
||||||
protocol: C.TypeRedirect,
|
protocol: C.TypeRedirect,
|
||||||
network: []string{N.NetworkTCP},
|
network: []string{N.NetworkTCP},
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
@ -29,9 +40,27 @@ func NewRedirect(ctx context.Context, router adapter.Router, logger log.ContextL
|
|||||||
tag: tag,
|
tag: tag,
|
||||||
listenOptions: options.ListenOptions,
|
listenOptions: options.ListenOptions,
|
||||||
},
|
},
|
||||||
|
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
|
redirect.connHandler = redirect
|
||||||
return redirect
|
return redirect, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Redirect) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
|
func (r *Redirect) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
|
||||||
@ -42,3 +71,72 @@ func (r *Redirect) NewConnection(ctx context.Context, conn net.Conn, metadata ad
|
|||||||
metadata.Destination = M.SocksaddrFromNetIP(destination)
|
metadata.Destination = M.SocksaddrFromNetIP(destination)
|
||||||
return r.newConnection(ctx, conn, metadata)
|
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 {
|
||||||
|
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(`
|
||||||
|
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
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
@ -2,6 +2,12 @@ package option
|
|||||||
|
|
||||||
type RedirectInboundOptions struct {
|
type RedirectInboundOptions struct {
|
||||||
ListenOptions
|
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 {
|
type TProxyInboundOptions struct {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user