Skip to content

[client] Fix iOS DNS upstream routing for deselected exit nodes#5803

Merged
mlsmaycon merged 4 commits intomainfrom
add-default-resolver
Apr 8, 2026
Merged

[client] Fix iOS DNS upstream routing for deselected exit nodes#5803
mlsmaycon merged 4 commits intomainfrom
add-default-resolver

Conversation

@mlsmaycon
Copy link
Copy Markdown
Collaborator

@mlsmaycon mlsmaycon commented Apr 6, 2026

Describe your changes

  • Add GetSelectedClientRoutes() to the route manager that filters through FilterSelectedExitNodes, returning only active routes instead of all management routes
  • Use GetSelectedClientRoutes() in the DNS route checker so deselected exit nodes' 0.0.0.0/0 no longer matches upstream DNS IPs — this prevented the resolver from switching
    away from the utun-bound socket after exit node deselection
  • Initialize iOS DNS server with host DNS fallback addresses (1.1.1.1:53, 1.0.0.1:53) and a permanent root zone handler, matching Android's behavior — without this, unmatched
    DNS queries arriving via the 0.0.0.0/0 tunnel route had no handler and were silently dropped

Test plan

  • Deselect exit node on WiFi — upstream DNS queries should use regular socket (needsPrivate=false), not utun-bound
  • Deselect exit node on cellular — root zone handler should forward unmatched queries to 1.1.1.1
  • Select exit node — DNS queries should route through the WireGuard peer (needsPrivate=true)
  • Verify GetClientRoutes() callers (TriggerSelection, addrViaRoutes) still work correctly with unfiltered routes

Issue ticket number and link

Stack

Checklist

  • Is it a bug fix
  • Is a typo/documentation fix
  • Is a feature enhancement
  • It is a refactor
  • Created tests that fail without the change (if possible)

By submitting this pull request, you confirm that you have read and agree to the terms of the Contributor License Agreement.

Documentation

Select exactly one:

  • I added/updated documentation for this change
  • Documentation is not needed for this change (explain why)

Docs PR URL (required if "docs added" is checked)

Paste the PR link from https://github.com/netbirdio/docs here:

https://github.com/netbirdio/docs/pull/__

Summary by CodeRabbit

  • Bug Fixes
    • iOS now correctly initializes and forwards host/system DNS addresses at startup.
    • Improved DNS resolution reliability and network stability on iOS clients.
    • Added default fallback DNS resolvers for iOS to reduce resolution failures during launch.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 6, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8fcd2069-e8d5-4d20-a8fe-48b43349cf1d

📥 Commits

Reviewing files that changed from the base of the PR and between 63857b1 and f08e445.

📒 Files selected for processing (1)
  • client/ios/NetBirdSDK/client.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • client/ios/NetBirdSDK/client.go

📝 Walkthrough

Walkthrough

DNS address propagation was added to the iOS startup path: ConnectClient.RunOniOS now accepts dnsAddresses and forwards them into MobileDependency.HostDNSAddresses; the iOS DNS server constructor stores and applies these host DNS addresses; the iOS client supplies two hard-coded host DNS endpoints.

Changes

Cohort / File(s) Summary
iOS RunOniOS parameter
client/internal/connect.go
Added dnsAddresses []netip.AddrPort parameter to ConnectClient.RunOniOS signature and propagate into MobileDependency.HostDNSAddresses.
iOS DNS server init
client/internal/dns/server.go
NewDefaultServerIos now accepts hostsDnsList []netip.AddrPort; logs the list, stores it via hostsDNSHolder.set(...), marks server permanent, and calls addHostRootZone().
Engine wiring
client/internal/engine.go
DNS server construction updated to pass e.mobileDep.HostDNSAddresses to dns.NewDefaultServerIos.
iOS client caller
client/ios/NetBirdSDK/client.go
Client.Run now builds hostDNS slice with 9.9.9.9:53 and 149.112.112.112:53 and passes it into c.connectClient.RunOniOS.
Minor cleanup
client/internal/routemanager/notifier/notifier_ios.go
Removed an extra blank line between methods (whitespace only).

Sequence Diagram(s)

(omitted — changes are straightforward plumbing without new multi-component sequential flow that requires visualization)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • lixmal

Poem

