Skip to content

Commit 9b15134

Browse files
committed
Add auto-redirect
1 parent a604946 commit 9b15134

File tree

9 files changed

+941
-7
lines changed

9 files changed

+941
-7
lines changed

go.mod

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,20 @@ require (
77
github.com/go-ole/go-ole v1.3.0
88
github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f
99
github.com/sagernet/netlink v0.0.0-20240523065131-45e60152f9ba
10-
github.com/sagernet/sing v0.4.0
10+
github.com/sagernet/nftables v0.3.0-beta.2
11+
github.com/sagernet/sing v0.5.0-alpha.7.0.20240601064531-e5caac20a367
1112
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
1213
golang.org/x/net v0.25.0
1314
golang.org/x/sys v0.20.0
1415
)
1516

1617
require (
1718
github.com/google/btree v1.1.2 // indirect
18-
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect
19+
github.com/google/go-cmp v0.5.9 // indirect
20+
github.com/josharian/native v1.1.0 // indirect
21+
github.com/mdlayher/netlink v1.7.2 // indirect
22+
github.com/mdlayher/socket v0.4.1 // indirect
23+
github.com/vishvananda/netns v0.0.4 // indirect
24+
golang.org/x/sync v0.1.0 // indirect
1925
golang.org/x/time v0.5.0 // indirect
2026
)

go.sum

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,32 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
55
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
66
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
77
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
8+
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
9+
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
10+
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
11+
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
12+
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
13+
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
14+
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
15+
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
816
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
917
github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f h1:NkhuupzH5ch7b/Y/6ZHJWrnNLoiNnSJaow6DPb8VW2I=
1018
github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f/go.mod h1:KXmw+ouSJNOsuRpg4wgwwCQuunrGz4yoAqQjsLjc6N0=
1119
github.com/sagernet/netlink v0.0.0-20240523065131-45e60152f9ba h1:EY5AS7CCtfmARNv2zXUOrsEMPFDGYxaw65JzA2p51Vk=
1220
github.com/sagernet/netlink v0.0.0-20240523065131-45e60152f9ba/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
13-
github.com/sagernet/sing v0.4.0 h1:sCLSqLHOptgFvzQO9FfaYMl4PONePZkclMznpeKhdHc=
14-
github.com/sagernet/sing v0.4.0/go.mod h1:Xh4KO9nGdvm4K/LVg9Xn9jSxJdqe9KcXbAzNC1S2qfw=
21+
github.com/sagernet/nftables v0.3.0-beta.2 h1:yKqMl4Dpb6nKxAmlE6fXjJRlLO2c1f2wyNFBg4hBr8w=
22+
github.com/sagernet/nftables v0.3.0-beta.2/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
23+
github.com/sagernet/sing v0.5.0-alpha.7.0.20240601064531-e5caac20a367 h1:XP/zuP4591/DappwlLDjl7a21QKE6olgkYOGSGx9nf8=
24+
github.com/sagernet/sing v0.5.0-alpha.7.0.20240601064531-e5caac20a367/go.mod h1:Xh4KO9nGdvm4K/LVg9Xn9jSxJdqe9KcXbAzNC1S2qfw=
1525
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
16-
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg=
17-
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
26+
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
27+
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
1828
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
1929
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
2030
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
2131
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
22-
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
32+
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
33+
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
2334
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
2435
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
2536
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

redirect.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package tun
2+
3+
import (
4+
"context"
5+
6+
"github.com/sagernet/sing/common/logger"
7+
)
8+
9+
type AutoRedirect interface {
10+
Start() error
11+
Close() error
12+
}
13+
14+
type AutoRedirectOptions struct {
15+
TunOptions *Options
16+
Context context.Context
17+
Handler Handler
18+
Logger logger.Logger
19+
TableName string
20+
DisableNFTables bool
21+
CustomRedirectPort func() int
22+
}

redirect_iptables.go

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
//go:build linux
2+
3+
package tun
4+
5+
import (
6+
"net/netip"
7+
"os/exec"
8+
"strings"
9+
10+
E "github.com/sagernet/sing/common/exceptions"
11+
F "github.com/sagernet/sing/common/format"
12+
13+
"golang.org/x/sys/unix"
14+
)
15+
16+
func (r *autoRedirect) iptablesPathForFamily(family int) string {
17+
if family == unix.AF_INET {
18+
return r.iptablesPath
19+
} else {
20+
return r.ip6tablesPath
21+
}
22+
}
23+
24+
func (r *autoRedirect) setupIPTables(family int) error {
25+
tableNameOutput := r.tableName + "-output"
26+
tableNameForward := r.tableName + "-forward"
27+
tableNamePreRouteing := r.tableName + "-prerouting"
28+
iptablesPath := r.iptablesPathForFamily(family)
29+
redirectPort := r.redirectPort()
30+
// OUTPUT
31+
err := r.runShell(iptablesPath, "-t nat -N", tableNameOutput)
32+
if err != nil {
33+
return err
34+
}
35+
err = r.runShell(iptablesPath, "-t nat -A", tableNameOutput,
36+
"-p tcp -o", r.tunOptions.Name,
37+
"-j REDIRECT --to-ports", redirectPort)
38+
if err != nil {
39+
return err
40+
}
41+
err = r.runShell(iptablesPath, "-t nat -I OUTPUT -j", tableNameOutput)
42+
if err != nil {
43+
return err
44+
}
45+
if r.androidSu {
46+
return nil
47+
}
48+
// FORWARD
49+
err = r.runShell(iptablesPath, "-N", tableNameForward)
50+
if err != nil {
51+
return err
52+
}
53+
err = r.runShell(iptablesPath, "-A", tableNameForward,
54+
"-i", r.tunOptions.Name, "-j", "ACCEPT")
55+
if err != nil {
56+
return err
57+
}
58+
err = r.runShell(iptablesPath, "-A", tableNameForward,
59+
"-o", r.tunOptions.Name, "-j", "ACCEPT")
60+
if err != nil {
61+
return err
62+
}
63+
err = r.runShell(iptablesPath, "-I FORWARD -j", tableNameForward)
64+
if err != nil {
65+
return err
66+
}
67+
// PREROUTING
68+
err = r.runShell(iptablesPath, "-t nat -N", tableNamePreRouteing)
69+
if err != nil {
70+
return err
71+
}
72+
var (
73+
routeAddress []netip.Prefix
74+
routeExcludeAddress []netip.Prefix
75+
)
76+
if family == unix.AF_INET {
77+
routeAddress = r.tunOptions.Inet4RouteAddress
78+
routeExcludeAddress = r.tunOptions.Inet4RouteExcludeAddress
79+
} else {
80+
routeAddress = r.tunOptions.Inet6RouteAddress
81+
routeExcludeAddress = r.tunOptions.Inet6RouteExcludeAddress
82+
}
83+
if len(routeAddress) > 0 && (len(r.tunOptions.IncludeInterface) > 0 || len(r.tunOptions.IncludeUID) > 0) {
84+
return E.New("`*_route_address` is conflict with `include_interface` or `include_uid`")
85+
}
86+
err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing,
87+
"-i", r.tunOptions.Name, "-j RETURN")
88+
if err != nil {
89+
return err
90+
}
91+
for _, address := range routeExcludeAddress {
92+
err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing,
93+
"-d", address.String(), "-j RETURN")
94+
if err != nil {
95+
return err
96+
}
97+
}
98+
for _, name := range r.tunOptions.ExcludeInterface {
99+
err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing,
100+
"-i", name, "-j RETURN")
101+
if err != nil {
102+
return err
103+
}
104+
}
105+
for _, uid := range r.tunOptions.ExcludeUID {
106+
err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing,
107+
"-m owner --uid-owner", uid, "-j RETURN")
108+
if err != nil {
109+
return err
110+
}
111+
}
112+
var dnsServerAddress netip.Addr
113+
if family == unix.AF_INET {
114+
dnsServerAddress = r.tunOptions.Inet4Address[0].Addr().Next()
115+
} else {
116+
dnsServerAddress = r.tunOptions.Inet6Address[0].Addr().Next()
117+
}
118+
if len(routeAddress) > 0 {
119+
for _, address := range routeAddress {
120+
err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing,
121+
"-d", address.String(), "-p udp --dport 53 -j DNAT --to", dnsServerAddress)
122+
if err != nil {
123+
return err
124+
}
125+
}
126+
} else if len(r.tunOptions.IncludeInterface) > 0 || len(r.tunOptions.IncludeUID) > 0 {
127+
for _, name := range r.tunOptions.IncludeInterface {
128+
err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing,
129+
"-i", name, "-p udp --dport 53 -j DNAT --to", dnsServerAddress)
130+
if err != nil {
131+
return err
132+
}
133+
}
134+
for _, uidRange := range r.tunOptions.IncludeUID {
135+
for uid := uidRange.Start; uid <= uidRange.End; uid++ {
136+
err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing,
137+
"-m owner --uid-owner", uid, "-p udp --dport 53 -j DNAT --to", dnsServerAddress)
138+
if err != nil {
139+
return err
140+
}
141+
}
142+
}
143+
} else {
144+
err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing,
145+
"-p udp --dport 53 -j DNAT --to", dnsServerAddress)
146+
if err != nil {
147+
return err
148+
}
149+
}
150+
151+
err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing, "-m addrtype --dst-type LOCAL -j RETURN")
152+
if err != nil {
153+
return err
154+
}
155+
156+
if len(routeAddress) > 0 {
157+
for _, address := range routeAddress {
158+
err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing,
159+
"-d", address.String(), "-p tcp -j REDIRECT --to-ports", redirectPort)
160+
if err != nil {
161+
return err
162+
}
163+
}
164+
} else if len(r.tunOptions.IncludeInterface) > 0 || len(r.tunOptions.IncludeUID) > 0 {
165+
for _, name := range r.tunOptions.IncludeInterface {
166+
err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing,
167+
"-i", name, "-p tcp -j REDIRECT --to-ports", redirectPort)
168+
if err != nil {
169+
return err
170+
}
171+
}
172+
for _, uidRange := range r.tunOptions.IncludeUID {
173+
for uid := uidRange.Start; uid <= uidRange.End; uid++ {
174+
err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing,
175+
"-m owner --uid-owner", uid, "-p tcp -j REDIRECT --to-ports", redirectPort)
176+
if err != nil {
177+
return err
178+
}
179+
}
180+
}
181+
} else {
182+
err = r.runShell(iptablesPath, "-t nat -A", tableNamePreRouteing,
183+
"-p tcp -j REDIRECT --to-ports", redirectPort)
184+
if err != nil {
185+
return err
186+
}
187+
}
188+
err = r.runShell(iptablesPath, "-t nat -I PREROUTING -j", tableNamePreRouteing)
189+
if err != nil {
190+
return err
191+
}
192+
return nil
193+
}
194+
195+
func (r *autoRedirect) cleanupIPTables(family int) {
196+
tableNameOutput := r.tableName + "-output"
197+
tableNameForward := r.tableName + "-forward"
198+
tableNamePreRouteing := r.tableName + "-prerouting"
199+
iptablesPath := r.iptablesPathForFamily(family)
200+
_ = r.runShell(iptablesPath, "-t nat -D OUTPUT -j", tableNameOutput)
201+
_ = r.runShell(iptablesPath, "-t nat -F", tableNameOutput)
202+
_ = r.runShell(iptablesPath, "-t nat -X", tableNameOutput)
203+
if !r.androidSu {
204+
_ = r.runShell(iptablesPath, "-D FORWARD -j", tableNameForward)
205+
_ = r.runShell(iptablesPath, "-F", tableNameForward)
206+
_ = r.runShell(iptablesPath, "-X", tableNameForward)
207+
_ = r.runShell(iptablesPath, "-t nat -D PREROUTING -j", tableNamePreRouteing)
208+
_ = r.runShell(iptablesPath, "-t nat -F", tableNamePreRouteing)
209+
_ = r.runShell(iptablesPath, "-t nat -X", tableNamePreRouteing)
210+
}
211+
}
212+
213+
func (r *autoRedirect) runShell(commands ...any) error {
214+
commandStr := strings.Join(F.MapToString(commands), " ")
215+
var command *exec.Cmd
216+
if r.androidSu {
217+
command = exec.Command(r.suPath, "-c", commandStr)
218+
} else {
219+
commandArray := strings.Split(commandStr, " ")
220+
command = exec.Command(commandArray[0], commandArray[1:]...)
221+
}
222+
combinedOutput, err := command.CombinedOutput()
223+
if err != nil {
224+
return E.Extend(err, F.ToString(commandStr, ": ", string(combinedOutput)))
225+
}
226+
return nil
227+
}

0 commit comments

Comments
 (0)