mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-01 20:41:28 +02:00
[management] fix private services calc on new db path (#7383)
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
"IP": "10.10.10.1",
|
||||
"IPv6": "fdf4:ba80:6aa5:89f1:44d7:8701:8699:4940",
|
||||
"RequiresApproval": false,
|
||||
"Connected": true,
|
||||
"ExtraDNSLabels": [
|
||||
"extra-peer-1"
|
||||
],
|
||||
@@ -66,6 +67,7 @@
|
||||
"IP": "10.10.100.1",
|
||||
"IPv6": "fdf5:ba80:6aa5:89f1:44d7:8701:8699:4940",
|
||||
"RequiresApproval": false,
|
||||
"Connected": true,
|
||||
"ExtraDNSLabels": [
|
||||
"extra-peer-2"
|
||||
],
|
||||
@@ -120,6 +122,7 @@
|
||||
"IP": "10.10.200.1",
|
||||
"IPv6": "fdf6:ba80:6aa5:89f1:44d7:8701:8699:4940",
|
||||
"RequiresApproval": false,
|
||||
"Connected": true,
|
||||
"ExtraDNSLabels": [
|
||||
"extra-peer-3"
|
||||
],
|
||||
@@ -542,5 +545,11 @@
|
||||
}
|
||||
],
|
||||
"PrivateServiceCandidates": null,
|
||||
"Services": null
|
||||
"Services": null,
|
||||
"Domains": [
|
||||
{
|
||||
"Domain": "test-331.com",
|
||||
"TargetCluster": "target-1.cluster.local"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -40,6 +40,7 @@ func TestGetPeers(t *testing.T) {
|
||||
IP: netip.MustParseAddr("10.10.10.1"),
|
||||
IPv6: netip.MustParseAddr("fdf4:ba80:6aa5:89f1:44d7:8701:8699:4940"),
|
||||
RequiresApproval: false,
|
||||
Connected: true,
|
||||
Meta: nmdata.PeerSystemMeta{
|
||||
WtVersion: "0.76.0",
|
||||
GoOS: "linux",
|
||||
@@ -82,6 +83,7 @@ func TestGetPeers(t *testing.T) {
|
||||
IP: netip.MustParseAddr("10.10.100.1"),
|
||||
IPv6: netip.MustParseAddr("fdf5:ba80:6aa5:89f1:44d7:8701:8699:4940"),
|
||||
RequiresApproval: false,
|
||||
Connected: true,
|
||||
Meta: nmdata.PeerSystemMeta{
|
||||
WtVersion: "0.76.1",
|
||||
GoOS: "linux",
|
||||
@@ -124,6 +126,7 @@ func TestGetPeers(t *testing.T) {
|
||||
IP: netip.MustParseAddr("10.10.200.1"),
|
||||
IPv6: netip.MustParseAddr("fdf6:ba80:6aa5:89f1:44d7:8701:8699:4940"),
|
||||
RequiresApproval: false,
|
||||
Connected: true,
|
||||
Meta: nmdata.PeerSystemMeta{
|
||||
WtVersion: "0.76.2",
|
||||
GoOS: "linux",
|
||||
|
||||
@@ -511,6 +511,7 @@ func (c *Controller) fetchNetworkMapData(ctx context.Context, accountID string)
|
||||
}
|
||||
|
||||
nmData.Services = c.proxyServicesFromRepo(ctx, accountID)
|
||||
nmData.BuildPrivateServiceCandidates()
|
||||
nmData.InjectProxyPolicies()
|
||||
nmData.PrecomputePostureValidation()
|
||||
|
||||
|
||||
@@ -183,6 +183,7 @@ func RunCase(t *testing.T, c Case) {
|
||||
ctx := context.Background()
|
||||
nmData := c.Data
|
||||
applyFixtureDefaults(nmData)
|
||||
nmData.BuildPrivateServiceCandidates()
|
||||
nmData.PrecomputePostureValidation()
|
||||
|
||||
dnsDomain := c.DNSDomain
|
||||
|
||||
@@ -3,10 +3,7 @@ package networkmapdb
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/exp/maps"
|
||||
|
||||
@@ -48,7 +45,7 @@ func (s *NetworkMapDBStoreImpl) GetNetworkMapData(ctx context.Context, accountId
|
||||
if err != nil {
|
||||
return rollbackAndReturnError(ctx, tx, fmt.Errorf("failed to get network: %w", err))
|
||||
}
|
||||
peers, proxyPeers, err := tx.GetPeers(ctx, accountId)
|
||||
peers, _, err := tx.GetPeers(ctx, accountId)
|
||||
if err != nil {
|
||||
return rollbackAndReturnError(ctx, tx, fmt.Errorf("failed to get peers: %w", err))
|
||||
}
|
||||
@@ -80,10 +77,6 @@ func (s *NetworkMapDBStoreImpl) GetNetworkMapData(ctx context.Context, accountId
|
||||
if err != nil {
|
||||
return rollbackAndReturnError(ctx, tx, err)
|
||||
}
|
||||
services, err := tx.GetPrivateServices(ctx, accountId)
|
||||
if err != nil {
|
||||
return rollbackAndReturnError(ctx, tx, err)
|
||||
}
|
||||
proxyTargetedDomainResourceIDs, err := tx.GetProxyTargetedDomainResourceIDs(ctx, accountId)
|
||||
if err != nil {
|
||||
return rollbackAndReturnError(ctx, tx, fmt.Errorf("failed to get proxy targeted domain resources: %w", err))
|
||||
@@ -113,7 +106,7 @@ func (s *NetworkMapDBStoreImpl) GetNetworkMapData(ctx context.Context, accountId
|
||||
GroupIDToUserIDs: groupsToUserIds,
|
||||
NetworkXIDToPublicID: networkXIDToPublicID, // TODO (dmitri) maybe we can switch to public ids everywhere?
|
||||
AppliedZoneCandidates: dnsZones,
|
||||
PrivateServiceCandidates: buildPrivateServiceCandidates(services, domains, proxyPeers),
|
||||
Domains: TwinProxyDomains(domains),
|
||||
PostureCheckXIDToPublicID: postureCheckXIDToPublicID,
|
||||
ProxyTargetedDomainResourceIDs: proxyTargetedDomainResourceIDs,
|
||||
}
|
||||
@@ -154,94 +147,6 @@ func toSliceOfPtrs[T any](all []T) []*T {
|
||||
return toret
|
||||
}
|
||||
|
||||
func serviceDomainZone(svc Service, ds []Domain) string {
|
||||
if domainFromSuffix(svc.Domain.String, svc.ProxyCluster.String) {
|
||||
return svc.ProxyCluster.String
|
||||
}
|
||||
|
||||
var zoneName string
|
||||
for _, domain := range ds {
|
||||
if domain.TargetCluster.String != svc.ProxyCluster.String {
|
||||
continue
|
||||
}
|
||||
if domainFromSuffix(svc.Domain.String, domain.Domain.String) && len(domain.Domain.String) > len(zoneName) {
|
||||
zoneName = domain.Domain.String
|
||||
}
|
||||
}
|
||||
|
||||
return zoneName
|
||||
}
|
||||
|
||||
func domainFromSuffix(domain, suffix string) bool {
|
||||
if suffix == "" {
|
||||
return false
|
||||
}
|
||||
return domain == suffix || strings.HasSuffix(domain, "."+suffix)
|
||||
}
|
||||
|
||||
func buildPrivateServiceCandidates(svcs []Service, domains []Domain, proxyPeersByCluster map[string][]*nmdata.Peer) []networkmap.PrivateServiceCandidate {
|
||||
var out []networkmap.PrivateServiceCandidate
|
||||
|
||||
if len(proxyPeersByCluster) == 0 {
|
||||
return out
|
||||
}
|
||||
|
||||
for _, svc := range svcs {
|
||||
if !svc.Enabled.Bool || !svc.Private.Bool {
|
||||
continue
|
||||
}
|
||||
if len(svc.AccessGroups) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
domainZone := serviceDomainZone(svc, domains)
|
||||
if domainZone == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// this is implied when domainZone != "", but for maintainability's sake the check is explicit
|
||||
// TODO (dmitri) make this an invariant
|
||||
if svc.Domain.String == "" {
|
||||
continue
|
||||
}
|
||||
var records []nmdata.SimpleRecord
|
||||
for _, proxyPeer := range proxyPeersByCluster[svc.ProxyCluster.String] {
|
||||
if record, ok := recordForProxyPeer(svc.Domain.String, proxyPeer.IP); ok {
|
||||
records = append(records, record)
|
||||
}
|
||||
}
|
||||
if len(records) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
out = append(out, networkmap.PrivateServiceCandidate{
|
||||
AccessGroups: svc.AccessGroups,
|
||||
Zone: nmdata.CustomZone{
|
||||
Domain: dns.Fqdn(domainZone),
|
||||
Records: records,
|
||||
NonAuthoritative: true,
|
||||
SearchDomainDisabled: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func recordForProxyPeer(fqdn string, ip netip.Addr) (nmdata.SimpleRecord, bool) {
|
||||
if !ip.IsValid() {
|
||||
return nmdata.SimpleRecord{}, false
|
||||
}
|
||||
|
||||
return nmdata.SimpleRecord{
|
||||
Name: dns.Fqdn(fqdn),
|
||||
Type: int(dns.TypeA),
|
||||
Class: "IN",
|
||||
TTL: 5,
|
||||
RData: ip.String(),
|
||||
}, true
|
||||
}
|
||||
|
||||
func buildResourcePolicies(networkResources []nmdata.NetworkResource,
|
||||
policies []nmdata.Policy,
|
||||
resourceToGroupIdx map[string]map[string]any,
|
||||
|
||||
@@ -1,253 +1,12 @@
|
||||
package networkmapdb
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap"
|
||||
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDomainFromSuffix(t *testing.T) {
|
||||
assert.False(t, domainFromSuffix("test", ""))
|
||||
assert.False(t, domainFromSuffix("test", "suffix")) // domain != suffix
|
||||
assert.True(t, domainFromSuffix("test", "test")) // domain == suffix
|
||||
assert.False(t, domainFromSuffix("test.anothersuffix", "suffix")) // domain doesn't contain suffix
|
||||
assert.True(t, domainFromSuffix("test.suffix", "suffix")) // domain contains suffix
|
||||
}
|
||||
|
||||
func TestServiceDomainZone(t *testing.T) {
|
||||
// shortcut -- service's domain is a subomain of proxy cluster
|
||||
assert.Equal(t, "cluster",
|
||||
serviceDomainZone(
|
||||
Service{
|
||||
Domain: sql.NullString{Valid: true, String: "test.cluster"},
|
||||
ProxyCluster: sql.NullString{Valid: true, String: "cluster"}},
|
||||
[]Domain{}))
|
||||
assert.Equal(t, "a.b", serviceDomainZone(
|
||||
Service{
|
||||
Domain: sql.NullString{Valid: true, String: "test.a.b"},
|
||||
ProxyCluster: sql.NullString{Valid: true, String: "cluster"}},
|
||||
[]Domain{
|
||||
{TargetCluster: sql.NullString{Valid: true, String: "a-cluster"}},
|
||||
{TargetCluster: sql.NullString{Valid: true, String: "cluster"},
|
||||
Domain: sql.NullString{Valid: true, String: "b"}},
|
||||
{TargetCluster: sql.NullString{Valid: true, String: "cluster"},
|
||||
Domain: sql.NullString{Valid: true, String: "a.b"}}, // should return this domain, as it's the longest match
|
||||
{TargetCluster: sql.NullString{Valid: true, String: "b-cluster"}},
|
||||
}))
|
||||
// service and domain clusters don't match
|
||||
assert.Empty(t, serviceDomainZone(
|
||||
Service{
|
||||
Domain: sql.NullString{Valid: true, String: "test.a.b"},
|
||||
ProxyCluster: sql.NullString{Valid: true, String: "c-cluster"}},
|
||||
[]Domain{
|
||||
{TargetCluster: sql.NullString{Valid: true, String: "cluster"},
|
||||
Domain: sql.NullString{Valid: true, String: "a.b"}},
|
||||
}))
|
||||
// service domain is empty
|
||||
assert.Empty(t, serviceDomainZone(
|
||||
Service{
|
||||
Domain: sql.NullString{Valid: false, String: ""},
|
||||
ProxyCluster: sql.NullString{Valid: true, String: "cluster"}},
|
||||
[]Domain{
|
||||
{TargetCluster: sql.NullString{Valid: true, String: "cluster"},
|
||||
Domain: sql.NullString{Valid: true, String: "a.b"}},
|
||||
}))
|
||||
}
|
||||
|
||||
func TestRecordForProxyPeer(t *testing.T) {
|
||||
record, ok := recordForProxyPeer("test.cluster", netip.MustParseAddr("127.0.0.1"))
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, nmdata.SimpleRecord{
|
||||
Name: "test.cluster.",
|
||||
Type: 1,
|
||||
Class: "IN",
|
||||
TTL: 5,
|
||||
RData: "127.0.0.1",
|
||||
}, record)
|
||||
|
||||
// invalid address
|
||||
var addr netip.Addr
|
||||
_, ok = recordForProxyPeer("test.cluster", addr)
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
var empty []networkmap.PrivateServiceCandidate
|
||||
|
||||
// empty proxyPeersByCluster results in empty []PrivateServiceCandidates
|
||||
func TestBuildPrivateServiceCandidates_EmptyProxyPeers(t *testing.T) {
|
||||
assert.Equal(t, empty, buildPrivateServiceCandidates([]Service{}, []Domain{}, nil))
|
||||
}
|
||||
|
||||
// disabled service returns an empty result
|
||||
func TestBuildPrivateServiceCandidates_DisabledService(t *testing.T) {
|
||||
assert.Equal(t, empty,
|
||||
buildPrivateServiceCandidates([]Service{
|
||||
{Enabled: sql.NullBool{Valid: true, Bool: false},
|
||||
Private: sql.NullBool{Valid: true, Bool: true},
|
||||
AccessGroups: []string{"group-1", "group-2"},
|
||||
Domain: sql.NullString{Valid: true, String: "test.a.b"},
|
||||
ProxyCluster: sql.NullString{Valid: true, String: "cluster"}},
|
||||
}, []Domain{
|
||||
{TargetCluster: sql.NullString{Valid: true, String: "cluster"},
|
||||
Domain: sql.NullString{Valid: true, String: "a.b"}},
|
||||
},
|
||||
map[string][]*nmdata.Peer{
|
||||
"cluster": {&nmdata.Peer{IP: netip.MustParseAddr("127.0.0.1")}, &nmdata.Peer{IP: netip.MustParseAddr("127.0.0.2")}},
|
||||
"a-cluster": {&nmdata.Peer{IP: netip.MustParseAddr("127.0.0.3")}, &nmdata.Peer{IP: netip.MustParseAddr("127.0.0.4")}},
|
||||
}))
|
||||
}
|
||||
|
||||
// non-private service results in empty []PrivateServiceCandidates
|
||||
func TestBuildPrivateServiceCandidates_PublicService(t *testing.T) {
|
||||
assert.Equal(t, empty,
|
||||
buildPrivateServiceCandidates([]Service{
|
||||
{Enabled: sql.NullBool{Valid: true, Bool: true},
|
||||
Private: sql.NullBool{Valid: true, Bool: false},
|
||||
AccessGroups: []string{"group-1", "group-2"},
|
||||
Domain: sql.NullString{Valid: true, String: "test.a.b"},
|
||||
ProxyCluster: sql.NullString{Valid: true, String: "cluster"}},
|
||||
}, []Domain{
|
||||
{TargetCluster: sql.NullString{Valid: true, String: "cluster"},
|
||||
Domain: sql.NullString{Valid: true, String: "a.b"}},
|
||||
},
|
||||
map[string][]*nmdata.Peer{
|
||||
"cluster": {&nmdata.Peer{IP: netip.MustParseAddr("127.0.0.1")}, &nmdata.Peer{IP: netip.MustParseAddr("127.0.0.2")}},
|
||||
"a-cluster": {&nmdata.Peer{IP: netip.MustParseAddr("127.0.0.3")}, &nmdata.Peer{IP: netip.MustParseAddr("127.0.0.4")}},
|
||||
}))
|
||||
}
|
||||
|
||||
// empty AccessList results in empty []PrivateServiceCandidates
|
||||
func TestBuildPrivateServiceCandidates_EmptyAccessList(t *testing.T) {
|
||||
assert.Equal(t, empty,
|
||||
buildPrivateServiceCandidates([]Service{
|
||||
{Enabled: sql.NullBool{Valid: true, Bool: true},
|
||||
Private: sql.NullBool{Valid: true, Bool: true},
|
||||
Domain: sql.NullString{Valid: true, String: "test.a.b"},
|
||||
ProxyCluster: sql.NullString{Valid: true, String: "cluster"}},
|
||||
}, []Domain{
|
||||
{TargetCluster: sql.NullString{Valid: true, String: "cluster"},
|
||||
Domain: sql.NullString{Valid: true, String: "a.b"}},
|
||||
},
|
||||
map[string][]*nmdata.Peer{
|
||||
"cluster": {&nmdata.Peer{IP: netip.MustParseAddr("127.0.0.1")}, &nmdata.Peer{IP: netip.MustParseAddr("127.0.0.2")}},
|
||||
"a-cluster": {&nmdata.Peer{IP: netip.MustParseAddr("127.0.0.3")}, &nmdata.Peer{IP: netip.MustParseAddr("127.0.0.4")}},
|
||||
}))
|
||||
}
|
||||
|
||||
// empty TragetCluster results in empty []PrivateServiceCandidates
|
||||
func TestBuildPrivateServiceCandidates_EmptyTargetCluster(t *testing.T) {
|
||||
assert.Equal(t, empty,
|
||||
buildPrivateServiceCandidates([]Service{
|
||||
{Enabled: sql.NullBool{Valid: true, Bool: true},
|
||||
Private: sql.NullBool{Valid: true, Bool: true},
|
||||
AccessGroups: []string{"group-1", "group-2"},
|
||||
Domain: sql.NullString{Valid: true, String: "test.a.b"},
|
||||
ProxyCluster: sql.NullString{Valid: true, String: "cluster"}},
|
||||
}, []Domain{
|
||||
{TargetCluster: sql.NullString{Valid: true, String: ""},
|
||||
Domain: sql.NullString{Valid: true, String: "a.b"}},
|
||||
},
|
||||
map[string][]*nmdata.Peer{
|
||||
"cluster": {&nmdata.Peer{IP: netip.MustParseAddr("127.0.0.1")}, &nmdata.Peer{IP: netip.MustParseAddr("127.0.0.2")}},
|
||||
"a-cluster": {&nmdata.Peer{IP: netip.MustParseAddr("127.0.0.3")}, &nmdata.Peer{IP: netip.MustParseAddr("127.0.0.4")}},
|
||||
}))
|
||||
}
|
||||
|
||||
func TestBuildPrivateServiceCandidates_EmptyServiceDomain(t *testing.T) {
|
||||
assert.Equal(t, empty,
|
||||
buildPrivateServiceCandidates([]Service{
|
||||
{Enabled: sql.NullBool{Valid: true, Bool: true},
|
||||
Private: sql.NullBool{Valid: true, Bool: true},
|
||||
Domain: sql.NullString{Valid: true, String: ""},
|
||||
ProxyCluster: sql.NullString{Valid: true, String: "cluster"}},
|
||||
}, []Domain{
|
||||
{TargetCluster: sql.NullString{Valid: true, String: "cluster"},
|
||||
Domain: sql.NullString{Valid: true, String: "a.b"}},
|
||||
},
|
||||
map[string][]*nmdata.Peer{
|
||||
"cluster": {&nmdata.Peer{IP: netip.MustParseAddr("127.0.0.1")}, &nmdata.Peer{IP: netip.MustParseAddr("127.0.0.2")}},
|
||||
"a-cluster": {&nmdata.Peer{IP: netip.MustParseAddr("127.0.0.3")}, &nmdata.Peer{IP: netip.MustParseAddr("127.0.0.4")}},
|
||||
}))
|
||||
}
|
||||
|
||||
func TestBuildPrivateServiceCandidates_HappyPath(t *testing.T) {
|
||||
assert.Equal(t, []networkmap.PrivateServiceCandidate{
|
||||
{
|
||||
AccessGroups: []string{"group-1", "group-2"},
|
||||
Zone: nmdata.CustomZone{
|
||||
Domain: "a.b.",
|
||||
SearchDomainDisabled: true,
|
||||
NonAuthoritative: true,
|
||||
Records: []nmdata.SimpleRecord{
|
||||
{
|
||||
Name: "test.a.b.",
|
||||
Type: 1,
|
||||
Class: "IN",
|
||||
TTL: 5,
|
||||
RData: "127.0.0.1",
|
||||
},
|
||||
{
|
||||
Name: "test.a.b.",
|
||||
Type: 1,
|
||||
Class: "IN",
|
||||
TTL: 5,
|
||||
RData: "127.0.0.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
AccessGroups: []string{"group-1", "group-2"},
|
||||
Zone: nmdata.CustomZone{
|
||||
Domain: "c.d.",
|
||||
SearchDomainDisabled: true,
|
||||
NonAuthoritative: true,
|
||||
Records: []nmdata.SimpleRecord{
|
||||
{
|
||||
Name: "test.c.d.",
|
||||
Type: 1,
|
||||
Class: "IN",
|
||||
TTL: 5,
|
||||
RData: "127.0.0.3",
|
||||
},
|
||||
{
|
||||
Name: "test.c.d.",
|
||||
Type: 1,
|
||||
Class: "IN",
|
||||
TTL: 5,
|
||||
RData: "127.0.0.4",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
buildPrivateServiceCandidates([]Service{
|
||||
{Enabled: sql.NullBool{Valid: true, Bool: true},
|
||||
Private: sql.NullBool{Valid: true, Bool: true},
|
||||
AccessGroups: []string{"group-1", "group-2"},
|
||||
Domain: sql.NullString{Valid: true, String: "test.a.b"},
|
||||
ProxyCluster: sql.NullString{Valid: true, String: "cluster"}},
|
||||
{Enabled: sql.NullBool{Valid: true, Bool: true},
|
||||
Private: sql.NullBool{Valid: true, Bool: true},
|
||||
AccessGroups: []string{"group-1", "group-2"},
|
||||
Domain: sql.NullString{Valid: true, String: "test.c.d"},
|
||||
ProxyCluster: sql.NullString{Valid: true, String: "a-cluster"}},
|
||||
}, []Domain{
|
||||
{TargetCluster: sql.NullString{Valid: true, String: "cluster"},
|
||||
Domain: sql.NullString{Valid: true, String: "a.b"}},
|
||||
{TargetCluster: sql.NullString{Valid: true, String: "a-cluster"},
|
||||
Domain: sql.NullString{Valid: true, String: "c.d"}},
|
||||
},
|
||||
map[string][]*nmdata.Peer{
|
||||
"cluster": {&nmdata.Peer{IP: netip.MustParseAddr("127.0.0.1")}, &nmdata.Peer{IP: netip.MustParseAddr("127.0.0.2")}},
|
||||
"a-cluster": {&nmdata.Peer{IP: netip.MustParseAddr("127.0.0.3")}, &nmdata.Peer{IP: netip.MustParseAddr("127.0.0.4")}},
|
||||
}))
|
||||
}
|
||||
|
||||
// disabled network resource shouldn't be in the resulting map
|
||||
func TestBuildResourcePolicies_DisabledNetworkResource(t *testing.T) {
|
||||
networkResources := []nmdata.NetworkResource{
|
||||
|
||||
@@ -302,6 +302,7 @@ func ConvertToNmdataPeers(peers []Peer) ([]nmdata.Peer, map[string][]*nmdata.Pee
|
||||
}
|
||||
dp.ProxyMeta.Cluster = p.ProxyMetaCluster.String
|
||||
// This is only used to build private service candidates, not connected peers are skipped
|
||||
dp.Connected = p.PeerStatusConnected.Bool
|
||||
if dp.ProxyMeta.Embedded && p.PeerStatusConnected.Bool {
|
||||
clusterToPeerIdx[p.ProxyMetaCluster.String] = append(clusterToPeerIdx[p.ProxyMetaCluster.String], &dp)
|
||||
}
|
||||
@@ -470,3 +471,16 @@ func ConvertToNmdataPolicy(policies []Policy) ([]nmdata.Policy, map[string]map[s
|
||||
|
||||
return toret, policyToDestinationResourceIdx, policyToDestinationGroupIdx, nil
|
||||
}
|
||||
|
||||
// TwinProxyDomains converts registered reverse-proxy domain rows to their slim
|
||||
// twins, so private-service zone apex resolution runs on the twin.
|
||||
func TwinProxyDomains(domains []Domain) []nmdata.ProxyDomain {
|
||||
if len(domains) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]nmdata.ProxyDomain, 0, len(domains))
|
||||
for _, d := range domains {
|
||||
out = append(out, nmdata.ProxyDomain{Domain: d.Domain.String, TargetCluster: d.TargetCluster.String})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"github.com/miekg/dns"
|
||||
|
||||
nbdns "github.com/netbirdio/netbird/dns"
|
||||
proxydomain "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/domain"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/zones"
|
||||
"github.com/netbirdio/netbird/management/internals/modules/zones/records"
|
||||
@@ -114,6 +115,7 @@ func (a *Account) toNetworkMapData(
|
||||
nmd.AppliedZoneCandidates = buildAppliedZoneCandidates(accountZones)
|
||||
nmd.PrivateServiceCandidates = a.buildPrivateServiceCandidates()
|
||||
nmd.Services = TwinServices(a.Services)
|
||||
nmd.Domains = twinProxyDomains(a.Domains)
|
||||
|
||||
return nmd
|
||||
}
|
||||
@@ -153,6 +155,7 @@ func TwinServices(services []*service.Service) []*nmdata.Service {
|
||||
Enabled: svc.Enabled,
|
||||
Private: svc.Private,
|
||||
Mode: svc.Mode,
|
||||
Domain: svc.Domain,
|
||||
ProxyCluster: svc.ProxyCluster,
|
||||
AccessGroups: svc.AccessGroups,
|
||||
Targets: targets,
|
||||
@@ -185,6 +188,7 @@ func twinPeer(p *nbpeer.Peer) *nmdata.Peer {
|
||||
IP: p.IP,
|
||||
IPv6: p.IPv6,
|
||||
RequiresApproval: p.Status != nil && p.Status.RequiresApproval,
|
||||
Connected: p.Status != nil && p.Status.Connected,
|
||||
ExtraDNSLabels: p.ExtraDNSLabels,
|
||||
ProxyMeta: nmdata.ProxyMeta{Embedded: p.ProxyMeta.Embedded, Cluster: p.ProxyMeta.Cluster},
|
||||
Meta: nmdata.PeerSystemMeta{
|
||||
@@ -621,3 +625,19 @@ func TwinCustomZone(z nbdns.CustomZone) nmdata.CustomZone {
|
||||
NonAuthoritative: z.NonAuthoritative,
|
||||
}
|
||||
}
|
||||
|
||||
// twinProxyDomains converts the account's registered reverse-proxy domains to
|
||||
// their slim twins, so private-service zone apex resolution runs on the twin.
|
||||
func twinProxyDomains(domains []*proxydomain.Domain) []nmdata.ProxyDomain {
|
||||
if len(domains) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]nmdata.ProxyDomain, 0, len(domains))
|
||||
for _, d := range domains {
|
||||
if d == nil {
|
||||
continue
|
||||
}
|
||||
out = append(out, nmdata.ProxyDomain{Domain: d.Domain, TargetCluster: d.TargetCluster})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
124
shared/management/networkmap/privatezones.go
Normal file
124
shared/management/networkmap/privatezones.go
Normal file
@@ -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
|
||||
}
|
||||
148
shared/management/networkmap/privatezones_test.go
Normal file
148
shared/management/networkmap/privatezones_test.go
Normal file
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user