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:
Brad Ison
2026-08-03 18:35:21 +02:00
parent 58d0793870
commit 91dd9fa239
5 changed files with 70 additions and 1 deletions

View File

@@ -255,6 +255,13 @@ type Service struct {
Private bool
// AccessGroups is the group ID allowlist for inbound peers on private services. Mutually exclusive with bearer SSO.
AccessGroups []string `json:"access_groups,omitempty" gorm:"serializer:json"`
// DNSZone is the parent zone a private service's synthesized mesh A record
// hangs under, for the case where that zone cannot be derived from
// ProxyCluster or a validated custom domain — i.e. placement-free
// agent-network endpoints, which are <subdomain>.<zone>. In-memory only:
// set by the agent-network synthesizer on services it builds per read,
// never stored and never exposed on the API or the proxy wire.
DNSZone string `gorm:"-" json:"-"`
}
// InitNewRecord generates a new unique ID and resets metadata for a newly created
@@ -1412,6 +1419,7 @@ func (s *Service) Copy() *Service {
PortAutoAssigned: s.PortAutoAssigned,
Private: s.Private,
AccessGroups: accessGroups,
DNSZone: s.DNSZone,
}
}

View File

@@ -1215,6 +1215,17 @@ func TestService_Copy_RoundtripsPrivate(t *testing.T) {
assert.Equal(t, []string{"grp-admins", "grp-ops"}, svc.AccessGroups)
}
// TestServiceCopy_PreservesDNSZone — DNSZone is in-memory only, so it is easy
// to omit from Copy()'s explicit field list; if it is dropped, a copied
// account silently loses its zone apex and the tenant's endpoint resolves to
// nothing.
func TestServiceCopy_PreservesDNSZone(t *testing.T) {
svc := &Service{Domain: "brave-otter.gateway.netbird.ai", DNSZone: "gateway.netbird.ai"}
cp := svc.Copy()
require.NotNil(t, cp)
assert.Equal(t, "gateway.netbird.ai", cp.DNSZone)
}
func TestService_APIRoundtrip_Private(t *testing.T) {
enabled := true
private := true