From 7483fec04882f348450d87f5455438dc178363db Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Mon, 13 Apr 2026 09:38:38 +0200 Subject: [PATCH] Fix Android internet blackhole caused by stale route re-injection on TUN rebuild (#5865) extraInitialRoutes() was meant to preserve only the fake IP route (240.0.0.0/8) across TUN rebuilds, but it re-injected any initial route missing from the current set. When the management server advertised exit node routes (0.0.0.0/0) that were later filtered by the route selector, extraInitialRoutes() re-added them, causing the Android VPN to capture all traffic with no peer to handle it. Store the fake IP route explicitly and append only that in notify(), removing the overly broad initial route diffing. --- client/internal/routemanager/manager.go | 1 + .../routemanager/notifier/notifier_android.go | 30 +++++++------------ .../routemanager/notifier/notifier_ios.go | 4 +++ .../routemanager/notifier/notifier_other.go | 4 +++ 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/client/internal/routemanager/manager.go b/client/internal/routemanager/manager.go index e7ca44239..3923e153b 100644 --- a/client/internal/routemanager/manager.go +++ b/client/internal/routemanager/manager.go @@ -168,6 +168,7 @@ func (m *DefaultManager) setupAndroidRoutes(config ManagerConfig) { NetworkType: route.IPv4Network, } cr = append(cr, fakeIPRoute) + m.notifier.SetFakeIPRoute(fakeIPRoute) } m.notifier.SetInitialClientRoutes(cr, routesForComparison) diff --git a/client/internal/routemanager/notifier/notifier_android.go b/client/internal/routemanager/notifier/notifier_android.go index 3d2784ae1..55e0b7421 100644 --- a/client/internal/routemanager/notifier/notifier_android.go +++ b/client/internal/routemanager/notifier/notifier_android.go @@ -16,6 +16,7 @@ import ( type Notifier struct { initialRoutes []*route.Route currentRoutes []*route.Route + fakeIPRoute *route.Route listener listener.NetworkChangeListener listenerMux sync.Mutex @@ -31,13 +32,17 @@ func (n *Notifier) SetListener(listener listener.NetworkChangeListener) { n.listener = listener } -// SetInitialClientRoutes stores the full initial route set (including fake IP blocks) -// and a separate comparison set (without fake IP blocks) for diff detection. +// SetInitialClientRoutes stores the initial route sets for TUN configuration. func (n *Notifier) SetInitialClientRoutes(initialRoutes []*route.Route, routesForComparison []*route.Route) { n.initialRoutes = filterStatic(initialRoutes) n.currentRoutes = filterStatic(routesForComparison) } +// SetFakeIPRoute stores the fake IP route to be included in every TUN rebuild. +func (n *Notifier) SetFakeIPRoute(r *route.Route) { + n.fakeIPRoute = r +} + func (n *Notifier) OnNewRoutes(idMap route.HAMap) { var newRoutes []*route.Route for _, routes := range idMap { @@ -69,7 +74,9 @@ func (n *Notifier) notify() { } allRoutes := slices.Clone(n.currentRoutes) - allRoutes = append(allRoutes, n.extraInitialRoutes()...) + if n.fakeIPRoute != nil { + allRoutes = append(allRoutes, n.fakeIPRoute) + } routeStrings := n.routesToStrings(allRoutes) sort.Strings(routeStrings) @@ -78,23 +85,6 @@ func (n *Notifier) notify() { }(n.listener) } -// extraInitialRoutes returns initialRoutes whose network prefix is absent -// from currentRoutes (e.g. the fake IP block added at setup time). -func (n *Notifier) extraInitialRoutes() []*route.Route { - currentNets := make(map[netip.Prefix]struct{}, len(n.currentRoutes)) - for _, r := range n.currentRoutes { - currentNets[r.Network] = struct{}{} - } - - var extra []*route.Route - for _, r := range n.initialRoutes { - if _, ok := currentNets[r.Network]; !ok { - extra = append(extra, r) - } - } - return extra -} - func filterStatic(routes []*route.Route) []*route.Route { out := make([]*route.Route, 0, len(routes)) for _, r := range routes { diff --git a/client/internal/routemanager/notifier/notifier_ios.go b/client/internal/routemanager/notifier/notifier_ios.go index 343d2799e..68c85067a 100644 --- a/client/internal/routemanager/notifier/notifier_ios.go +++ b/client/internal/routemanager/notifier/notifier_ios.go @@ -34,6 +34,10 @@ func (n *Notifier) SetInitialClientRoutes([]*route.Route, []*route.Route) { // iOS doesn't care about initial routes } +func (n *Notifier) SetFakeIPRoute(*route.Route) { + // Not used on iOS +} + func (n *Notifier) OnNewRoutes(route.HAMap) { // Not used on iOS } diff --git a/client/internal/routemanager/notifier/notifier_other.go b/client/internal/routemanager/notifier/notifier_other.go index 0521e3dc2..97c815cf0 100644 --- a/client/internal/routemanager/notifier/notifier_other.go +++ b/client/internal/routemanager/notifier/notifier_other.go @@ -23,6 +23,10 @@ func (n *Notifier) SetInitialClientRoutes([]*route.Route, []*route.Route) { // Not used on non-mobile platforms } +func (n *Notifier) SetFakeIPRoute(*route.Route) { + // Not used on non-mobile platforms +} + func (n *Notifier) OnNewRoutes(idMap route.HAMap) { // Not used on non-mobile platforms }