🐰 I hopped through code with careful paws,
Carried DNS across iOS laws,
Two resolvers tucked in my pack tight,
From client to server — routed right! 🥕✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: fixing iOS DNS upstream routing for deselected exit nodes, which aligns with the core objective of the PR.
Description check ✅ Passed The description provides comprehensive details about changes, test plan, and rationale. All key sections are filled, including the bug fix checklist item selection and documentation justification.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch add-default-resolver

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@mlsmaycon mlsmaycon changed the title [client] Add support for configuring custom DNS hosts on iOS [client] Fix iOS DNS upstream routing for deselected exit nodes Apr 6, 2026
@mlsmaycon mlsmaycon marked this pull request as ready for review April 6, 2026 20:28
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@client/internal/dns/server.go`:
- Around line 193-199: The root-zone handler is being created too early because
addHostRootZone() is called immediately after newDefaultServer(...) and thus
captures a nil s.routeMatch before SetRouteChecker(...) is injected; remove the
immediate call to addHostRootZone() here and instead invoke addHostRootZone()
from SetRouteChecker (or make addHostRootZone lazy/refreshable) so the handler
is created after the server's routeMatch is set; update references to ds (the
server returned by newDefaultServer) to ensure addHostRootZone uses the
populated routeMatch when called.

In `@client/ios/NetBirdSDK/client.go`:
- Around line 164-168: The hostDNS list for iOS only includes IPv4 resolvers
which breaks DNS on IPv6-only networks; update the hostDNS construction used
when calling c.connectClient.RunOniOS to include IPv6 fallback addresses (e.g.,
parse and append IPv6 resolver AddrPort entries like [2606:4700:4700::1111]:53
and [2606:4700:4700::1001]:53) using netip.MustParseAddrPort so both v4 and v6
upstreams are provided to c.dnsManager; ensure the variable name hostDNS and the
call c.connectClient.RunOniOS(fd, c.networkChangeListener, c.dnsManager,
hostDNS, c.stateFile) remain unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e44a6b1f-b0dd-4421-8eeb-a7105f34d38a

📥 Commits

Reviewing files that changed from the base of the PR and between decb5dd and 63857b1.

📒 Files selected for processing (5)
  • client/internal/connect.go
  • client/internal/dns/server.go
  • client/internal/engine.go
  • client/internal/routemanager/notifier/notifier_ios.go
  • client/ios/NetBirdSDK/client.go
💤 Files with no reviewable changes (1)
  • client/internal/routemanager/notifier/notifier_ios.go

Comment on lines +193 to 199
log.Debugf("iOS host dns address list is: %v", hostsDnsList)
ds := newDefaultServer(ctx, wgInterface, NewServiceViaMemory(wgInterface), statusRecorder, nil, disableSys)
ds.iosDnsManager = iosDnsManager
ds.hostsDNSHolder.set(hostsDnsList)
ds.permanent = true
ds.addHostRootZone()
return ds
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Root fallback handler is created too early for route-aware upstream routing.

At Line 198, addHostRootZone() runs before the engine injects SetRouteChecker(...). The handler snapshots s.routeMatch at creation, so it can stay nil and skip selected-route matching.

🔧 Proposed fix (outside this hunk, in SetRouteChecker)
func (s *DefaultServer) SetRouteChecker(f func(netip.Addr) bool) {
	s.mux.Lock()
	defer s.mux.Unlock()
	s.routeMatch = f
+	if s.permanent {
+		s.deregisterHandler([]string{nbdns.RootZone}, PriorityDefault)
+		s.addHostRootZone()
+	}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/internal/dns/server.go` around lines 193 - 199, The root-zone handler
is being created too early because addHostRootZone() is called immediately after
newDefaultServer(...) and thus captures a nil s.routeMatch before
SetRouteChecker(...) is injected; remove the immediate call to addHostRootZone()
here and instead invoke addHostRootZone() from SetRouteChecker (or make
addHostRootZone lazy/refreshable) so the handler is created after the server's
routeMatch is set; update references to ds (the server returned by
newDefaultServer) to ensure addHostRootZone uses the populated routeMatch when
called.

Comment on lines +164 to +168
hostDNS := []netip.AddrPort{
netip.MustParseAddrPort("1.1.1.1:53"),
netip.MustParseAddrPort("1.0.0.1:53"),
}
return c.connectClient.RunOniOS(fd, c.networkChangeListener, c.dnsManager, hostDNS, c.stateFile)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add IPv6 fallback resolvers for iOS host DNS.

Line 164 configures only IPv4 upstreams. On IPv6-only networks, fallback DNS can fail and leave unmatched queries unresolved.

🔧 Proposed fix
  hostDNS := []netip.AddrPort{
    netip.MustParseAddrPort("1.1.1.1:53"),
    netip.MustParseAddrPort("1.0.0.1:53"),
+   netip.MustParseAddrPort("[2606:4700:4700::1111]:53"),
+   netip.MustParseAddrPort("[2606:4700:4700::1001]:53"),
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/ios/NetBirdSDK/client.go` around lines 164 - 168, The hostDNS list for
iOS only includes IPv4 resolvers which breaks DNS on IPv6-only networks; update
the hostDNS construction used when calling c.connectClient.RunOniOS to include
IPv6 fallback addresses (e.g., parse and append IPv6 resolver AddrPort entries
like [2606:4700:4700::1111]:53 and [2606:4700:4700::1001]:53) using
netip.MustParseAddrPort so both v4 and v6 upstreams are provided to
c.dnsManager; ensure the variable name hostDNS and the call
c.connectClient.RunOniOS(fd, c.networkChangeListener, c.dnsManager, hostDNS,
c.stateFile) remain unchanged.

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Apr 7, 2026

@mlsmaycon mlsmaycon merged commit e2c2f64 into main Apr 8, 2026
42 checks passed
@mlsmaycon mlsmaycon deleted the add-default-resolver branch April 8, 2026 06:43
@lixmal lixmal mentioned this pull request Apr 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants