mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-09-07 01:38:48 +08:00
199 lines
5.8 KiB
Go
199 lines
5.8 KiB
Go
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) {
|
|
redirect := &Redirect{
|
|
myInboundAdapter: myInboundAdapter{
|
|
protocol: C.TypeRedirect,
|
|
network: []string{N.NetworkTCP},
|
|
ctx: ctx,
|
|
router: router,
|
|
logger: logger,
|
|
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
|
|
}
|
|
|
|
func (r *Redirect) 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 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
|
|
}
|