Skip to content

Commit 93c7ad8

Browse files
committed
Fix auto_redirect dropping SO_BINDTODEVICE traffic
REDIRECT in the OUTPUT chain rewrites the destination to 127.0.0.1, then ip_route_me_harder() reroutes with the socket's bound interface constraint (flowi4_oif). Since 127.0.0.1 is only reachable via lo, the routing lookup fails and the packet is silently dropped. Add a fallback routing table with `local 127.0.0.1` entries for each non-loopback interface. When the local table lookup fails due to OIF mismatch, the fallback table provides a matching RTN_LOCAL route. The kernel then overrides dev_out to loopback (route.c:2857), so the packet is delivered locally to the redirect server as intended. This fixes NetworkManager connectivity checks and other tools that use SO_BINDTODEVICE (e.g. curl --interface).
1 parent 0efb319 commit 93c7ad8

3 files changed

Lines changed: 212 additions & 23 deletions

File tree

redirect_linux.go

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,29 +19,31 @@ import (
1919
)
2020

2121
type autoRedirect struct {
22-
tunOptions *Options
23-
ctx context.Context
24-
handler Handler
25-
logger logger.Logger
26-
tableName string
27-
networkMonitor NetworkUpdateMonitor
28-
networkListener *list.Element[NetworkUpdateCallback]
29-
interfaceFinder control.InterfaceFinder
30-
localAddresses []netip.Prefix
31-
customRedirectPortFunc func() int
32-
customRedirectPort int
33-
redirectServer *redirectServer
34-
enableIPv4 bool
35-
enableIPv6 bool
36-
iptablesPath string
37-
ip6tablesPath string
38-
useNFTables bool
39-
androidSu bool
40-
suPath string
41-
routeAddressSet *[]*netipx.IPSet
42-
routeExcludeAddressSet *[]*netipx.IPSet
43-
nfqueueHandler *nfqueueHandler
44-
nfqueueEnabled bool
22+
tunOptions *Options
23+
ctx context.Context
24+
handler Handler
25+
logger logger.Logger
26+
tableName string
27+
networkMonitor NetworkUpdateMonitor
28+
networkListener *list.Element[NetworkUpdateCallback]
29+
interfaceFinder control.InterfaceFinder
30+
localAddresses []netip.Prefix
31+
customRedirectPortFunc func() int
32+
customRedirectPort int
33+
redirectServer *redirectServer
34+
enableIPv4 bool
35+
enableIPv6 bool
36+
iptablesPath string
37+
ip6tablesPath string
38+
useNFTables bool
39+
androidSu bool
40+
suPath string
41+
routeAddressSet *[]*netipx.IPSet
42+
routeExcludeAddressSet *[]*netipx.IPSet
43+
nfqueueHandler *nfqueueHandler
44+
nfqueueEnabled bool
45+
redirectRouteTableIndex int
46+
redirectInterfaces []control.Interface
4547
}
4648

4749
func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) {
@@ -152,6 +154,12 @@ func (r *autoRedirect) Start() error {
152154
}
153155
r.cleanupNFTables()
154156
err = r.setupNFTables()
157+
if err == nil && r.tunOptions.AutoRedirectMarkMode {
158+
err = r.setupRedirectRoutes()
159+
if err != nil {
160+
r.cleanupNFTables()
161+
}
162+
}
155163
} else {
156164
r.cleanupIPTables()
157165
err = r.setupIPTables()
@@ -164,6 +172,7 @@ func (r *autoRedirect) Close() error {
164172
r.nfqueueHandler.Close()
165173
}
166174
if r.useNFTables {
175+
r.cleanupRedirectRoutes()
167176
r.cleanupNFTables()
168177
} else {
169178
r.cleanupIPTables()

redirect_nftables.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,12 @@ func (r *autoRedirect) setupNFTables() error {
293293
if err != nil {
294294
r.logger.Error("update local address set: ", err)
295295
}
296+
if r.tunOptions.AutoRedirectMarkMode {
297+
err = r.updateRedirectRoutes()
298+
if err != nil {
299+
r.logger.Error("update redirect routes: ", err)
300+
}
301+
}
296302
})
297303
return nil
298304
}

