[GH-ISSUE #6387] macOS: stale public DNS answers cached after wake from sleep (split-DNS race) #12988

Open
opened 2026-08-05 02:07:11 -04:00 by saavagebueno · 2 comments
Owner

Originally created by @DeviaVir on GitHub (Jun 10, 2026).
Original GitHub issue: https://github.com/netbirdio/netbird/issues/6387

Describe the problem

On macOS, names covered by a NetBird match domain intermittently resolve to their public DNS answer for several minutes after the machine wakes from sleep, even though the NetBird client is connected and netbird status -d shows the nameserver groups as available.

Our setup: a self-hosted management plane with a nameserver group for an internal domain (blockstream.io) pointing at an overlay-routed resolver. The same names also exist in public DNS pointing at a public ingress that cannot serve them (returns 404). Linux and Windows clients are unaffected.

To Reproduce

  1. macOS client with a match-domain nameserver group whose upstream is reachable only through the overlay (e.g. behind a routing peer).
  2. The same name also resolvable in public DNS (split-horizon), TTL 300.
  3. Let the machine sleep, wake it, and immediately use the name (browser tab reload, git fetch, etc.). Repeat across several sleep/wake cycles — it doesn't happen every time.
  4. Sometimes the app gets the public answer, and keeps getting it for ~TTL minutes. dscacheutil -q host -a name <name> shows the public A record. Flushing manually (sudo killall -HUP mDNSResponder) fixes it instantly.

Expected behavior

Once the overlay upstream is reachable again, clients should resolve the internal answer; a stale public answer should not be served for its full TTL.

Are you using NetBird Cloud?

Self-hosted.

NetBird version

Observed with 0.7x desktop clients on macOS; the relevant code path is unchanged on main.

Analysis

Right after wake, the WireGuard tunnels take a few seconds to re-establish. Queries for match domains issued in that window fail at the overlay upstream and end up answered by the public resolvers; mDNSResponder caches that answer for the record's full TTL.

systemConfigurator.flushDNSCache() (host_darwin.go) already handles the config-change case — it runs from applyDNSConfig/restoreHostDNS. But on a wake where the network does not change, the engine isn't restarted, the host DNS config is not re-applied, and nothing flushes the poisoned cache. The health projection in client/internal/dns/server.go already detects exactly the relevant transition (projectHealthy after an unhealthy streak) — it just doesn't flush.

Proposed fix

Flush the OS-level DNS cache when a nameserver group transitions unhealthy → healthy, via the existing flushDNSCache (type-asserted, so a no-op on other platforms). PR attached.

A more complete alternative would be subscribing to IOKit sleep/wake notifications and flushing on wake, but the health-transition hook covers the observed failure with much less machinery.

Originally created by @DeviaVir on GitHub (Jun 10, 2026). Original GitHub issue: https://github.com/netbirdio/netbird/issues/6387 ## Describe the problem On macOS, names covered by a NetBird match domain intermittently resolve to their *public* DNS answer for several minutes after the machine wakes from sleep, even though the NetBird client is connected and `netbird status -d` shows the nameserver groups as available. Our setup: a self-hosted management plane with a nameserver group for an internal domain (`blockstream.io`) pointing at an overlay-routed resolver. The same names also exist in public DNS pointing at a public ingress that cannot serve them (returns 404). Linux and Windows clients are unaffected. ## To Reproduce 1. macOS client with a match-domain nameserver group whose upstream is reachable only through the overlay (e.g. behind a routing peer). 2. The same name also resolvable in public DNS (split-horizon), TTL 300. 3. Let the machine sleep, wake it, and immediately use the name (browser tab reload, `git fetch`, etc.). Repeat across several sleep/wake cycles — it doesn't happen every time. 4. Sometimes the app gets the public answer, and keeps getting it for ~TTL minutes. `dscacheutil -q host -a name <name>` shows the public A record. Flushing manually (`sudo killall -HUP mDNSResponder`) fixes it instantly. ## Expected behavior Once the overlay upstream is reachable again, clients should resolve the internal answer; a stale public answer should not be served for its full TTL. ## Are you using NetBird Cloud? Self-hosted. ## NetBird version Observed with 0.7x desktop clients on macOS; the relevant code path is unchanged on `main`. ## Analysis Right after wake, the WireGuard tunnels take a few seconds to re-establish. Queries for match domains issued in that window fail at the overlay upstream and end up answered by the public resolvers; mDNSResponder caches that answer for the record's full TTL. `systemConfigurator.flushDNSCache()` (host_darwin.go) already handles the *config-change* case — it runs from `applyDNSConfig`/`restoreHostDNS`. But on a wake where the network does not change, the engine isn't restarted, the host DNS config is not re-applied, and nothing flushes the poisoned cache. The health projection in `client/internal/dns/server.go` already detects exactly the relevant transition (`projectHealthy` after an unhealthy streak) — it just doesn't flush. ## Proposed fix Flush the OS-level DNS cache when a nameserver group transitions unhealthy → healthy, via the existing `flushDNSCache` (type-asserted, so a no-op on other platforms). PR attached. A more complete alternative would be subscribing to IOKit sleep/wake notifications and flushing on wake, but the health-transition hook covers the observed failure with much less machinery.
Author
Owner

@linear-code[bot] commented on GitHub (Jun 10, 2026):

NET-1257

<!-- gh-comment-id:4667857063 --> @linear-code[bot] commented on GitHub (Jun 10, 2026): <!-- linear-linkback --> <p><a href="https://linear.app/netbird/issue/NET-1257">NET-1257</a></p>
Author
Owner

@smereczynski commented on GitHub (Jun 26, 2026):

+1 Getting back from sleep is probably little bit wider issue.

<!-- gh-comment-id:4811949574 --> @smereczynski commented on GitHub (Jun 26, 2026): +1 Getting back from sleep is probably little bit wider issue.
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: DYNR/netbird#12988