Catch-all NameServerGroup shadows fallback handler on embedded proxy and routing peers #6994
Unanswered
renne
asked this question in
Issue Triage
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
NetBird version and upgrade status
Regression details
Not a regression. This never worked — the catch-all NameServerGroup has always been incompatible with reverse-proxy backend resolution on embedded proxy peers. We kept the catch-all NameServerGroup disabled since discovering this.
Summary
A primary (catch-all) NameServerGroup at
PriorityDefault(1) permanently shadows the fallback handler atPriorityFallback(-100) on embedded proxy peers and routing peers. When the catch-all NameServerGroup is enabled, NXDOMAIN from public upstream resolvers terminates the DNS handler chain before it reaches the fallback handler (original system DNS, e.g. Docker's embedded DNS at 127.0.0.11). This prevents reverse proxy peers from resolving backend hostnames (e.g.*.internalcontainer names) that are only resolvable via the fallback, causing 502 Bad Gateway for all reverse-proxied services.Current behavior
When a catch-all NameServerGroup (primary=true, no match domains) is enabled and distributed to peers running embedded NetBird clients (reverse proxy containers) or routing peers:
PriorityDefault(1) with pattern.(root zone)/etc/resolv.conf, e.g. Docker DNS 127.0.0.11) registers atPriorityFallback(-100), also with pattern.vault.internal:PriorityDNSRoute(100) matches — the DnsInterceptor is only registered on client peers, not on embedded proxy peers or the routing peer itselfPriorityLocal(75) doesn't match —*.internalis not a custom/synthesized zonePriorityUpstream(50) doesn't match — no domain-matched NameServerGroup forinternalPriorityDefault(1) matches (catch-all, pattern.) → queries public DNS → returns NXDOMAINrm.MsgHdr.Zero = false) so NXDOMAIN is terminalPriorityFallback(-100) is never reachedEvidence from proxy logs
Both proxies use 100.106.255.254 (embedded NB DNS) as their resolver. The embedded NB client receives the catch-all NameServerGroup automatically — embedded proxy peers are not members of any user-assigned group, but the management server pushes all enabled NSGs to them regardless of group membership (by design, so they can resolve any backend hostname).
Evidence from routing peer handler chain (debug logs)
No handler at priority 100 (DnsInterceptor) exists on the routing peer. Before the catch-all is enabled,
*.internalfalls through to the fallback at -100 (Docker DNS) and resolves correctly. After the catch-all is enabled, NXDOMAIN at priority 1 terminates the chain.Test results
With catch-all NameServerGroup disabled (current production state):
With catch-all NameServerGroup enabled:
Expected behavior
A catch-all (primary) NameServerGroup should not permanently prevent the fallback handler from resolving domains that the catch-all cannot answer authoritatively. Specifically:
.localmDNS) should fall through the catch-all to the fallback handler when the catch-all returns NXDOMAINSteps to reproduce
vault.internal) that are resolvable only via Docker's embedded DNS (127.0.0.11)RoutingPeerDNSResolutionEnabledis enabled (account setting) and the routing peer's DNS forwarder is runningResult: 502 Bad Gateway — the proxy cannot resolve backend hostnames.
Result: All services work again — backends resolve via the fallback handler (Docker DNS).
Environment and topology
example.comwith wildcard*.example.com → 192.168.178.64. Synthesized private-service zones for private=1 services → proxy NB IPs.Self-hosted details
Logs, status output, or debug evidence
Routing peer handler chain (debug logs, catch-all enabled)
Note: No handler at priority 100 (DnsInterceptor). The DnsInterceptor is only registered on client peers, not on the routing peer or embedded proxy peers.
Proxy error logs (catch-all enabled)
DNS resolution comparison
Related issues or discussions
RoutingPeerDNSResolutionEnabledon for embedded proxy peers and routing peers involved in reverse-proxy services. Ensures the DNS forwarder starts, but does NOT address the catch-all shadowing the fallback — the forwarder still needs the fallback to resolve via Docker DNS.Impact
Additional context
Root cause analysis (from source code)
The DNS handler chain in
client/internal/dns/handler_chain.godispatches queries in priority order. TheshouldContinuefallthrough mechanism uses NXDOMAIN-with-Zero-bit-set as a "try next handler" signal. However, the upstream handler (client/internal/dns/upstream.go) actively clears the Zero bit:This means NXDOMAIN from a catch-all NameServerGroup is always terminal — the chain stops and the fallback handler is never reached. This is by design (to prevent external DNS servers from triggering internal fallthrough), but it has the side effect of permanently shadowing the fallback handler for all domains the catch-all doesn't know about.
The priority gap
The 99-priority gap between PriorityDefault (1) and PriorityFallback (-100) is where the catch-all sits. Any domain not matched by a higher-priority handler hits the catch-all, gets NXDOMAIN, and terminates — the fallback is permanently shadowed.
On client peers this is fine (DnsInterceptor at 100 handles routing-peer domains before the catch-all). On embedded proxy peers and routing peers there is no DnsInterceptor, so the fallback at -100 is the only resolver for backend hostnames, and the catch-all kills it.
Proposed solution
Option A: NXDOMAIN fallthrough for root-zone upstream handlers (minimal change)
When a root-zone (pattern
.) upstream handler atPriorityDefaultreturns NXDOMAIN, setshouldContinue = trueinstead of treating it as terminal. This would allow the chain to fall through to the fallback handler atPriorityFallback.Risk: This changes the semantics of NXDOMAIN for all catch-all NameServerGroups. If the catch-all is intentionally authoritative for NXDOMAIN (e.g. for DNS filtering/blocking), fallthrough would undermine that. Mitigation: only apply fallthrough when a
PriorityFallbackhandler exists for the same pattern, or make the behavior opt-in via an NameServerGroup flag (e.g. "fallthrough on NXDOMAIN").Option B: Register fallback handler above catch-all for embedded/routing peers
On embedded proxy peers and routing peers, register the fallback handler at a priority above
PriorityDefault(e.g.PriorityUpstream - 1= 49) instead ofPriorityFallback(-100). This ensures the fallback is reached before the catch-all.Risk: This changes the fallback's position in the chain for all peers, potentially affecting other use cases where the catch-all should take priority over system DNS. Mitigation: only adjust the fallback priority for peers that have no DnsInterceptor (embedded/routing peers), or make it conditional on the peer type.
Option C: Domain exclusion list for catch-all NameServerGroups
Allow catch-all NameServerGroups to specify a list of excluded domains/suffixes (e.g.
internal,.local,.svc.cluster.local). Queries for excluded domains would skip the catch-all and fall through to the fallback.Risk: Adds configuration complexity. Mitigation: This is the most flexible approach and aligns with the existing "match domains" feature — it's the inverse (exclude domains).
Option D: Register DnsInterceptor on routing peers for their own domain resources
Currently the DnsInterceptor is only registered on client peers. If it were also registered on routing peers for their own domain resources (at
PriorityDNSRoute= 100), the routing peer's DNS forwarder would intercept*.internalbefore the catch-all, breaking the loop and resolving directly.Risk: This changes the routing peer's DNS architecture. The forwarder would need to resolve via Docker DNS directly (127.0.0.11) instead of looping through the NB DNS chain. Mitigation: The forwarder already uses
net.DefaultResolverwhich reads/etc/resolv.conf; if/etc/resolv.confpointed to the original Docker DNS instead of the NB DNS, the loop would be broken.Compounding factor: the forwarder loop on the routing peer
Any solution must account for a secondary problem on the routing peer itself. NetBird rewrites the routing peer's
/etc/resolv.conffrom the original Docker DNS (127.0.0.11) to its own NB DNS address (the routing peer's NetBird IP). The DNS forwarder usesnet.DefaultResolver, which reads/etc/resolv.conf— so the forwarder queries its own NB DNS on port 53. On the routing peer, the NB DNS chain has no DnsInterceptor (priority 100) for*.internal, so the query hits the catch-all (priority 1) or falls through to the fallback (priority -100, which is the original Docker DNS). Before the catch-all is enabled, this loop works: the forwarder → NB DNS → fallback → Docker DNS → resolves. With the catch-all enabled, the loop produces NXDOMAIN: forwarder → NB DNS → catch-all → public DNS → NXDOMAIN → terminal. Solutions that only fix the priority ordering on proxy peers (Options A-C) would not fix the routing peer unless they also ensure the catch-all does not terminate the chain before the fallback is reached.All reactions