redirect_route_linux.go

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
//go:build linux
2+
3+
package tun
4+
5+
import (
6+
"math/rand"
7+
"net"
8+
9+
"github.com/sagernet/netlink"
10+
"github.com/sagernet/sing/common"
11+
"github.com/sagernet/sing/common/control"
12+
13+
"golang.org/x/sys/unix"
14+
)
15+
16+
const redirectRouteRulePriority = 1
17+
18+
func (r *autoRedirect) setupRedirectRoutes() error {
19+
for {
20+
r.redirectRouteTableIndex = int(rand.Uint32())
21+
if r.redirectRouteTableIndex == r.tunOptions.IPRoute2TableIndex {
22+
continue
23+
}
24+
routeList, fErr := netlink.RouteListFiltered(netlink.FAMILY_ALL,
25+
&netlink.Route{Table: r.redirectRouteTableIndex},
26+
netlink.RT_FILTER_TABLE)
27+
if len(routeList) == 0 || fErr != nil {
28+
break
29+
}
30+
}
31+
err := r.interfaceFinder.Update()
32+
if err != nil {
33+
return err
34+
}
35+
tunName := r.tunOptions.Name
36+
r.redirectInterfaces = common.Filter(r.interfaceFinder.Interfaces(), func(it control.Interface) bool {
37+
return it.Name != "lo" && it.Name != tunName && it.Flags&net.FlagUp != 0
38+
})
39+
r.cleanupRedirectRoutes()
40+
for _, iface := range r.redirectInterfaces {
41+
err = r.addRedirectRoutes(iface.Index)
42+
if err != nil {
43+
return err
44+
}
45+
}
46+
if r.enableIPv4 {
47+
rule := netlink.NewRule()
48+
rule.Priority = redirectRouteRulePriority
49+
rule.Table = r.redirectRouteTableIndex
50+
rule.Family = unix.AF_INET
51+
err = netlink.RuleAdd(rule)
52+
if err != nil {
53+
return err
54+
}
55+
}
56+
if r.enableIPv6 {
57+
rule := netlink.NewRule()
58+
rule.Priority = redirectRouteRulePriority
59+
rule.Table = r.redirectRouteTableIndex
60+
rule.Family = unix.AF_INET6
61+
err = netlink.RuleAdd(rule)
62+
if err != nil {
63+
return err
64+
}
65+
}
66+
return nil
67+
}
68+
69+
func (r *autoRedirect) addRedirectRoutes(linkIndex int) error {
70+
if r.enableIPv4 {
71+
err := netlink.RouteAppend(&netlink.Route{
72+
LinkIndex: linkIndex,
73+
Dst: &net.IPNet{IP: net.IPv4(127, 0, 0, 1), Mask: net.CIDRMask(32, 32)},
74+
Table: r.redirectRouteTableIndex,
75+
Type: unix.RTN_LOCAL,
76+
Scope: netlink.SCOPE_HOST,
77+
})
78+
if err != nil {
79+
return err
80+
}
81+
}
82+
if r.enableIPv6 {
83+
err := netlink.RouteAppend(&netlink.Route{
84+
LinkIndex: linkIndex,
85+
Dst: &net.IPNet{IP: net.IPv6loopback, Mask: net.CIDRMask(128, 128)},
86+
Table: r.redirectRouteTableIndex,
87+
Type: unix.RTN_LOCAL,
88+
Scope: netlink.SCOPE_HOST,
89+
})
90+
if err != nil {
91+
return err
92+
}
93+
}
94+
return nil
95+
}
96+
97+
func (r *autoRedirect) removeRedirectRoutes(linkIndex int) {
98+
if r.enableIPv4 {
99+
_ = netlink.RouteDel(&netlink.Route{
100+
LinkIndex: linkIndex,
101+
Dst: &net.IPNet{IP: net.IPv4(127, 0, 0, 1), Mask: net.CIDRMask(32, 32)},
102+
Table: r.redirectRouteTableIndex,
103+
Type: unix.RTN_LOCAL,
104+
})
105+
}
106+
if r.enableIPv6 {
107+
_ = netlink.RouteDel(&netlink.Route{
108+
LinkIndex: linkIndex,
109+
Dst: &net.IPNet{IP: net.IPv6loopback, Mask: net.CIDRMask(128, 128)},
110+
Table: r.redirectRouteTableIndex,
111+
Type: unix.RTN_LOCAL,
112+
})
113+
}
114+
}
115+
116+
func (r *autoRedirect) updateRedirectRoutes() error {
117+
err := r.interfaceFinder.Update()
118+
if err != nil {
119+
return err
120+
}
121+
tunName := r.tunOptions.Name
122+
newInterfaces := common.Filter(r.interfaceFinder.Interfaces(), func(it control.Interface) bool {
123+
return it.Name != "lo" && it.Name != tunName && it.Flags&net.FlagUp != 0
124+
})
125+
oldMap := make(map[int]bool, len(r.redirectInterfaces))
126+
for _, iface := range r.redirectInterfaces {
127+
oldMap[iface.Index] = true
128+
}
129+
newMap := make(map[int]bool, len(newInterfaces))
130+
for _, iface := range newInterfaces {
131+
newMap[iface.Index] = true
132+
}
133+
for _, iface := range newInterfaces {
134+
if !oldMap[iface.Index] {
135+
err = r.addRedirectRoutes(iface.Index)
136+
if err != nil {
137+
return err
138+
}
139+
}
140+
}
141+
for _, iface := range r.redirectInterfaces {
142+
if !newMap[iface.Index] {
143+
r.removeRedirectRoutes(iface.Index)
144+
}
145+
}
146+
r.redirectInterfaces = newInterfaces
147+
return nil
148+
}
149+
150+
func (r *autoRedirect) cleanupRedirectRoutes() {
151+
if r.redirectRouteTableIndex == 0 {
152+
return
153+
}
154+
routes, _ := netlink.RouteListFiltered(netlink.FAMILY_ALL,
155+
&netlink.Route{Table: r.redirectRouteTableIndex},
156+
netlink.RT_FILTER_TABLE)
157+
for _, route := range routes {
158+
_ = netlink.RouteDel(&route)
159+
}
160+
if r.enableIPv4 {
161+
rule := netlink.NewRule()
162+
rule.Priority = redirectRouteRulePriority
163+
rule.Table = r.redirectRouteTableIndex
164+
rule.Family = unix.AF_INET
165+
_ = netlink.RuleDel(rule)
166+
}
167+
if r.enableIPv6 {
168+
rule := netlink.NewRule()
169+
rule.Priority = redirectRouteRulePriority
170+
rule.Table = r.redirectRouteTableIndex
171+
rule.Family = unix.AF_INET6
172+
_ = netlink.RuleDel(rule)
173+
}
174+
}

0 commit comments

Comments
 (0)