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).
This commit is contained in:
@@ -19,29 +19,31 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type autoRedirect struct {
|
type autoRedirect struct {
|
||||||
tunOptions *Options
|
tunOptions *Options
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
handler Handler
|
handler Handler
|
||||||
logger logger.Logger
|
logger logger.Logger
|
||||||
tableName string
|
tableName string
|
||||||
networkMonitor NetworkUpdateMonitor
|
networkMonitor NetworkUpdateMonitor
|
||||||
networkListener *list.Element[NetworkUpdateCallback]
|
networkListener *list.Element[NetworkUpdateCallback]
|
||||||
interfaceFinder control.InterfaceFinder
|
interfaceFinder control.InterfaceFinder
|
||||||
localAddresses []netip.Prefix
|
localAddresses []netip.Prefix
|
||||||
customRedirectPortFunc func() int
|
customRedirectPortFunc func() int
|
||||||
customRedirectPort int
|
customRedirectPort int
|
||||||
redirectServer *redirectServer
|
redirectServer *redirectServer
|
||||||
enableIPv4 bool
|
enableIPv4 bool
|
||||||
enableIPv6 bool
|
enableIPv6 bool
|
||||||
iptablesPath string
|
iptablesPath string
|
||||||
ip6tablesPath string
|
ip6tablesPath string
|
||||||
useNFTables bool
|
useNFTables bool
|
||||||
androidSu bool
|
androidSu bool
|
||||||
suPath string
|
suPath string
|
||||||
routeAddressSet *[]*netipx.IPSet
|
routeAddressSet *[]*netipx.IPSet
|
||||||
routeExcludeAddressSet *[]*netipx.IPSet
|
routeExcludeAddressSet *[]*netipx.IPSet
|
||||||
nfqueueHandler *nfqueueHandler
|
nfqueueHandler *nfqueueHandler
|
||||||
nfqueueEnabled bool
|
nfqueueEnabled bool
|
||||||
|
redirectRouteTableIndex int
|
||||||
|
redirectInterfaces []control.Interface
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) {
|
func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) {
|
||||||
@@ -152,6 +154,12 @@ func (r *autoRedirect) Start() error {
|
|||||||
}
|
}
|
||||||
r.cleanupNFTables()
|
r.cleanupNFTables()
|
||||||
err = r.setupNFTables()
|
err = r.setupNFTables()
|
||||||
|
if err == nil && r.tunOptions.AutoRedirectMarkMode {
|
||||||
|
err = r.setupRedirectRoutes()
|
||||||
|
if err != nil {
|
||||||
|
r.cleanupNFTables()
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
r.cleanupIPTables()
|
r.cleanupIPTables()
|
||||||
err = r.setupIPTables()
|
err = r.setupIPTables()
|
||||||
@@ -164,6 +172,7 @@ func (r *autoRedirect) Close() error {
|
|||||||
r.nfqueueHandler.Close()
|
r.nfqueueHandler.Close()
|
||||||
}
|
}
|
||||||
if r.useNFTables {
|
if r.useNFTables {
|
||||||
|
r.cleanupRedirectRoutes()
|
||||||
r.cleanupNFTables()
|
r.cleanupNFTables()
|
||||||
} else {
|
} else {
|
||||||
r.cleanupIPTables()
|
r.cleanupIPTables()
|
||||||
|
|||||||
@@ -293,6 +293,12 @@ func (r *autoRedirect) setupNFTables() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
r.logger.Error("update local address set: ", err)
|
r.logger.Error("update local address set: ", err)
|
||||||
}
|
}
|
||||||
|
if r.tunOptions.AutoRedirectMarkMode {
|
||||||
|
err = r.updateRedirectRoutes()
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("update redirect routes: ", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
174
redirect_route_linux.go
Normal file
174
redirect_route_linux.go
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package tun
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/sagernet/netlink"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
"github.com/sagernet/sing/common/control"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
const redirectRouteRulePriority = 1
|
||||||
|
|
||||||
|
func (r *autoRedirect) setupRedirectRoutes() error {
|
||||||
|
for {
|
||||||
|
r.redirectRouteTableIndex = int(rand.Uint32())
|
||||||
|
if r.redirectRouteTableIndex == r.tunOptions.IPRoute2TableIndex {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
routeList, fErr := netlink.RouteListFiltered(netlink.FAMILY_ALL,
|
||||||
|
&netlink.Route{Table: r.redirectRouteTableIndex},
|
||||||
|
netlink.RT_FILTER_TABLE)
|
||||||
|
if len(routeList) == 0 || fErr != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err := r.interfaceFinder.Update()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tunName := r.tunOptions.Name
|
||||||
|
r.redirectInterfaces = common.Filter(r.interfaceFinder.Interfaces(), func(it control.Interface) bool {
|
||||||
|
return it.Name != "lo" && it.Name != tunName && it.Flags&net.FlagUp != 0
|
||||||
|
})
|
||||||
|
r.cleanupRedirectRoutes()
|
||||||
|
for _, iface := range r.redirectInterfaces {
|
||||||
|
err = r.addRedirectRoutes(iface.Index)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.enableIPv4 {
|
||||||
|
rule := netlink.NewRule()
|
||||||
|
rule.Priority = redirectRouteRulePriority
|
||||||
|
rule.Table = r.redirectRouteTableIndex
|
||||||
|
rule.Family = unix.AF_INET
|
||||||
|
err = netlink.RuleAdd(rule)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.enableIPv6 {
|
||||||
|
rule := netlink.NewRule()
|
||||||
|
rule.Priority = redirectRouteRulePriority
|
||||||
|
rule.Table = r.redirectRouteTableIndex
|
||||||
|
rule.Family = unix.AF_INET6
|
||||||
|
err = netlink.RuleAdd(rule)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *autoRedirect) addRedirectRoutes(linkIndex int) error {
|
||||||
|
if r.enableIPv4 {
|
||||||
|
err := netlink.RouteAppend(&netlink.Route{
|
||||||
|
LinkIndex: linkIndex,
|
||||||
|
Dst: &net.IPNet{IP: net.IPv4(127, 0, 0, 1), Mask: net.CIDRMask(32, 32)},
|
||||||
|
Table: r.redirectRouteTableIndex,
|
||||||
|
Type: unix.RTN_LOCAL,
|
||||||
|
Scope: netlink.SCOPE_HOST,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.enableIPv6 {
|
||||||
|
err := netlink.RouteAppend(&netlink.Route{
|
||||||
|
LinkIndex: linkIndex,
|
||||||
|
Dst: &net.IPNet{IP: net.IPv6loopback, Mask: net.CIDRMask(128, 128)},
|
||||||
|
Table: r.redirectRouteTableIndex,
|
||||||
|
Type: unix.RTN_LOCAL,
|
||||||
|
Scope: netlink.SCOPE_HOST,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *autoRedirect) removeRedirectRoutes(linkIndex int) {
|
||||||
|
if r.enableIPv4 {
|
||||||
|
_ = netlink.RouteDel(&netlink.Route{
|
||||||
|
LinkIndex: linkIndex,
|
||||||
|
Dst: &net.IPNet{IP: net.IPv4(127, 0, 0, 1), Mask: net.CIDRMask(32, 32)},
|
||||||
|
Table: r.redirectRouteTableIndex,
|
||||||
|
Type: unix.RTN_LOCAL,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if r.enableIPv6 {
|
||||||
|
_ = netlink.RouteDel(&netlink.Route{
|
||||||
|
LinkIndex: linkIndex,
|
||||||
|
Dst: &net.IPNet{IP: net.IPv6loopback, Mask: net.CIDRMask(128, 128)},
|
||||||
|
Table: r.redirectRouteTableIndex,
|
||||||
|
Type: unix.RTN_LOCAL,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *autoRedirect) updateRedirectRoutes() error {
|
||||||
|
err := r.interfaceFinder.Update()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tunName := r.tunOptions.Name
|
||||||
|
newInterfaces := common.Filter(r.interfaceFinder.Interfaces(), func(it control.Interface) bool {
|
||||||
|
return it.Name != "lo" && it.Name != tunName && it.Flags&net.FlagUp != 0
|
||||||
|
})
|
||||||
|
oldMap := make(map[int]bool, len(r.redirectInterfaces))
|
||||||
|
for _, iface := range r.redirectInterfaces {
|
||||||
|
oldMap[iface.Index] = true
|
||||||
|
}
|
||||||
|
newMap := make(map[int]bool, len(newInterfaces))
|
||||||
|
for _, iface := range newInterfaces {
|
||||||
|
newMap[iface.Index] = true
|
||||||
|
}
|
||||||
|
for _, iface := range newInterfaces {
|
||||||
|
if !oldMap[iface.Index] {
|
||||||
|
err = r.addRedirectRoutes(iface.Index)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, iface := range r.redirectInterfaces {
|
||||||
|
if !newMap[iface.Index] {
|
||||||
|
r.removeRedirectRoutes(iface.Index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.redirectInterfaces = newInterfaces
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *autoRedirect) cleanupRedirectRoutes() {
|
||||||
|
if r.redirectRouteTableIndex == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
routes, _ := netlink.RouteListFiltered(netlink.FAMILY_ALL,
|
||||||
|
&netlink.Route{Table: r.redirectRouteTableIndex},
|
||||||
|
netlink.RT_FILTER_TABLE)
|
||||||
|
for _, route := range routes {
|
||||||
|
_ = netlink.RouteDel(&route)
|
||||||
|
}
|
||||||
|
if r.enableIPv4 {
|
||||||
|
rule := netlink.NewRule()
|
||||||
|
rule.Priority = redirectRouteRulePriority
|
||||||
|
rule.Table = r.redirectRouteTableIndex
|
||||||
|
rule.Family = unix.AF_INET
|
||||||
|
_ = netlink.RuleDel(rule)
|
||||||
|
}
|
||||||
|
if r.enableIPv6 {
|
||||||
|
rule := netlink.NewRule()
|
||||||
|
rule.Priority = redirectRouteRulePriority
|
||||||
|
rule.Table = r.redirectRouteTableIndex
|
||||||
|
rule.Family = unix.AF_INET6
|
||||||
|
_ = netlink.RuleDel(rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user