Fix redirect routes

This commit is contained in:
世界 2026-07-14 10:20:18 +08:00
parent eccdcee073
commit 3a09076491
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
3 changed files with 176 additions and 106 deletions

View file

@ -17,6 +17,7 @@ import (
type networkUpdateMonitor struct { type networkUpdateMonitor struct {
routeUpdate chan netlink.RouteUpdate routeUpdate chan netlink.RouteUpdate
linkUpdate chan netlink.LinkUpdate linkUpdate chan netlink.LinkUpdate
addressUpdate chan netlink.AddrUpdate
close chan struct{} close chan struct{}
access sync.Mutex access sync.Mutex
@ -34,6 +35,7 @@ func NewNetworkUpdateMonitor(logger logger.Logger) (NetworkUpdateMonitor, error)
monitor := &networkUpdateMonitor{ monitor := &networkUpdateMonitor{
routeUpdate: make(chan netlink.RouteUpdate, 2), routeUpdate: make(chan netlink.RouteUpdate, 2),
linkUpdate: make(chan netlink.LinkUpdate, 2), linkUpdate: make(chan netlink.LinkUpdate, 2),
addressUpdate: make(chan netlink.AddrUpdate, 2),
close: make(chan struct{}), close: make(chan struct{}),
logger: logger, logger: logger,
} }
@ -63,28 +65,46 @@ func (m *networkUpdateMonitor) Start() error {
if err != nil { if err != nil {
return E.Cause(err, "subscribe link updates") return E.Cause(err, "subscribe link updates")
} }
go m.loopUpdate() err = netlink.AddrSubscribe(m.addressUpdate, m.close)
if err != nil {
return E.Cause(err, "subscribe address updates")
}
go m.loopUpdate(time.Second)
return nil return nil
} }
func (m *networkUpdateMonitor) loopUpdate() { func (m *networkUpdateMonitor) loopUpdate(minDuration time.Duration) {
const minDuration = time.Second
timer := time.NewTimer(minDuration) timer := time.NewTimer(minDuration)
timer.Stop()
defer timer.Stop() defer timer.Stop()
var (
timerC <-chan time.Time
pending bool
)
for { for {
select { select {
case <-m.close: case <-m.close:
return return
case <-m.routeUpdate: case <-m.routeUpdate:
case <-m.linkUpdate: case <-m.linkUpdate:
case <-m.addressUpdate:
case <-timerC:
if pending {
m.emit()
pending = false
timer.Reset(minDuration)
continue
}
timerC = nil
continue
}
if timerC != nil {
pending = true
continue
} }
m.emit() m.emit()
select {
case <-m.close:
return
case <-timer.C:
timer.Reset(minDuration) timer.Reset(minDuration)
} timerC = timer.C
} }
} }

View file

@ -6,6 +6,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"runtime" "runtime"
"sync"
"github.com/sagernet/nftables" "github.com/sagernet/nftables"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
@ -43,7 +44,8 @@ type autoRedirect struct {
nfqueueHandler *nfqueueHandler nfqueueHandler *nfqueueHandler
nfqueueEnabled bool nfqueueEnabled bool
redirectRouteTableIndex int redirectRouteTableIndex int
redirectInterfaces []control.Interface redirectRouteAccess sync.Mutex
redirectRoutesActive bool
} }
func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) { func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) {
@ -179,8 +181,8 @@ func (r *autoRedirect) Close() error {
r.nfqueueHandler.Close() r.nfqueueHandler.Close()
} }
if r.useNFTables { if r.useNFTables {
r.cleanupRedirectRoutes()
r.cleanupNFTables() r.cleanupNFTables()
r.cleanupRedirectRoutes()
} else { } else {
r.cleanupIPTables() r.cleanupIPTables()
} }

View file

@ -3,6 +3,7 @@
package tun package tun
import ( import (
"errors"
"math/rand" "math/rand"
"net" "net"
@ -33,16 +34,19 @@ func (r *autoRedirect) setupRedirectRoutes() error {
if err != nil { if err != nil {
return E.Cause(err, "update interfaces") return E.Cause(err, "update interfaces")
} }
tunName := r.tunOptions.Name redirectInterfaces := r.currentRedirectInterfaces()
r.redirectInterfaces = common.Filter(r.interfaceFinder.Interfaces(), func(it control.Interface) bool { r.redirectRouteAccess.Lock()
return it.Name != "lo" && it.Name != tunName && it.Flags&net.FlagUp != 0 defer r.redirectRouteAccess.Unlock()
}) r.redirectRoutesActive = false
r.cleanupRedirectRoutes() r.cleanupRedirectRoutesLocked()
for _, iface := range r.redirectInterfaces { defer func() {
err = r.addRedirectRoutes(iface)
if err != nil { if err != nil {
return E.Cause(err, "add redirect routes for ", iface.Name) r.cleanupRedirectRoutesLocked()
} }
}()
err = r.reconcileRedirectRoutesLocked(redirectInterfaces)
if err != nil {
return err
} }
if r.enableIPv4 { if r.enableIPv4 {
rule := netlink.NewRule() rule := netlink.NewRule()
@ -64,36 +68,15 @@ func (r *autoRedirect) setupRedirectRoutes() error {
return E.Cause(err, "add ipv6 redirect rule") return E.Cause(err, "add ipv6 redirect rule")
} }
} }
r.redirectRoutesActive = true
return nil return nil
} }
func (r *autoRedirect) addRedirectRoutes(iface control.Interface) error { func (r *autoRedirect) currentRedirectInterfaces() []control.Interface {
hasIPv4Address, hasIPv6Address := redirectRouteAddressFamilies(iface) tunName := r.tunOptions.Name
if r.enableIPv4 && hasIPv4Address { return common.Filter(r.interfaceFinder.Interfaces(), func(it control.Interface) bool {
err := netlink.RouteAppend(&netlink.Route{ return it.Name != "lo" && it.Name != tunName && it.Flags&net.FlagUp != 0
LinkIndex: iface.Index,
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 E.Cause(err, "append ipv4 loopback route")
}
}
if r.enableIPv6 && hasIPv6Address {
err := netlink.RouteAppend(&netlink.Route{
LinkIndex: iface.Index,
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 E.Cause(err, "append ipv6 loopback route")
}
}
return nil
} }
func redirectRouteAddressFamilies(iface control.Interface) (hasIPv4Address bool, hasIPv6Address bool) { func redirectRouteAddressFamilies(iface control.Interface) (hasIPv4Address bool, hasIPv6Address bool) {
@ -108,68 +91,28 @@ func redirectRouteAddressFamilies(iface control.Interface) (hasIPv4Address bool,
return return
} }
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 { func (r *autoRedirect) updateRedirectRoutes() error {
err := r.interfaceFinder.Update() err := r.interfaceFinder.Update()
if err != nil { if err != nil {
return E.Cause(err, "update interfaces") return E.Cause(err, "update interfaces")
} }
tunName := r.tunOptions.Name redirectInterfaces := r.currentRedirectInterfaces()
newInterfaces := common.Filter(r.interfaceFinder.Interfaces(), func(it control.Interface) bool { r.redirectRouteAccess.Lock()
return it.Name != "lo" && it.Name != tunName && it.Flags&net.FlagUp != 0 defer r.redirectRouteAccess.Unlock()
}) if !r.redirectRoutesActive {
oldMap := make(map[int]control.Interface, len(r.redirectInterfaces))
for _, iface := range r.redirectInterfaces {
oldMap[iface.Index] = iface
}
newMap := make(map[int]bool, len(newInterfaces))
for _, iface := range newInterfaces {
newMap[iface.Index] = true
}
for _, iface := range newInterfaces {
oldInterface, loaded := oldMap[iface.Index]
if loaded {
oldHasIPv4Address, oldHasIPv6Address := redirectRouteAddressFamilies(oldInterface)
hasIPv4Address, hasIPv6Address := redirectRouteAddressFamilies(iface)
if (!r.enableIPv4 || oldHasIPv4Address == hasIPv4Address) &&
(!r.enableIPv6 || oldHasIPv6Address == hasIPv6Address) {
continue
}
r.removeRedirectRoutes(iface.Index)
}
err = r.addRedirectRoutes(iface)
if err != nil {
return E.Cause(err, "add redirect routes for ", iface.Name)
}
}
for _, iface := range r.redirectInterfaces {
if !newMap[iface.Index] {
r.removeRedirectRoutes(iface.Index)
}
}
r.redirectInterfaces = newInterfaces
return nil return nil
} }
return r.reconcileRedirectRoutesLocked(redirectInterfaces)
}
func (r *autoRedirect) cleanupRedirectRoutes() { func (r *autoRedirect) cleanupRedirectRoutes() {
r.redirectRouteAccess.Lock()
defer r.redirectRouteAccess.Unlock()
r.redirectRoutesActive = false
r.cleanupRedirectRoutesLocked()
}
func (r *autoRedirect) cleanupRedirectRoutesLocked() {
if r.redirectRouteTableIndex == 0 { if r.redirectRouteTableIndex == 0 {
return return
} }
@ -194,3 +137,108 @@ func (r *autoRedirect) cleanupRedirectRoutes() {
_ = netlink.RuleDel(rule) _ = netlink.RuleDel(rule)
} }
} }
type redirectRouteKey struct {
linkIndex int
family int
}
func (r *autoRedirect) reconcileRedirectRoutesLocked(redirectInterfaces []control.Interface) error {
// Interface snapshots are not sufficient here: network managers can flush a
// route while a fast reconnect leaves the interface index and address families
// unchanged. Reconcile against the kernel's route table on every update.
currentRoutes, err := netlink.RouteListFiltered(netlink.FAMILY_ALL,
&netlink.Route{Table: r.redirectRouteTableIndex},
netlink.RT_FILTER_TABLE)
if err != nil {
return E.Cause(err, "list redirect routes")
}
routesToAdd, routesToDelete := calculateRedirectRouteChanges(
r.redirectRouteTableIndex,
redirectInterfaces,
currentRoutes,
r.enableIPv4,
r.enableIPv6,
)
for index := range routesToDelete {
route := &routesToDelete[index]
err = netlink.RouteDel(route)
if err != nil && !errors.Is(err, unix.ESRCH) && !errors.Is(err, unix.ENOENT) {
return E.Cause(err, "delete redirect route ", route)
}
}
for index := range routesToAdd {
route := &routesToAdd[index]
err = netlink.RouteAppend(route)
if err != nil {
return E.Cause(err, "append redirect route ", route)
}
}
return nil
}
func calculateRedirectRouteChanges(
tableIndex int,
redirectInterfaces []control.Interface,
currentRoutes []netlink.Route,
enableIPv4 bool,
enableIPv6 bool,
) (routesToAdd []netlink.Route, routesToDelete []netlink.Route) {
desiredRoutes := make(map[redirectRouteKey]struct{}, len(redirectInterfaces)*2)
for _, iface := range redirectInterfaces {
hasIPv4Address, hasIPv6Address := redirectRouteAddressFamilies(iface)
if enableIPv4 && hasIPv4Address {
desiredRoutes[redirectRouteKey{linkIndex: iface.Index, family: unix.AF_INET}] = struct{}{}
}
if enableIPv6 && hasIPv6Address {
desiredRoutes[redirectRouteKey{linkIndex: iface.Index, family: unix.AF_INET6}] = struct{}{}
}
}
for _, route := range currentRoutes {
key, isRedirectRoute := redirectRouteKeyFromRoute(route)
if !isRedirectRoute {
continue
}
if _, desired := desiredRoutes[key]; desired {
delete(desiredRoutes, key)
continue
}
routesToDelete = append(routesToDelete, route)
}
for key := range desiredRoutes {
routesToAdd = append(routesToAdd, newRedirectRoute(tableIndex, key))
}
return
}
func newRedirectRoute(tableIndex int, key redirectRouteKey) netlink.Route {
destination := net.IPv6loopback
if key.family == unix.AF_INET {
destination = net.IPv4(127, 0, 0, 1)
}
return netlink.Route{
LinkIndex: key.linkIndex,
Dst: netlink.NewIPNet(destination),
Table: tableIndex,
Type: unix.RTN_LOCAL,
Scope: netlink.SCOPE_HOST,
}
}
func redirectRouteKeyFromRoute(route netlink.Route) (redirectRouteKey, bool) {
if redirectRouteDestinationMatches(route.Dst, net.IPv4(127, 0, 0, 1), 32) {
return redirectRouteKey{linkIndex: route.LinkIndex, family: unix.AF_INET}, true
}
if redirectRouteDestinationMatches(route.Dst, net.IPv6loopback, 128) {
return redirectRouteKey{linkIndex: route.LinkIndex, family: unix.AF_INET6}, true
}
return redirectRouteKey{}, false
}
func redirectRouteDestinationMatches(destination *net.IPNet, address net.IP, prefixBits int) bool {
if destination == nil || !destination.IP.Equal(address) {
return false
}
ones, bits := destination.Mask.Size()
return ones == prefixBits && bits == prefixBits
}