Compare commits

...

2 Commits
v0.8.2 ... dev

Author SHA1 Message Date
世界
5485872f60 Add MAC address include/exclude filtering for nftables auto-redirect
Support filtering traffic by source MAC address in the prerouting chain,
using ether addr payload matching with set lookups for multiple addresses.
2026-03-11 21:25:53 +08:00
世界
caaf8469e0 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).
2026-03-11 21:15:11 +08:00
5 changed files with 358 additions and 23 deletions

View File

@@ -19,29 +19,31 @@ import (
)
type autoRedirect struct {
tunOptions *Options
ctx context.Context
handler Handler
logger logger.Logger
tableName string
networkMonitor NetworkUpdateMonitor
networkListener *list.Element[NetworkUpdateCallback]
interfaceFinder control.InterfaceFinder
localAddresses []netip.Prefix
customRedirectPortFunc func() int
customRedirectPort int
redirectServer *redirectServer
enableIPv4 bool
enableIPv6 bool
iptablesPath string
ip6tablesPath string
useNFTables bool
androidSu bool
suPath string
routeAddressSet *[]*netipx.IPSet
routeExcludeAddressSet *[]*netipx.IPSet
nfqueueHandler *nfqueueHandler
nfqueueEnabled bool
tunOptions *Options
ctx context.Context
handler Handler
logger logger.Logger
tableName string
networkMonitor NetworkUpdateMonitor
networkListener *list.Element[NetworkUpdateCallback]
interfaceFinder control.InterfaceFinder
localAddresses []netip.Prefix
customRedirectPortFunc func() int
customRedirectPort int
redirectServer *redirectServer
enableIPv4 bool
enableIPv6 bool
iptablesPath string
ip6tablesPath string
useNFTables bool
androidSu bool
suPath string
routeAddressSet *[]*netipx.IPSet
routeExcludeAddressSet *[]*netipx.IPSet
nfqueueHandler *nfqueueHandler
nfqueueEnabled bool
redirectRouteTableIndex int
redirectInterfaces []control.Interface
}
func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) {
@@ -152,6 +154,12 @@ func (r *autoRedirect) Start() error {
}
r.cleanupNFTables()
err = r.setupNFTables()
if err == nil && r.tunOptions.AutoRedirectMarkMode {
err = r.setupRedirectRoutes()
if err != nil {
r.cleanupNFTables()
}
}
} else {
r.cleanupIPTables()
err = r.setupIPTables()
@@ -164,6 +172,7 @@ func (r *autoRedirect) Close() error {
r.nfqueueHandler.Close()
}
if r.useNFTables {
r.cleanupRedirectRoutes()
r.cleanupNFTables()
} else {
r.cleanupIPTables()

View File

@@ -293,6 +293,12 @@ func (r *autoRedirect) setupNFTables() error {
if err != nil {
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
}

View File

@@ -3,6 +3,7 @@
package tun
import (
"net"
"net/netip"
_ "unsafe"
@@ -373,6 +374,149 @@ func (r *autoRedirect) nftablesCreateExcludeRules(nft *nftables.Conn, table *nft
})
}
}
if len(r.tunOptions.IncludeMACAddress) > 0 {
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chain,
Exprs: []expr.Any{
&expr.Meta{Key: expr.MetaKeyIIFTYPE, Register: 1},
&expr.Cmp{
Op: expr.CmpOpNeq,
Register: 1,
Data: binaryutil.NativeEndian.PutUint16(unix.ARPHRD_ETHER),
},
&expr.Counter{},
&expr.Verdict{
Kind: expr.VerdictReturn,
},
},
})
if len(r.tunOptions.IncludeMACAddress) > 1 {
includeMACSet := &nftables.Set{
Table: table,
Anonymous: true,
Constant: true,
KeyType: nftables.TypeEtherAddr,
}
err := nft.AddSet(includeMACSet, common.Map(r.tunOptions.IncludeMACAddress, func(it net.HardwareAddr) nftables.SetElement {
return nftables.SetElement{
Key: []byte(it),
}
}))
if err != nil {
return err
}
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chain,
Exprs: []expr.Any{
&expr.Payload{
OperationType: expr.PayloadLoad,
DestRegister: 1,
Base: expr.PayloadBaseLLHeader,
Offset: 6,
Len: 6,
},
&expr.Lookup{
SourceRegister: 1,
SetID: includeMACSet.ID,
SetName: includeMACSet.Name,
Invert: true,
},
&expr.Counter{},
&expr.Verdict{
Kind: expr.VerdictReturn,
},
},
})
} else {
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chain,
Exprs: []expr.Any{
&expr.Payload{
OperationType: expr.PayloadLoad,
DestRegister: 1,
Base: expr.PayloadBaseLLHeader,
Offset: 6,
Len: 6,
},
&expr.Cmp{
Op: expr.CmpOpNeq,
Register: 1,
Data: []byte(r.tunOptions.IncludeMACAddress[0]),
},
&expr.Counter{},
&expr.Verdict{
Kind: expr.VerdictReturn,
},
},
})
}
}
if len(r.tunOptions.ExcludeMACAddress) > 0 {
if len(r.tunOptions.ExcludeMACAddress) > 1 {
excludeMACSet := &nftables.Set{
Table: table,
Anonymous: true,
Constant: true,
KeyType: nftables.TypeEtherAddr,
}
err := nft.AddSet(excludeMACSet, common.Map(r.tunOptions.ExcludeMACAddress, func(it net.HardwareAddr) nftables.SetElement {
return nftables.SetElement{
Key: []byte(it),
}
}))
if err != nil {
return err
}
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chain,
Exprs: []expr.Any{
&expr.Payload{
OperationType: expr.PayloadLoad,
DestRegister: 1,
Base: expr.PayloadBaseLLHeader,
Offset: 6,
Len: 6,
},
&expr.Lookup{
SourceRegister: 1,
SetID: excludeMACSet.ID,
SetName: excludeMACSet.Name,
},
&expr.Counter{},
&expr.Verdict{
Kind: expr.VerdictReturn,
},
},
})
} else {
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chain,
Exprs: []expr.Any{
&expr.Payload{
OperationType: expr.PayloadLoad,
DestRegister: 1,
Base: expr.PayloadBaseLLHeader,
Offset: 6,
Len: 6,
},
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: []byte(r.tunOptions.ExcludeMACAddress[0]),
},
&expr.Counter{},
&expr.Verdict{
Kind: expr.VerdictReturn,
},
},
})
}
}
} else {
if len(r.tunOptions.IncludeUID) > 0 {
if len(r.tunOptions.IncludeUID) > 1 || r.tunOptions.IncludeUID[0].Start != r.tunOptions.IncludeUID[0].End {

174
redirect_route_linux.go Normal file
View 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)
}
}

2
tun.go
View File

@@ -102,6 +102,8 @@ type Options struct {
IncludeAndroidUser []int
IncludePackage []string
ExcludePackage []string
IncludeMACAddress []net.HardwareAddr
ExcludeMACAddress []net.HardwareAddr
InterfaceFinder control.InterfaceFinder
InterfaceMonitor DefaultInterfaceMonitor
FileDescriptor int