From 3cdfa11cb83565567163e03074647c852e84d42c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Papp?= Date: Fri, 24 Apr 2026 22:48:25 +0200 Subject: [PATCH] client/dns/mgmt: strip wildcard prefix in pool-root membership check isUnderPoolRoot lowercased and trimmed the trailing dot, but did not strip a leading "*." wildcard the way server.toZone does via nbdns.NormalizeZone. If a mgmt-advertised Relay URL ever comes through as "*.relay.netbird.io", the handler-chain registration side strips the wildcard (toZone) but the membership check here would keep it, so HasSuffix(".*.relay.netbird.io") would never match legitimate instance subdomains and on-demand resolves would not fire. Today the extractor lowercases + IDNA-normalizes URLs and rejects the wildcard form, so the divergence is latent. Close it anyway by running both sides of the membership check through a shared canonicalizePoolDomain helper that mirrors toZone's transformation (modulo trailing-dot orientation, which is self-consistent within this function). toZone itself lives in the parent dns package and cannot be imported here without a cycle. --- client/internal/dns/mgmt/mgmt.go | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/client/internal/dns/mgmt/mgmt.go b/client/internal/dns/mgmt/mgmt.go index 34d158c51..ee40720c1 100644 --- a/client/internal/dns/mgmt/mgmt.go +++ b/client/internal/dns/mgmt/mgmt.go @@ -202,15 +202,24 @@ func (m *Resolver) MatchSubdomains() bool { // e.g. "streamline-de-fra1-0.relay.netbird.io." is under "relay.netbird.io". // The pool-root itself is not considered a subdomain (it matches the exact // cache entry populated by AddDomain instead). +// +// Canonicalization mirrors server.toZone — lowercase, strip trailing dot, +// and strip a leading "*." wildcard (via canonicalizePoolDomain) — so the +// membership check is consistent with the handler-chain registration that +// runs the same set through toZone. toZone itself lives in the parent dns +// package and cannot be imported from here without a cycle. func (m *Resolver) isUnderPoolRoot(fqdn string) bool { m.mutex.RLock() defer m.mutex.RUnlock() if m.serverDomains == nil { return false } - fqdn = strings.ToLower(strings.TrimSuffix(fqdn, ".")) + fqdn = canonicalizePoolDomain(fqdn) + if fqdn == "" { + return false + } for _, root := range m.serverDomains.Relay { - r := strings.ToLower(strings.TrimSuffix(root.PunycodeString(), ".")) + r := canonicalizePoolDomain(root.PunycodeString()) if r == "" || fqdn == r { continue } @@ -221,6 +230,17 @@ func (m *Resolver) isUnderPoolRoot(fqdn string) bool { return false } +// canonicalizePoolDomain normalizes a domain for pool-root membership +// comparison: lowercase, trailing dot stripped, leading "*." wildcard +// stripped. Matches the transformation server.toZone applies on the +// handler-registration side (modulo trailing-dot orientation, which is +// self-consistent within this file). +func canonicalizePoolDomain(s string) string { + s = strings.ToLower(strings.TrimSuffix(s, ".")) + s = strings.TrimPrefix(s, "*.") + return s +} + // resolveOnDemand resolves an uncached pool-root subdomain (e.g. a relay // instance FQDN) through the bypass resolver path, caches the result, and // writes it back to w. Falls through to the next handler on error so the