From 145d82f322e9dfddf2fca981fea3c06b6a3bfeff Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Thu, 26 Mar 2026 18:11:05 +0800 Subject: [PATCH] [client] Replace iOS DNS IsPrivate heuristic with route manager check (#5694) --- client/internal/dns/mock_server.go | 5 +++++ client/internal/dns/server.go | 13 +++++++++++++ client/internal/dns/upstream.go | 1 + client/internal/dns/upstream_ios.go | 8 +++++--- client/internal/engine.go | 11 +++++++++++ 5 files changed, 35 insertions(+), 3 deletions(-) diff --git a/client/internal/dns/mock_server.go b/client/internal/dns/mock_server.go index fe160e20a..1df57d1db 100644 --- a/client/internal/dns/mock_server.go +++ b/client/internal/dns/mock_server.go @@ -85,6 +85,11 @@ func (m *MockServer) PopulateManagementDomain(mgmtURL *url.URL) error { return nil } +// SetRouteChecker mock implementation of SetRouteChecker from Server interface +func (m *MockServer) SetRouteChecker(func(netip.Addr) bool) { + // Mock implementation - no-op +} + // BeginBatch mock implementation of BeginBatch from Server interface func (m *MockServer) BeginBatch() { // Mock implementation - no-op diff --git a/client/internal/dns/server.go b/client/internal/dns/server.go index 6ca4f7957..3c47f4ee6 100644 --- a/client/internal/dns/server.go +++ b/client/internal/dns/server.go @@ -57,6 +57,7 @@ type Server interface { ProbeAvailability() UpdateServerConfig(domains dnsconfig.ServerDomains) error PopulateManagementDomain(mgmtURL *url.URL) error + SetRouteChecker(func(netip.Addr) bool) } type nsGroupsByDomain struct { @@ -104,6 +105,7 @@ type DefaultServer struct { statusRecorder *peer.Status stateManager *statemanager.Manager + routeMatch func(netip.Addr) bool probeMu sync.Mutex probeCancel context.CancelFunc @@ -229,6 +231,14 @@ func newDefaultServer( return defaultServer } +// SetRouteChecker sets the function used by upstream resolvers to determine +// whether an IP is routed through the tunnel. +func (s *DefaultServer) SetRouteChecker(f func(netip.Addr) bool) { + s.mux.Lock() + defer s.mux.Unlock() + s.routeMatch = f +} + // RegisterHandler registers a handler for the given domains with the given priority. // Any previously registered handler for the same domain and priority will be replaced. func (s *DefaultServer) RegisterHandler(domains domain.List, handler dns.Handler, priority int) { @@ -743,6 +753,7 @@ func (s *DefaultServer) registerFallback(config HostDNSConfig) { log.Errorf("failed to create upstream resolver for original nameservers: %v", err) return } + handler.routeMatch = s.routeMatch for _, ns := range originalNameservers { if ns == config.ServerIP { @@ -852,6 +863,7 @@ func (s *DefaultServer) createHandlersForDomainGroup(domainGroup nsGroupsByDomai if err != nil { return nil, fmt.Errorf("create upstream resolver: %v", err) } + handler.routeMatch = s.routeMatch for _, ns := range nsGroup.NameServers { if ns.NSType != nbdns.UDPNameServerType { @@ -1036,6 +1048,7 @@ func (s *DefaultServer) addHostRootZone() { log.Errorf("unable to create a new upstream resolver, error: %v", err) return } + handler.routeMatch = s.routeMatch handler.upstreamServers = maps.Keys(hostDNSServers) handler.deactivate = func(error) {} diff --git a/client/internal/dns/upstream.go b/client/internal/dns/upstream.go index 18128a942..5b8135132 100644 --- a/client/internal/dns/upstream.go +++ b/client/internal/dns/upstream.go @@ -70,6 +70,7 @@ type upstreamResolverBase struct { deactivate func(error) reactivate func() statusRecorder *peer.Status + routeMatch func(netip.Addr) bool } type upstreamFailure struct { diff --git a/client/internal/dns/upstream_ios.go b/client/internal/dns/upstream_ios.go index 4d053a5a1..02c11173b 100644 --- a/client/internal/dns/upstream_ios.go +++ b/client/internal/dns/upstream_ios.go @@ -65,11 +65,13 @@ func (u *upstreamResolverIOS) exchange(ctx context.Context, upstream string, r * } else { upstreamIP = upstreamIP.Unmap() } - if u.lNet.Contains(upstreamIP) || upstreamIP.IsPrivate() { - log.Debugf("using private client to query upstream: %s", upstream) + needsPrivate := u.lNet.Contains(upstreamIP) || + (u.routeMatch != nil && u.routeMatch(upstreamIP)) + if needsPrivate { + log.Debugf("using private client to query %s via upstream %s", r.Question[0].Name, upstream) client, err = GetClientPrivate(u.lIP, u.interfaceName, timeout) if err != nil { - return nil, 0, fmt.Errorf("error while creating private client: %s", err) + return nil, 0, fmt.Errorf("create private client: %s", err) } } diff --git a/client/internal/engine.go b/client/internal/engine.go index ea1d3bec9..7b100bd0c 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -499,6 +499,17 @@ func (e *Engine) Start(netbirdConfig *mgmProto.NetbirdConfig, mgmtURL *url.URL) e.routeManager.SetRouteChangeListener(e.mobileDep.NetworkChangeListener) + e.dnsServer.SetRouteChecker(func(ip netip.Addr) bool { + for _, routes := range e.routeManager.GetClientRoutes() { + for _, r := range routes { + if r.Network.Contains(ip) { + return true + } + } + } + return false + }) + if err = e.wgInterfaceCreate(); err != nil { log.Errorf("failed creating tunnel interface %s: [%s]", e.config.WgIfaceName, err.Error()) e.close()