Add nftables support for auto-redirect

This commit is contained in:
世界
2024-05-31 17:44:28 +08:00
parent 6875a33c28
commit a2575526b6
8 changed files with 760 additions and 325 deletions

View File

@@ -208,7 +208,7 @@ func (t *Tun) Start() error {
}
if t.autoRedirect != nil {
monitor.Start("initiating auto redirect")
err = t.autoRedirect.Start(t.tunOptions.Name)
err = t.autoRedirect.Start()
monitor.Finish()
if err != nil {
return E.Cause(err, "auto redirect")

View File

@@ -1,3 +1,5 @@
//go:build linux
package inbound
import (
@@ -6,51 +8,35 @@ import (
"net/netip"
"os"
"os/exec"
"strings"
"strconv"
"github.com/sagernet/nftables"
"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"
"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"
"github.com/sagernet/sing/common/x/list"
"golang.org/x/exp/slices"
)
const (
tableNameOutput = "sing-box-output"
tableNameForward = "sing-box-forward"
tableNamePreRouteing = "sing-box-prerouting"
"golang.org/x/sys/unix"
)
type tunAutoRedirect struct {
myInboundAdapter
tunOptions *tun.Options
interfaceFinder control.InterfaceFinder
networkMonitor tun.NetworkUpdateMonitor
networkCallback *list.Element[tun.NetworkUpdateCallback]
enableIPv4 bool
enableIPv6 bool
localAddresses4 []netip.Prefix
localAddresses6 []netip.Prefix
iptablesPath string
ip6tablesPath string
androidSu bool
suPath string
tunOptions *tun.Options
enableIPv4 bool
enableIPv6 bool
iptablesPath string
ip6tablesPath string
useNfTables bool
androidSu bool
suPath string
}
func newAutoRedirect(t *Tun) (*tunAutoRedirect, error) {
if !C.IsLinux {
return nil, E.New("only supported on linux")
}
server := &tunAutoRedirect{
s := &tunAutoRedirect{
myInboundAdapter: myInboundAdapter{
protocol: C.TypeRedirect,
network: []string{N.NetworkTCP},
@@ -62,160 +48,105 @@ func newAutoRedirect(t *Tun) (*tunAutoRedirect, error) {
InboundOptions: t.inboundOptions,
},
},
tunOptions: &t.tunOptions,
interfaceFinder: t.router.InterfaceFinder(),
networkMonitor: t.router.NetworkMonitor(),
tunOptions: &t.tunOptions,
}
server.connHandler = server
if len(t.tunOptions.Inet4Address) > 0 {
server.enableIPv4 = true
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"))
}
s.connHandler = s
if C.IsAndroid {
s.enableIPv4 = true
s.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")
}
} else {
iptablesPath, err := exec.LookPath("iptables")
if err != nil {
return nil, E.Cause(err, "iptables is required")
if err == nil {
s.androidSu = true
s.suPath = suPath
} else {
return nil, E.Extend(E.Cause(err, "root permission is required for auto redirect"), os.Getenv("PATH"))
}
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)
} else {
err := s.initializeNfTables()
if err != nil && err != os.ErrInvalid {
t.logger.Debug("device has no nftables support: ", err)
}
if len(t.tunOptions.Inet4Address) > 0 {
s.enableIPv4 = true
if !s.useNfTables {
s.iptablesPath, err = exec.LookPath("iptables")
if err != nil {
return nil, E.Cause(err, "iptables is required")
}
}
}
if len(t.tunOptions.Inet6Address) > 0 {
s.enableIPv6 = true
if !s.useNfTables {
s.ip6tablesPath, err = exec.LookPath("ip6tables")
if err != nil {
if !s.enableIPv4 {
return nil, E.Cause(err, "ip6tables is required")
} else {
s.enableIPv6 = false
t.logger.Error("device has no ip6tables nat support: ", err)
}
}
}
}
}
var listenAddr netip.Addr
if C.IsAndroid {
listenAddr = netip.AddrFrom4([4]byte{127, 0, 0, 1})
} else if server.enableIPv6 {
} else if s.enableIPv6 {
listenAddr = netip.IPv6Unspecified()
} else {
listenAddr = netip.IPv4Unspecified()
}
server.listenOptions.Listen = option.NewListenAddress(listenAddr)
return server, nil
s.listenOptions.Listen = option.NewListenAddress(listenAddr)
return s, nil
}
func (t *tunAutoRedirect) initializeIP6Tables() error {
ip6tablesPath, err := exec.LookPath("ip6tables")
func (t *tunAutoRedirect) initializeNfTables() error {
disabled, err := strconv.ParseBool(os.Getenv("AUTO_REDIRECT_DISABLE_NFTABLES"))
if err == nil && disabled {
return os.ErrInvalid
}
nft, err := nftables.New()
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:
defer nft.CloseLasting()
_, err = nft.ListTablesOfFamily(unix.AF_INET)
if err != nil {
return err
}*/
t.ip6tablesPath = ip6tablesPath
t.enableIPv6 = true
}
t.useNfTables = true
return nil
}
func (t *tunAutoRedirect) Start(tunName string) error {
func (t *tunAutoRedirect) Start() error {
err := t.myInboundAdapter.Start()
if err != nil {
return E.Cause(err, "start redirect server")
}
if t.enableIPv4 {
t.cleanupIPTables(t.iptablesPath)
}
if t.enableIPv6 {
t.cleanupIPTables(t.ip6tablesPath)
}
err = t.updateInterfaces(false)
t.cleanupTables()
err = t.setupTables()
if err != nil {
return err
}
if t.enableIPv4 {
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
}
}
t.networkCallback = t.networkMonitor.RegisterCallback(func() {
rErr := t.updateInterfaces(true)
if rErr != nil {
t.logger.Error("recreate prerouting rules: ", rErr)
}
})
return nil
}
func (t *tunAutoRedirect) updateInterfaces(recreate bool) error {
addresses := common.Filter(common.FlatMap(common.Filter(t.interfaceFinder.Interfaces(), func(it control.Interface) bool {
return it.Name != t.tunOptions.Name
}), func(it control.Interface) []netip.Prefix {
return it.Addresses
}), func(it netip.Prefix) bool {
address := it.Addr()
return !(address.IsLoopback() || address.IsLinkLocalUnicast())
})
oldLocalAddresses4 := t.localAddresses4
oldLocalAddresses6 := t.localAddresses6
localAddresses4 := common.Filter(addresses, func(it netip.Prefix) bool { return it.Addr().Is4() })
localAddresses6 := common.Filter(addresses, func(it netip.Prefix) bool { return it.Addr().Is6() })
t.localAddresses4 = localAddresses4
t.localAddresses6 = localAddresses6
if !recreate || t.androidSu {
return nil
}
if t.enableIPv4 {
if !slices.Equal(localAddresses4, oldLocalAddresses4) {
err := t.setupIPTablesPreRouting(t.iptablesPath, true)
if err != nil {
return err
}
}
}
if t.enableIPv6 {
if !slices.Equal(localAddresses6, oldLocalAddresses6) {
err := t.setupIPTablesPreRouting(t.ip6tablesPath, true)
if err != nil {
return err
}
}
}
return nil
}
func (t *tunAutoRedirect) Close() error {
t.networkMonitor.UnregisterCallback(t.networkCallback)
if t.enableIPv4 {
t.cleanupIPTables(t.iptablesPath)
}
if t.enableIPv6 {
t.cleanupIPTables(t.ip6tablesPath)
}
t.cleanupTables()
return t.myInboundAdapter.Close()
}
@@ -228,44 +159,21 @@ func (t *tunAutoRedirect) NewConnection(ctx context.Context, conn net.Conn, meta
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
func (t *tunAutoRedirect) setupTables() error {
var setupTables func(int) error
if t.useNfTables {
setupTables = t.setupNfTables
} else {
setupTables = t.setupIPTables
}
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
if t.enableIPv4 {
err := setupTables(unix.AF_INET)
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, false)
if t.enableIPv6 {
err := setupTables(unix.AF_INET6)
if err != nil {
return err
}
@@ -273,134 +181,17 @@ func (t *tunAutoRedirect) setupIPTables(iptablesPath string, tunName string) err
return nil
}
func (t *tunAutoRedirect) setupIPTablesPreRouting(iptablesPath string, recreate bool) error {
var err error
if !recreate {
err = t.runShell(iptablesPath, "-t nat -N", tableNamePreRouteing)
func (t *tunAutoRedirect) cleanupTables() {
var cleanupTables func(int)
if t.useNfTables {
cleanupTables = t.cleanupNfTables
} else {
err = t.runShell(iptablesPath, "-t nat -F", tableNamePreRouteing)
cleanupTables = t.cleanupIPTables
}
if err != nil {
return err
if t.enableIPv4 {
cleanupTables(unix.AF_INET)
}
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,
"-i", 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
}
}
}
var addresses []netip.Prefix
if t.iptablesPath == iptablesPath {
addresses = t.localAddresses4
} else {
addresses = t.localAddresses6
}
for _, address := range addresses {
err = t.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing, "-d", address.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,
"-i", 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)
if t.enableIPv6 {
cleanupTables(unix.AF_INET6)
}
}
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
}

View File

@@ -0,0 +1,235 @@
//go:build linux
package inbound
import (
"net/netip"
"os/exec"
"strings"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
M "github.com/sagernet/sing/common/metadata"
"golang.org/x/sys/unix"
)
const (
iptablesTableNameOutput = "sing-box-output"
iptablesTableNameForward = "sing-box-forward"
iptablesTableNamePreRouteing = "sing-box-prerouting"
)
func (t *tunAutoRedirect) iptablesPathForFamily(family int) string {
if family == unix.AF_INET {
return t.iptablesPath
} else {
return t.ip6tablesPath
}
}
func (t *tunAutoRedirect) setupIPTables(family int) error {
iptablesPath := t.iptablesPathForFamily(family)
// OUTPUT
err := t.runShell(iptablesPath, "-t nat -N", iptablesTableNameOutput)
if err != nil {
return err
}
err = t.runShell(iptablesPath, "-t nat -A", iptablesTableNameOutput,
"-p tcp -o", t.tunOptions.Name,
"-j REDIRECT --to-ports", M.AddrPortFromNet(t.tcpListener.Addr()).Port())
if err != nil {
return err
}
err = t.runShell(iptablesPath, "-t nat -I OUTPUT -j", iptablesTableNameOutput)
if err != nil {
return err
}
if !t.androidSu {
// FORWARD
err = t.runShell(iptablesPath, "-N", iptablesTableNameForward)
if err != nil {
return err
}
err = t.runShell(iptablesPath, "-A", iptablesTableNameForward,
"-i", t.tunOptions.Name, "-j", "ACCEPT")
if err != nil {
return err
}
err = t.runShell(iptablesPath, "-A", iptablesTableNameForward,
"-o", t.tunOptions.Name, "-j", "ACCEPT")
if err != nil {
return err
}
err = t.runShell(iptablesPath, "-I FORWARD -j", iptablesTableNameForward)
if err != nil {
return err
}
// PREROUTING
err = t.setupIPTablesPreRouting(family)
if err != nil {
return err
}
}
return nil
}
func (t *tunAutoRedirect) setupIPTablesPreRouting(family int) error {
iptablesPath := t.iptablesPathForFamily(family)
err := t.runShell(iptablesPath, "-t nat -N", iptablesTableNamePreRouteing)
if err != nil {
return err
}
var (
routeAddress []netip.Prefix
routeExcludeAddress []netip.Prefix
)
if family == unix.AF_INET {
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`")
}
err = t.runShell(iptablesPath, "-t nat -A", iptablesTableNamePreRouteing,
"-i", t.tunOptions.Name, "-j RETURN")
if err != nil {
return err
}
for _, address := range routeExcludeAddress {
err = t.runShell(iptablesPath, "-t nat -A", iptablesTableNamePreRouteing,
"-d", address.String(), "-j RETURN")
if err != nil {
return err
}
}
for _, name := range t.tunOptions.ExcludeInterface {
err = t.runShell(iptablesPath, "-t nat -A", iptablesTableNamePreRouteing,
"-i", name, "-j RETURN")
if err != nil {
return err
}
}
for _, uid := range t.tunOptions.ExcludeUID {
err = t.runShell(iptablesPath, "-t nat -A", iptablesTableNamePreRouteing,
"-m owner --uid-owner", uid, "-j RETURN")
if err != nil {
return err
}
}
var dnsServerAddress netip.Addr
if family == unix.AF_INET {
dnsServerAddress = t.tunOptions.Inet4Address[0].Addr().Next()
} else {
dnsServerAddress = t.tunOptions.Inet6Address[0].Addr().Next()
}
if len(routeAddress) > 0 {
for _, address := range routeAddress {
err = t.runShell(iptablesPath, "-t nat -A", iptablesTableNamePreRouteing,
"-d", address.String(), "-p udp --dport 53 -j DNAT --to", dnsServerAddress)
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", iptablesTableNamePreRouteing,
"-i", name, "-p udp --dport 53 -j DNAT --to", dnsServerAddress)
if err != nil {
return err
}
}
for _, uidRange := range t.tunOptions.IncludeUID {
for uid := uidRange.Start; uid <= uidRange.End; uid++ {
err = t.runShell(iptablesPath, "-t nat -A", iptablesTableNamePreRouteing,
"-m owner --uid-owner", uid, "-p udp --dport 53 -j DNAT --to", dnsServerAddress)
if err != nil {
return err
}
}
}
} else {
err = t.runShell(iptablesPath, "-t nat -A", iptablesTableNamePreRouteing,
"-p udp --dport 53 -j DNAT --to", dnsServerAddress)
if err != nil {
return err
}
}
err = t.runShell(iptablesPath, "-t nat -A", iptablesTableNamePreRouteing, "-m addrtype --dst-type LOCAL -j RETURN")
if err != nil {
return err
}
if len(routeAddress) > 0 {
for _, address := range routeAddress {
err = t.runShell(iptablesPath, "-t nat -A", iptablesTableNamePreRouteing,
"-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", iptablesTableNamePreRouteing,
"-i", 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 uid := uidRange.Start; uid <= uidRange.End; uid++ {
err = t.runShell(iptablesPath, "-t nat -A", iptablesTableNamePreRouteing,
"-m owner --uid-owner", uid, "-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", iptablesTableNamePreRouteing,
"-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", iptablesTableNamePreRouteing)
if err != nil {
return err
}
return nil
}
func (t *tunAutoRedirect) cleanupIPTables(family int) {
iptablesPath := t.iptablesPathForFamily(family)
_ = t.runShell(iptablesPath, "-t nat -D OUTPUT -j", iptablesTableNameOutput)
_ = t.runShell(iptablesPath, "-t nat -F", iptablesTableNameOutput)
_ = t.runShell(iptablesPath, "-t nat -X", iptablesTableNameOutput)
if !t.androidSu {
_ = t.runShell(iptablesPath, "-D FORWARD -j", iptablesTableNameForward)
_ = t.runShell(iptablesPath, "-F", iptablesTableNameForward)
_ = t.runShell(iptablesPath, "-X", iptablesTableNameForward)
_ = t.runShell(iptablesPath, "-t nat -D PREROUTING -j", iptablesTableNamePreRouteing)
_ = t.runShell(iptablesPath, "-t nat -F", iptablesTableNamePreRouteing)
_ = t.runShell(iptablesPath, "-t nat -X", iptablesTableNamePreRouteing)
}
}
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
}

View File

@@ -0,0 +1,231 @@
//go:build linux
package inbound
import (
"net/netip"
"github.com/sagernet/nftables"
"github.com/sagernet/nftables/binaryutil"
"github.com/sagernet/nftables/expr"
F "github.com/sagernet/sing/common/format"
M "github.com/sagernet/sing/common/metadata"
"golang.org/x/sys/unix"
)
const (
nftablesTableName = "sing-box"
nftablesChainOutput = "output"
nftablesChainForward = "forward"
nftablesChainPreRouting = "prerouting"
)
func nftablesFamily(family int) nftables.TableFamily {
switch family {
case unix.AF_INET:
return nftables.TableFamilyIPv4
case unix.AF_INET6:
return nftables.TableFamilyIPv6
default:
panic(F.ToString("unknown family ", family))
}
}
func (t *tunAutoRedirect) setupNfTables(family int) error {
nft, err := nftables.New()
if err != nil {
return err
}
defer nft.CloseLasting()
table := nft.AddTable(&nftables.Table{
Name: nftablesTableName,
Family: nftablesFamily(family),
})
chainOutput := nft.AddChain(&nftables.Chain{
Name: nftablesChainOutput,
Table: table,
Hooknum: nftables.ChainHookOutput,
Priority: nftables.ChainPriorityMangle,
Type: nftables.ChainTypeNAT,
})
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chainOutput,
Exprs: nftablesRuleIfName(expr.MetaKeyOIFNAME, t.tunOptions.Name, nftablesRuleRedirectToPorts(M.AddrPortFromNet(t.tcpListener.Addr()).Port())...),
})
chainForward := nft.AddChain(&nftables.Chain{
Name: nftablesChainForward,
Table: table,
Hooknum: nftables.ChainHookForward,
Priority: nftables.ChainPriorityMangle,
})
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chainForward,
Exprs: nftablesRuleIfName(expr.MetaKeyIIFNAME, t.tunOptions.Name, &expr.Verdict{
Kind: expr.VerdictAccept,
}),
})
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chainForward,
Exprs: nftablesRuleIfName(expr.MetaKeyOIFNAME, t.tunOptions.Name, &expr.Verdict{
Kind: expr.VerdictAccept,
}),
})
t.setupNfTablesPreRouting(nft, table)
return nft.Flush()
}
func (t *tunAutoRedirect) setupNfTablesPreRouting(nft *nftables.Conn, table *nftables.Table) {
chainPreRouting := nft.AddChain(&nftables.Chain{
Name: nftablesChainPreRouting,
Table: table,
Hooknum: nftables.ChainHookPrerouting,
Priority: nftables.ChainPriorityMangle,
Type: nftables.ChainTypeNAT,
})
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chainPreRouting,
Exprs: nftablesRuleIfName(expr.MetaKeyIIFNAME, t.tunOptions.Name, &expr.Verdict{
Kind: expr.VerdictReturn,
}),
})
var (
routeAddress []netip.Prefix
routeExcludeAddress []netip.Prefix
)
if table.Family == nftables.TableFamilyIPv4 {
routeAddress = t.tunOptions.Inet4RouteAddress
routeExcludeAddress = t.tunOptions.Inet4RouteExcludeAddress
} else {
routeAddress = t.tunOptions.Inet6RouteAddress
routeExcludeAddress = t.tunOptions.Inet6RouteExcludeAddress
}
for _, address := range routeExcludeAddress {
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chainPreRouting,
Exprs: nftablesRuleDestinationAddress(address, &expr.Verdict{
Kind: expr.VerdictReturn,
}),
})
}
for _, name := range t.tunOptions.ExcludeInterface {
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chainPreRouting,
Exprs: nftablesRuleIfName(expr.MetaKeyIIFNAME, name, &expr.Verdict{
Kind: expr.VerdictReturn,
}),
})
}
for _, uidRange := range t.tunOptions.ExcludeUID {
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chainPreRouting,
Exprs: nftablesRuleMetaUInt32Range(expr.MetaKeySKUID, uidRange, &expr.Verdict{
Kind: expr.VerdictReturn,
}),
})
}
var routeExprs []expr.Any
if len(routeAddress) > 0 {
for _, address := range routeAddress {
routeExprs = append(routeExprs, nftablesRuleDestinationAddress(address)...)
}
}
redirectPort := M.AddrPortFromNet(t.tcpListener.Addr()).Port()
var dnsServerAddress netip.Addr
if table.Family == nftables.TableFamilyIPv4 {
dnsServerAddress = t.tunOptions.Inet4Address[0].Addr().Next()
} else {
dnsServerAddress = t.tunOptions.Inet6Address[0].Addr().Next()
}
if len(t.tunOptions.IncludeInterface) > 0 || len(t.tunOptions.IncludeUID) > 0 {
for _, name := range t.tunOptions.IncludeInterface {
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chainPreRouting,
Exprs: nftablesRuleIfName(expr.MetaKeyIIFNAME, name, append(routeExprs, nftablesRuleHijackDNS(table.Family, dnsServerAddress)...)...),
})
}
for _, uidRange := range t.tunOptions.IncludeUID {
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chainPreRouting,
Exprs: nftablesRuleMetaUInt32Range(expr.MetaKeySKUID, uidRange, append(routeExprs, nftablesRuleHijackDNS(table.Family, dnsServerAddress)...)...),
})
}
} else {
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chainPreRouting,
Exprs: append(routeExprs, nftablesRuleHijackDNS(table.Family, dnsServerAddress)...),
})
}
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chainPreRouting,
Exprs: []expr.Any{
&expr.Fib{
Register: 1,
FlagDADDR: true,
ResultADDRTYPE: true,
},
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: binaryutil.NativeEndian.PutUint32(unix.RTN_LOCAL),
},
&expr.Verdict{
Kind: expr.VerdictReturn,
},
},
})
if len(t.tunOptions.IncludeInterface) > 0 || len(t.tunOptions.IncludeUID) > 0 {
for _, name := range t.tunOptions.IncludeInterface {
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chainPreRouting,
Exprs: nftablesRuleIfName(expr.MetaKeyIIFNAME, name, append(routeExprs, nftablesRuleRedirectToPorts(redirectPort)...)...),
})
}
for _, uidRange := range t.tunOptions.IncludeUID {
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chainPreRouting,
Exprs: nftablesRuleMetaUInt32Range(expr.MetaKeySKUID, uidRange, append(routeExprs, nftablesRuleRedirectToPorts(redirectPort)...)...),
})
}
} else {
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chainPreRouting,
Exprs: append(routeExprs, nftablesRuleRedirectToPorts(redirectPort)...),
})
}
}
func (t *tunAutoRedirect) cleanupNfTables(family int) {
conn, err := nftables.New()
if err != nil {
return
}
defer conn.CloseLasting()
conn.FlushTable(&nftables.Table{
Name: nftablesTableName,
Family: nftablesFamily(family),
})
conn.DelTable(&nftables.Table{
Name: nftablesTableName,
Family: nftablesFamily(family),
})
_ = conn.Flush()
}

View File

@@ -0,0 +1,153 @@
//go:build linux
package inbound
import (
"net"
"net/netip"
"github.com/sagernet/nftables"
"github.com/sagernet/nftables/binaryutil"
"github.com/sagernet/nftables/expr"
"github.com/sagernet/sing/common/ranges"
"golang.org/x/sys/unix"
)
func nftablesIfname(n string) []byte {
b := make([]byte, 16)
copy(b, n+"\x00")
return b
}
func nftablesRuleIfName(key expr.MetaKey, value string, exprs ...expr.Any) []expr.Any {
newExprs := []expr.Any{
&expr.Meta{Key: key, Register: 1},
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: nftablesIfname(value),
},
}
newExprs = append(newExprs, exprs...)
return newExprs
}
func nftablesRuleMetaUInt32Range(key expr.MetaKey, uidRange ranges.Range[uint32], exprs ...expr.Any) []expr.Any {
newExprs := []expr.Any{
&expr.Meta{Key: key, Register: 1},
&expr.Range{
Op: expr.CmpOpEq,
Register: 1,
FromData: binaryutil.BigEndian.PutUint32(uidRange.Start),
ToData: binaryutil.BigEndian.PutUint32(uidRange.End),
},
}
newExprs = append(newExprs, exprs...)
return newExprs
}
func nftablesRuleDestinationAddress(address netip.Prefix, exprs ...expr.Any) []expr.Any {
var newExprs []expr.Any
if address.Addr().Is4() {
newExprs = append(newExprs, &expr.Payload{
OperationType: expr.PayloadLoad,
DestRegister: 1,
SourceRegister: 0,
Base: expr.PayloadBaseNetworkHeader,
Offset: 16,
Len: 4,
}, &expr.Bitwise{
SourceRegister: 1,
DestRegister: 1,
Len: 4,
Xor: make([]byte, 4),
Mask: net.CIDRMask(address.Bits(), 32),
})
} else {
newExprs = append(newExprs, &expr.Payload{
OperationType: expr.PayloadLoad,
DestRegister: 1,
SourceRegister: 0,
Base: expr.PayloadBaseNetworkHeader,
Offset: 24,
Len: 16,
}, &expr.Bitwise{
SourceRegister: 1,
DestRegister: 1,
Len: 16,
Xor: make([]byte, 16),
Mask: net.CIDRMask(address.Bits(), 128),
})
}
newExprs = append(newExprs, &expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: address.Masked().Addr().AsSlice(),
})
newExprs = append(newExprs, exprs...)
return newExprs
}
func nftablesRuleHijackDNS(family nftables.TableFamily, dnsServerAddress netip.Addr) []expr.Any {
return []expr.Any{
&expr.Meta{
Key: expr.MetaKeyL4PROTO,
Register: 1,
},
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: []byte{unix.IPPROTO_UDP},
},
&expr.Payload{
OperationType: expr.PayloadLoad,
DestRegister: 1,
SourceRegister: 0,
Base: expr.PayloadBaseTransportHeader,
Offset: 2,
Len: 2,
}, &expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: binaryutil.BigEndian.PutUint16(53),
}, &expr.Immediate{
Register: 1,
Data: dnsServerAddress.AsSlice(),
}, &expr.NAT{
Type: expr.NATTypeDestNAT,
Family: uint32(family),
RegAddrMin: 1,
},
}
}
const (
NF_NAT_RANGE_MAP_IPS = 1 << iota
NF_NAT_RANGE_PROTO_SPECIFIED
NF_NAT_RANGE_PROTO_RANDOM
NF_NAT_RANGE_PERSISTENT
NF_NAT_RANGE_PROTO_RANDOM_FULLY
NF_NAT_RANGE_PROTO_OFFSET
)
func nftablesRuleRedirectToPorts(redirectPort uint16) []expr.Any {
return []expr.Any{
&expr.Meta{
Key: expr.MetaKeyL4PROTO,
Register: 1,
},
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: []byte{unix.IPPROTO_TCP},
},
&expr.Immediate{
Register: 1,
Data: binaryutil.BigEndian.PutUint16(redirectPort),
}, &expr.Redir{
RegisterProtoMin: 1,
Flags: NF_NAT_RANGE_PROTO_SPECIFIED,
},
}
}

View File

@@ -0,0 +1,23 @@
//go:build !linux
package inbound
import (
"os"
E "github.com/sagernet/sing/common/exceptions"
)
type tunAutoRedirect struct{}
func newAutoRedirect(t *Tun) (*tunAutoRedirect, error) {
return nil, E.New("only supported on linux")
}
func (t *tunAutoRedirect) Start() error {
return os.ErrInvalid
}
func (t *tunAutoRedirect) Close() error {
return os.ErrInvalid
}