mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-27 01:51:30 +02:00
feat(dns): derive the mesh DNS apex for zone-based endpoints
The zone a private service's synthesized A record hangs under was derived from the serving proxy's address or from a validated custom domain. A placement-free endpoint matches neither, so the apex came out empty, the service was skipped, and the tenant's hostname resolved to nothing -- with no error logged. Synthesized services now carry their zone explicitly and it is preferred when deriving the apex. The field is in-memory only: these services are built per read and never persisted, and the zone cannot be supplied as a parameter instead because it is captured per account at allocation time, so a single current-config value would misclassify any tenant allocated under a previous one. A blanket "use the parent of the hostname" fallback was rejected: the same empty apex also occurs for a service whose domain has no validated entry for its cluster, and those resolve to nothing deliberately, so a blanket fallback would turn domain validation into a no-op.
This commit is contained in:
@@ -272,6 +272,12 @@ func (a *Account) SynthesizePrivateServiceZones(peerID string) []nbdns.CustomZon
|
||||
|
||||
serviceDomainZone := a.privateServiceDomainZone(svc)
|
||||
if serviceDomainZone == "" {
|
||||
// This service passed every gate above (enabled, private,
|
||||
// AccessGroups, connected proxy peers) and would otherwise have
|
||||
// emitted a record, but its domain matches neither its DNSZone,
|
||||
// its ProxyCluster, nor any validated custom-domain row.
|
||||
log.Debugf("private-zone synth: svc %s domain=%s cluster=%s dns_zone=%q has no matching zone apex, skipping",
|
||||
svc.ID, svc.Domain, svc.ProxyCluster, svc.DNSZone)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -344,8 +350,18 @@ func (a *Account) SynthesizePrivateServiceZones(peerID string) []nbdns.CustomZon
|
||||
}
|
||||
|
||||
// privateServiceDomainZone returns the DNS zone name for the given private service domain by
|
||||
// looking at the proxy cluster domain then the custom domains.
|
||||
// checking its DNSZone, then the proxy cluster domain, then the custom domains.
|
||||
func (a *Account) privateServiceDomainZone(svc *service.Service) string {
|
||||
// Placement-free endpoints (<subdomain>.<zone>) carry their zone
|
||||
// explicitly: it is server config, so it matches neither the serving
|
||||
// proxy's address nor any per-account custom-domain row. Checked first so
|
||||
// the apex stays the zone even once ProxyCluster becomes the tenant
|
||||
// hostname itself (a private managed proxy), which would otherwise make the
|
||||
// apex the full hostname and churn the client's zone set on cutover.
|
||||
if svc.DNSZone != "" && domainFromSuffix(svc.Domain, svc.DNSZone) {
|
||||
return svc.DNSZone
|
||||
}
|
||||
|
||||
if domainFromSuffix(svc.Domain, svc.ProxyCluster) {
|
||||
return svc.ProxyCluster
|
||||
}
|
||||
|
||||
@@ -423,6 +423,39 @@ func TestSynthesizePrivateServiceZones_MixedClusterCustomAndPublic(t *testing.T)
|
||||
"only the 4 private custom services surface in the custom zone (public one excluded)")
|
||||
}
|
||||
|
||||
// TestSynthesizePrivateServiceZones_ZoneBasedEndpoint_UsesZoneApex — a
|
||||
// zone-based tenant still served by the SHARED proxy has a hostname whose
|
||||
// parent is the zone, matching neither ProxyCluster nor any validated
|
||||
// custom-domain row. Without DNSZone the apex resolves to "" and the service is
|
||||
// skipped entirely, so the tenant's endpoint resolves to nothing.
|
||||
func TestSynthesizePrivateServiceZones_ZoneBasedEndpoint_UsesZoneApex(t *testing.T) {
|
||||
account := privateZoneTestAccount(t)
|
||||
svc := account.Services[0]
|
||||
svc.Domain = "brave-otter.gateway.netbird.ai"
|
||||
svc.DNSZone = "gateway.netbird.ai"
|
||||
// ProxyCluster stays the shared cluster address — the pre-private cohort.
|
||||
|
||||
zones := account.SynthesizePrivateServiceZones("user-peer")
|
||||
require.Len(t, zones, 1, "a zone-based endpoint must still produce one zone")
|
||||
assert.Equal(t, "gateway.netbird.ai.", zones[0].Domain, "apex must be the placement-free zone, not the cluster")
|
||||
require.Len(t, zones[0].Records, 1)
|
||||
assert.Equal(t, "brave-otter.gateway.netbird.ai.", zones[0].Records[0].Name)
|
||||
assert.Equal(t, "100.64.0.99", zones[0].Records[0].RData, "still points at the serving proxy peer")
|
||||
}
|
||||
|
||||
// TestSynthesizePrivateServiceZones_UnvalidatedDomain_StillSkipped locks the
|
||||
// scope of the fix: a service matching no cluster suffix, no validated custom
|
||||
// domain, AND carrying no DNSZone must keep resolving to nothing. A blanket
|
||||
// "use the parent domain" fallback would hand it mesh DNS and bypass domain
|
||||
// validation.
|
||||
func TestSynthesizePrivateServiceZones_UnvalidatedDomain_StillSkipped(t *testing.T) {
|
||||
account := privateZoneTestAccount(t)
|
||||
account.Services[0].Domain = "api.unvalidated.example.com"
|
||||
|
||||
zones := account.SynthesizePrivateServiceZones("user-peer")
|
||||
assert.Empty(t, zones, "no cluster suffix, no validated Domains row, no DNSZone → no records")
|
||||
}
|
||||
|
||||
// recordNames returns the record names of a zone for order-independent assertions.
|
||||
func recordNames(zone nbdns.CustomZone) []string {
|
||||
names := make([]string, 0, len(zone.Records))
|
||||
|
||||
Reference in New Issue
Block a user