Files
netbird/shared/management/networkmap/privatezones.go

125 lines
3.8 KiB
Go

package networkmap
import (
"slices"
"strings"
"github.com/miekg/dns"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
)
// privateServiceDNSRecordTTL is short so proxy-peer changes propagate quickly.
const privateServiceDNSRecordTTL = 5
// BuildPrivateServiceCandidates derives the per-service DNS records a private
// service publishes, from the twin's own services. It is the counterpart of
// InjectProxyPolicies: that one synthesises the ACL half of a private service,
// this one the DNS half, and both read nmd.Services so a service added to the
// twin after it was loaded — an agent-network service is synthesised in memory
// and never persisted — reaches the peer with both halves rather than one.
//
// The per-peer access-group gate and the merge by apex stay in the components
// calculation; this only precomputes what is account-wide.
func (nmd *NetworkMapData) BuildPrivateServiceCandidates() {
if len(nmd.Services) == 0 {
nmd.PrivateServiceCandidates = nil
return
}
proxyPeersByCluster := nmd.connectedProxyPeersByCluster()
if len(proxyPeersByCluster) == 0 {
nmd.PrivateServiceCandidates = nil
return
}
var out []PrivateServiceCandidate
for _, svc := range nmd.Services {
if svc == nil || !svc.Enabled || !svc.Private || len(svc.AccessGroups) == 0 || svc.Domain == "" {
continue
}
proxyPeers := proxyPeersByCluster[svc.ProxyCluster]
if len(proxyPeers) == 0 {
continue
}
apex := nmd.privateServiceApex(svc)
if apex == "" {
continue
}
records := make([]nmdata.SimpleRecord, 0, len(proxyPeers))
for _, p := range proxyPeers {
records = append(records, nmdata.SimpleRecord{
Name: dns.Fqdn(svc.Domain),
Type: int(dns.TypeA),
Class: "IN",
TTL: privateServiceDNSRecordTTL,
RData: p.IP.String(),
})
}
out = append(out, PrivateServiceCandidate{
AccessGroups: svc.AccessGroups,
Zone: nmdata.CustomZone{
// NonAuthoritative keeps the zone match-only, so names without
// an explicit record fall through to the upstream resolver
// instead of returning NXDOMAIN for the whole apex.
Domain: dns.Fqdn(apex),
Records: records,
NonAuthoritative: true,
SearchDomainDisabled: true,
},
})
}
nmd.PrivateServiceCandidates = out
}
// privateServiceApex resolves the zone a service's record hangs under: the
// cluster when the service sits directly beneath it, otherwise the longest
// registered custom domain pointing at that same cluster. A service whose
// domain matches no registered apex publishes nothing, since a zone the client
// never intercepts cannot answer the query.
func (nmd *NetworkMapData) privateServiceApex(svc *nmdata.Service) string {
if domainUnderSuffix(svc.Domain, svc.ProxyCluster) {
return svc.ProxyCluster
}
apex := ""
for _, d := range nmd.Domains {
if d.TargetCluster != svc.ProxyCluster {
continue
}
if domainUnderSuffix(svc.Domain, d.Domain) && len(d.Domain) > len(apex) {
apex = d.Domain
}
}
return apex
}
func domainUnderSuffix(domain, suffix string) bool {
if suffix == "" {
return false
}
return domain == suffix || strings.HasSuffix(domain, "."+suffix)
}
// connectedProxyPeersByCluster groups the account's embedded proxy peers by the
// cluster they serve, keeping only connected ones.
func (nmd *NetworkMapData) connectedProxyPeersByCluster() map[string][]*nmdata.Peer {
var out map[string][]*nmdata.Peer
for _, peer := range nmd.Peers {
if peer == nil || !peer.ProxyMeta.Embedded || !peer.Connected || !peer.IP.IsValid() {
continue
}
if out == nil {
out = make(map[string][]*nmdata.Peer)
}
out[peer.ProxyMeta.Cluster] = append(out[peer.ProxyMeta.Cluster], peer)
}
for _, peers := range out {
slices.SortFunc(peers, func(a, b *nmdata.Peer) int { return strings.Compare(a.ID, b.ID) })
}
return out
}