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.
This commit is contained in:
Zoltán Papp
2026-04-24 22:48:25 +02:00
parent 12d0edabc0
commit 3cdfa11cb8

View File

@@ -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