From 2b5df8866244f056c0e1ce6b8f11936bb03ce024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Papp?= Date: Thu, 30 Jul 2026 13:58:46 +0200 Subject: [PATCH] [client] Pull fresh TUN settings on Android rebuild instead of pushing state The Android TUN rebuild consumed state pushed through notifications and a Java-side snapshot, and both sources were unreliable. The DNS search-domain notifier fired OnNetworkChanged with an empty string, which the rebuild handler treated as the new route list, so any search domain change rebuilt the TUN with zero routes and cut all tunnel traffic. The rebuild also reused the search domains cached at the last establish, so search domain updates never reached the TUN at runtime. Make the notification a pure trigger and let the Java side pull a fresh snapshot instead. Expose GetTunSettings on the Android SDK client: it returns the current TUN route ranges, derived on demand by the route manager from the client routes, the exit-node selection and the fake IP blocks, together with the DNS search domains. The route notifier keeps only its last-announced baseline to suppress triggers for unchanged syncs; the TUN route state is owned by the route manager. SearchDomains now locks the DNS server mutex since the pull arrives from a Java thread. Requires the matching android-client change that switches recreateTUN to the pull API. --- client/android/client.go | 24 +++++++ client/internal/dns/server.go | 10 ++- client/internal/engine_tunsettings.go | 20 ++++++ client/internal/routemanager/manager.go | 32 ++++++++- client/internal/routemanager/mock.go | 5 ++ .../routemanager/notifier/notifier_android.go | 69 ++++++++++--------- .../routemanager/notifier/notifier_ios.go | 2 +- .../routemanager/notifier/notifier_other.go | 2 +- 8 files changed, 125 insertions(+), 39 deletions(-) create mode 100644 client/internal/engine_tunsettings.go diff --git a/client/android/client.go b/client/android/client.go index 1a8dd7d09c4..524685437e6 100644 --- a/client/android/client.go +++ b/client/android/client.go @@ -57,6 +57,12 @@ type DnsReadyListener interface { dns.ReadyListener } +// TunSettings is a snapshot of the settings the TUN device is rebuilt with +type TunSettings struct { + Routes string + SearchDomains string +} + func init() { formatter.SetLogcatFormatter(log.StandardLogger()) } @@ -240,6 +246,24 @@ func (c *Client) RenewTun(fd int) error { return e.RenewTun(fd) } +func (c *Client) GetTunSettings() (*TunSettings, error) { + cc := c.getConnectClient() + if cc == nil { + return nil, fmt.Errorf("engine not running") + } + + e := cc.Engine() + if e == nil { + return nil, fmt.Errorf("engine not initialized") + } + + routes, searchDomains := e.TunSettings() + return &TunSettings{ + Routes: strings.Join(routes, ";"), + SearchDomains: strings.Join(searchDomains, ";"), + }, nil +} + // DebugBundle generates a debug bundle, uploads it, and returns the upload key. // It works both with and without a running engine. func (c *Client) DebugBundle(platformFiles PlatformFiles, anonymize bool) (string, error) { diff --git a/client/internal/dns/server.go b/client/internal/dns/server.go index f794544572b..3af91279284 100644 --- a/client/internal/dns/server.go +++ b/client/internal/dns/server.go @@ -252,7 +252,7 @@ func NewDefaultServerPermanentUpstream( ds.hostsDNSHolder.set(hostsDnsList) ds.permanent = true ds.currentConfig = dnsConfigToHostDNSConfig(config, ds.service.RuntimeIP(), ds.service.RuntimePort()) - ds.searchDomainNotifier = newNotifier(ds.SearchDomains()) + ds.searchDomainNotifier = newNotifier(ds.searchDomains()) ds.searchDomainNotifier.setListener(listener) setServerDns(ds) return ds @@ -602,6 +602,12 @@ func (s *DefaultServer) UpdateDNSServer(serial uint64, update nbdns.Config) erro } func (s *DefaultServer) SearchDomains() []string { + s.mux.Lock() + defer s.mux.Unlock() + return s.searchDomains() +} + +func (s *DefaultServer) searchDomains() []string { var searchDomains []string for _, dConf := range s.currentConfig.Domains { @@ -686,7 +692,7 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error { }() if s.searchDomainNotifier != nil { - s.searchDomainNotifier.onNewSearchDomains(s.SearchDomains()) + s.searchDomainNotifier.onNewSearchDomains(s.searchDomains()) } s.updateNSGroupStates(update.NameServerGroups) diff --git a/client/internal/engine_tunsettings.go b/client/internal/engine_tunsettings.go new file mode 100644 index 00000000000..34a59671a33 --- /dev/null +++ b/client/internal/engine_tunsettings.go @@ -0,0 +1,20 @@ +package internal + +func (e *Engine) TunSettings() ([]string, []string) { + e.syncMsgMux.Lock() + routeManager := e.routeManager + dnsServer := e.dnsServer + e.syncMsgMux.Unlock() + + var routes []string + if routeManager != nil { + routes = routeManager.CurrentRouteRange() + } + + var searchDomains []string + if dnsServer != nil { + searchDomains = dnsServer.SearchDomains() + } + + return routes, searchDomains +} diff --git a/client/internal/routemanager/manager.go b/client/internal/routemanager/manager.go index 0cb74fd456b..eb48d97cf1e 100644 --- a/client/internal/routemanager/manager.go +++ b/client/internal/routemanager/manager.go @@ -9,6 +9,7 @@ import ( "net/url" "runtime" "slices" + "sort" "strings" "sync" "sync/atomic" @@ -63,6 +64,7 @@ type Manager interface { GetClientRoutesWithNetID() map[route.NetID][]*route.Route SetRouteChangeListener(listener listener.NetworkChangeListener) InitialRouteRange() []string + CurrentRouteRange() []string SetFirewall(firewall.Manager) error SetDNSForwarderPort(port uint16) ReconcilePeerAllowedIPs(peerKey string) error @@ -191,7 +193,7 @@ func (m *DefaultManager) enableFakeIPRoutes() []*route.Route { NetworkType: route.IPv6Network, } fakeRoutes := []*route.Route{fakeIPRoute, fakeIPv6Route} - m.notifier.SetFakeIPRoutes(fakeRoutes) + m.notifier.NotifyRouteChange() return fakeRoutes } @@ -513,6 +515,34 @@ func (m *DefaultManager) InitialRouteRange() []string { return m.notifier.GetInitialRouteRanges() } +// CurrentRouteRange returns the current TUN route list. It is used by mobile systems +func (m *DefaultManager) CurrentRouteRange() []string { + m.mux.Lock() + defer m.mux.Unlock() + + if m.disableClientRoutes { + return nil + } + + filtered := m.routeSelector.FilterSelectedExitNodes(m.clientRoutes) + var nets []string + for _, routes := range filtered { + for _, r := range routes { + if r.IsDynamic() { + continue + } + nets = append(nets, r.NetString()) + } + } + + if m.fakeIPManager != nil { + nets = append(nets, m.fakeIPManager.GetFakeIPBlock().String(), m.fakeIPManager.GetFakeIPv6Block().String()) + } + + sort.Strings(nets) + return nets +} + // GetRouteSelector returns the route selector func (m *DefaultManager) GetRouteSelector() *routeselector.RouteSelector { return m.routeSelector diff --git a/client/internal/routemanager/mock.go b/client/internal/routemanager/mock.go index cf761091d2a..28f97fc0a7d 100644 --- a/client/internal/routemanager/mock.go +++ b/client/internal/routemanager/mock.go @@ -35,6 +35,11 @@ func (m *MockManager) InitialRouteRange() []string { return nil } +// CurrentRouteRange mock implementation of CurrentRouteRange from Manager interface +func (m *MockManager) CurrentRouteRange() []string { + return nil +} + // UpdateRoutes mock implementation of UpdateRoutes from Manager interface func (m *MockManager) UpdateRoutes(updateSerial uint64, newRoutes map[route.ID]*route.Route, clientRoutes route.HAMap, useNewDNSRoute bool) error { if m.UpdateRoutesFunc != nil { diff --git a/client/internal/routemanager/notifier/notifier_android.go b/client/internal/routemanager/notifier/notifier_android.go index 60e1d0a0ff3..c9db0a4a01e 100644 --- a/client/internal/routemanager/notifier/notifier_android.go +++ b/client/internal/routemanager/notifier/notifier_android.go @@ -6,7 +6,6 @@ import ( "net/netip" "slices" "sort" - "strings" "sync" "github.com/netbirdio/netbird/client/internal/listener" @@ -14,12 +13,16 @@ import ( ) type Notifier struct { + mu sync.Mutex + initialRoutes []*route.Route + // currentRoutes is the last announced route set. It exists only to + // suppress noise: without it every network map sync would trigger the + // Java side, even when the routes did not change. The actual TUN route + // state is owned by the route manager and pulled from there. currentRoutes []*route.Route - fakeIPRoutes []*route.Route - listener listener.NetworkChangeListener - listenerMux sync.Mutex + listener listener.NetworkChangeListener } func NewNotifier() *Notifier { @@ -27,21 +30,23 @@ func NewNotifier() *Notifier { } func (n *Notifier) SetListener(listener listener.NetworkChangeListener) { - n.listenerMux.Lock() - defer n.listenerMux.Unlock() + n.mu.Lock() + defer n.mu.Unlock() n.listener = listener } // SetInitialClientRoutes stores the initial route sets for TUN configuration. func (n *Notifier) SetInitialClientRoutes(initialRoutes []*route.Route, routesForComparison []*route.Route) { + n.mu.Lock() + defer n.mu.Unlock() n.initialRoutes = filterStatic(initialRoutes) n.currentRoutes = filterStatic(routesForComparison) } -// SetFakeIPRoutes stores the fake IP routes to be included in every TUN rebuild. -func (n *Notifier) SetFakeIPRoutes(routes []*route.Route) { - n.fakeIPRoutes = routes - n.notify() +func (n *Notifier) NotifyRouteChange() { + n.mu.Lock() + defer n.mu.Unlock() + n.notifyLocked() } func (n *Notifier) OnNewRoutes(idMap route.HAMap) { @@ -55,31 +60,37 @@ func (n *Notifier) OnNewRoutes(idMap route.HAMap) { } } - if !n.hasRouteDiff(n.currentRoutes, newRoutes) { + n.mu.Lock() + defer n.mu.Unlock() + if !hasRouteDiff(n.currentRoutes, newRoutes) { return } n.currentRoutes = newRoutes - n.notify() + n.notifyLocked() } func (n *Notifier) OnNewPrefixes([]netip.Prefix) { // Not used on Android } -func (n *Notifier) notify() { - n.listenerMux.Lock() - defer n.listenerMux.Unlock() +func (n *Notifier) notifyLocked() { if n.listener == nil { return } + n.listener.OnNetworkChanged("") +} - allRoutes := slices.Clone(n.currentRoutes) - allRoutes = append(allRoutes, n.fakeIPRoutes...) +func (n *Notifier) GetInitialRouteRanges() []string { + n.mu.Lock() + defer n.mu.Unlock() + initialStrings := routesToStrings(n.initialRoutes) + sort.Strings(initialStrings) + return initialStrings +} - routeStrings := n.routesToStrings(allRoutes) - sort.Strings(routeStrings) - n.listener.OnNetworkChanged(strings.Join(routeStrings, ",")) +func (n *Notifier) Close() { + // unused } func filterStatic(routes []*route.Route) []*route.Route { @@ -92,7 +103,7 @@ func filterStatic(routes []*route.Route) []*route.Route { return out } -func (n *Notifier) routesToStrings(routes []*route.Route) []string { +func routesToStrings(routes []*route.Route) []string { nets := make([]string, 0, len(routes)) for _, r := range routes { nets = append(nets, r.NetString()) @@ -100,20 +111,10 @@ func (n *Notifier) routesToStrings(routes []*route.Route) []string { return nets } -func (n *Notifier) hasRouteDiff(a []*route.Route, b []*route.Route) bool { - as := n.routesToStrings(a) - bs := n.routesToStrings(b) +func hasRouteDiff(a []*route.Route, b []*route.Route) bool { + as := routesToStrings(a) + bs := routesToStrings(b) sort.Strings(as) sort.Strings(bs) return !slices.Equal(as, bs) } - -func (n *Notifier) GetInitialRouteRanges() []string { - initialStrings := n.routesToStrings(n.initialRoutes) - sort.Strings(initialStrings) - return initialStrings -} - -func (n *Notifier) Close() { - // unused -} diff --git a/client/internal/routemanager/notifier/notifier_ios.go b/client/internal/routemanager/notifier/notifier_ios.go index c91a7655193..8d41dce64ee 100644 --- a/client/internal/routemanager/notifier/notifier_ios.go +++ b/client/internal/routemanager/notifier/notifier_ios.go @@ -33,7 +33,7 @@ func (n *Notifier) SetInitialClientRoutes([]*route.Route, []*route.Route) { // iOS doesn't care about initial routes } -func (n *Notifier) SetFakeIPRoutes([]*route.Route) { +func (n *Notifier) NotifyRouteChange() { // Not used on iOS } diff --git a/client/internal/routemanager/notifier/notifier_other.go b/client/internal/routemanager/notifier/notifier_other.go index 71b1096c229..5730eb65a24 100644 --- a/client/internal/routemanager/notifier/notifier_other.go +++ b/client/internal/routemanager/notifier/notifier_other.go @@ -23,7 +23,7 @@ func (n *Notifier) SetInitialClientRoutes([]*route.Route, []*route.Route) { // Not used on non-mobile platforms } -func (n *Notifier) SetFakeIPRoutes([]*route.Route) { +func (n *Notifier) NotifyRouteChange() { // Not used on non-mobile platforms }