From 2d8d9a19598b46db2189d5be452e85dd12f31202 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 26 May 2024 21:27:25 +0800 Subject: [PATCH] Add simple auto redirect for Android --- docs/configuration/inbound/redirect.md | 33 +++++++ docs/configuration/inbound/redirect.zh.md | 34 +++++++ inbound/builder.go | 2 +- inbound/redirect.go | 104 +++++++++++++++++++++- option/redir.go | 6 ++ 5 files changed, 175 insertions(+), 4 deletions(-) diff --git a/docs/configuration/inbound/redirect.md b/docs/configuration/inbound/redirect.md index 50a5bacd..67f33ff8 100644 --- a/docs/configuration/inbound/redirect.md +++ b/docs/configuration/inbound/redirect.md @@ -1,3 +1,11 @@ +--- +icon: material/new-box +--- + +!!! quote "Changes in sing-box 1.9.0" + + :material-plus: [auto_redirect](#auto_redirect) + !!! quote "" Only supported on Linux and macOS. @@ -9,6 +17,11 @@ "type": "redirect", "tag": "redirect-in", + "auto_redirect": { + "enabled": false, + "continue_on_no_permission": false + }, + ... // Listen Fields } ``` @@ -16,3 +29,23 @@ ### 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 a03049e5..53572bc5 100644 --- a/docs/configuration/inbound/redirect.zh.md +++ b/docs/configuration/inbound/redirect.zh.md @@ -1,3 +1,11 @@ +--- +icon: material/new-box +--- + +!!! quote "sing-box 1.10.0 中的更改" + + :material-plus: [auto_redirect](#auto_redirect) + !!! quote "" 仅支持 Linux 和 macOS。 @@ -9,9 +17,35 @@ "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/inbound/builder.go b/inbound/builder.go index 513b016f..201ab800 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), nil + return NewRedirect(ctx, router, logger, options.Tag, options.RedirectOptions) 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 4c7cf1d5..9b79fa1c 100644 --- a/inbound/redirect.go +++ b/inbound/redirect.go @@ -2,25 +2,36 @@ package inbound import ( "context" + "errors" "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/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 + 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{ - myInboundAdapter{ + myInboundAdapter: myInboundAdapter{ protocol: C.TypeRedirect, network: []string{N.NetworkTCP}, ctx: ctx, @@ -29,9 +40,27 @@ func NewRedirect(ctx context.Context, router adapter.Router, logger log.ContextL tag: tag, 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 - return redirect + return redirect, nil } 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) 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 +} diff --git a/option/redir.go b/option/redir.go index 743a6e10..a2af951c 100644 --- a/option/redir.go +++ b/option/redir.go @@ -2,6 +2,12 @@ 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 {