diff --git a/go.mod b/go.mod index 818ef912..daaee0d6 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1 github.com/sagernet/gomobile v0.1.3 github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f + github.com/sagernet/nftables v0.3.0-beta.2 github.com/sagernet/quic-go v0.43.1-beta.1 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 github.com/sagernet/sing v0.5.0-alpha.7 @@ -66,6 +67,7 @@ require ( github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/google/btree v1.1.2 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -73,7 +75,8 @@ require ( github.com/klauspost/compress v1.17.4 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/libdns/libdns v0.2.2 // indirect - github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect + github.com/mdlayher/netlink v1.7.2 // indirect + github.com/mdlayher/socket v0.4.1 // indirect github.com/onsi/ginkgo/v2 v2.9.7 // indirect github.com/pierrec/lz4/v4 v4.1.14 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect @@ -82,7 +85,7 @@ require ( github.com/sagernet/netlink v0.0.0-20240523065131-45e60152f9ba // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect - github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect + github.com/vishvananda/netns v0.0.4 // indirect github.com/zeebo/blake3 v0.2.3 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.17.0 // indirect @@ -91,7 +94,6 @@ require ( golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.21.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect - gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.2.1 // indirect ) diff --git a/go.sum b/go.sum index cf54da05..ba83799d 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,7 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a h1:fEBsGL/sjAuJrgah5XqmmYsTLzJp/TO9Lhy39gkverk= github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= @@ -57,9 +58,6 @@ github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6K github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/libdns/alidns v1.0.3 h1:LFHuGnbseq5+HCeGa1aW8awyX/4M2psB9962fdD2+yQ= github.com/libdns/alidns v1.0.3/go.mod h1:e18uAG6GanfRhcJj6/tps2rCMzQJaYVcGKT+ELjdjGE= github.com/libdns/cloudflare v0.1.1 h1:FVPfWwP8zZCqj268LZjmkDleXlHPlFU9KC4OJ3yn054= @@ -69,12 +67,14 @@ github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s= github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= +github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= +github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= +github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30= github.com/mholt/acmez v1.2.0/go.mod h1:VT9YwH1xgNX1kmYY89gY8xPJC84BFAisjo8Egigt4kE= github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs= github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/onsi/ginkgo/v2 v2.9.7 h1:06xGQy5www2oN160RtEZoTvnP2sPhEfePYmCDc2szss= github.com/onsi/ginkgo/v2 v2.9.7/go.mod h1:cxrmXWykAwTwhQsJOPfdIDiJ+l2RYq7U8hFU+M/1uw0= github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU= @@ -101,6 +101,8 @@ github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f h1:NkhuupzH5ch7b/Y github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f/go.mod h1:KXmw+ouSJNOsuRpg4wgwwCQuunrGz4yoAqQjsLjc6N0= github.com/sagernet/netlink v0.0.0-20240523065131-45e60152f9ba h1:EY5AS7CCtfmARNv2zXUOrsEMPFDGYxaw65JzA2p51Vk= github.com/sagernet/netlink v0.0.0-20240523065131-45e60152f9ba/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= +github.com/sagernet/nftables v0.3.0-beta.2 h1:yKqMl4Dpb6nKxAmlE6fXjJRlLO2c1f2wyNFBg4hBr8w= +github.com/sagernet/nftables v0.3.0-beta.2/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= github.com/sagernet/quic-go v0.43.1-beta.1 h1:alizUjpvWYcz08dBCQsULOd+1xu0o7UtlyYf6SLbRNg= github.com/sagernet/quic-go v0.43.1-beta.1/go.mod h1:BkrQYeop7Jx3hN3TW8/76CXcdhYiNPyYEBL/BVJ1ifc= github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc= @@ -146,8 +148,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA= github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= -github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg= -github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= +github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg= @@ -176,7 +178,6 @@ golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -205,9 +206,8 @@ google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/inbound/tun.go b/inbound/tun.go index 530cb71e..2275fa06 100644 --- a/inbound/tun.go +++ b/inbound/tun.go @@ -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") diff --git a/inbound/tun_auto_redirect.go b/inbound/tun_auto_redirect.go index 433b61a1..8ebc04d2 100644 --- a/inbound/tun_auto_redirect.go +++ b/inbound/tun_auto_redirect.go @@ -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 -} diff --git a/inbound/tun_auto_redirect_iptables.go b/inbound/tun_auto_redirect_iptables.go new file mode 100644 index 00000000..b1aa7dff --- /dev/null +++ b/inbound/tun_auto_redirect_iptables.go @@ -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 +} diff --git a/inbound/tun_auto_redirect_nftables.go b/inbound/tun_auto_redirect_nftables.go new file mode 100644 index 00000000..ed84adf4 --- /dev/null +++ b/inbound/tun_auto_redirect_nftables.go @@ -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() +} diff --git a/inbound/tun_auto_redirect_nftables_expr.go b/inbound/tun_auto_redirect_nftables_expr.go new file mode 100644 index 00000000..27c0309f --- /dev/null +++ b/inbound/tun_auto_redirect_nftables_expr.go @@ -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, + }, + } +} diff --git a/inbound/tun_auto_redirect_stub.go b/inbound/tun_auto_redirect_stub.go new file mode 100644 index 00000000..4bfdafa3 --- /dev/null +++ b/inbound/tun_auto_redirect_stub.go @@ -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 +}