Merge branch 'main' into embedded-vnc

This commit is contained in:
Viktor Liu
2026-09-02 19:10:03 +02:00
153 changed files with 9089 additions and 1934 deletions
@@ -54,6 +54,10 @@ type NetworkMapData struct { //nolint:revive // established name across the code
// builder can load because they are never written to the database.
Services []*nmdata.Service
// Domains are the account's registered reverse-proxy domains, used to
// resolve the zone apex a private service's records hang under.
Domains []nmdata.ProxyDomain
peerGroupsOnce sync.Once
peerGroupsIdx map[string]map[string]struct{}
@@ -27,6 +27,7 @@ type Peer struct {
IP netip.Addr
IPv6 netip.Addr
RequiresApproval bool
Connected bool
ExtraDNSLabels []string
Meta PeerSystemMeta
ProxyMeta ProxyMeta
@@ -39,6 +40,14 @@ type ProxyMeta struct {
Cluster string
}
// ProxyDomain is the slim twin of a registered reverse-proxy domain, carrying
// what private-service zone resolution needs: the apex a service domain can sit
// under, and the cluster it is registered against.
type ProxyDomain struct {
Domain string
TargetCluster string
}
// PeerSystemMeta is the slim twin of peer.PeerSystemMeta.
type PeerSystemMeta struct {
WtVersion string
@@ -9,6 +9,7 @@ type Service struct {
Enabled bool
Private bool
Mode string
Domain string
ProxyCluster string
AccessGroups []string
Targets []*ServiceTarget
@@ -0,0 +1,124 @@
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
}
@@ -0,0 +1,148 @@
package networkmap
import (
"net/netip"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
)
func proxyPeer(id, ip, cluster string, connected bool) *nmdata.Peer {
return &nmdata.Peer{
ID: id, Key: id + "-key", IP: netip.MustParseAddr(ip), Connected: connected,
ProxyMeta: nmdata.ProxyMeta{Embedded: true, Cluster: cluster},
}
}
func privateService(id, domain, cluster string, groups ...string) *nmdata.Service {
return &nmdata.Service{
ID: id, Enabled: true, Private: true, Mode: "http",
Domain: domain, ProxyCluster: cluster, AccessGroups: groups,
}
}
func twinWithProxy(services ...*nmdata.Service) *NetworkMapData {
return &NetworkMapData{
Peers: map[string]*nmdata.Peer{
"proxy-1": proxyPeer("proxy-1", "100.64.0.99", "eu.proxy.netbird.io", true),
},
Services: services,
}
}
// An agent-network service is synthesised in memory and never persisted, so it
// only ever reaches the twin through nmd.Services. Deriving the zone from that
// same field is what stops it from arriving with an ACL and no name.
func TestBuildPrivateServiceCandidates_SynthesisedServiceGetsAZone(t *testing.T) {
nmd := twinWithProxy(privateService(
"agent-network-acct-1", "acct-1.agent.netbird.io", "eu.proxy.netbird.io", "grp-admins"))
nmd.BuildPrivateServiceCandidates()
require.Len(t, nmd.PrivateServiceCandidates, 0,
"a service whose domain sits under no registered apex publishes nothing")
nmd.Domains = []nmdata.ProxyDomain{
{Domain: "agent.netbird.io", TargetCluster: "eu.proxy.netbird.io"},
}
nmd.BuildPrivateServiceCandidates()
require.Len(t, nmd.PrivateServiceCandidates, 1)
got := nmd.PrivateServiceCandidates[0]
assert.Equal(t, []string{"grp-admins"}, got.AccessGroups)
assert.Equal(t, "agent.netbird.io.", got.Zone.Domain, "apex is the registered domain, not the service FQDN")
assert.True(t, got.Zone.NonAuthoritative, "zone stays match-only")
assert.True(t, got.Zone.SearchDomainDisabled)
require.Len(t, got.Zone.Records, 1)
assert.Equal(t, nmdata.SimpleRecord{
Name: "acct-1.agent.netbird.io.", Type: 1, Class: "IN", TTL: 5, RData: "100.64.0.99",
}, got.Zone.Records[0])
}
func TestBuildPrivateServiceCandidates_ClusterApexNeedsNoRegisteredDomain(t *testing.T) {
nmd := twinWithProxy(privateService("svc-1", "myapp.eu.proxy.netbird.io", "eu.proxy.netbird.io", "grp-admins"))
nmd.BuildPrivateServiceCandidates()
require.Len(t, nmd.PrivateServiceCandidates, 1)
assert.Equal(t, "eu.proxy.netbird.io.", nmd.PrivateServiceCandidates[0].Zone.Domain)
}
func TestBuildPrivateServiceCandidates_LongestRegisteredApexWins(t *testing.T) {
nmd := twinWithProxy(privateService("svc-1", "app.sub.example.com", "eu.proxy.netbird.io", "grp-admins"))
nmd.Domains = []nmdata.ProxyDomain{
{Domain: "example.com", TargetCluster: "eu.proxy.netbird.io"},
{Domain: "sub.example.com", TargetCluster: "eu.proxy.netbird.io"},
{Domain: "other.com", TargetCluster: "eu.proxy.netbird.io"},
}
nmd.BuildPrivateServiceCandidates()
require.Len(t, nmd.PrivateServiceCandidates, 1)
assert.Equal(t, "sub.example.com.", nmd.PrivateServiceCandidates[0].Zone.Domain)
}
func TestBuildPrivateServiceCandidates_RegisteredApexOfAnotherClusterIsIgnored(t *testing.T) {
nmd := twinWithProxy(privateService("svc-1", "app.example.com", "eu.proxy.netbird.io", "grp-admins"))
nmd.Domains = []nmdata.ProxyDomain{
{Domain: "example.com", TargetCluster: "us.proxy.netbird.io"},
}
nmd.BuildPrivateServiceCandidates()
assert.Empty(t, nmd.PrivateServiceCandidates)
}
// A disconnected proxy peer's tunnel IP does not answer, so publishing it
// black-holes the name for as long as a client caches the record.
func TestBuildPrivateServiceCandidates_OnlyConnectedProxyPeersSurface(t *testing.T) {
nmd := twinWithProxy(privateService("svc-1", "myapp.eu.proxy.netbird.io", "eu.proxy.netbird.io", "grp-admins"))
nmd.Peers["proxy-2"] = proxyPeer("proxy-2", "100.64.0.100", "eu.proxy.netbird.io", false)
nmd.BuildPrivateServiceCandidates()
require.Len(t, nmd.PrivateServiceCandidates, 1)
require.Len(t, nmd.PrivateServiceCandidates[0].Zone.Records, 1)
assert.Equal(t, "100.64.0.99", nmd.PrivateServiceCandidates[0].Zone.Records[0].RData)
nmd.Peers["proxy-1"].Connected = false
nmd.BuildPrivateServiceCandidates()
assert.Empty(t, nmd.PrivateServiceCandidates, "no connected proxy peer means no zone at all")
}
func TestBuildPrivateServiceCandidates_SkipsServicesThatGrantNothing(t *testing.T) {
cases := map[string]func(*nmdata.Service){
"disabled": func(s *nmdata.Service) { s.Enabled = false },
"not private": func(s *nmdata.Service) { s.Private = false },
"no access groups": func(s *nmdata.Service) { s.AccessGroups = nil },
"no domain": func(s *nmdata.Service) { s.Domain = "" },
"other cluster": func(s *nmdata.Service) { s.ProxyCluster = "us.proxy.netbird.io" },
}
for name, mutate := range cases {
t.Run(name, func(t *testing.T) {
svc := privateService("svc-1", "myapp.eu.proxy.netbird.io", "eu.proxy.netbird.io", "grp-admins")
mutate(svc)
nmd := twinWithProxy(svc)
nmd.BuildPrivateServiceCandidates()
assert.Empty(t, nmd.PrivateServiceCandidates)
})
}
}
func TestBuildPrivateServiceCandidates_MultipleConnectedProxyPeersEachGetARecord(t *testing.T) {
nmd := twinWithProxy(privateService("svc-1", "myapp.eu.proxy.netbird.io", "eu.proxy.netbird.io", "grp-admins"))
nmd.Peers["proxy-2"] = proxyPeer("proxy-2", "100.64.0.100", "eu.proxy.netbird.io", true)
nmd.BuildPrivateServiceCandidates()
require.Len(t, nmd.PrivateServiceCandidates, 1)
records := nmd.PrivateServiceCandidates[0].Zone.Records
require.Len(t, records, 2)
assert.Equal(t, "100.64.0.99", records[0].RData, "records are ordered by proxy peer id")
assert.Equal(t, "100.64.0.100", records[1].RData)
}