mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-04-04 12:27:36 +03:00
Add nftables support for auto-redirect
This commit is contained in:
parent
6875a33c28
commit
a2575526b6
8 changed files with 760 additions and 325 deletions
8
go.mod
8
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
|
||||
)
|
||||
|
|
20
go.sum
20
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=
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
235
inbound/tun_auto_redirect_iptables.go
Normal file
235
inbound/tun_auto_redirect_iptables.go
Normal 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
|
||||
}
|
231
inbound/tun_auto_redirect_nftables.go
Normal file
231
inbound/tun_auto_redirect_nftables.go
Normal 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()
|
||||
}
|
153
inbound/tun_auto_redirect_nftables_expr.go
Normal file
153
inbound/tun_auto_redirect_nftables_expr.go
Normal 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,
|
||||
},
|
||||
}
|
||||
}
|
23
inbound/tun_auto_redirect_stub.go
Normal file
23
inbound/tun_auto_redirect_stub.go
Normal 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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue