From 3a09076491df700df90456a1ba2c09f68c5f16fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 14 Jul 2026 10:20:18 +0800 Subject: [PATCH] Fix redirect routes --- monitor_linux.go | 52 +++++++--- redirect_linux.go | 6 +- redirect_route_linux.go | 224 ++++++++++++++++++++++++---------------- 3 files changed, 176 insertions(+), 106 deletions(-) diff --git a/monitor_linux.go b/monitor_linux.go index 4725c16..784ec35 100644 --- a/monitor_linux.go +++ b/monitor_linux.go @@ -15,9 +15,10 @@ import ( ) type networkUpdateMonitor struct { - routeUpdate chan netlink.RouteUpdate - linkUpdate chan netlink.LinkUpdate - close chan struct{} + routeUpdate chan netlink.RouteUpdate + linkUpdate chan netlink.LinkUpdate + addressUpdate chan netlink.AddrUpdate + close chan struct{} access sync.Mutex callbacks list.List[NetworkUpdateCallback] @@ -32,10 +33,11 @@ var ErrNetlinkBanned = E.New( func NewNetworkUpdateMonitor(logger logger.Logger) (NetworkUpdateMonitor, error) { monitor := &networkUpdateMonitor{ - routeUpdate: make(chan netlink.RouteUpdate, 2), - linkUpdate: make(chan netlink.LinkUpdate, 2), - close: make(chan struct{}), - logger: logger, + routeUpdate: make(chan netlink.RouteUpdate, 2), + linkUpdate: make(chan netlink.LinkUpdate, 2), + addressUpdate: make(chan netlink.AddrUpdate, 2), + close: make(chan struct{}), + logger: logger, } // check is netlink banned by google if runtime.GOOS == "android" { @@ -63,28 +65,46 @@ func (m *networkUpdateMonitor) Start() error { if err != nil { 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 } -func (m *networkUpdateMonitor) loopUpdate() { - const minDuration = time.Second +func (m *networkUpdateMonitor) loopUpdate(minDuration time.Duration) { timer := time.NewTimer(minDuration) + timer.Stop() defer timer.Stop() + var ( + timerC <-chan time.Time + pending bool + ) for { select { case <-m.close: return case <-m.routeUpdate: 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() - select { - case <-m.close: - return - case <-timer.C: - timer.Reset(minDuration) - } + timer.Reset(minDuration) + timerC = timer.C } } diff --git a/redirect_linux.go b/redirect_linux.go index e9c892c..97758d3 100644 --- a/redirect_linux.go +++ b/redirect_linux.go @@ -6,6 +6,7 @@ import ( "os" "os/exec" "runtime" + "sync" "github.com/sagernet/nftables" "github.com/sagernet/sing/common" @@ -43,7 +44,8 @@ type autoRedirect struct { nfqueueHandler *nfqueueHandler nfqueueEnabled bool redirectRouteTableIndex int - redirectInterfaces []control.Interface + redirectRouteAccess sync.Mutex + redirectRoutesActive bool } func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) { @@ -179,8 +181,8 @@ func (r *autoRedirect) Close() error { r.nfqueueHandler.Close() } if r.useNFTables { - r.cleanupRedirectRoutes() r.cleanupNFTables() + r.cleanupRedirectRoutes() } else { r.cleanupIPTables() } diff --git a/redirect_route_linux.go b/redirect_route_linux.go index e51ed4b..f73df11 100644 --- a/redirect_route_linux.go +++ b/redirect_route_linux.go @@ -3,6 +3,7 @@ package tun import ( + "errors" "math/rand" "net" @@ -33,16 +34,19 @@ func (r *autoRedirect) setupRedirectRoutes() error { if err != nil { return E.Cause(err, "update interfaces") } - 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) + redirectInterfaces := r.currentRedirectInterfaces() + r.redirectRouteAccess.Lock() + defer r.redirectRouteAccess.Unlock() + r.redirectRoutesActive = false + r.cleanupRedirectRoutesLocked() + defer func() { 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 { rule := netlink.NewRule() @@ -64,36 +68,15 @@ func (r *autoRedirect) setupRedirectRoutes() error { return E.Cause(err, "add ipv6 redirect rule") } } + r.redirectRoutesActive = true return nil } -func (r *autoRedirect) addRedirectRoutes(iface control.Interface) error { - hasIPv4Address, hasIPv6Address := redirectRouteAddressFamilies(iface) - if r.enableIPv4 && hasIPv4Address { - err := netlink.RouteAppend(&netlink.Route{ - 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 (r *autoRedirect) currentRedirectInterfaces() []control.Interface { + tunName := r.tunOptions.Name + return common.Filter(r.interfaceFinder.Interfaces(), func(it control.Interface) bool { + return it.Name != "lo" && it.Name != tunName && it.Flags&net.FlagUp != 0 + }) } func redirectRouteAddressFamilies(iface control.Interface) (hasIPv4Address bool, hasIPv6Address bool) { @@ -108,68 +91,28 @@ func redirectRouteAddressFamilies(iface control.Interface) (hasIPv4Address bool, 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 { err := r.interfaceFinder.Update() if err != nil { return E.Cause(err, "update interfaces") } - 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]control.Interface, len(r.redirectInterfaces)) - for _, iface := range r.redirectInterfaces { - oldMap[iface.Index] = iface + redirectInterfaces := r.currentRedirectInterfaces() + r.redirectRouteAccess.Lock() + defer r.redirectRouteAccess.Unlock() + if !r.redirectRoutesActive { + return nil } - 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 r.reconcileRedirectRoutesLocked(redirectInterfaces) } func (r *autoRedirect) cleanupRedirectRoutes() { + r.redirectRouteAccess.Lock() + defer r.redirectRouteAccess.Unlock() + r.redirectRoutesActive = false + r.cleanupRedirectRoutesLocked() +} + +func (r *autoRedirect) cleanupRedirectRoutesLocked() { if r.redirectRouteTableIndex == 0 { return } @@ -194,3 +137,108 @@ func (r *autoRedirect) cleanupRedirectRoutes() { _ = 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 +}