Compare commits

...

33 Commits

Author SHA1 Message Date
pascal
d5adfc799f handle policy with no rules to avoid index panic 2026-08-04 20:57:57 +02:00
pascal
60fe56e442 fix assert equal ordering 2026-08-04 20:56:16 +02:00
pascal
327fa4c0d6 fix group clone 2026-08-04 20:52:16 +02:00
pascal
ed015a7972 skip resource from proto failure 2026-08-04 20:46:10 +02:00
pascal
b43708e31b silently skip resource to proto failure 2026-08-04 20:42:43 +02:00
pascal
ce55b8406b handle not found peer on posture vali 2026-08-04 20:34:30 +02:00
pascal
0456b64ea1 silent skip on non existing groups references 2026-08-04 20:28:06 +02:00
pascal
942ee81ec0 add benchmark 2026-08-04 20:19:17 +02:00
Dmitri Dolguikh
33a0e1bc2b adding integration tests
Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
2026-08-04 18:58:29 +02:00
Dmitri Dolguikh
dbfdd04c7b adding network-routers integration query test
Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
2026-08-04 16:43:17 +02:00
Dmitri Dolguikh
4d010f60ce fix another import
Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
2026-08-04 15:40:28 +02:00
Dmitri Dolguikh
3272058e56 fix extra setting manager package
Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
2026-08-04 13:34:25 +02:00
Dmitri Dolguikh
70c3feb05b Merge remote-tracking branch 'origin/revert/component-types' into revert/component-types
Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
2026-08-04 13:04:51 +02:00
Dmitri Dolguikh
a0fe80cd99 wire up validated peers
Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
2026-08-04 12:52:09 +02:00
pascal
6fadf8f24a fix nmdata store expanding router peer groups alongside static peer 2026-08-03 23:58:47 +02:00
pascal
ef0032685b fix nmdata store shipping zones for disabled/public services 2026-08-03 23:54:22 +02:00
pascal
5ed38569f7 fix PrivateServiceCandidates nil assumption 2026-08-03 23:19:55 +02:00
pascal
d4e1c8978e add all group to user group lookup 2026-08-03 23:08:43 +02:00
pascal
d5ac70d806 fix missing ForceRoutingPeerDNSResolution on the nmdata store path 2026-08-03 22:50:06 +02:00
pascal
23579e4dd4 Skip the left-join NULL row instead of failing the account load + drop unsupported record types silently to match buildAppliedZoneCandidates 2026-08-03 22:38:12 +02:00
pascal
a00c5164a8 continue on resource policy loop + exclude disabled policies form index 2026-08-03 22:29:47 +02:00
pascal
02138dffdd fix group id mapping 2026-08-03 22:24:28 +02:00
pascal
02ec1f5dcb fix equivalence test 2026-08-03 22:24:11 +02:00
pascal
07d0440e34 fix slice initialization to avoid nil pointer dereference 2026-08-03 21:51:48 +02:00
pascal
2c85d94c6c hookup validated peers 2026-08-03 21:47:41 +02:00
Dmitri Dolguikh
1f190a50cf Merge remote-tracking branch 'origin/revert/component-types' into revert/component-types
Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
2026-08-03 18:22:34 +02:00
Dmitri Dolguikh
52fdfd5bdc add an posture-check-id to public-id index
Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
2026-08-03 18:21:07 +02:00
Dmitri Dolguikh
e35a0f3318 added PrivateServiceCandidates
Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
2026-08-03 18:04:11 +02:00
pascal
56c411babd fixed networks query and extended error wrapping 2026-08-03 17:52:33 +02:00
Dmitri Dolguikh
164b2baa4d Merge remote-tracking branch 'origin/revert/component-types' into revert/component-types
Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
2026-08-03 17:29:42 +02:00
Dmitri Dolguikh
0b29c6ed1a adding buildPrivateServiceCandidates
Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
2026-08-03 17:29:09 +02:00
pascal
724b61440b use new path on initial sync 2026-08-03 17:27:58 +02:00
pascal
22590ad66e hookup network map store 2026-08-03 17:27:24 +02:00
53 changed files with 1746 additions and 382 deletions

2
go.sum
View File

@@ -484,8 +484,6 @@ github.com/netbirdio/easyjson v0.9.0 h1:6Nw2lghSVuy8RSkAYDhDv1thBVEmfVbKZnV7T7Z6
github.com/netbirdio/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/netbirdio/ice/v4 v4.0.0-20250908184934-6202be846b51 h1:Ov4qdafATOgGMB1wbSuh+0aAHcwz9hdvB6VZjh1mVMI=
github.com/netbirdio/ice/v4 v4.0.0-20250908184934-6202be846b51/go.mod h1:ZSIbPdBn5hePO8CpF1PekH2SfpTxg1PDhEwtbqZS7R8=
github.com/netbirdio/management-integrations/integrations v0.0.0-20260416123949-2355d972be42 h1:F3zS5fT9xzD1OFLfcdAE+3FfyiwjGukF1hvj0jErgs8=
github.com/netbirdio/management-integrations/integrations v0.0.0-20260416123949-2355d972be42/go.mod h1:n47r67ZSPgwSmT/Z1o48JjZQW9YJ6m/6Bd/uAXkL3Pg=
github.com/netbirdio/management-integrations/integrations v0.0.0-20260803100840-78e79ba20f87 h1:iJeUvSMC0BTpkw7u4JyWcY4/3dl7fEL9DR/TpKf2+1w=
github.com/netbirdio/management-integrations/integrations v0.0.0-20260803100840-78e79ba20f87/go.mod h1:pmsCPx1S0nuZRxCextGpc9AV4hLgGSuTsc4NMuwGeCo=
github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502 h1:3tHlFmhTdX9axERMVN63dqyFqnvuD+EMJHzM7mNGON8=

View File

@@ -6,6 +6,7 @@ import (
networkmap_pgsql "github.com/netbirdio/netbird/management/internals/network_map_db/pgsql"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
"github.com/rs/xid"
"github.com/stretchr/testify/assert"
)
@@ -15,25 +16,67 @@ func TestGetGroups(t *testing.T) {
s, err := networkmap_pgsql.NewPostgresqlStore(ctx, dsn)
assert.NoError(t, err)
acctId := xid.New().String()
_, err = s.Pool.Query(ctx,
"insert into accounts (id) VALUES('account-id-1')")
"insert into accounts (id) VALUES($1)", acctId)
assert.NoError(t, err)
_, err = s.Pool.Query(ctx,
"insert into groups (id, account_id, name, resources, public_id) VALUES('test-group-id-1','account-id-1','test-group-1', '[{\"ID\":\"host-id-1\",\"Type\":\"host\"}]','public-id-1')")
"insert into groups (id, account_id, name, resources, public_id) VALUES('g1-test-group-id-1',$1,'test-group-1', '[{\"ID\":\"host-id-1\",\"Type\":\"host\"}]','public-id-1')", acctId)
assert.NoError(t, err)
_, err = s.Pool.Query(ctx,
"insert into groups (id, account_id, name, resources, public_id) VALUES('test-group-id-2','account-id-1','test-group-2', '[{\"ID\":\"subnet-id-1\",\"Type\":\"subnet\"}, {\"ID\":\"host-id-2\",\"Type\":\"host\"}]','public-id-2')")
"insert into groups (id, account_id, name, resources, public_id) VALUES('g1-test-group-id-2',$1,'test-group-2', '[{\"ID\":\"subnet-id-1\",\"Type\":\"subnet\"}, {\"ID\":\"host-id-2\",\"Type\":\"host\"}]','public-id-2')", acctId)
assert.NoError(t, err)
_, err = s.Pool.Query(ctx,
"insert into group_peers (peer_id, group_id) VALUES('peer-id-1','g1-test-group-id-1')")
assert.NoError(t, err)
_, err = s.Pool.Query(ctx,
"insert into group_peers (peer_id, group_id) VALUES('peer-id-2','g1-test-group-id-2')")
assert.NoError(t, err)
_, err = s.Pool.Query(ctx,
"insert into group_peers (peer_id, group_id) VALUES('peer-id-3','g1-test-group-id-2')")
assert.NoError(t, err)
groups, err := s.GetGroups(ctx, "account-id-1")
groups, resourceToGroupIdx, err := s.GetGroups(ctx, acctId)
assert.NoError(t, err)
assert.Contains(t,
groups,
nmdata.Group{Name: "test-group-1", PublicID: "public-id-1", Resources: []nmdata.Resource{{ID: "host-id-1", Type: "host"}}},
nmdata.Group{ID: "g1-test-group-id-1", Name: "test-group-1", PublicID: "public-id-1", Resources: []nmdata.Resource{{ID: "host-id-1", Type: "host"}}, Peers: []string{"peer-id-1"}},
)
assert.NotNil(t, resourceToGroupIdx["host-id-1"]["g1-test-group-id-1"])
assert.Contains(t,
groups,
nmdata.Group{Name: "test-group-2", PublicID: "public-id-2", Resources: []nmdata.Resource{{ID: "subnet-id-1", Type: "subnet"}, {ID: "host-id-2", Type: "host"}}},
nmdata.Group{ID: "g1-test-group-id-2", Name: "test-group-2", PublicID: "public-id-2",
Resources: []nmdata.Resource{{ID: "subnet-id-1", Type: "subnet"}, {ID: "host-id-2", Type: "host"}},
Peers: []string{"peer-id-2", "peer-id-3"}},
)
assert.NotNil(t, resourceToGroupIdx["host-id-2"]["g1-test-group-id-2"])
assert.NotNil(t, resourceToGroupIdx["subnet-id-1"]["g1-test-group-id-2"])
}
// Verify handling of empty fields in groups table
// Verify that group's PublicID gets populated on retrieval
// TODO (dmitri) PublicID should not be populated with delta updates,
// which require stable PublicIDs
func TestGetGroupsWithoutExpectedFields(t *testing.T) {
ctx := context.TODO()
s, err := networkmap_pgsql.NewPostgresqlStore(ctx, dsn)
assert.NoError(t, err)
acctId := xid.New().String()
_, err = s.Pool.Query(ctx,
"insert into accounts (id) VALUES($1)", acctId)
assert.NoError(t, err)
_, err = s.Pool.Query(ctx,
"insert into groups (id, account_id) VALUES('g2-test-group-id-1',$1)", acctId)
assert.NoError(t, err)
groups, _, err := s.GetGroups(ctx, acctId)
assert.NoError(t, err)
assert.Len(t, groups, 1)
assert.NotEmpty(t, groups[0].PublicID)
}

View File

@@ -0,0 +1,57 @@
package networkmap_pgsql
import (
"context"
"testing"
networkmap_pgsql "github.com/netbirdio/netbird/management/internals/network_map_db/pgsql"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
"github.com/rs/xid"
"github.com/stretchr/testify/assert"
)
func TestGetNetworkRouters(t *testing.T) {
ctx := context.TODO()
s, err := networkmap_pgsql.NewPostgresqlStore(ctx, dsn)
assert.NoError(t, err)
acctId := xid.New().String()
_, err = s.Pool.Query(ctx,
"insert into accounts (id) VALUES($1)", acctId)
assert.NoError(t, err)
_, err = s.Pool.Query(ctx,
`insert into network_routers (id, account_id, public_id, peer, network_id, masquerade, metric, enabled, peer_groups)
VALUES('test-nr-id-1',$1,'public-id-1','peer-id-1','network-id-1',TRUE,999,TRUE,'["nr-test-group-id-1"]')`,
acctId)
_, err = s.Pool.Query(ctx,
`insert into network_routers (id, account_id, public_id, peer, network_id, masquerade, metric, enabled, peer_groups)
VALUES('test-nr-id-2',$1,'public-id-2','','network-id-2',TRUE,333,TRUE,'["nr-test-group-id-1","nr-test-group-id-2"]')`, acctId)
_, err = s.Pool.Query(ctx,
"insert into groups (id, account_id, public_id) VALUES('nr-test-group-id-1',$1,'public-id-1')", acctId)
_, err = s.Pool.Query(ctx,
"insert into groups (id, account_id, public_id) VALUES('nr-test-group-id-2',$1,'public-id-2')", acctId)
_, err = s.Pool.Query(ctx,
"insert into group_peers (peer_id, group_id) VALUES('peer-id-11','nr-test-group-id-1')")
assert.NoError(t, err)
_, err = s.Pool.Query(ctx,
"insert into group_peers (peer_id, group_id) VALUES('peer-id-22','nr-test-group-id-2')")
assert.NoError(t, err)
_, err = s.Pool.Query(ctx,
"insert into group_peers (peer_id, group_id) VALUES('peer-id-33','nr-test-group-id-2')")
assert.NoError(t, err)
routers, err := s.GetNetworkRouters(ctx, acctId)
assert.NoError(t, err)
assert.NotEmpty(t, routers)
assert.Equal(t, routers["network-id-1"],
map[string]*nmdata.NetworkRouter{"peer-id-1": {PublicID: "public-id-1", Masquerade: true, Metric: 999, Enabled: true, PeerGroups: []string{"peer-id-11"}}})
assert.Equal(t, routers["network-id-2"],
map[string]*nmdata.NetworkRouter{
"peer-id-11": {PublicID: "public-id-2", Masquerade: true, Metric: 333, Enabled: true, PeerGroups: []string{"peer-id-11", "peer-id-22", "peer-id-33"}},
"peer-id-22": {PublicID: "public-id-2", Masquerade: true, Metric: 333, Enabled: true, PeerGroups: []string{"peer-id-11", "peer-id-22", "peer-id-33"}},
"peer-id-33": {PublicID: "public-id-2", Masquerade: true, Metric: 333, Enabled: true, PeerGroups: []string{"peer-id-11", "peer-id-22", "peer-id-33"}}})
}

View File

@@ -18,6 +18,7 @@ import (
"github.com/netbirdio/netbird/management/internals/controllers/network_map"
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller/cache"
"github.com/netbirdio/netbird/management/internals/modules/peers/ephemeral"
networkmapdb "github.com/netbirdio/netbird/management/internals/network_map_db"
"github.com/netbirdio/netbird/management/internals/server/config"
"github.com/netbirdio/netbird/management/internals/shared/grpc"
"github.com/netbirdio/netbird/management/server/account"
@@ -30,6 +31,8 @@ import (
"github.com/netbirdio/netbird/management/server/telemetry"
"github.com/netbirdio/netbird/management/server/types"
sharedgrpc "github.com/netbirdio/netbird/shared/management/grpc"
"github.com/netbirdio/netbird/shared/management/networkmap"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
"github.com/netbirdio/netbird/shared/management/proto"
"github.com/netbirdio/netbird/shared/management/status"
"github.com/netbirdio/netbird/util"
@@ -61,6 +64,8 @@ type Controller struct {
serverSupportedSyncMessageVersion sharedgrpc.SyncMessageVersion
perAccountServerSupportedSyncMessageVersions map[string]sharedgrpc.SyncMessageVersion
nmdataStore *networkmapdb.NetworkMapDBStoreImpl
}
type bufferUpdate struct {
@@ -78,7 +83,7 @@ type bufferAffectedUpdate struct {
var _ network_map.Controller = (*Controller)(nil)
func NewController(ctx context.Context, store store.Store, metrics telemetry.AppMetrics, peersUpdateManager network_map.PeersUpdateManager, requestBuffer account.RequestBuffer, integratedPeerValidator integrated_validator.IntegratedValidator, settingsManager settings.Manager, dnsDomain string, proxyController port_forwarding.Controller, ephemeralPeersManager ephemeral.Manager, config *config.Config) *Controller {
func NewController(ctx context.Context, store store.Store, metrics telemetry.AppMetrics, peersUpdateManager network_map.PeersUpdateManager, requestBuffer account.RequestBuffer, integratedPeerValidator integrated_validator.IntegratedValidator, settingsManager settings.Manager, dnsDomain string, proxyController port_forwarding.Controller, ephemeralPeersManager ephemeral.Manager, config *config.Config, nmdataStore *networkmapdb.NetworkMapDBStoreImpl) *Controller {
nMetrics, err := newMetrics(metrics.UpdateChannelMetrics())
if err != nil {
log.Fatal(fmt.Errorf("error creating metrics: %w", err))
@@ -99,6 +104,7 @@ func NewController(ctx context.Context, store store.Store, metrics telemetry.App
EphemeralPeersManager: ephemeralPeersManager,
serverSupportedSyncMessageVersion: sharedgrpc.SyncMessageVersionFromConfig(config.HighestSupportedSyncMessageVersion),
perAccountServerSupportedSyncMessageVersions: sharedgrpc.SyncMessageVersionsFromMap(config.PerAccountHighestSupportedSyncMessageVersion),
nmdataStore: nmdataStore,
}
}
@@ -147,6 +153,11 @@ func (c *Controller) CountStreams() int {
func (c *Controller) sendUpdateAccountPeers(ctx context.Context, accountID string, reason types.UpdateReason) error {
log.WithContext(ctx).Tracef("updating peers for account %s from %s", accountID, util.GetCallerName())
if nmData := c.getNetworkMapData(ctx, accountID); nmData != nil {
return c.sendUpdateAccountPeersFromData(ctx, accountID, reason, nmData)
}
account, err := c.requestBuffer.GetAccountWithBackpressure(ctx, accountID)
if err != nil {
return fmt.Errorf("failed to get account: %v", err)
@@ -254,7 +265,7 @@ func (c *Controller) sendUpdateAccountPeers(ctx context.Context, accountID strin
// proxyNetworkMap rides the envelope as a ProxyPatch sidecar;
// the client merges it into Calculate()'s output the same
// way the legacy server did via NetworkMap.Merge.
update = grpc.ToComponentSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, p, nil, nil, components, proxyNetworkMap, dnsDomain, postureChecks, account.Settings, extraSetting, maps.Keys(peerGroups), dnsFwdPort)
update = grpc.ToComponentSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, types.TwinPeer(p), nil, nil, components, proxyNetworkMap, dnsDomain, postureChecks, types.TwinAccountSettings(account.Settings), extraSetting, maps.Keys(peerGroups), dnsFwdPort)
c.metrics.CountToComponentSyncResponseDuration(time.Since(start))
c.peersUpdateManager.SendUpdate(ctx, p.ID, &network_map.UpdateMessage{
@@ -275,7 +286,7 @@ func (c *Controller) sendUpdateAccountPeers(ctx context.Context, accountID strin
}
start = time.Now()
update = grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, p, nil, nil, nmap, dnsDomain, postureChecks, dnsCache, account.Settings, extraSetting, maps.Keys(peerGroups), dnsFwdPort)
update = grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, types.TwinPeer(p), nil, nil, nmap, dnsDomain, postureChecks, dnsCache, types.TwinAccountSettings(account.Settings), extraSetting, maps.Keys(peerGroups), dnsFwdPort)
c.metrics.CountToSyncResponseDuration(time.Since(start))
c.peersUpdateManager.SendUpdate(ctx, p.ID, &network_map.UpdateMessage{
@@ -293,6 +304,247 @@ func (c *Controller) sendUpdateAccountPeers(ctx context.Context, accountID strin
return nil
}
// sendUpdateAccountPeersFromData is the account-free variant of
// sendUpdateAccountPeers: everything is computed from the network-map DB
// store's twin data; only extra settings and validated peers are resolved at
// runtime. Proxy network maps and policy injection, private-service zones,
// group-to-user SSH mappings and forced routing-peer DNS resolution have no
// DB-backed source yet and are omitted.
func (c *Controller) sendUpdateAccountPeersFromData(ctx context.Context, accountID string, reason types.UpdateReason, nmData *networkmap.NetworkMapData) error {
peersToUpdate := c.connectedPeersFromData(nmData, nil)
if len(peersToUpdate) == 0 {
return nil
}
return c.sendUpdatesFromData(ctx, accountID, nmData, peersToUpdate, &reason)
}
// sendUpdateForAffectedPeersFromData is the account-free variant of
// sendUpdateForAffectedPeers.
func (c *Controller) sendUpdateForAffectedPeersFromData(ctx context.Context, accountID string, peerIDs []string, nmData *networkmap.NetworkMapData) error {
affected := make(map[string]struct{}, len(peerIDs))
for _, id := range peerIDs {
affected[id] = struct{}{}
}
peersToUpdate := c.connectedPeersFromData(nmData, affected)
if len(peersToUpdate) == 0 {
log.WithContext(ctx).Tracef("sendUpdateForAffectedPeersFromData: no peers to update (affected peers not found in data or no channels)")
return nil
}
log.WithContext(ctx).Tracef("sendUpdateForAffectedPeersFromData: sending network map to %d connected peers", len(peersToUpdate))
return c.sendUpdatesFromData(ctx, accountID, nmData, peersToUpdate, nil)
}
func (c *Controller) connectedPeersFromData(nmData *networkmap.NetworkMapData, affected map[string]struct{}) []*nmdata.Peer {
var result []*nmdata.Peer
for _, peer := range nmData.Peers {
if affected != nil {
if _, ok := affected[peer.ID]; !ok {
continue
}
}
if c.peersUpdateManager.HasChannel(peer.ID) {
result = append(result, peer)
}
}
return result
}
func (c *Controller) sendUpdatesFromData(ctx context.Context, accountID string, nmData *networkmap.NetworkMapData, peersToUpdate []*nmdata.Peer, reason *types.UpdateReason) error {
globalStart := time.Now()
extraSettings, err := c.settingsManager.GetExtraSettings(ctx, accountID)
if err != nil {
return fmt.Errorf("failed to get flow enabled status: %v", err)
}
dnsCache := &cache.DNSConfigCache{}
dnsDomain := c.getDNSDomainFromData(nmData.AccountSettings)
peersCustomZone := networkmap.PeersCustomZone(ctx, accountID, dnsDomain, nmData.Peers, ipv6AllowedPeersFromData(nmData))
dnsFwdPort := computeForwarderPortFromData(nmData.Peers, network_map.DnsForwarderPortMinVersion)
var wg sync.WaitGroup
semaphore := make(chan struct{}, 10)
for _, peer := range peersToUpdate {
if reason != nil && c.accountManagerMetrics != nil {
c.accountManagerMetrics.CountNmapTriggered(string(reason.Resource), string(reason.Operation))
}
wg.Add(1)
semaphore <- struct{}{}
go func(p *nmdata.Peer) {
defer wg.Done()
defer func() { <-semaphore }()
start := time.Now()
postureChecks := peerPostureChecksFromData(nmData, p.ID)
c.metrics.CountCalcPostureChecksDuration(time.Since(start))
start = time.Now()
peerGroups := maps.Keys(nmData.GetPeerGroups(p.ID))
var update *proto.SyncResponse
commonSyncMessageVersion := sharedgrpc.HighestCommonSyncMessageVersion(
c.perAccountOrGlobalSupportedSyncMessageVersions(accountID),
sharedgrpc.SyncMessageVersionFromConfig(&p.Meta.SyncMessageVersion))
log.WithContext(ctx).
WithFields(log.Fields{
"sync_message_version": commonSyncMessageVersion,
"server_sync_message_version": c.perAccountOrGlobalSupportedSyncMessageVersions(accountID),
"peer_sync_message_version": sharedgrpc.SyncMessageVersionFromConfig(&p.Meta.SyncMessageVersion),
}).Debug("common highest sync message version")
if commonSyncMessageVersion == sharedgrpc.ComponentNetworkMap {
components := nmData.GetPeerNetworkMapComponents(p.ID, peersCustomZone)
c.metrics.CountCalcPeerNetworkMapDuration(time.Since(start))
start = time.Now()
update = grpc.ToComponentSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, p, nil, nil, components, nil, dnsDomain, postureChecks, nmData.AccountSettings, extraSettings, peerGroups, dnsFwdPort)
c.metrics.CountToComponentSyncResponseDuration(time.Since(start))
c.peersUpdateManager.SendUpdate(ctx, p.ID, &network_map.UpdateMessage{
Update: update,
MessageType: network_map.MessageTypeNetworkMap,
})
return
}
nmap := networkMapFromData(ctx, nmData, p.ID, peersCustomZone)
c.metrics.CountCalcPeerNetworkMapDuration(time.Since(start))
start = time.Now()
update = grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, p, nil, nil, nmap, dnsDomain, postureChecks, dnsCache, nmData.AccountSettings, extraSettings, peerGroups, dnsFwdPort)
c.metrics.CountToSyncResponseDuration(time.Since(start))
c.peersUpdateManager.SendUpdate(ctx, p.ID, &network_map.UpdateMessage{
Update: update,
MessageType: network_map.MessageTypeNetworkMap,
})
}(peer)
}
wg.Wait()
if c.accountManagerMetrics != nil {
c.accountManagerMetrics.CountUpdateAccountPeersDuration(time.Since(globalStart))
}
return nil
}
func (c *Controller) getNetworkMapData(ctx context.Context, accountID string) *networkmap.NetworkMapData {
if c.nmdataStore == nil {
return nil
}
nmData, err := c.nmdataStore.GetNetworkMapData(ctx, accountID)
if err != nil {
log.WithContext(ctx).Errorf("failed to get network map data for account %s, falling back to account-based computation: %v", accountID, err)
return nil
}
return nmData
}
func (c *Controller) getDNSDomainFromData(settings *nmdata.AccountSettingsInfo) string {
if settings == nil || settings.DNSDomain == "" {
return c.dnsDomain
}
return settings.DNSDomain
}
func ipv6AllowedPeersFromData(nmData *networkmap.NetworkMapData) map[string]struct{} {
result := make(map[string]struct{})
if nmData.AccountSettings != nil {
for _, groupID := range nmData.AccountSettings.IPv6EnabledGroups {
group := nmData.Groups[groupID]
if group == nil {
continue
}
for _, peerID := range group.Peers {
result[peerID] = struct{}{}
}
}
}
for id, p := range nmData.Peers {
if p != nil && p.ProxyMeta.Embedded {
result[id] = struct{}{}
}
}
return result
}
func networkMapFromData(ctx context.Context, nmData *networkmap.NetworkMapData, peerID string, peersCustomZone nmdata.CustomZone) *types.NetworkMap {
components := nmData.GetPeerNetworkMapComponents(peerID, peersCustomZone)
if components.IsEmpty() {
return &types.NetworkMap{Network: components.Network}
}
return types.CalculateNetworkMapFromComponents(ctx, components)
}
// peerPostureChecksFromData mirrors getPeerPostureChecks on the twin store. The
// sync response only encodes process-check file paths, so only ProcessCheck is
// converted back to the server posture type.
func peerPostureChecksFromData(nmData *networkmap.NetworkMapData, peerID string) []*posture.Checks {
if len(nmData.PostureChecks) == 0 {
return nil
}
peerPostureChecks := make(map[string]*posture.Checks)
for _, policy := range nmData.Policies {
if policy == nil || !policy.Enabled || len(policy.SourcePostureChecks) == 0 {
continue
}
if !isPeerInPolicySourceGroupsFromData(nmData, peerID, policy) {
continue
}
for _, checkID := range policy.SourcePostureChecks {
twin := nmData.PostureChecks[checkID]
if twin == nil {
continue
}
peerPostureChecks[checkID] = postureChecksFromTwin(twin)
}
}
return maps.Values(peerPostureChecks)
}
func isPeerInPolicySourceGroupsFromData(nmData *networkmap.NetworkMapData, peerID string, policy *nmdata.Policy) bool {
for _, rule := range policy.Rules {
if rule == nil || !rule.Enabled {
continue
}
for _, groupID := range rule.Sources {
if group := nmData.Groups[groupID]; group != nil && slices.Contains(group.Peers, peerID) {
return true
}
}
}
return false
}
func postureChecksFromTwin(twin *nmdata.PostureChecks) *posture.Checks {
checks := &posture.Checks{ID: twin.ID}
if twin.Checks.ProcessCheck != nil {
processes := make([]posture.Process, 0, len(twin.Checks.ProcessCheck.Processes))
for _, p := range twin.Checks.ProcessCheck.Processes {
processes = append(processes, posture.Process{LinuxPath: p.LinuxPath, MacPath: p.MacPath, WindowsPath: p.WindowsPath})
}
checks.Checks.ProcessCheck = &posture.ProcessCheck{Processes: processes}
}
return checks
}
func (c *Controller) perAccountOrGlobalSupportedSyncMessageVersions(accountId string) sharedgrpc.SyncMessageVersion {
if perAccount, ok := c.perAccountServerSupportedSyncMessageVersions[accountId]; ok {
return perAccount
@@ -325,6 +577,10 @@ func (c *Controller) sendUpdateForAffectedPeers(ctx context.Context, accountID s
return nil
}
if nmData := c.getNetworkMapData(ctx, accountID); nmData != nil {
return c.sendUpdateForAffectedPeersFromData(ctx, accountID, peerIDs, nmData)
}
account, err := c.requestBuffer.GetAccountWithBackpressure(ctx, accountID)
if err != nil {
return fmt.Errorf("failed to get account: %v", err)
@@ -426,7 +682,7 @@ func (c *Controller) sendUpdateForAffectedPeers(ctx context.Context, accountID s
// proxyNetworkMap rides the envelope as a ProxyPatch sidecar;
// the client merges it into Calculate()'s output the same
// way the legacy server did via NetworkMap.Merge.
update = grpc.ToComponentSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, p, nil, nil, components, proxyNetworkMap, dnsDomain, postureChecks, account.Settings, extraSetting, maps.Keys(peerGroups), dnsFwdPort)
update = grpc.ToComponentSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, types.TwinPeer(p), nil, nil, components, proxyNetworkMap, dnsDomain, postureChecks, types.TwinAccountSettings(account.Settings), extraSetting, maps.Keys(peerGroups), dnsFwdPort)
c.metrics.CountToComponentSyncResponseDuration(time.Since(start))
c.peersUpdateManager.SendUpdate(ctx, p.ID, &network_map.UpdateMessage{
@@ -447,7 +703,7 @@ func (c *Controller) sendUpdateForAffectedPeers(ctx context.Context, accountID s
}
start = time.Now()
update = grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, p, nil, nil, nmap, dnsDomain, postureChecks, dnsCache, account.Settings, extraSetting, maps.Keys(peerGroups), dnsFwdPort)
update = grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, types.TwinPeer(p), nil, nil, nmap, dnsDomain, postureChecks, dnsCache, types.TwinAccountSettings(account.Settings), extraSetting, maps.Keys(peerGroups), dnsFwdPort)
c.metrics.CountToSyncResponseDuration(time.Since(start))
c.peersUpdateManager.SendUpdate(ctx, p.ID, &network_map.UpdateMessage{
@@ -564,7 +820,7 @@ func (c *Controller) UpdateAccountPeer(ctx context.Context, accountId string, pe
// proxyNetworkMap rides the envelope as a ProxyPatch sidecar;
// the client merges it into Calculate()'s output the same
// way the legacy server did via NetworkMap.Merge.
update = grpc.ToComponentSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, peer, nil, nil, components, proxyNetworkMap, dnsDomain, postureChecks, account.Settings, extraSettings, maps.Keys(peerGroups), dnsFwdPort)
update = grpc.ToComponentSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, types.TwinPeer(peer), nil, nil, components, proxyNetworkMap, dnsDomain, postureChecks, types.TwinAccountSettings(account.Settings), extraSettings, maps.Keys(peerGroups), dnsFwdPort)
c.peersUpdateManager.SendUpdate(ctx, peer.ID, &network_map.UpdateMessage{
Update: update,
@@ -581,7 +837,7 @@ func (c *Controller) UpdateAccountPeer(ctx context.Context, accountId string, pe
nmap.Merge(proxyNetworkMap)
}
update = grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, peer, nil, nil, nmap, dnsDomain, postureChecks, dnsCache, account.Settings, extraSettings, maps.Keys(peerGroups), dnsFwdPort)
update = grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, types.TwinPeer(peer), nil, nil, nmap, dnsDomain, postureChecks, dnsCache, types.TwinAccountSettings(account.Settings), extraSettings, maps.Keys(peerGroups), dnsFwdPort)
c.peersUpdateManager.SendUpdate(ctx, peer.ID, &network_map.UpdateMessage{
Update: update,
@@ -644,6 +900,10 @@ func (c *Controller) GetValidatedPeerWithComponents(ctx context.Context, isRequi
return peer, &types.NetworkMapComponents{Network: types.TwinNetwork(network)}, nil, nil, 0, nil
}
if nmData := c.getNetworkMapData(ctx, accountID); nmData != nil {
return c.getValidatedPeerWithComponentsFromData(ctx, accountID, peer, nmData)
}
account, err := c.requestBuffer.GetAccountWithBackpressure(ctx, accountID)
if err != nil {
return nil, nil, nil, nil, 0, err
@@ -688,6 +948,21 @@ func (c *Controller) GetValidatedPeerWithComponents(ctx context.Context, isRequi
return peer, components, proxyNetworkMaps[peer.ID], postureChecks, dnsFwdPort, nil
}
// getValidatedPeerWithComponentsFromData is the account-free variant of
// GetValidatedPeerWithComponents. The proxy network map fragment is omitted
// like on the other nmdata paths.
func (c *Controller) getValidatedPeerWithComponentsFromData(ctx context.Context, accountID string, peer *nbpeer.Peer, nmData *networkmap.NetworkMapData) (*nbpeer.Peer, *types.NetworkMapComponents, *types.NetworkMap, []*posture.Checks, int64, error) {
postureChecks := peerPostureChecksFromData(nmData, peer.ID)
dnsDomain := c.getDNSDomainFromData(nmData.AccountSettings)
peersCustomZone := networkmap.PeersCustomZone(ctx, accountID, dnsDomain, nmData.Peers, ipv6AllowedPeersFromData(nmData))
components := nmData.GetPeerNetworkMapComponents(peer.ID, peersCustomZone)
dnsFwdPort := computeForwarderPortFromData(nmData.Peers, network_map.DnsForwarderPortMinVersion)
return peer, components, nil, postureChecks, dnsFwdPort, nil
}
// BufferUpdateAffectedPeers accumulates peer IDs and flushes them after the buffer interval.
func (c *Controller) BufferUpdateAffectedPeers(ctx context.Context, accountID string, peerIDs []string, reason types.UpdateReason) error {
if len(peerIDs) == 0 {
@@ -799,6 +1074,10 @@ func (c *Controller) GetValidatedPeerWithMap(ctx context.Context, isRequiresAppr
return emptyMap, nil, 0, nil
}
if nmData := c.getNetworkMapData(ctx, accountID); nmData != nil {
return c.getValidatedPeerWithMapFromData(ctx, accountID, peerID, nmData)
}
account, err := c.requestBuffer.GetAccountWithBackpressure(ctx, accountID)
if err != nil {
return nil, nil, 0, err
@@ -846,6 +1125,21 @@ func (c *Controller) GetValidatedPeerWithMap(ctx context.Context, isRequiresAppr
return networkMap, postureChecks, dnsFwdPort, nil
}
// getValidatedPeerWithMapFromData is the account-free variant of
// GetValidatedPeerWithMap. The proxy network map fragment is omitted like on
// the other nmdata paths.
func (c *Controller) getValidatedPeerWithMapFromData(ctx context.Context, accountID string, peerID string, nmData *networkmap.NetworkMapData) (*types.NetworkMap, []*posture.Checks, int64, error) {
postureChecks := peerPostureChecksFromData(nmData, peerID)
dnsDomain := c.getDNSDomainFromData(nmData.AccountSettings)
peersCustomZone := networkmap.PeersCustomZone(ctx, accountID, dnsDomain, nmData.Peers, ipv6AllowedPeersFromData(nmData))
networkMap := networkMapFromData(ctx, nmData, peerID, peersCustomZone)
dnsFwdPort := computeForwarderPortFromData(nmData.Peers, network_map.DnsForwarderPortMinVersion)
return networkMap, postureChecks, dnsFwdPort, nil
}
// GetDNSDomain returns the configured dnsDomain
func (c *Controller) GetDNSDomain(settings *types.Settings) string {
if settings == nil {
@@ -908,20 +1202,36 @@ func (c *Controller) StartWarmup(ctx context.Context) {
// computeForwarderPort checks if all peers in the account have updated to a specific version or newer.
// If all peers have the required version, it returns the new well-known port (22054), otherwise returns 0.
func computeForwarderPort(peers []*nbpeer.Peer, requiredVersion string) int64 {
if len(peers) == 0 {
versions := make([]string, 0, len(peers))
for _, peer := range peers {
versions = append(versions, peer.Meta.WtVersion)
}
return computeForwarderPortFromVersions(versions, requiredVersion)
}
func computeForwarderPortFromData(peers map[string]*nmdata.Peer, requiredVersion string) int64 {
versions := make([]string, 0, len(peers))
for _, peer := range peers {
versions = append(versions, peer.Meta.WtVersion)
}
return computeForwarderPortFromVersions(versions, requiredVersion)
}
func computeForwarderPortFromVersions(wtVersions []string, requiredVersion string) int64 {
if len(wtVersions) == 0 {
return int64(network_map.OldForwarderPort)
}
reqVer := semver.Canonical(requiredVersion)
// Check if all peers have the required version or newer
for _, peer := range peers {
for _, wtVersion := range wtVersions {
// Development version is always supported
if version.IsDevelopmentVersion(peer.Meta.WtVersion) {
if version.IsDevelopmentVersion(wtVersion) {
continue
}
peerVersion := semver.Canonical("v" + peer.Meta.WtVersion)
peerVersion := semver.Canonical("v" + wtVersion)
if peerVersion == "" {
// If any peer doesn't have version info, return 0
return int64(network_map.OldForwarderPort)

View File

@@ -8,10 +8,13 @@ import (
"reflect"
"strings"
"github.com/rs/xid"
"golang.org/x/exp/maps"
"github.com/netbirdio/netbird/management/server/integrations/integrated_validator"
"github.com/netbirdio/netbird/management/server/settings"
"github.com/netbirdio/netbird/shared/management/networkmap"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
"github.com/rs/xid"
)
const (
@@ -22,7 +25,7 @@ const (
type NetworkMapDBStore interface {
GetGroups(ctx context.Context, accountId string) ([]nmdata.Group, map[string]map[string]any, error)
GetPeers(ctx context.Context, accountId string) ([]nmdata.Peer, map[string]*nmdata.Peer, error)
GetPeers(ctx context.Context, accountId string) ([]nmdata.Peer, map[string][]*nmdata.Peer, error)
GetPolicies(ctx context.Context, accountId string) ([]nmdata.Policy, map[string]map[string]any, map[string]map[string]any, error)
GetRoutes(ctx context.Context, accountId string) ([]nmdata.Route, error)
GetNameServerGroups(ctx context.Context, accountId string) ([]nmdata.NameServerGroup, error)
@@ -31,7 +34,7 @@ type NetworkMapDBStore interface {
GetNetwork(ctx context.Context, accountId string) (nmdata.Network, error)
GetAppliedZoneCandidates(ctx context.Context, accountId string) ([]networkmap.AppliedZoneCandidate, error)
GetAccountSettings(ctx context.Context, accountId string) (nmdata.AccountSettingsInfo, error)
GetPostureChecks(ctx context.Context, accountId string) ([]nmdata.PostureChecks, error)
GetPostureChecks(ctx context.Context, accountId string) ([]nmdata.PostureChecks, map[string]string, error)
GetNetworkMapData(ctx context.Context, accountId string) (*networkmap.NetworkMapData, error)
GetAllowedUsers(ctx context.Context, accountId string) (map[string]struct{}, map[string][]string, error)
GetDnsSettings(ctx context.Context, accountId string) (nmdata.DNSSettings, error)
@@ -40,6 +43,34 @@ type NetworkMapDBStore interface {
type NetworkMapDBStoreImpl struct {
store NetworkMapDBStore
integratedPeerValidator integrated_validator.IntegratedValidator
extraSettingsManager settings.Manager
}
func NewNetworkMapDBStoreImpl(store NetworkMapDBStore, integratedPeerValidator integrated_validator.IntegratedValidator, extraSettingsManager settings.Manager) *NetworkMapDBStoreImpl {
return &NetworkMapDBStoreImpl{
store: store,
integratedPeerValidator: integratedPeerValidator,
extraSettingsManager: extraSettingsManager,
}
}
func (s *NetworkMapDBStoreImpl) GetNetworkMapData(ctx context.Context, accountId string) (*networkmap.NetworkMapData, error) {
nmdata, err := s.store.GetNetworkMapData(ctx, accountId)
if err != nil {
return nil, err
}
extraSettings, err := s.extraSettingsManager.GetExtraSettings(ctx, accountId)
if err != nil {
return nil, err
}
nmdata.ValidatedPeers, err = s.integratedPeerValidator.GetValidatedPeers(ctx, accountId, maps.Values(nmdata.Groups), maps.Values(nmdata.Peers), extraSettings)
if err != nil {
return nil, err
}
return nmdata, nil
}
func FromSqlTypesToSharedTypes(src reflect.Value, dst reflect.Value) error {

View File

@@ -3,6 +3,7 @@ package networkmap_pgsql
import (
"context"
"database/sql"
"encoding/json"
"time"
"github.com/jackc/pgx/v5"
@@ -14,7 +15,14 @@ const (
select settings_peer_login_expiration_enabled as peer_login_expiration_enabled,
settings_peer_login_expiration as peer_login_expiration,
settings_peer_inactivity_expiration_enabled as peer_inactivity_expiration_enabled,
settings_peer_inactivity_expiration as peer_inactivity_expiration
settings_peer_inactivity_expiration as peer_inactivity_expiration,
settings_dns_domain as dns_domain,
settings_ipv6_enabled_groups as ipv6_enabled_groups,
settings_routing_peer_dns_resolution_enabled as routing_peer_dns_resolution_enabled,
settings_lazy_connection_enabled as lazy_connection_enabled,
settings_auto_update_version as auto_update_version,
settings_auto_update_always as auto_update_always,
settings_metrics_push_enabled as metrics_push_enabled
from accounts
where id=$1
`
@@ -40,12 +48,25 @@ func GetAccountSettingsViaPgxConnection(ctx context.Context, con *pgx.Conn, acco
return nmdata.AccountSettingsInfo{}, err
}
return nmdata.AccountSettingsInfo{
settingsInfo := nmdata.AccountSettingsInfo{
PeerLoginExpirationEnabled: settings.PeerLoginExpirationEnabled.Bool,
PeerLoginExpiration: time.Duration(settings.PeerLoginExpiration.Int64),
PeerInactivityExpirationEnabled: settings.PeerLoginExpirationEnabled.Bool,
PeerInactivityExpirationEnabled: settings.PeerInactivityExpirationEnabled.Bool,
PeerInactivityExpiration: time.Duration(settings.PeerInactivityExpiration.Int64),
}, nil
DNSDomain: settings.DNSDomain.String,
RoutingPeerDNSResolutionEnabled: settings.RoutingPeerDNSResolutionEnabled.Bool,
LazyConnectionEnabled: settings.LazyConnectionEnabled.Bool,
AutoUpdateVersion: settings.AutoUpdateVersion.String,
AutoUpdateAlways: settings.AutoUpdateAlways.Bool,
MetricsPushEnabled: settings.MetricsPushEnabled.Bool,
}
if settings.IPv6EnabledGroups != nil {
if err := json.Unmarshal(settings.IPv6EnabledGroups, &settingsInfo.IPv6EnabledGroups); err != nil {
return nmdata.AccountSettingsInfo{}, err
}
}
return settingsInfo, nil
}
type account struct {
@@ -53,4 +74,11 @@ type account struct {
PeerLoginExpiration sql.NullInt64
PeerInactivityExpirationEnabled sql.NullBool
PeerInactivityExpiration sql.NullInt64
DNSDomain sql.NullString
IPv6EnabledGroups json.RawMessage
RoutingPeerDNSResolutionEnabled sql.NullBool
LazyConnectionEnabled sql.NullBool
AutoUpdateVersion sql.NullString
AutoUpdateAlways sql.NullBool
MetricsPushEnabled sql.NullBool
}

View File

@@ -49,6 +49,10 @@ func GetAppliedZoneCandidatesViaPgxConnection(ctx context.Context, conn *pgx.Con
toret := make([]networkmap.AppliedZoneCandidate, 0, len(zones))
currentZoneId := ""
for _, z := range zones {
if !z.RecordType.Valid {
continue
}
zone := nmdata.CustomZone{}
err := networkmapdb.FromSqlTypesToSharedTypes(
reflect.ValueOf(&z), reflect.ValueOf(&zone))
@@ -61,33 +65,28 @@ func GetAppliedZoneCandidatesViaPgxConnection(ctx context.Context, conn *pgx.Con
return nil, err
}
if z.Id != currentZoneId {
zone.Records = []nmdata.SimpleRecord{}
toret = append(toret, appliedZoneCandidateFromZone(zone, distributionGroups))
currentZoneId = z.Id
}
rtype, rdata, err := recordTypeAndRdata(z.RecordType.String, z.RecordRData.String)
if err != nil {
if errors.Is(err, DnsUnsupportedRecordTypeError) {
continue
}
return nil, err
}
record := nmdata.SimpleRecord{
lastZone := &toret[len(toret)-1]
lastZone.Zone.Records = append(lastZone.Zone.Records, nmdata.SimpleRecord{
Name: z.RecordName.String,
Class: z.RecordClass.String,
TTL: int(z.RecordTTL.Int64),
RData: rdata,
Type: rtype,
}
zone.Records = []nmdata.SimpleRecord{record}
if len(toret) == 0 {
toret = append(toret, appliedZoneCandidateFromZone(zone, distributionGroups))
currentZoneId = z.Id
continue
}
if z.Id == currentZoneId {
lastZone := &toret[len(toret)-1]
lastZone.Zone.Records = append(lastZone.Zone.Records, record)
continue
}
toret = append(toret, appliedZoneCandidateFromZone(zone, distributionGroups))
currentZoneId = z.Id
})
}
return toret, nil
}

View File

@@ -0,0 +1,38 @@
package networkmap_pgsql
import (
"context"
"database/sql"
"github.com/jackc/pgx/v5"
)
const (
GetDomainsQuery = `
select domain, target_cluster
from domains
where account_id=$1
`
)
func (pg *PgStore) GetDomains(ctx context.Context, accountId string) ([]domain, error) {
c, err := pg.Pool.Acquire(ctx)
if err != nil {
return nil, err
}
return GetDomainsViaPgxConnection(ctx, c.Conn(), accountId)
}
func GetDomainsViaPgxConnection(ctx context.Context, conn *pgx.Conn, accountId string) ([]domain, error) {
rows, err := conn.Query(ctx, GetDomainsQuery, accountId)
if err != nil {
return nil, err
}
return pgx.CollectRows(rows, pgx.RowToStructByName[domain])
}
type domain struct {
Domain sql.NullString
TargetCluster sql.NullString
}

View File

@@ -241,8 +241,9 @@ func TestGetPostureChecks(t *testing.T) {
// err = loadSQL(ctx, s.pool, initDb)
//assert.NoError(t, err)
checks, err := s.GetPostureChecks(ctx, "cdfcks2t2r9s73a58us0") //"ckd7ee2fic3c73dtendg")
checks, idx, err := s.GetPostureChecks(ctx, "cdfcks2t2r9s73a58us0") //"ckd7ee2fic3c73dtendg")
assert.NoError(t, err)
assert.NotEmpty(t, idx)
fmt.Print(checks)
// assert.Contains(t,
@@ -281,3 +282,29 @@ func TestGetDnsSettings(t *testing.T) {
assert.NoError(t, err)
assert.NotEmpty(t, set.DisabledManagementGroups)
}
func TestGetDomains(t *testing.T) {
ctx := context.TODO()
s, err := NewPostgresqlStore(ctx, "postgresql://root:netbird@localhost:5432/netbird")
assert.NoError(t, err)
// err = loadSQL(ctx, s.pool, initDb)
//assert.NoError(t, err)
domains, err := s.GetDomains(ctx, "d8f79r2fadhs73c6uc0g") //"ckd7ee2fic3c73dtendg")
assert.NoError(t, err)
assert.NotEmpty(t, domains)
}
func TestGetServices(t *testing.T) {
ctx := context.TODO()
s, err := NewPostgresqlStore(ctx, "postgresql://root:netbird@localhost:5432/netbird")
assert.NoError(t, err)
// err = loadSQL(ctx, s.pool, initDb)
//assert.NoError(t, err)
svcs, err := s.GetPrivateServices(ctx, "d7jlh32fadhs73btp9u0") //"d6snsejl0ubs738s3f40") //"ckd7ee2fic3c73dtendg")
assert.NoError(t, err)
assert.NotEmpty(t, svcs)
}

View File

@@ -2,8 +2,11 @@ package networkmap_pgsql
import (
"context"
"fmt"
"strings"
"github.com/jackc/pgx/v5"
"github.com/miekg/dns"
"github.com/netbirdio/netbird/shared/management/networkmap"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
)
@@ -16,60 +19,72 @@ func (pg *PgStore) GetNetworkMapData(ctx context.Context, accountId string) (*ne
acctSettings, err := GetAccountSettingsViaPgxConnection(ctx, tx.Conn(), accountId)
if err != nil {
return rollbackAndReturnError(ctx, tx, err)
return rollbackAndReturnError(ctx, tx, fmt.Errorf("failed to get account settings: %w", err))
}
dnsZones, err := GetAppliedZoneCandidatesViaPgxConnection(ctx, tx.Conn(), accountId)
if err != nil {
return rollbackAndReturnError(ctx, tx, err)
return rollbackAndReturnError(ctx, tx, fmt.Errorf("failed to get applied zone candidates: %w", err))
}
groups, resourceToGroupIdx, err := GetGroupsViaPgxConnection(ctx, tx.Conn(), accountId)
if err != nil {
return rollbackAndReturnError(ctx, tx, err)
return rollbackAndReturnError(ctx, tx, fmt.Errorf("failed to get groups: %w", err))
}
nsGroups, err := GetNameServerGroupsViaPgxConnection(ctx, tx.Conn(), accountId)
if err != nil {
return rollbackAndReturnError(ctx, tx, err)
return rollbackAndReturnError(ctx, tx, fmt.Errorf("failed to get nameserver groups: %w", err))
}
networkResources, err := GetNetworkResourcesViaPgxConnection(ctx, tx.Conn(), accountId)
if err != nil {
return rollbackAndReturnError(ctx, tx, err)
return rollbackAndReturnError(ctx, tx, fmt.Errorf("failed to get network resources: %w", err))
}
routers, err := GetNetworkRoutersViaPgxConnection(ctx, tx.Conn(), accountId)
if err != nil {
return rollbackAndReturnError(ctx, tx, err)
return rollbackAndReturnError(ctx, tx, fmt.Errorf("failed to get network routers: %w", err))
}
network, err := GetNetworkViaPgxConnection(ctx, tx.Conn(), accountId)
if err != nil {
return rollbackAndReturnError(ctx, tx, err)
return rollbackAndReturnError(ctx, tx, fmt.Errorf("failed to get network: %w", err))
}
peers, _, err := GetPeersViaPgxConnection(ctx, tx.Conn(), accountId)
peers, proxyPeers, err := GetPeersViaPgxConnection(ctx, tx.Conn(), accountId)
if err != nil {
return rollbackAndReturnError(ctx, tx, err)
return rollbackAndReturnError(ctx, tx, fmt.Errorf("failed to get peers: %w", err))
}
policies, policyToDestinationResourceIdx, policyToDestinationGroupIdx, err := GetPoliciesViaPgxConnection(ctx, tx.Conn(), accountId)
if err != nil {
return rollbackAndReturnError(ctx, tx, err)
return rollbackAndReturnError(ctx, tx, fmt.Errorf("failed to get policies: %w", err))
}
postureChecks, err := GetPostureChecksViaPgxConnection(ctx, tx.Conn(), accountId)
postureChecks, postureCheckXIDToPublicID, err := GetPostureChecksViaPgxConnection(ctx, tx.Conn(), accountId)
if err != nil {
return rollbackAndReturnError(ctx, tx, err)
return rollbackAndReturnError(ctx, tx, fmt.Errorf("failed to get posture checks: %w", err))
}
routes, err := GetRoutesViaPgxConnection(ctx, tx.Conn(), accountId)
if err != nil {
return rollbackAndReturnError(ctx, tx, err)
return rollbackAndReturnError(ctx, tx, fmt.Errorf("failed to get routes: %w", err))
}
networkXIDToPublicID, err := GetNetworkXIDToPublicIdMapViaPgxConnection(ctx, tx.Conn(), accountId)
if err != nil {
return rollbackAndReturnError(ctx, tx, err)
return rollbackAndReturnError(ctx, tx, fmt.Errorf("failed to get network xid to public id map: %w", err))
}
allowedUserIds, groupsToUserIds, err := GetAllowedUsersViaPgxConnection(ctx, tx.Conn(), accountId)
if err != nil {
return rollbackAndReturnError(ctx, tx, err)
return rollbackAndReturnError(ctx, tx, fmt.Errorf("failed to get allowed users: %w", err))
}
dnsSettings, err := GetDnsSettingsViaPgxConnection(ctx, tx.Conn(), accountId)
if err != nil {
return rollbackAndReturnError(ctx, tx, fmt.Errorf("failed to get dns settings: %w", err))
}
domains, err := GetDomainsViaPgxConnection(ctx, tx.Conn(), accountId)
if err != nil {
return rollbackAndReturnError(ctx, tx, err)
}
services, err := GetPrivateServicesViaPgxConnection(ctx, tx.Conn(), accountId)
if err != nil {
return rollbackAndReturnError(ctx, tx, err)
}
proxyTargetedDomainResourceIDs, err := GetProxyTargetedDomainResourceIDsViaPgxConnection(ctx, tx.Conn(), accountId)
if err != nil {
return rollbackAndReturnError(ctx, tx, fmt.Errorf("failed to get proxy targeted domain resources: %w", err))
}
resourcePolicies := make(map[string][]*nmdata.Policy)
for _, resource := range networkResources {
@@ -83,7 +98,7 @@ func (pg *PgStore) GetNetworkMapData(ctx context.Context, accountId string) (*ne
}
if _, ok := policyToDestinationResourceIdx[policy.ID][resource.ID]; ok {
resourcePolicies[resource.ID] = append(resourcePolicies[resource.ID], &policy) // TODO (dmitri) maybe use public id?
break
continue
}
if groupIds, ok := policyToDestinationGroupIdx[policy.ID]; ok {
for networkResourceGroup := range networkResourceGroups {
@@ -102,22 +117,25 @@ func (pg *PgStore) GetNetworkMapData(ctx context.Context, accountId string) (*ne
}
toret := networkmap.NetworkMapData{
AccountSettings: &acctSettings,
DNSSettings: &dnsSettings,
Network: &network,
Peers: toMap(peers, func(p nmdata.Peer) string { return p.ID }),
Groups: toMap(groups, func(g nmdata.Group) string { return g.PublicID }),
Policies: toSliceOfPtrs(policies),
ResourcePolicies: resourcePolicies,
Routes: toSliceOfPtrs(routes),
Routers: routers,
NameServerGroups: toSliceOfPtrs(nsGroups),
NetworkResources: toSliceOfPtrs(networkResources),
PostureChecks: toMap(postureChecks, func(pc nmdata.PostureChecks) string { return pc.ID }),
AllowedUserIDs: allowedUserIds,
GroupIDToUserIDs: groupsToUserIds,
NetworkXIDToPublicID: networkXIDToPublicID, // TODO (dmitri) maybe we can switch to public ids everywhere?
AppliedZoneCandidates: dnsZones,
AccountSettings: &acctSettings,
DNSSettings: &dnsSettings,
Network: &network,
Peers: toMap(peers, func(p nmdata.Peer) string { return p.ID }),
Groups: toMap(groups, func(g nmdata.Group) string { return g.ID }),
Policies: toSliceOfPtrs(policies),
ResourcePolicies: resourcePolicies,
Routes: toSliceOfPtrs(routes),
Routers: routers,
NameServerGroups: toSliceOfPtrs(nsGroups),
NetworkResources: toSliceOfPtrs(networkResources),
PostureChecks: toMap(postureChecks, func(pc nmdata.PostureChecks) string { return pc.ID }),
AllowedUserIDs: allowedUserIds,
GroupIDToUserIDs: groupsToUserIds,
NetworkXIDToPublicID: networkXIDToPublicID, // TODO (dmitri) maybe we can switch to public ids everywhere?
AppliedZoneCandidates: dnsZones,
PrivateServiceCandidates: buildPrivateServiceCandidates(services, domains, proxyPeers),
PostureCheckXIDToPublicID: postureCheckXIDToPublicID,
ProxyTargetedDomainResourceIDs: proxyTargetedDomainResourceIDs,
}
return &toret, nil
@@ -139,9 +157,85 @@ func toMap[T any](all []T, id func(t T) string) map[string]*T {
}
func toSliceOfPtrs[T any](all []T) []*T {
toret := make([]*T, len(all))
toret := make([]*T, 0, len(all))
for _, t := range all {
toret = append(toret, &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
}
var records []nmdata.SimpleRecord
for _, proxyPeer := range proxyPeersByCluster[svc.ProxyCluster.String] {
if !proxyPeer.IP.IsValid() {
continue
}
records = append(records, nmdata.SimpleRecord{
Name: dns.Fqdn(svc.Domain.String),
Type: int(dns.TypeA),
Class: "IN",
TTL: 5,
RData: proxyPeer.IP.String(),
})
}
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
}

View File

@@ -65,6 +65,7 @@ func GetNetworkRoutersViaPgxConnection(ctx context.Context, con *pgx.Conn, accou
}
if router.Peer.String != "" {
toret[networkId][router.Peer.String] = &nmdatarouter
continue
}
for _, peerId := range router.PeersViaGroups {
toret[networkId][peerId] = &nmdatarouter
@@ -78,7 +79,7 @@ type networkrouter struct {
PublicID sql.NullString
NetworkID sql.NullString `nmap:"skip"`
Peer sql.NullString `nmap:"skip"`
PeersViaGroups []string `nmap:"skip"`
PeersViaGroups []string `nmap:"map_to:PeerGroups"`
Masquerade sql.NullBool
Metric sql.NullInt64
Enabled sql.NullBool

View File

@@ -23,7 +23,7 @@ func (pg *PgStore) GetNetworks(ctx context.Context, accountId string) ([]network
}
func GetNetworksViaPgxConnection(ctx context.Context, con *pgx.Conn, accountId string) ([]network, error) {
rows, err := con.Query(ctx, GetGroupsQuery, accountId)
rows, err := con.Query(ctx, GetNetworksQuery, accountId)
if err != nil {
return nil, err
}

View File

@@ -13,16 +13,16 @@ import (
const (
GetPeersQuery = `
select id, key, ssh_key, dns_label, user_id, ssh_enabled, login_expiration_enabled, last_login, ip, ipv6,
peer_status_requires_approval, proxy_meta_embedded, proxy_meta_cluster,
meta_wt_version, meta_go_os, meta_os_version, meta_kernel_version, meta_network_addresses, meta_files, meta_capabilities, meta_flags,
select id, key, ssh_key, dns_label, extra_dns_labels, user_id, ssh_enabled, login_expiration_enabled, last_login, ip, ipv6,
peer_status_requires_approval, peer_status_connected, proxy_meta_embedded, proxy_meta_cluster,
meta_wt_version, meta_go_os, meta_os_version, meta_kernel_version, meta_network_addresses, meta_files, meta_capabilities, meta_flags, meta_sync_message_version,
location_country_code, location_city_name, location_connection_ip
from peers
where account_id = $1
`
)
func (pg *PgStore) GetPeers(ctx context.Context, accountId string) ([]nmdata.Peer, map[string]*nmdata.Peer, error) {
func (pg *PgStore) GetPeers(ctx context.Context, accountId string) ([]nmdata.Peer, map[string][]*nmdata.Peer, error) {
c, err := pg.Pool.Acquire(ctx)
if err != nil {
return nil, nil, err
@@ -30,7 +30,7 @@ func (pg *PgStore) GetPeers(ctx context.Context, accountId string) ([]nmdata.Pee
return GetPeersViaPgxConnection(ctx, c.Conn(), accountId)
}
func GetPeersViaPgxConnection(ctx context.Context, con *pgx.Conn, accountId string) ([]nmdata.Peer, map[string]*nmdata.Peer, error) {
func GetPeersViaPgxConnection(ctx context.Context, con *pgx.Conn, accountId string) ([]nmdata.Peer, map[string][]*nmdata.Peer, error) {
rows, err := con.Query(ctx, GetPeersQuery, accountId)
if err != nil {
return nil, nil, err
@@ -42,7 +42,7 @@ func GetPeersViaPgxConnection(ctx context.Context, con *pgx.Conn, accountId stri
}
toret := make([]nmdata.Peer, 0, len(peers))
clusterToPeerIdx := make(map[string]*nmdata.Peer)
clusterToPeerIdx := make(map[string][]*nmdata.Peer)
for _, p := range peers {
dp := nmdata.Peer{}
err := networkmapdb.FromSqlTypesToSharedTypes(
@@ -54,12 +54,16 @@ func GetPeersViaPgxConnection(ctx context.Context, con *pgx.Conn, accountId stri
if p.ProxyMetaEmbedded.Valid {
dp.ProxyMeta.Embedded = p.ProxyMetaEmbedded.Bool
}
if dp.ProxyMeta.Embedded {
clusterToPeerIdx[p.ProxyMetaCluster.String] = &dp
// This is only used to build private service candidates, not connected peers are skipped
if dp.ProxyMeta.Embedded && p.PeerStatusConnected.Bool {
clusterToPeerIdx[p.ProxyMetaCluster.String] = append(clusterToPeerIdx[p.ProxyMetaCluster.String], &dp)
}
if p.MetaWtVersion.Valid {
dp.Meta.WtVersion = p.MetaWtVersion.String
}
if p.MetaSyncMessageVersion.Valid {
dp.Meta.SyncMessageVersion = int(p.MetaSyncMessageVersion.Int64)
}
if p.MetaGoOS.Valid {
dp.Meta.GoOS = p.MetaGoOS.String
}
@@ -105,6 +109,8 @@ func GetPeersViaPgxConnection(ctx context.Context, con *pgx.Conn, accountId stri
return toret, nil, err
}
}
toret = append(toret, dp)
}
return toret, clusterToPeerIdx, nil
@@ -116,10 +122,12 @@ type peer struct {
Key sql.NullString
SSHKey sql.NullString
DNSLabel sql.NullString
ExtraDNSLabels json.RawMessage
UserID sql.NullString
LastLogin sql.NullTime
SSHEnabled sql.NullBool
LoginExpirationEnabled sql.NullBool
PeerStatusConnected sql.NullBool `nmap:"skip"`
PeerStatusRequiresApproval sql.NullBool `nmap:"map_to:RequiresApproval"`
ProxyMetaEmbedded sql.NullBool `nmap:"skip"`
ProxyMetaCluster sql.NullString `nmap:"skip"`
@@ -134,6 +142,7 @@ type peer struct {
MetaGoOS sql.NullString `nmap:"skip"`
MetaOSVersion sql.NullString `nmap:"skip"`
MetaKernelVersion sql.NullString `nmap:"skip"`
MetaSyncMessageVersion sql.NullInt64 `nmap:"skip"`
LocationCountryCode sql.NullString `nmap:"skip"`
LocationCityName sql.NullString `nmap:"skip"`
}

View File

@@ -23,7 +23,7 @@ type PgStore struct {
}
func NewPostgresqlStore(ctx context.Context, dsn string) (*PgStore, error) {
pool, err := connectToPgDb(context.Background(), dsn)
pool, err := connectToPgDb(ctx, dsn)
if err != nil {
return nil, err
}

View File

@@ -86,11 +86,13 @@ func GetPoliciesViaPgxConnection(ctx context.Context, con *pgx.Conn, accountId s
return toret, nil, nil, err
}
for _, dst := range pr().Destinations {
if _, ok := policyToDestinationGroupIdx[p.ID]; !ok {
policyToDestinationGroupIdx[p.ID] = make(map[string]any)
if p.RuleEnabled.Valid && p.RuleEnabled.Bool {
for _, dst := range pr().Destinations {
if _, ok := policyToDestinationGroupIdx[p.ID]; !ok {
policyToDestinationGroupIdx[p.ID] = make(map[string]any)
}
policyToDestinationGroupIdx[p.ID][dst] = struct{}{}
}
policyToDestinationGroupIdx[p.ID][dst] = struct{}{}
}
}
if len(p.SourceResource) > 0 {
@@ -105,10 +107,12 @@ func GetPoliciesViaPgxConnection(ctx context.Context, con *pgx.Conn, accountId s
return toret, nil, nil, err
}
if _, ok := policyToDestinationResourceIdx[p.ID]; !ok {
policyToDestinationResourceIdx[p.ID] = make(map[string]any)
if p.RuleEnabled.Valid && p.RuleEnabled.Bool {
if _, ok := policyToDestinationResourceIdx[p.ID]; !ok {
policyToDestinationResourceIdx[p.ID] = make(map[string]any)
}
policyToDestinationResourceIdx[p.ID][pr().DestinationResource.ID] = struct{}{}
}
policyToDestinationResourceIdx[p.ID][pr().DestinationResource.ID] = struct{}{}
}
if len(p.Ports) > 0 {
err := json.Unmarshal([]byte(p.Ports), &pr().Ports)

View File

@@ -2,6 +2,7 @@ package networkmap_pgsql
import (
"context"
"database/sql"
"encoding/json"
"reflect"
@@ -12,45 +13,48 @@ import (
const (
GetPostureChecksQuery = `
select public_id as id, checks
select id, public_id, checks
from posture_checks
where account_id=$1
`
)
func (pg *PgStore) GetPostureChecks(ctx context.Context, accountId string) ([]nmdata.PostureChecks, error) {
func (pg *PgStore) GetPostureChecks(ctx context.Context, accountId string) ([]nmdata.PostureChecks, map[string]string, error) {
c, err := pg.Pool.Acquire(ctx)
if err != nil {
return nil, err
return nil, nil, err
}
return GetPostureChecksViaPgxConnection(ctx, c.Conn(), accountId)
}
func GetPostureChecksViaPgxConnection(ctx context.Context, con *pgx.Conn, accountId string) ([]nmdata.PostureChecks, error) {
func GetPostureChecksViaPgxConnection(ctx context.Context, con *pgx.Conn, accountId string) ([]nmdata.PostureChecks, map[string]string, error) {
rows, err := con.Query(ctx, GetPostureChecksQuery, accountId)
if err != nil {
return nil, err
return nil, nil, err
}
checks, err := pgx.CollectRows(rows, pgx.RowToStructByName[posturechecks])
if err != nil {
return nil, err
return nil, nil, err
}
toret := make([]nmdata.PostureChecks, 0, len(checks))
idToPublicIDIdx := make(map[string]string)
for _, c := range checks {
checks := nmdata.PostureChecks{}
err := networkmapdb.FromSqlTypesToSharedTypes(reflect.ValueOf(&c), reflect.ValueOf(&checks))
if err != nil {
return nil, err
return nil, nil, err
}
toret = append(toret, checks)
idToPublicIDIdx[checks.ID] = c.PublicID.String
}
return toret, nil
return toret, idToPublicIDIdx, nil
}
type posturechecks struct {
ID string
Checks json.RawMessage
ID string
PublicID sql.NullString `nmap:"skip"`
Checks json.RawMessage
}

View File

@@ -0,0 +1,67 @@
package networkmap_pgsql
import (
"context"
"database/sql"
"github.com/jackc/pgx/v5"
)
const (
GetServicesQuery = `
select enabled, private, array (select json_array_elements_text(access_groups::json)) as access_groups, proxy_cluster, domain
from services
where account_id=$1
`
GetProxyTargetedDomainResourcesQuery = `
select t.target_id
from targets as t
join services as s on s.id = t.service_id
where s.account_id=$1 and s.enabled and not coalesce(s.terminated, false)
and t.enabled and t.target_type='domain' and t.target_id is not null
`
)
func (pg *PgStore) GetPrivateServices(ctx context.Context, accountId string) ([]service, error) {
c, err := pg.Pool.Acquire(ctx)
if err != nil {
return nil, err
}
return GetPrivateServicesViaPgxConnection(ctx, c.Conn(), accountId)
}
func GetPrivateServicesViaPgxConnection(ctx context.Context, conn *pgx.Conn, accountId string) ([]service, error) {
rows, err := conn.Query(ctx, GetServicesQuery, accountId)
if err != nil {
return nil, err
}
return pgx.CollectRows(rows, pgx.RowToStructByName[service])
}
func GetProxyTargetedDomainResourceIDsViaPgxConnection(ctx context.Context, conn *pgx.Conn, accountId string) (map[string]struct{}, error) {
rows, err := conn.Query(ctx, GetProxyTargetedDomainResourcesQuery, accountId)
if err != nil {
return nil, err
}
ids, err := pgx.CollectRows(rows, pgx.RowTo[string])
if err != nil {
return nil, err
}
toret := make(map[string]struct{}, len(ids))
for _, id := range ids {
toret[id] = struct{}{}
}
return toret, nil
}
type service struct {
Enabled sql.NullBool
Private sql.NullBool
AccessGroups []string
ProxyCluster sql.NullString
Domain sql.NullString
}

View File

@@ -2,17 +2,21 @@ package networkmap_pgsql
import (
"context"
"encoding/json"
"github.com/jackc/pgx/v5"
)
const (
GetAllowedUserIdsQuery = `
select id, auto_groups
select id, array (select json_array_elements_text(auto_groups::json)) as auto_groups
from users
where account_id=$1 and not blocked and not is_service_user
`
GetAllGroupIdQuery = `
select id from groups
where account_id=$1 and name='All'
`
)
func (pg *PgStore) GetAllowedUsers(ctx context.Context, accountId string) (map[string]struct{}, map[string][]string, error) {
@@ -34,18 +38,29 @@ func GetAllowedUsersViaPgxConnection(ctx context.Context, con *pgx.Conn, account
return nil, nil, err
}
rows, err = con.Query(ctx, GetAllGroupIdQuery, accountId)
if err != nil {
return nil, nil, err
}
allGroupIds, err := pgx.CollectRows(rows, pgx.RowTo[string])
if err != nil {
return nil, nil, err
}
allGroupId := ""
if len(allGroupIds) > 0 {
allGroupId = allGroupIds[0]
}
userIdIdx := make(map[string]struct{})
groupIdToUserIds := make(map[string][]string)
for _, user := range users {
userIdIdx[user.ID] = struct{}{}
var groupIds []string
if err := json.Unmarshal(user.AutoGroups, &groupIds); err != nil {
return nil, nil, err
}
for _, groupId := range groupIds {
for _, groupId := range user.AutoGroups {
groupIdToUserIds[groupId] = append(groupIdToUserIds[groupId], user.ID)
}
if allGroupId != "" {
groupIdToUserIds[allGroupId] = append(groupIdToUserIds[allGroupId], user.ID)
}
}
return userIdIdx, groupIdToUserIds, nil
@@ -53,5 +68,5 @@ func GetAllowedUsersViaPgxConnection(ctx context.Context, con *pgx.Conn, account
type user struct {
ID string
AutoGroups json.RawMessage
AutoGroups []string
}

View File

@@ -7,6 +7,7 @@ import (
"crypto/tls"
"net/http"
"net/netip"
"os"
"slices"
"time"
@@ -24,13 +25,15 @@ import (
"github.com/netbirdio/netbird/encryption"
"github.com/netbirdio/netbird/formatter/hook"
"github.com/netbirdio/netbird/management/internals/modules/agentnetwork"
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/accesslogs"
accesslogsmanager "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/accesslogs/manager"
rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
networkmapdb "github.com/netbirdio/netbird/management/internals/network_map_db"
networkmap_pgsql "github.com/netbirdio/netbird/management/internals/network_map_db/pgsql"
nbgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
"github.com/netbirdio/netbird/management/server/activity"
activitystore "github.com/netbirdio/netbird/management/server/activity/store"
"github.com/netbirdio/netbird/management/internals/modules/agentnetwork"
nbcache "github.com/netbirdio/netbird/management/server/cache"
nbContext "github.com/netbirdio/netbird/management/server/context"
nbhttp "github.com/netbirdio/netbird/management/server/http"
@@ -99,6 +102,22 @@ func (s *BaseServer) Store() store.Store {
})
}
func (s *BaseServer) NetworkMapStore() *networkmapdb.NetworkMapDBStoreImpl {
return Create(s, func() *networkmapdb.NetworkMapDBStoreImpl {
dsn := os.Getenv("NETBIRD_NMAP_STORE_DSN") // Todo: this needs to be hoocked up properly
if dsn == "" {
return nil
}
store, err := networkmap_pgsql.NewPostgresqlStore(context.Background(), dsn)
if err != nil {
log.Fatalf("failed to create network map store: %v", err)
}
return networkmapdb.NewNetworkMapDBStoreImpl(store, s.IntegratedValidator(), s.SettingsManager())
})
}
func (s *BaseServer) EventStore() activity.Store {
return Create(s, func() activity.Store {
var err error

View File

@@ -123,7 +123,7 @@ func (s *BaseServer) EphemeralManager() ephemeral.Manager {
func (s *BaseServer) NetworkMapController() network_map.Controller {
return Create(s, func() network_map.Controller {
return nmapcontroller.NewController(context.Background(), s.Store(), s.Metrics(), s.PeersUpdateManager(), s.AccountRequestBuffer(), s.IntegratedValidator(), s.SettingsManager(), s.DNSDomain(), s.ProxyController(), s.EphemeralManager(), s.Config)
return nmapcontroller.NewController(context.Background(), s.Store(), s.Metrics(), s.PeersUpdateManager(), s.AccountRequestBuffer(), s.IntegratedValidator(), s.SettingsManager(), s.DNSDomain(), s.ProxyController(), s.EphemeralManager(), s.Config, s.NetworkMapStore())
})
}

View File

@@ -6,7 +6,7 @@ import (
"github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/shared/management/networkmap"
nmdata "github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
"github.com/netbirdio/netbird/shared/management/proto"
)
@@ -215,7 +215,9 @@ func (e *componentEncoder) encodeGroups() []*proto.GroupCompact {
groupCompactResources := func() []*proto.ResourceCompact {
var toret []*proto.ResourceCompact
for _, r := range g.Resources {
toret = append(toret, e.resourceToProto(r))
if pr := e.resourceToProto(r); pr != nil {
toret = append(toret, pr)
}
}
return toret
}

View File

@@ -7,7 +7,6 @@ import (
"github.com/netbirdio/netbird/client/ssh/auth"
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/posture"
"github.com/netbirdio/netbird/management/server/types"
sharedgrpc "github.com/netbirdio/netbird/shared/management/grpc"
@@ -32,14 +31,14 @@ func ToComponentSyncResponse(
config *nbconfig.Config,
httpConfig *nbconfig.HttpServerConfig,
deviceFlowConfig *nbconfig.DeviceAuthorizationFlow,
peer *nbpeer.Peer,
peer *nmdata.Peer,
turnCredentials *Token,
relayCredentials *Token,
components *types.NetworkMapComponents,
proxyPatch *types.NetworkMap,
dnsName string,
checks []*posture.Checks,
settings *types.Settings,
settings *nmdata.AccountSettingsInfo,
extraSettings *types.ExtraSettings,
peerGroups []string,
dnsFwdPort int64,
@@ -146,7 +145,7 @@ func toProxyPatch(nm *types.NetworkMap, dnsName string, includeIPv6, useSourcePr
//
// The full SSH AuthorizedUsers map is still produced by the client when it
// runs Calculate() over the envelope.
func computeSSHEnabledForPeer(c *types.NetworkMapComponents, peer *nbpeer.Peer) bool {
func computeSSHEnabledForPeer(c *types.NetworkMapComponents, peer *nmdata.Peer) bool {
if c == nil || peer == nil {
return false
}
@@ -171,7 +170,7 @@ func computeSSHEnabledForPeer(c *types.NetworkMapComponents, peer *nbpeer.Peer)
// ruleEnablesSSHForPeer returns true when rule is active, targets peer, and
// either explicitly authorises SSH or covers the legacy TCP/22 path while the
// peer itself has SSH enabled locally.
func ruleEnablesSSHForPeer(c *types.NetworkMapComponents, rule *nmdata.PolicyRule, peer *nbpeer.Peer) bool {
func ruleEnablesSSHForPeer(c *types.NetworkMapComponents, rule *nmdata.PolicyRule, peer *nmdata.Peer) bool {
if rule == nil || !rule.Enabled {
return false
}

View File

@@ -5,7 +5,6 @@ import (
"github.com/stretchr/testify/assert"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
)
@@ -18,10 +17,10 @@ func TestComputeSSHEnabledForPeer(t *testing.T) {
const targetPeerID = "target"
const targetGroupID = "g_dst"
mkComponents := func(rule *nmdata.PolicyRule, sshEnabled bool) (*types.NetworkMapComponents, *nbpeer.Peer) {
peer := &nbpeer.Peer{ID: targetPeerID, SSHEnabled: sshEnabled}
mkComponents := func(rule *nmdata.PolicyRule, sshEnabled bool) (*types.NetworkMapComponents, *nmdata.Peer) {
peer := &nmdata.Peer{ID: targetPeerID, SSHEnabled: sshEnabled}
return &types.NetworkMapComponents{
Peers: map[string]*nmdata.Peer{targetPeerID: types.TwinPeer(peer)},
Peers: map[string]*nmdata.Peer{targetPeerID: peer},
Groups: map[string]*nmdata.Group{targetGroupID: {Name: "dst", Peers: []string{targetPeerID}}},
Policies: []*nmdata.Policy{{
ID: "p",
@@ -156,7 +155,7 @@ func TestComputeSSHEnabledForPeer(t *testing.T) {
// belt-and-suspenders presence guard mirroring Calculate's
// getAllPeersFromGroups invariant.
func TestComputeSSHEnabledForPeer_TargetMissingFromComponents(t *testing.T) {
peer := &nbpeer.Peer{ID: "missing", SSHEnabled: true}
peer := &nmdata.Peer{ID: "missing", SSHEnabled: true}
c := &types.NetworkMapComponents{
Peers: map[string]*nmdata.Peer{}, // target peer NOT present
Groups: map[string]*nmdata.Group{
@@ -179,6 +178,6 @@ func TestComputeSSHEnabledForPeer_TargetMissingFromComponents(t *testing.T) {
// exported indirectly via ToComponentSyncResponse and may receive nil
// components on graceful-degrade paths.
func TestComputeSSHEnabledForPeer_NilInputs(t *testing.T) {
assert.False(t, computeSSHEnabledForPeer(nil, &nbpeer.Peer{ID: "x"}))
assert.False(t, computeSSHEnabledForPeer(nil, &nmdata.Peer{ID: "x"}))
assert.False(t, computeSSHEnabledForPeer(&types.NetworkMapComponents{}, nil))
}

View File

@@ -18,7 +18,6 @@ import (
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller/cache"
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/posture"
"github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/shared/management/networkmap"
@@ -48,7 +47,7 @@ func init() {
// nil when no server config is set (the fan-out network-map path) because clients treat any
// non-nil config as authoritative: a config without a relay section is interpreted as relay
// disabled and wipes the clients' relay URLs.
func toNetbirdConfig(config *nbconfig.Config, turnCredentials *Token, relayToken *Token, extraSettings *types.ExtraSettings, settings *types.Settings) *proto.NetbirdConfig {
func toNetbirdConfig(config *nbconfig.Config, turnCredentials *Token, relayToken *Token, extraSettings *types.ExtraSettings, settings *nmdata.AccountSettingsInfo) *proto.NetbirdConfig {
if config == nil {
return nil
}
@@ -120,7 +119,7 @@ func toNetbirdConfig(config *nbconfig.Config, turnCredentials *Token, relayToken
return nbConfig
}
func toPeerConfig(peer *nbpeer.Peer, network *nmdata.Network, dnsName string, settings *types.Settings, httpConfig *nbconfig.HttpServerConfig, deviceFlowConfig *nbconfig.DeviceAuthorizationFlow, enableSSH bool, forceRoutingPeerDNS bool) *proto.PeerConfig {
func toPeerConfig(peer *nmdata.Peer, network *nmdata.Network, dnsName string, settings *nmdata.AccountSettingsInfo, httpConfig *nbconfig.HttpServerConfig, deviceFlowConfig *nbconfig.DeviceAuthorizationFlow, enableSSH bool, forceRoutingPeerDNS bool) *proto.PeerConfig {
netmask, _ := network.Net.Mask.Size()
fqdn := peer.FQDN(dnsName)
@@ -155,7 +154,7 @@ func toPeerConfig(peer *nbpeer.Peer, network *nmdata.Network, dnsName string, se
return peerConfig
}
func ToSyncResponse(ctx context.Context, config *nbconfig.Config, httpConfig *nbconfig.HttpServerConfig, deviceFlowConfig *nbconfig.DeviceAuthorizationFlow, peer *nbpeer.Peer, turnCredentials *Token, relayCredentials *Token, networkMap *types.NetworkMap, dnsName string, checks []*posture.Checks, dnsCache *cache.DNSConfigCache, settings *types.Settings, extraSettings *types.ExtraSettings, peerGroups []string, dnsFwdPort int64) *proto.SyncResponse {
func ToSyncResponse(ctx context.Context, config *nbconfig.Config, httpConfig *nbconfig.HttpServerConfig, deviceFlowConfig *nbconfig.DeviceAuthorizationFlow, peer *nmdata.Peer, turnCredentials *Token, relayCredentials *Token, networkMap *types.NetworkMap, dnsName string, checks []*posture.Checks, dnsCache *cache.DNSConfigCache, settings *nmdata.AccountSettingsInfo, extraSettings *types.ExtraSettings, peerGroups []string, dnsFwdPort int64) *proto.SyncResponse {
// IPv6 data in AllowedIPs and SourcePrefixes wildcard expansion depends on
// whether the target peer supports IPv6. Routes and firewall rules are already
// filtered at the source (network map builder).

View File

@@ -276,7 +276,7 @@ func TestToNetbirdConfig_RelayInvariant(t *testing.T) {
settings := &types.Settings{MetricsPushEnabled: true}
t.Run("nil server config returns nil config", func(t *testing.T) {
nbCfg := toNetbirdConfig(nil, nil, nil, nil, settings)
nbCfg := toNetbirdConfig(nil, nil, nil, nil, types.TwinAccountSettings(settings))
assert.Nil(t, nbCfg, "fan-out updates must not carry a partial NetbirdConfig even when settings are present")
})
@@ -291,7 +291,7 @@ func TestToNetbirdConfig_RelayInvariant(t *testing.T) {
}
relayToken := &Token{Payload: "token-payload", Signature: "token-signature"}
nbCfg := toNetbirdConfig(cfg, nil, relayToken, nil, settings)
nbCfg := toNetbirdConfig(cfg, nil, relayToken, nil, types.TwinAccountSettings(settings))
require.NotNil(t, nbCfg)
require.NotNil(t, nbCfg.Relay, "non-nil NetbirdConfig must include the relay section")
assert.Equal(t, cfg.Relay.Addresses, nbCfg.Relay.Urls, "relay URLs should match the server config")

View File

@@ -920,8 +920,8 @@ func (s *Server) prepareLoginResponse(ctx context.Context, peer *nbpeer.Peer, ne
// if peer has reached this point then it has logged in
loginResp := &proto.LoginResponse{
NetbirdConfig: toNetbirdConfig(s.config, nil, relayToken, nil, settings),
PeerConfig: toPeerConfig(peer, types.TwinNetwork(network), s.networkMapController.GetDNSDomain(settings), settings, s.config.HttpConfig, s.config.DeviceAuthorizationFlow, enableSSH, false),
NetbirdConfig: toNetbirdConfig(s.config, nil, relayToken, nil, types.TwinAccountSettings(settings)),
PeerConfig: toPeerConfig(types.TwinPeer(peer), types.TwinNetwork(network), s.networkMapController.GetDNSDomain(settings), types.TwinAccountSettings(settings), s.config.HttpConfig, s.config.DeviceAuthorizationFlow, enableSSH, false),
Checks: toProtocolChecks(ctx, postureChecks),
}
@@ -1052,9 +1052,9 @@ func (s *Server) sendInitialSync(ctx context.Context, peerKey wgtypes.Key, peer
log.WithContext(ctx).Errorf("failed to build components for peer %s on initial sync: %v", peer.ID, err)
return status.Errorf(codes.Internal, "failed to build initial sync envelope")
}
plainResp = ToComponentSyncResponse(ctx, s.config, s.config.HttpConfig, s.config.DeviceAuthorizationFlow, freshPeer, turnToken, relayToken, components, proxyPatch, dnsName, freshPostureChecks, settings, settings.Extra, peerGroups, freshDnsFwdPort)
plainResp = ToComponentSyncResponse(ctx, s.config, s.config.HttpConfig, s.config.DeviceAuthorizationFlow, types.TwinPeer(freshPeer), turnToken, relayToken, components, proxyPatch, dnsName, freshPostureChecks, types.TwinAccountSettings(settings), settings.Extra, peerGroups, freshDnsFwdPort)
} else {
plainResp = ToSyncResponse(ctx, s.config, s.config.HttpConfig, s.config.DeviceAuthorizationFlow, peer, turnToken, relayToken, networkMap, dnsName, postureChecks, nil, settings, settings.Extra, peerGroups, dnsFwdPort)
plainResp = ToSyncResponse(ctx, s.config, s.config.HttpConfig, s.config.DeviceAuthorizationFlow, types.TwinPeer(peer), turnToken, relayToken, networkMap, dnsName, postureChecks, nil, types.TwinAccountSettings(settings), settings.Extra, peerGroups, dnsFwdPort)
}
key, err := s.secretsManager.GetWGKey()

View File

@@ -3330,7 +3330,7 @@ func createManager(t testing.TB) (*DefaultAccountManager, *update_channel.PeersU
updateManager := update_channel.NewPeersUpdateManager(metrics)
requestBuffer := NewAccountRequestBuffer(ctx, store)
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peers.NewManager(store, permissionsManager)), &config.Config{})
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peers.NewManager(store, permissionsManager)), &config.Config{}, nil)
manager, err := BuildManager(ctx, &config.Config{}, store, networkMapController, job.NewJobManager(nil, store, peersManager), nil, "", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore)
if err != nil {
return nil, nil, err

View File

@@ -234,7 +234,7 @@ func createDNSManager(t *testing.T) (*DefaultAccountManager, error) {
updateManager := update_channel.NewPeersUpdateManager(metrics)
requestBuffer := NewAccountRequestBuffer(ctx, store)
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.test", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peers.NewManager(store, permissionsManager)), &config.Config{})
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.test", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peers.NewManager(store, permissionsManager)), &config.Config{}, nil)
return BuildManager(context.Background(), nil, store, networkMapController, job.NewJobManager(nil, store, peersManager), nil, "", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore)
}

View File

@@ -96,7 +96,7 @@ func BuildApiBlackBoxWithDBState(t testing_tools.TB, sqlFile string, expectedPee
}
requestBuffer := server.NewAccountRequestBuffer(ctx, store)
networkMapController := controller.NewController(ctx, store, metrics, peersUpdateManager, requestBuffer, server.MockIntegratedValidator{}, settingsManager, "", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peersManager), &config.Config{})
networkMapController := controller.NewController(ctx, store, metrics, peersUpdateManager, requestBuffer, server.MockIntegratedValidator{}, settingsManager, "", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peersManager), &config.Config{}, nil)
am, err := server.BuildManager(ctx, nil, store, networkMapController, jobManager, nil, "", &activity.InMemoryEventStore{}, geoMock, false, validatorMock, metrics, proxyController, settingsManager, permissionsManager, false, cacheStore)
if err != nil {
t.Fatalf("Failed to create manager: %v", err)
@@ -226,7 +226,7 @@ func BuildApiBlackBoxWithDBStateAndPeerChannel(t testing_tools.TB, sqlFile strin
}
requestBuffer := server.NewAccountRequestBuffer(ctx, store)
networkMapController := controller.NewController(ctx, store, metrics, peersUpdateManager, requestBuffer, server.MockIntegratedValidator{}, settingsManager, "", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peersManager), &config.Config{})
networkMapController := controller.NewController(ctx, store, metrics, peersUpdateManager, requestBuffer, server.MockIntegratedValidator{}, settingsManager, "", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peersManager), &config.Config{}, nil)
am, err := server.BuildManager(ctx, nil, store, networkMapController, jobManager, nil, "", &activity.InMemoryEventStore{}, geoMock, false, validatorMock, metrics, proxyController, settingsManager, permissionsManager, false, cacheStore)
if err != nil {
t.Fatalf("Failed to create manager: %v", err)

View File

@@ -92,7 +92,7 @@ func createManagerWithEmbeddedIdP(t testing.TB) (*DefaultAccountManager, *update
updateManager := update_channel.NewPeersUpdateManager(metrics)
requestBuffer := NewAccountRequestBuffer(ctx, testStore)
networkMapController := controller.NewController(ctx, testStore, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(testStore, peersManager), &config.Config{})
networkMapController := controller.NewController(ctx, testStore, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(testStore, peersManager), &config.Config{}, nil)
manager, err := BuildManager(ctx, &config.Config{}, testStore, networkMapController, job.NewJobManager(nil, testStore, peersManager), idpManager, "", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore)
if err != nil {
return nil, nil, err

View File

@@ -376,7 +376,7 @@ func startManagementForTest(t *testing.T, testFile string, config *config.Config
return nil, nil, "", cleanup, err
}
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), ephemeralMgr, config)
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), ephemeralMgr, config, nil)
accountManager, err := BuildManager(ctx, nil, store, networkMapController, jobManager, nil, "",
eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore)

View File

@@ -216,7 +216,7 @@ func startServer(
updateManager := update_channel.NewPeersUpdateManager(metrics)
requestBuffer := server.NewAccountRequestBuffer(ctx, str)
networkMapController := controller.NewController(ctx, str, metrics, updateManager, requestBuffer, server.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(str, peers.NewManager(str, permissionsManager)), config)
networkMapController := controller.NewController(ctx, str, metrics, updateManager, requestBuffer, server.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(str, peers.NewManager(str, permissionsManager)), config, nil)
accountManager, err := server.BuildManager(
context.Background(),

View File

@@ -803,7 +803,7 @@ func createNSManager(t *testing.T) (*DefaultAccountManager, error) {
updateManager := update_channel.NewPeersUpdateManager(metrics)
requestBuffer := NewAccountRequestBuffer(ctx, store)
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peers.NewManager(store, permissionsManager)), &config.Config{})
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peers.NewManager(store, permissionsManager)), &config.Config{}, nil)
return BuildManager(context.Background(), nil, store, networkMapController, job.NewJobManager(nil, store, peersManager), nil, "", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore)
}

View File

@@ -1181,7 +1181,7 @@ func TestToSyncResponse(t *testing.T) {
}
dnsCache := &cache.DNSConfigCache{}
accountSettings := &types.Settings{RoutingPeerDNSResolutionEnabled: true}
response := grpc.ToSyncResponse(context.Background(), config, config.HttpConfig, config.DeviceAuthorizationFlow, peer, turnRelayToken, turnRelayToken, networkMap, dnsName, checks, dnsCache, accountSettings, nil, []string{}, int64(dnsForwarderPort))
response := grpc.ToSyncResponse(context.Background(), config, config.HttpConfig, config.DeviceAuthorizationFlow, types.TwinPeer(peer), turnRelayToken, turnRelayToken, networkMap, dnsName, checks, dnsCache, types.TwinAccountSettings(accountSettings), nil, []string{}, int64(dnsForwarderPort))
assert.NotNil(t, response)
// assert peer config
@@ -1301,7 +1301,7 @@ func Test_RegisterPeerByUser(t *testing.T) {
updateManager := update_channel.NewPeersUpdateManager(metrics)
requestBuffer := NewAccountRequestBuffer(ctx, s)
networkMapController := controller.NewController(ctx, s, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(s, peers.NewManager(s, permissionsManager)), &config.Config{})
networkMapController := controller.NewController(ctx, s, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(s, peers.NewManager(s, permissionsManager)), &config.Config{}, nil)
am, err := BuildManager(context.Background(), nil, s, networkMapController, job.NewJobManager(nil, s, peersManager), nil, "", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore)
assert.NoError(t, err)
@@ -1392,7 +1392,7 @@ func Test_RegisterPeerBySetupKey(t *testing.T) {
updateManager := update_channel.NewPeersUpdateManager(metrics)
requestBuffer := NewAccountRequestBuffer(ctx, s)
networkMapController := controller.NewController(ctx, s, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(s, peers.NewManager(s, permissionsManager)), &config.Config{})
networkMapController := controller.NewController(ctx, s, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(s, peers.NewManager(s, permissionsManager)), &config.Config{}, nil)
am, err := BuildManager(context.Background(), nil, s, networkMapController, job.NewJobManager(nil, s, peersManager), nil, "", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore)
assert.NoError(t, err)
@@ -1551,7 +1551,7 @@ func Test_RegisterPeerRollbackOnFailure(t *testing.T) {
updateManager := update_channel.NewPeersUpdateManager(metrics)
requestBuffer := NewAccountRequestBuffer(ctx, s)
networkMapController := controller.NewController(ctx, s, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(s, peers.NewManager(s, permissionsManager)), &config.Config{})
networkMapController := controller.NewController(ctx, s, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(s, peers.NewManager(s, permissionsManager)), &config.Config{}, nil)
am, err := BuildManager(context.Background(), nil, s, networkMapController, job.NewJobManager(nil, s, peersManager), nil, "", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore)
assert.NoError(t, err)
@@ -1636,7 +1636,7 @@ func Test_LoginPeer(t *testing.T) {
updateManager := update_channel.NewPeersUpdateManager(metrics)
requestBuffer := NewAccountRequestBuffer(ctx, s)
networkMapController := controller.NewController(ctx, s, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(s, peers.NewManager(s, permissionsManager)), &config.Config{})
networkMapController := controller.NewController(ctx, s, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(s, peers.NewManager(s, permissionsManager)), &config.Config{}, nil)
am, err := BuildManager(context.Background(), nil, s, networkMapController, job.NewJobManager(nil, s, peersManager), nil, "", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore)
assert.NoError(t, err)

View File

@@ -1299,7 +1299,7 @@ func createRouterManager(t *testing.T) (*DefaultAccountManager, *update_channel.
updateManager := update_channel.NewPeersUpdateManager(metrics)
requestBuffer := NewAccountRequestBuffer(ctx, store)
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peers.NewManager(store, permissionsManager)), &config.Config{})
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peers.NewManager(store, permissionsManager)), &config.Config{}, nil)
am, err := BuildManager(context.Background(), nil, store, networkMapController, job.NewJobManager(nil, store, peersManager), nil, "", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore)
if err != nil {

View File

@@ -9,7 +9,6 @@ import (
"strings"
"time"
"github.com/hashicorp/go-multierror"
"github.com/miekg/dns"
"github.com/rs/xid"
log "github.com/sirupsen/logrus"
@@ -26,6 +25,8 @@ import (
"github.com/netbirdio/netbird/management/server/util"
"github.com/netbirdio/netbird/route"
"github.com/netbirdio/netbird/shared/management/domain"
"github.com/netbirdio/netbird/shared/management/networkmap"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
"github.com/netbirdio/netbird/shared/management/status"
)
@@ -380,94 +381,11 @@ func peerInDistributionGroups(peerGroups LookupMap, distributionGroups []string)
}
func (a *Account) GetPeersCustomZone(ctx context.Context, dnsDomain string) nbdns.CustomZone {
var merr *multierror.Error
if dnsDomain == "" {
log.WithContext(ctx).Error("no dns domain is set, returning empty zone")
return nbdns.CustomZone{}
twins := make(map[string]*nmdata.Peer, len(a.Peers))
for id, p := range a.Peers {
twins[id] = twinPeer(p)
}
customZone := nbdns.CustomZone{
Domain: dns.Fqdn(dnsDomain),
Records: make([]nbdns.SimpleRecord, 0, len(a.Peers)),
}
domainSuffix := "." + dnsDomain
ipv6AllowedPeers := a.peerIPv6AllowedSet()
var sb strings.Builder
for _, peer := range a.Peers {
if peer.DNSLabel == "" {
merr = multierror.Append(merr, fmt.Errorf("peer %s has an empty DNS label", peer.Name))
continue
}
sb.Grow(len(peer.DNSLabel) + len(domainSuffix))
sb.WriteString(peer.DNSLabel)
sb.WriteString(domainSuffix)
fqdn := sb.String()
customZone.Records = append(customZone.Records, nbdns.SimpleRecord{
Name: fqdn,
Type: int(dns.TypeA),
Class: nbdns.DefaultClass,
TTL: defaultTTL,
RData: peer.IP.String(),
})
// Only advertise AAAA for peers that have a valid IPv6, whose client supports it,
// and that belong to an IPv6-enabled group. Old clients don't configure v6 on their
// WireGuard interface, so resolving their AAAA causes connections to hang.
// Capability changes (client upgrade/downgrade, --disable-ipv6 toggle) propagate
// to other peers via SyncPeer/LoginPeer regardless of version change, so AAAA
// records refresh when a peer first reports the IPv6 overlay capability.
_, peerAllowed := ipv6AllowedPeers[peer.ID]
hasIPv6 := peer.IPv6.IsValid() && peer.SupportsIPv6() && peerAllowed
if hasIPv6 {
customZone.Records = append(customZone.Records, nbdns.SimpleRecord{
Name: fqdn,
Type: int(dns.TypeAAAA),
Class: nbdns.DefaultClass,
TTL: defaultTTL,
RData: peer.IPv6.String(),
})
}
sb.Reset()
for _, extraLabel := range peer.ExtraDNSLabels {
sb.Grow(len(extraLabel) + len(domainSuffix))
sb.WriteString(extraLabel)
sb.WriteString(domainSuffix)
extraFqdn := sb.String()
customZone.Records = append(customZone.Records, nbdns.SimpleRecord{
Name: extraFqdn,
Type: int(dns.TypeA),
Class: nbdns.DefaultClass,
TTL: defaultTTL,
RData: peer.IP.String(),
})
if hasIPv6 {
customZone.Records = append(customZone.Records, nbdns.SimpleRecord{
Name: extraFqdn,
Type: int(dns.TypeAAAA),
Class: nbdns.DefaultClass,
TTL: defaultTTL,
RData: peer.IPv6.String(),
})
}
sb.Reset()
}
}
go func() {
if merr != nil {
log.WithContext(ctx).Errorf("error generating custom zone for account %s: %v", a.Id, merr)
}
}()
return customZone
return fromTwinCustomZone(networkmap.PeersCustomZone(ctx, a.Id, dnsDomain, twins, a.peerIPv6AllowedSet()))
}
// GetExpiredPeers returns peers that have been expired
@@ -1060,34 +978,6 @@ func (a *Account) GetPeerConnectionResources(ctx context.Context, peer *nbpeer.P
return peers, fwRules, authorizedUsers, sshEnabled
}
// forcesRoutingPeerDNSResolution reports whether the given peer must run
// routing-peer DNS resolution regardless of the account-global
// RoutingPeerDNSResolutionEnabled setting. It returns true when the peer is a
// router for a domain network resource that is targeted by an enabled
// reverse-proxy service, so the peer's DNS forwarder starts and can resolve
// the target for the embedded proxy peers. Embedded proxy peers themselves are
// handled at PeerConfig build time.
func (a *Account) forcesRoutingPeerDNSResolution(peerID string, routers map[string]map[string]*routerTypes.NetworkRouter) bool {
targeted := a.proxyTargetedDomainResourceIDs()
if len(targeted) == 0 {
return false
}
for _, resource := range a.NetworkResources {
if resource == nil || !resource.Enabled || resource.Type != resourceTypes.Domain {
continue
}
if _, ok := targeted[resource.ID]; !ok {
continue
}
if _, isRouter := routers[resource.NetworkID][peerID]; isRouter {
return true
}
}
return false
}
// proxyTargetedDomainResourceIDs returns the set of domain network resource IDs
// targeted by an enabled, non-terminated reverse-proxy service.
func (a *Account) proxyTargetedDomainResourceIDs() map[string]struct{} {

View File

@@ -104,9 +104,5 @@ func (a *Account) GetPeerNetworkMapComponents(
groupIDToUserIDs map[string][]string,
) *NetworkMapComponents {
nmd := a.toNetworkMapData(accountZones, validatedPeersMap, resourcePolicies, routers, groupIDToUserIDs)
components := nmd.GetPeerNetworkMapComponents(peerID, toTwinCustomZone(peersCustomZone))
if components != nil {
components.ForceRoutingPeerDNSResolution = a.forcesRoutingPeerDNSResolution(peerID, routers)
}
return components
return nmd.GetPeerNetworkMapComponents(peerID, TwinCustomZone(peersCustomZone))
}

View File

@@ -45,14 +45,7 @@ func (a *Account) toNetworkMapData(
nmd.Network = TwinNetwork(a.Network)
}
nmd.DNSSettings = &nmdata.DNSSettings{DisabledManagementGroups: a.DNSSettings.DisabledManagementGroups}
if a.Settings != nil {
nmd.AccountSettings = &nmdata.AccountSettingsInfo{
PeerLoginExpirationEnabled: a.Settings.PeerLoginExpirationEnabled,
PeerLoginExpiration: a.Settings.PeerLoginExpiration,
PeerInactivityExpirationEnabled: a.Settings.PeerInactivityExpirationEnabled,
PeerInactivityExpiration: a.Settings.PeerInactivityExpiration,
}
}
nmd.AccountSettings = TwinAccountSettings(a.Settings)
for id, p := range a.Peers {
nmd.Peers[id] = twinPeer(p)
@@ -115,6 +108,7 @@ func (a *Account) toNetworkMapData(
nmd.Routers[networkID] = twinInner
}
nmd.ProxyTargetedDomainResourceIDs = a.proxyTargetedDomainResourceIDs()
nmd.AppliedZoneCandidates = buildAppliedZoneCandidates(accountZones)
nmd.PrivateServiceCandidates = a.buildPrivateServiceCandidates()
@@ -145,15 +139,17 @@ func twinPeer(p *nbpeer.Peer) *nmdata.Peer {
IP: p.IP,
IPv6: p.IPv6,
RequiresApproval: p.Status != nil && p.Status.RequiresApproval,
ExtraDNSLabels: p.ExtraDNSLabels,
ProxyMeta: nmdata.ProxyMeta{Embedded: p.ProxyMeta.Embedded},
Meta: nmdata.PeerSystemMeta{
WtVersion: p.Meta.WtVersion,
GoOS: p.Meta.GoOS,
OSVersion: p.Meta.OSVersion,
KernelVersion: p.Meta.KernelVersion,
NetworkAddresses: networkAddresses,
Files: files,
Capabilities: p.Meta.Capabilities,
WtVersion: p.Meta.WtVersion,
GoOS: p.Meta.GoOS,
OSVersion: p.Meta.OSVersion,
KernelVersion: p.Meta.KernelVersion,
NetworkAddresses: networkAddresses,
Files: files,
Capabilities: p.Meta.Capabilities,
SyncMessageVersion: p.Meta.SyncMessageVersion,
Flags: nmdata.Flags{
ServerSSHAllowed: p.Meta.Flags.ServerSSHAllowed,
DisableIPv6: p.Meta.Flags.DisableIPv6,
@@ -508,7 +504,50 @@ func (a *Account) buildPrivateServiceCandidates() []networkmap.PrivateServiceCan
return out
}
func toTwinCustomZone(z nbdns.CustomZone) nmdata.CustomZone {
// TwinAccountSettings converts real account settings to the slim nmdata twin.
// Exported for callers of the twin-based sync response builders.
func TwinAccountSettings(s *Settings) *nmdata.AccountSettingsInfo {
if s == nil {
return nil
}
return &nmdata.AccountSettingsInfo{
PeerLoginExpirationEnabled: s.PeerLoginExpirationEnabled,
PeerLoginExpiration: s.PeerLoginExpiration,
PeerInactivityExpirationEnabled: s.PeerInactivityExpirationEnabled,
PeerInactivityExpiration: s.PeerInactivityExpiration,
DNSDomain: s.DNSDomain,
IPv6EnabledGroups: s.IPv6EnabledGroups,
RoutingPeerDNSResolutionEnabled: s.RoutingPeerDNSResolutionEnabled,
LazyConnectionEnabled: s.LazyConnectionEnabled,
AutoUpdateVersion: s.AutoUpdateVersion,
AutoUpdateAlways: s.AutoUpdateAlways,
MetricsPushEnabled: s.MetricsPushEnabled,
}
}
func fromTwinCustomZone(z nmdata.CustomZone) nbdns.CustomZone {
records := make([]nbdns.SimpleRecord, 0, len(z.Records))
for _, r := range z.Records {
records = append(records, nbdns.SimpleRecord{
Name: r.Name,
Type: r.Type,
Class: r.Class,
TTL: r.TTL,
RData: r.RData,
})
}
return nbdns.CustomZone{
Domain: z.Domain,
Records: records,
SearchDomainDisabled: z.SearchDomainDisabled,
NonAuthoritative: z.NonAuthoritative,
}
}
// TwinCustomZone converts a real DNS custom zone to its slim nmdata twin.
// Exported for the network-map controller's DB-store path, which feeds real
// zones into the twin-based components calculation.
func TwinCustomZone(z nbdns.CustomZone) nmdata.CustomZone {
records := make([]nmdata.SimpleRecord, 0, len(z.Records))
for _, r := range z.Records {
records = append(records, nmdata.SimpleRecord{

View File

@@ -0,0 +1,350 @@
//go:build nmapequiv
// Account-load benchmark: the legacy store.GetAccount hydration (pgx fast
// path, as in production) vs the nmdata store's GetNetworkMapData, against the
// same Postgres copy as the equivalence test.
//
// NETBIRD_STORE_ENGINE_POSTGRES_DSN='...' go test -tags nmapequiv \
// -run '^$' -bench . -benchtime 5x -timeout 60m \
// ./management/server/types/legacynmap/
//
// NETMAP_ACCOUNTS selects the accounts (comma-separated); by default the ten
// accounts with the most peers are used. Each account is a sub-benchmark, so
// the two paths can be compared per account. One warmup call runs untimed
// before each measurement so Postgres buffer-cache state is comparable.
//
// Reported metrics beyond ns/op and allocs:
//
// - queries/op round trips, counted client-side via a pgx tracer
// (GetNetworkMapData only — the legacy store's pool is internal)
// - xact/op committed transactions from pg_stat_database; the legacy
// pgx path runs autocommit statements, so this approximates its round
// trips, while GetNetworkMapData runs a single transaction
// - tup_returned/op, tup_fetched/op rows scanned/fetched server-side
// - blks_read/op, blks_hit/op buffer cache misses/hits
//
// The pg_stat_database numbers are database-global: run without concurrent
// load. The two stat snapshots per sub-benchmark add a small constant
// overhead to the server-side deltas.
package legacynmap_test
import (
"context"
"os"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/stretchr/testify/require"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller/cache"
networkmap_pgsql "github.com/netbirdio/netbird/management/internals/network_map_db/pgsql"
mgmtgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
"github.com/netbirdio/netbird/management/server/store"
"github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/shared/management/networkmap"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
)
func BenchmarkGetAccount(b *testing.B) {
dsn := equivDSN()
if dsn == "" {
b.Skip("NETBIRD_STORE_ENGINE_POSTGRES_DSN not set")
}
ctx := context.Background()
statsConn, err := pgx.Connect(ctx, dsn)
require.NoError(b, err, "connect stats connection")
b.Cleanup(func() { statsConn.Close(ctx) })
testStore, err := store.NewPostgresqlStore(ctx, dsn, nil, true)
require.NoError(b, err, "connect to postgres")
b.Cleanup(func() { testStore.Close(ctx) })
for _, accountID := range benchAccountIDs(b, ctx, statsConn) {
b.Run(accountID, func(b *testing.B) {
logAccountShape(b, ctx, statsConn, accountID)
benchDBLoad(b, ctx, statsConn, nil, func() error {
_, err := testStore.GetAccount(ctx, accountID)
return err
})
})
}
}
func BenchmarkGetNetworkMapData(b *testing.B) {
dsn := equivDSN()
if dsn == "" {
b.Skip("NETBIRD_STORE_ENGINE_POSTGRES_DSN not set")
}
ctx := context.Background()
statsConn, err := pgx.Connect(ctx, dsn)
require.NoError(b, err, "connect stats connection")
b.Cleanup(func() { statsConn.Close(ctx) })
tracer := &queryCountTracer{}
cfg, err := pgxpool.ParseConfig(dsn)
require.NoError(b, err, "parse dsn")
cfg.ConnConfig.Tracer = tracer
pool, err := pgxpool.NewWithConfig(ctx, cfg)
require.NoError(b, err, "connect nmdata store")
b.Cleanup(pool.Close)
nmStore := &networkmap_pgsql.PgStore{Pool: pool}
for _, accountID := range benchAccountIDs(b, ctx, statsConn) {
b.Run(accountID, func(b *testing.B) {
logAccountShape(b, ctx, statsConn, accountID)
benchDBLoad(b, ctx, statsConn, tracer, func() error {
_, err := nmStore.GetNetworkMapData(ctx, accountID)
return err
})
})
}
}
// BenchmarkAccountFullRound measures store load plus the full per-peer fan-out
// to *proto.SyncResponse for every peer of the account, the way the production
// account path runs it: index maps and per-peer twin building happen after
// GetAccount and are part of the measured op. BenchmarkNetworkMapDataFullRound
// is the equivalent for the nmdata path, whose index building happens inside
// GetNetworkMapData. Select both with -bench FullRound.
func BenchmarkAccountFullRound(b *testing.B) {
dsn := equivDSN()
if dsn == "" {
b.Skip("NETBIRD_STORE_ENGINE_POSTGRES_DSN not set")
}
ctx := context.Background()
statsConn, err := pgx.Connect(ctx, dsn)
require.NoError(b, err, "connect stats connection")
b.Cleanup(func() { statsConn.Close(ctx) })
testStore, err := store.NewPostgresqlStore(ctx, dsn, nil, true)
require.NoError(b, err, "connect to postgres")
b.Cleanup(func() { testStore.Close(ctx) })
for _, accountID := range benchAccountIDs(b, ctx, statsConn) {
b.Run(accountID, func(b *testing.B) {
logAccountShape(b, ctx, statsConn, accountID)
benchDBLoad(b, ctx, statsConn, nil, func() error {
account, err := testStore.GetAccount(ctx, accountID)
if err != nil {
return err
}
buildAccountSyncResponses(ctx, account)
return nil
})
})
}
}
func BenchmarkNetworkMapDataFullRound(b *testing.B) {
dsn := equivDSN()
if dsn == "" {
b.Skip("NETBIRD_STORE_ENGINE_POSTGRES_DSN not set")
}
ctx := context.Background()
statsConn, err := pgx.Connect(ctx, dsn)
require.NoError(b, err, "connect stats connection")
b.Cleanup(func() { statsConn.Close(ctx) })
tracer := &queryCountTracer{}
cfg, err := pgxpool.ParseConfig(dsn)
require.NoError(b, err, "parse dsn")
cfg.ConnConfig.Tracer = tracer
pool, err := pgxpool.NewWithConfig(ctx, cfg)
require.NoError(b, err, "connect nmdata store")
b.Cleanup(pool.Close)
nmStore := &networkmap_pgsql.PgStore{Pool: pool}
for _, accountID := range benchAccountIDs(b, ctx, statsConn) {
b.Run(accountID, func(b *testing.B) {
logAccountShape(b, ctx, statsConn, accountID)
benchDBLoad(b, ctx, statsConn, tracer, func() error {
nmData, err := nmStore.GetNetworkMapData(ctx, accountID)
if err != nil {
return err
}
buildDataSyncResponses(ctx, nmData)
return nil
})
})
}
}
// buildAccountSyncResponses fans out to every peer like the controller's
// account path: index maps once, twin conversion and network-map computation
// per peer.
func buildAccountSyncResponses(ctx context.Context, account *types.Account) {
validated := make(map[string]struct{}, len(account.Peers))
for peerID := range account.Peers {
validated[peerID] = struct{}{}
}
resourcePolicies := account.GetResourcePoliciesMap()
routers := account.GetResourceRoutersMap()
groupUsers := account.GetActiveGroupUsers()
settings := account.Settings
if settings == nil {
settings = &types.Settings{}
}
dnsCache := &cache.DNSConfigCache{}
for peerID, peer := range account.Peers {
nm := account.GetPeerNetworkMapFromComponents(
ctx, peerID, nbdns.CustomZone{}, nil, validated, resourcePolicies, routers, nil, groupUsers,
)
mgmtgrpc.ToSyncResponse(
ctx, nil, nil, nil, types.TwinPeer(peer), nil, nil, nm, equivDNSName, nil,
dnsCache, types.TwinAccountSettings(settings), settings.Extra, nil, 0,
)
}
}
// buildDataSyncResponses is the nmdata-path equivalent of
// buildAccountSyncResponses.
func buildDataSyncResponses(ctx context.Context, nmData *networkmap.NetworkMapData) {
validated := make(map[string]struct{}, len(nmData.Peers))
for peerID := range nmData.Peers {
validated[peerID] = struct{}{}
}
nmData.ValidatedPeers = validated
dnsCache := &cache.DNSConfigCache{}
for peerID, peer := range nmData.Peers {
components := nmData.GetPeerNetworkMapComponents(peerID, nmdata.CustomZone{})
nm := &types.NetworkMap{Network: components.Network}
if !components.IsEmpty() {
nm = types.CalculateNetworkMapFromComponents(ctx, components)
}
mgmtgrpc.ToSyncResponse(
ctx, nil, nil, nil, peer, nil, nil, nm, equivDNSName, nil,
dnsCache, nmData.AccountSettings, nil, nil, 0,
)
}
}
// benchDBLoad runs op b.N times and reports server-side pg_stat_database
// deltas per op. A non-nil tracer additionally reports exact client round
// trips per op.
//
// Backends flush cumulative stats at most once per second and only while
// processing commands, so around each snapshot the load settles: sleep past
// the flush interval, then run one extra untimed op whose command end flushes
// everything pending. The trailing extra op lands inside the measured window,
// hence the b.N+1 denominator for the server-side metrics.
func benchDBLoad(b *testing.B, ctx context.Context, statsConn *pgx.Conn, tracer *queryCountTracer, op func() error) {
b.Helper()
require.NoError(b, op(), "warmup")
settleDBStats(b, op)
before, err := snapshotDBStats(ctx, statsConn)
require.NoError(b, err, "stats snapshot")
var queriesBefore int64
if tracer != nil {
queriesBefore = tracer.queries.Load()
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if err := op(); err != nil {
b.Fatal(err)
}
}
b.StopTimer()
settleDBStats(b, op)
after, err := snapshotDBStats(ctx, statsConn)
require.NoError(b, err, "stats snapshot")
ops := float64(b.N + 1)
if tracer != nil {
b.ReportMetric(float64(tracer.queries.Load()-queriesBefore)/ops, "queries/op")
}
b.ReportMetric(float64(after.xactCommit-before.xactCommit)/ops, "xact/op")
b.ReportMetric(float64(after.tupReturned-before.tupReturned)/ops, "tup_returned/op")
b.ReportMetric(float64(after.tupFetched-before.tupFetched)/ops, "tup_fetched/op")
b.ReportMetric(float64(after.blksRead-before.blksRead)/ops, "blks_read/op")
b.ReportMetric(float64(after.blksHit-before.blksHit)/ops, "blks_hit/op")
}
func settleDBStats(b *testing.B, op func() error) {
b.Helper()
time.Sleep(1100 * time.Millisecond)
require.NoError(b, op(), "stats flush op")
time.Sleep(100 * time.Millisecond)
}
func benchAccountIDs(b *testing.B, ctx context.Context, conn *pgx.Conn) []string {
b.Helper()
if ids := strings.TrimSpace(os.Getenv("NETMAP_ACCOUNTS")); ids != "" {
var out []string
for _, id := range strings.Split(ids, ",") {
if id = strings.TrimSpace(id); id != "" {
out = append(out, id)
}
}
return out
}
rows, err := conn.Query(ctx,
"select account_id from peers group by account_id order by count(*) desc, account_id limit 10")
require.NoError(b, err, "list benchmark accounts")
ids, err := pgx.CollectRows(rows, pgx.RowTo[string])
require.NoError(b, err, "collect benchmark accounts")
require.NotEmpty(b, ids, "no accounts found")
return ids
}
func logAccountShape(b *testing.B, ctx context.Context, conn *pgx.Conn, accountID string) {
b.Helper()
var peers, groups, users, policies, routes, resources, nsGroups int
err := conn.QueryRow(ctx, `select
(select count(*) from peers where account_id=$1),
(select count(*) from groups where account_id=$1),
(select count(*) from users where account_id=$1),
(select count(*) from policies where account_id=$1),
(select count(*) from routes where account_id=$1),
(select count(*) from network_resources where account_id=$1),
(select count(*) from name_server_groups where account_id=$1)`, accountID).
Scan(&peers, &groups, &users, &policies, &routes, &resources, &nsGroups)
require.NoError(b, err, "account shape")
b.Logf("account=%s peers=%d groups=%d users=%d policies=%d routes=%d resources=%d nsgroups=%d",
accountID, peers, groups, users, policies, routes, resources, nsGroups)
}
type dbStats struct {
xactCommit int64
tupReturned int64
tupFetched int64
blksRead int64
blksHit int64
}
func snapshotDBStats(ctx context.Context, conn *pgx.Conn) (dbStats, error) {
var s dbStats
err := conn.QueryRow(ctx, `select xact_commit, tup_returned, tup_fetched, blks_read, blks_hit
from pg_stat_database where datname = current_database()`).
Scan(&s.xactCommit, &s.tupReturned, &s.tupFetched, &s.blksRead, &s.blksHit)
return s, err
}
type queryCountTracer struct {
queries atomic.Int64
}
func (t *queryCountTracer) TraceQueryStart(ctx context.Context, _ *pgx.Conn, _ pgx.TraceQueryStartData) context.Context {
t.queries.Add(1)
return ctx
}
func (t *queryCountTracer) TraceQueryEnd(context.Context, *pgx.Conn, pgx.TraceQueryEndData) {}

View File

@@ -3,10 +3,14 @@
// Main-vs-branch equivalence check. For every peer of every account in a real
// Postgres copy it computes the client-facing proto.NetworkMap twice:
//
// - legacy path: main's Account → NetworkMapComponents → Calculate → proto
// - legacy path: main's Account → NetworkMapComponents → Calculate → proto
// (the frozen copy in this package)
// - new path: this branch's Account → NetworkMapData → components →
// Calculate → ToSyncResponse → proto
// - store path: the pgsql nmdata store's NetworkMapData → components →
// Calculate → ToSyncResponse → proto (no Account involved)
// - account path: Account → toNetworkMapData twins → components → Calculate
// → ToSyncResponse → proto (the in-memory builder, no store queries)
//
// Both new paths are checked against the legacy proto.
//
// proto.NetworkMap is generated code identical in both trees, which is what
// makes it the one usable comparison surface — the intermediate Go types differ
@@ -47,10 +51,12 @@ import (
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller/cache"
networkmap_pgsql "github.com/netbirdio/netbird/management/internals/network_map_db/pgsql"
mgmtgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
"github.com/netbirdio/netbird/management/server/store"
"github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/management/server/types/legacynmap"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
"github.com/netbirdio/netbird/shared/management/proto"
)
@@ -62,7 +68,6 @@ const (
type equivStats struct {
accounts int
peersChecked int
skippedNilNM int
}
func TestNetworkMapProtoEquivalence(t *testing.T) {
@@ -81,6 +86,10 @@ func TestNetworkMapProtoEquivalence(t *testing.T) {
require.NoError(t, err, "connect to postgres")
t.Cleanup(func() { testStore.Close(ctx) })
nmStore, err := networkmap_pgsql.NewPostgresqlStore(ctx, dsn)
require.NoError(t, err, "connect nmdata store")
t.Cleanup(func() { nmStore.Pool.Close() })
accountIDs := equivAccountIDs(t, dsn)
require.NotEmpty(t, accountIDs, "no accounts selected")
@@ -94,7 +103,7 @@ func TestNetworkMapProtoEquivalence(t *testing.T) {
continue
}
checkAccount(ctx, t, account, maxPeers, stats)
checkAccount(ctx, t, nmStore, account, maxPeers, stats)
account = nil
debug.FreeOSMemory()
@@ -106,19 +115,22 @@ func TestNetworkMapProtoEquivalence(t *testing.T) {
}
}
t.Logf("equivalence: accounts=%d peers_checked=%d skipped_nil_nm=%d — no divergence",
stats.accounts, stats.peersChecked, stats.skippedNilNM)
t.Logf("equivalence: accounts=%d peers_checked=%d — no divergence",
stats.accounts, stats.peersChecked)
}
// checkAccount compares both paths for every peer of one account. Nothing is
// retained across peers, so memory stays flat within an account.
func checkAccount(ctx context.Context, t *testing.T, account *types.Account, maxPeers int, stats *equivStats) {
func checkAccount(ctx context.Context, t *testing.T, nmStore *networkmap_pgsql.PgStore, account *types.Account, maxPeers int, stats *equivStats) {
t.Helper()
if len(account.Peers) == 0 {
return
}
nmData, err := nmStore.GetNetworkMapData(ctx, account.Id)
require.NoError(t, err, "account %s: nmdata store load", account.Id)
validated := make(map[string]struct{}, len(account.Peers))
peerIDs := make([]string, 0, len(account.Peers))
for peerID := range account.Peers {
@@ -130,6 +142,15 @@ func checkAccount(ctx context.Context, t *testing.T, account *types.Account, max
peerIDs = peerIDs[:maxPeers]
}
// Production fills ValidatedPeers via the integrated-validator wrapper; here
// every peer counts as validated, matching the legacy side's map.
nmData.ValidatedPeers = validated
// The legacy side receives no account zones (main sourced them from the
// external zones manager), so the DB-sourced applied-zone candidates must be
// dropped to keep the comparison surface identical. PrivateServiceCandidates
// stay: all paths derive them from account/DB data.
nmData.AppliedZoneCandidates = nil
resourcePolicies := account.GetResourcePoliciesMap()
routers := account.GetResourceRoutersMap()
groupUsers := account.GetActiveGroupUsers()
@@ -144,18 +165,32 @@ func checkAccount(ctx context.Context, t *testing.T, account *types.Account, max
if peer == nil {
continue
}
dataPeer := nmData.Peers[peerID]
if dataPeer == nil {
t.Fatalf("after %d peers: account=%s peer=%s present in account store, missing in nmdata store", stats.peersChecked, account.Id, peerID)
}
// NEW PATH — this branch, through the production conversion.
newNM := account.GetPeerNetworkMapFromComponents(
// STORE PATH — nmdata store through the production computation, mirroring
// the controller's networkMapFromData.
components := nmData.GetPeerNetworkMapComponents(peerID, nmdata.CustomZone{})
storeNM := &types.NetworkMap{Network: components.Network}
if !components.IsEmpty() {
storeNM = types.CalculateNetworkMapFromComponents(ctx, components)
}
// A separate cache per side: sharing one would let the first path
// populate entries the second then reuses, which can mask a real diff.
storeProto := mgmtgrpc.ToSyncResponse(
ctx, nil, nil, nil, dataPeer, nil, nil, storeNM, equivDNSName, nil,
&cache.DNSConfigCache{}, nmData.AccountSettings, settings.Extra, nil, 0,
).NetworkMap
// ACCOUNT PATH — Account → toNetworkMapData twins → components.
acctNM := account.GetPeerNetworkMapFromComponents(
ctx, peerID, nbdns.CustomZone{}, nil, validated, resourcePolicies, routers, nil, groupUsers,
)
if newNM == nil {
stats.skippedNilNM++
continue
}
newProto := mgmtgrpc.ToSyncResponse(
ctx, nil, nil, nil, peer, nil, nil, newNM, equivDNSName, nil,
&cache.DNSConfigCache{}, settings, settings.Extra, nil, 0,
acctProto := mgmtgrpc.ToSyncResponse(
ctx, nil, nil, nil, types.TwinPeer(peer), nil, nil, acctNM, equivDNSName, nil,
&cache.DNSConfigCache{}, types.TwinAccountSettings(settings), settings.Extra, nil, 0,
).NetworkMap
// LEGACY PATH — main's frozen copy.
@@ -165,18 +200,20 @@ func checkAccount(ctx context.Context, t *testing.T, account *types.Account, max
if legacyNM == nil {
t.Fatalf("after %d peers: account=%s peer=%s legacy NetworkMap nil, new non-nil", stats.peersChecked, account.Id, peerID)
}
// A separate cache per side: sharing one would let the first path
// populate entries the second then reuses, which can mask a real diff.
legacyProto := legacynmap.ToProtoNetworkMap(
ctx, peer, legacyNM, equivDNSName, settings, nil, &cache.DNSConfigCache{}, 0,
)
canonicalize(legacyProto)
canonicalize(newProto)
canonicalize(storeProto)
canonicalize(acctProto)
stats.peersChecked++
if !goproto.Equal(legacyProto, newProto) {
t.Fatalf("after %d peers: %s", stats.peersChecked, describeDivergence(legacyProto, newProto, account.Id, peerID))
if !goproto.Equal(legacyProto, storeProto) {
t.Fatalf("after %d peers: store path: %s", stats.peersChecked, describeDivergence(legacyProto, storeProto, account.Id, peerID))
}
if !goproto.Equal(legacyProto, acctProto) {
t.Fatalf("after %d peers: account path: %s", stats.peersChecked, describeDivergence(legacyProto, acctProto, account.Id, peerID))
}
}
}
@@ -508,17 +545,18 @@ func describeDivergence(legacy, updated *proto.NetworkMap, accountID, peerID str
lens := []struct {
field string
a, b int
diff func() string
}{
{"RemotePeers", len(legacy.RemotePeers), len(updated.RemotePeers)},
{"OfflinePeers", len(legacy.OfflinePeers), len(updated.OfflinePeers)},
{"Routes", len(legacy.Routes), len(updated.Routes)},
{"FirewallRules", len(legacy.FirewallRules), len(updated.FirewallRules)},
{"RoutesFirewallRules", len(legacy.RoutesFirewallRules), len(updated.RoutesFirewallRules)},
{"ForwardingRules", len(legacy.ForwardingRules), len(updated.ForwardingRules)},
{"RemotePeers", len(legacy.RemotePeers), len(updated.RemotePeers), func() string { return diffLists(legacy.RemotePeers, updated.RemotePeers) }},
{"OfflinePeers", len(legacy.OfflinePeers), len(updated.OfflinePeers), func() string { return diffLists(legacy.OfflinePeers, updated.OfflinePeers) }},
{"Routes", len(legacy.Routes), len(updated.Routes), func() string { return diffLists(legacy.Routes, updated.Routes) }},
{"FirewallRules", len(legacy.FirewallRules), len(updated.FirewallRules), func() string { return diffLists(legacy.FirewallRules, updated.FirewallRules) }},
{"RoutesFirewallRules", len(legacy.RoutesFirewallRules), len(updated.RoutesFirewallRules), func() string { return diffLists(legacy.RoutesFirewallRules, updated.RoutesFirewallRules) }},
{"ForwardingRules", len(legacy.ForwardingRules), len(updated.ForwardingRules), func() string { return diffLists(legacy.ForwardingRules, updated.ForwardingRules) }},
}
for _, l := range lens {
if l.a != l.b {
return prefix + " field=" + l.field + " legacy_len=" + strconv.Itoa(l.a) + " new_len=" + strconv.Itoa(l.b)
return prefix + " field=" + l.field + " legacy_len=" + strconv.Itoa(l.a) + " new_len=" + strconv.Itoa(l.b) + l.diff()
}
}
@@ -557,6 +595,39 @@ func describeDivergence(legacy, updated *proto.NetworkMap, accountID, peerID str
return prefix + " (repeated fields equal element-wise — scalar/oneof mismatch)"
}
// diffLists reports the multiset difference of two repeated proto fields, so a
// length mismatch shows which elements each side is missing.
func diffLists[M goproto.Message](legacy, updated []M) string {
counts := make(map[string]int)
for _, m := range legacy {
counts[prototext.MarshalOptions{}.Format(m)]++
}
for _, m := range updated {
counts[prototext.MarshalOptions{}.Format(m)]--
}
var onlyLegacy, onlyNew []string
for k, c := range counts {
for ; c > 0; c-- {
onlyLegacy = append(onlyLegacy, k)
}
for ; c < 0; c++ {
onlyNew = append(onlyNew, k)
}
}
slices.Sort(onlyLegacy)
slices.Sort(onlyNew)
var b strings.Builder
for _, k := range onlyLegacy {
b.WriteString("\n only_legacy: " + k)
}
for _, k := range onlyNew {
b.WriteString("\n only_new: " + k)
}
return b.String()
}
func protoStr(m goproto.Message) string {
if m == nil {
return "<nil>"

View File

@@ -66,7 +66,7 @@ func BenchmarkNetworkMapWireEncode(b *testing.B) {
// Pre-encode once so the size metric is identical for every run inside
// the same scale; the b.Loop call only re-runs encode + Marshal.
legacyResp := mgmtgrpc.ToSyncResponse(ctx, nil, nil, nil, peer, nil, nil, networkMap, "netbird.cloud", nil, dnsCache, settings, nil, nil, 0)
legacyResp := mgmtgrpc.ToSyncResponse(ctx, nil, nil, nil, types.TwinPeer(peer), nil, nil, networkMap, "netbird.cloud", nil, dnsCache, types.TwinAccountSettings(settings), nil, nil, 0)
legacyBytes, err := goproto.Marshal(legacyResp.NetworkMap)
if err != nil {
b.Fatalf("marshal legacy networkmap: %v", err)
@@ -88,7 +88,7 @@ func BenchmarkNetworkMapWireEncode(b *testing.B) {
b.ReportMetric(float64(len(legacyBytes)), "bytes/msg")
b.ResetTimer()
for range b.N {
resp := mgmtgrpc.ToSyncResponse(ctx, nil, nil, nil, peer, nil, nil, networkMap, "netbird.cloud", nil, dnsCache, settings, nil, nil, 0)
resp := mgmtgrpc.ToSyncResponse(ctx, nil, nil, nil, types.TwinPeer(peer), nil, nil, networkMap, "netbird.cloud", nil, dnsCache, types.TwinAccountSettings(settings), nil, nil, 0)
if _, err := goproto.Marshal(resp.NetworkMap); err != nil {
b.Fatal(err)
}
@@ -135,7 +135,7 @@ func BenchmarkNetworkMapWireSize(b *testing.B) {
dnsCache := &cache.DNSConfigCache{}
settings := &types.Settings{}
legacyResp := mgmtgrpc.ToSyncResponse(ctx, nil, nil, nil, peer, nil, nil, networkMap, "netbird.cloud", nil, dnsCache, settings, nil, nil, 0)
legacyResp := mgmtgrpc.ToSyncResponse(ctx, nil, nil, nil, types.TwinPeer(peer), nil, nil, networkMap, "netbird.cloud", nil, dnsCache, types.TwinAccountSettings(settings), nil, nil, 0)
legacyBytes, err := goproto.Marshal(legacyResp.NetworkMap)
if err != nil {
b.Fatalf("marshal legacy networkmap: %v", err)

View File

@@ -45,7 +45,7 @@ func TestNetworkMapWireBreakdown(t *testing.T) {
dnsCache := &cache.DNSConfigCache{}
settings := &types.Settings{}
legacyResp := mgmtgrpc.ToSyncResponse(ctx, nil, nil, nil, peer, nil, nil, networkMap, "netbird.cloud", nil, dnsCache, settings, nil, nil, 0)
legacyResp := mgmtgrpc.ToSyncResponse(ctx, nil, nil, nil, types.TwinPeer(peer), nil, nil, networkMap, "netbird.cloud", nil, dnsCache, types.TwinAccountSettings(settings), nil, nil, 0)
legacyTotal := mustMarshalSize(t, legacyResp.NetworkMap)
envelope := mgmtgrpc.EncodeNetworkMapEnvelope(mgmtgrpc.ComponentsEnvelopeInput{

View File

@@ -126,7 +126,7 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) {
updateManager := update_channel.NewPeersUpdateManager(metrics)
requestBuffer := mgmt.NewAccountRequestBuffer(ctx, store)
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, mgmt.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peersManger), config)
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, mgmt.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peersManger), config, nil)
accountManager, err := mgmt.BuildManager(context.Background(), config, store, networkMapController, jobManager, nil, "", eventStore, nil, false, ia, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManagerMock, false, cacheStore)
if err != nil {
t.Fatal(err)

View File

@@ -1,6 +1,7 @@
package networkmap
import (
"context"
"encoding/base64"
"fmt"
"net"
@@ -13,7 +14,7 @@ import (
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/shared/management/domain"
nmdata "github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
"github.com/netbirdio/netbird/shared/management/proto"
"github.com/netbirdio/netbird/shared/management/types"
)
@@ -25,7 +26,7 @@ import (
// ID scheme on the client side:
//
// Peers base64(wg_pub_key) // stable across snapshots
func DecodeEnvelope(env *proto.NetworkMapEnvelope) (*types.NetworkMapComponents, error) {
func DecodeEnvelope(ctx context.Context, env *proto.NetworkMapEnvelope) (*types.NetworkMapComponents, error) {
full := env.GetFull()
if full == nil {
return nil, fmt.Errorf("envelope has no Full payload")
@@ -104,7 +105,12 @@ func DecodeEnvelope(env *proto.NetworkMapEnvelope) (*types.NetworkMapComponents,
var toret []nmdata.Resource
for _, r := range gc.Resources {
toret = append(toret, resourceFromProto(r, peerIDByIndex))
res := resourceFromProto(r, peerIDByIndex)
if res == (nmdata.Resource{}) {
log.WithContext(ctx).Warnf("skipping invalid resource in group compact: %s", r.ResourceId)
continue
}
toret = append(toret, res)
}
return toret
@@ -264,7 +270,7 @@ func policiesForNetworkResource(resourceId string, allPolicies []*nmdata.Policy,
networkResourceGroups := networkResourceGroups(resourceId, groups)
for _, p := range allPolicies {
if p == nil || !p.Enabled {
if p == nil || !p.Enabled || len(p.Rules) == 0 {
continue
}

View File

@@ -10,31 +10,31 @@ import (
func TestDecodePolicy(t *testing.T) {
assert.Equal(t,
nmdata.Resource{Type: "peer", ID: "valid-id"},
resourceFromProto(
&proto.ResourceCompact{Type: proto.ResourceCompactType_peer, ResourceId: &proto.ResourceCompact_PeerIndex{PeerIndex: uint32(1)}},
[]string{"invalid-id-0", "valid-id", "invalid-id-2"}),
nmdata.Resource{Type: "peer", ID: "valid-id"})
[]string{"invalid-id-0", "valid-id", "invalid-id-2"}))
// check invalid peer index returns an empty resource
assert.Equal(t,
nmdata.Resource{},
resourceFromProto(
&proto.ResourceCompact{Type: proto.ResourceCompactType_peer, ResourceId: &proto.ResourceCompact_PeerIndex{PeerIndex: uint32(100)}},
[]string{"invalid-id-0", "valid-id", "invalid-id-2"}),
nmdata.Resource{})
[]string{"invalid-id-0", "valid-id", "invalid-id-2"}))
assert.Equal(t,
nmdata.Resource{Type: "domain", ID: "domain"},
resourceFromProto(
&proto.ResourceCompact{Type: proto.ResourceCompactType_domain, ResourceId: &proto.ResourceCompact_Id{Id: "domain"}}, []string{}),
nmdata.Resource{Type: "domain", ID: "domain"})
&proto.ResourceCompact{Type: proto.ResourceCompactType_domain, ResourceId: &proto.ResourceCompact_Id{Id: "domain"}}, []string{}))
assert.Equal(t,
nmdata.Resource{Type: "host", ID: "host"},
resourceFromProto(
&proto.ResourceCompact{Type: proto.ResourceCompactType_host, ResourceId: &proto.ResourceCompact_Id{Id: "host"}}, []string{}),
nmdata.Resource{Type: "host", ID: "host"})
&proto.ResourceCompact{Type: proto.ResourceCompactType_host, ResourceId: &proto.ResourceCompact_Id{Id: "host"}}, []string{}))
assert.Equal(t,
nmdata.Resource{Type: "subnet", ID: "subnet"},
resourceFromProto(
&proto.ResourceCompact{Type: proto.ResourceCompactType_subnet, ResourceId: &proto.ResourceCompact_Id{Id: "subnet"}}, []string{}),
nmdata.Resource{Type: "subnet", ID: "subnet"})
&proto.ResourceCompact{Type: proto.ResourceCompactType_subnet, ResourceId: &proto.ResourceCompact_Id{Id: "subnet"}}, []string{}))
// an unknown resource type return an empty resource
assert.Equal(t,
nmdata.Resource{},
resourceFromProto(
&proto.ResourceCompact{Type: proto.ResourceCompactType_unknown_type, ResourceId: &proto.ResourceCompact_Id{Id: "boom"}}, []string{}),
nmdata.Resource{})
&proto.ResourceCompact{Type: proto.ResourceCompactType_unknown_type, ResourceId: &proto.ResourceCompact_Id{Id: "boom"}}, []string{}))
}

View File

@@ -36,7 +36,7 @@ type EnvelopeResult struct {
// dnsName is the account's DNS domain ("netbird.cloud" etc.); used when
// rebuilding the per-peer FQDNs that proto.RemotePeerConfig carries.
func EnvelopeToNetworkMap(ctx context.Context, env *proto.NetworkMapEnvelope, localPeerKey, dnsName string) (*EnvelopeResult, error) {
components, err := DecodeEnvelope(env)
components, err := DecodeEnvelope(ctx, env)
if err != nil {
return nil, fmt.Errorf("decode envelope: %w", err)
}

View File

@@ -17,37 +17,42 @@ type sshRequirements struct {
// exactly, operating on nmdata twins throughout — no Account reference and no
// twin↔real conversion, since the produced components hold twins.
func (nmd *NetworkMapData) GetPeerNetworkMapComponents(peerID string, peersCustomZone nmdata.CustomZone) *types.NetworkMapComponents {
forceRoutingPeerDNS := nmd.forcesRoutingPeerDNSResolution(peerID)
peer := nmd.Peers[peerID]
if peer == nil {
return types.EmptyNetworkMapComponents(&types.NetworkMapComponents{
PeerID: peerID,
Network: nmd.Network,
Peers: map[string]*nmdata.Peer{peerID: peer},
PeerID: peerID,
Network: nmd.Network,
Peers: map[string]*nmdata.Peer{peerID: peer},
ForceRoutingPeerDNSResolution: forceRoutingPeerDNS,
})
}
if _, ok := nmd.ValidatedPeers[peerID]; !ok {
return types.EmptyNetworkMapComponents(&types.NetworkMapComponents{
PeerID: peerID,
Network: nmd.Network,
Peers: map[string]*nmdata.Peer{peerID: peer},
PeerID: peerID,
Network: nmd.Network,
Peers: map[string]*nmdata.Peer{peerID: peer},
ForceRoutingPeerDNSResolution: forceRoutingPeerDNS,
})
}
components := &types.NetworkMapComponents{
PeerID: peerID,
Network: nmd.Network,
AccountSettings: nmd.AccountSettings,
DNSSettings: nmd.DNSSettings,
CustomZoneDomain: peersCustomZone.Domain,
NameServerGroups: make([]*nmdata.NameServerGroup, 0),
ResourcePoliciesMap: make(map[string][]*nmdata.Policy),
RoutersMap: make(map[string]map[string]*nmdata.NetworkRouter),
NetworkResources: make([]*nmdata.NetworkResource, 0),
PostureFailedPeers: make(map[string]map[string]struct{}, len(nmd.PostureChecks)),
RouterPeers: make(map[string]*nmdata.Peer),
NetworkXIDToPublicID: nmd.NetworkXIDToPublicID,
PostureCheckXIDToPublicID: nmd.PostureCheckXIDToPublicID,
PeerID: peerID,
Network: nmd.Network,
AccountSettings: nmd.AccountSettings,
DNSSettings: nmd.DNSSettings,
CustomZoneDomain: peersCustomZone.Domain,
NameServerGroups: make([]*nmdata.NameServerGroup, 0),
ResourcePoliciesMap: make(map[string][]*nmdata.Policy),
RoutersMap: make(map[string]map[string]*nmdata.NetworkRouter),
NetworkResources: make([]*nmdata.NetworkResource, 0),
PostureFailedPeers: make(map[string]map[string]struct{}, len(nmd.PostureChecks)),
RouterPeers: make(map[string]*nmdata.Peer),
NetworkXIDToPublicID: nmd.NetworkXIDToPublicID,
PostureCheckXIDToPublicID: nmd.PostureCheckXIDToPublicID,
ForceRoutingPeerDNSResolution: forceRoutingPeerDNS,
}
relevantPeers, relevantGroups, relevantPolicies, relevantRoutes, sshReqs := nmd.getPeersGroupsPoliciesRoutes(peerID, peer.SSHEnabled, &components.PostureFailedPeers)
@@ -65,7 +70,7 @@ func (nmd *NetworkMapData) GetPeerNetworkMapComponents(peerID string, peersCusto
components.Routes = relevantRoutes
components.AllDNSRecords = filterDNSRecordsByPeers(peersCustomZone.Records, relevantPeers, peer.SupportsIPv6() && peer.IPv6.IsValid())
peerGroups := nmd.getPeerGroups(peerID)
peerGroups := nmd.GetPeerGroups(peerID)
components.AccountZones = nmd.appliedZones(peerGroups)
components.AccountZones = append(components.AccountZones, nmd.privateServiceZones(peerGroups)...)
@@ -229,14 +234,20 @@ func (nmd *NetworkMapData) getPeersGroupsPoliciesRoutes(
}
for _, groupID := range r.PeerGroups {
relevantGroupIDs[groupID] = nmd.Groups[groupID]
if g := nmd.Groups[groupID]; g != nil {
relevantGroupIDs[groupID] = g
}
}
for _, groupID := range r.Groups {
relevantGroupIDs[groupID] = nmd.Groups[groupID]
if g := nmd.Groups[groupID]; g != nil {
relevantGroupIDs[groupID] = g
}
}
if r.Enabled {
for _, groupID := range r.AccessControlGroups {
relevantGroupIDs[groupID] = nmd.Groups[groupID]
if g := nmd.Groups[groupID]; g != nil {
relevantGroupIDs[groupID] = g
}
routeAccessControlGroups[groupID] = struct{}{}
}
}
@@ -284,10 +295,14 @@ func (nmd *NetworkMapData) getPeersGroupsPoliciesRoutes(
if _, needed := routeAccessControlGroups[destGroupID]; needed {
policyRelevant = true
for _, srcGroupID := range rule.Sources {
relevantGroupIDs[srcGroupID] = nmd.Groups[srcGroupID]
if g := nmd.Groups[srcGroupID]; g != nil {
relevantGroupIDs[srcGroupID] = g
}
}
for _, dstGroupID := range rule.Destinations {
relevantGroupIDs[dstGroupID] = nmd.Groups[dstGroupID]
if g := nmd.Groups[dstGroupID]; g != nil {
relevantGroupIDs[dstGroupID] = g
}
}
break
}
@@ -321,7 +336,9 @@ func (nmd *NetworkMapData) getPeersGroupsPoliciesRoutes(
relevantPeerIDs[pid] = nmd.Peers[pid]
}
for _, dstGroupID := range rule.Destinations {
relevantGroupIDs[dstGroupID] = nmd.Groups[dstGroupID]
if g := nmd.Groups[dstGroupID]; g != nil {
relevantGroupIDs[dstGroupID] = g
}
}
}
@@ -331,7 +348,9 @@ func (nmd *NetworkMapData) getPeersGroupsPoliciesRoutes(
relevantPeerIDs[pid] = nmd.Peers[pid]
}
for _, srcGroupID := range rule.Sources {
relevantGroupIDs[srcGroupID] = nmd.Groups[srcGroupID]
if g := nmd.Groups[srcGroupID]; g != nil {
relevantGroupIDs[srcGroupID] = g
}
}
if rule.Protocol == string(types.PolicyRuleProtocolNetbirdSSH) {
@@ -465,6 +484,9 @@ func (nmd *NetworkMapData) getPostureValidPeersSaveFailed(inputPeers []string, p
dest = append(dest, peerID)
continue
}
if pname == "" {
continue
}
if _, ok := (*postureFailedPeers)[pname]; !ok {
(*postureFailedPeers)[pname] = make(map[string]struct{})
}
@@ -473,7 +495,32 @@ func (nmd *NetworkMapData) getPostureValidPeersSaveFailed(inputPeers []string, p
return dest
}
func (nmd *NetworkMapData) getPeerGroups(peerID string) map[string]struct{} {
// forcesRoutingPeerDNSResolution reports whether the given peer must run
// routing-peer DNS resolution regardless of the account-global
// RoutingPeerDNSResolutionEnabled setting: true when the peer routes a domain
// network resource targeted by an enabled reverse-proxy service, so the peer's
// DNS forwarder starts and can resolve the target for the embedded proxy peers.
func (nmd *NetworkMapData) forcesRoutingPeerDNSResolution(peerID string) bool {
if len(nmd.ProxyTargetedDomainResourceIDs) == 0 {
return false
}
for _, resource := range nmd.NetworkResources {
if resource == nil || !resource.Enabled || resource.Type != string(types.ResourceTypeDomain) {
continue
}
if _, ok := nmd.ProxyTargetedDomainResourceIDs[resource.ID]; !ok {
continue
}
if _, isRouter := nmd.Routers[resource.NetworkID][peerID]; isRouter {
return true
}
}
return false
}
func (nmd *NetworkMapData) GetPeerGroups(peerID string) map[string]struct{} {
groups := make(map[string]struct{})
for groupID, group := range nmd.Groups {
if slices.Contains(group.Peers, peerID) {

View File

@@ -32,6 +32,11 @@ type NetworkMapData struct {
GroupIDToUserIDs map[string][]string
DNSDomain string
// ProxyTargetedDomainResourceIDs is the account-level half of
// forcesRoutingPeerDNSResolution: domain network resources targeted by an
// enabled reverse-proxy service.
ProxyTargetedDomainResourceIDs map[string]struct{}
AppliedZoneCandidates []AppliedZoneCandidate
PrivateServiceCandidates []PrivateServiceCandidate
}

View File

@@ -8,4 +8,11 @@ type AccountSettingsInfo struct {
PeerLoginExpiration time.Duration
PeerInactivityExpirationEnabled bool
PeerInactivityExpiration time.Duration
DNSDomain string
IPv6EnabledGroups []string
RoutingPeerDNSResolutionEnabled bool
LazyConnectionEnabled bool
AutoUpdateVersion string
AutoUpdateAlways bool
MetricsPushEnabled bool
}

View File

@@ -19,9 +19,10 @@ func (g *Group) IsGroupAll() bool {
func (g *Group) Copy() *Group {
return &Group{
ID: g.ID,
Name: g.Name,
PublicID: g.PublicID,
Peers: slices.Clone(g.Peers),
ID: g.ID,
Name: g.Name,
PublicID: g.PublicID,
Peers: slices.Clone(g.Peers),
Resources: slices.Clone(g.Resources),
}
}

View File

@@ -0,0 +1,84 @@
package nmdata
import (
"reflect"
"testing"
)
// TestGroupCopy_AllFieldsCopied fills every Group field with a unique non-zero
// value derived from its field path, so a field added to Group but forgotten
// in Copy fails here by name without the test needing an update. The unique
// per-path values also catch fields swapped inside Copy.
func TestGroupCopy_AllFieldsCopied(t *testing.T) {
src := &Group{}
seed := 0
fillValue(t, reflect.ValueOf(src).Elem(), "Group", &seed)
copied := src.Copy()
srcV := reflect.ValueOf(src).Elem()
copiedV := reflect.ValueOf(copied).Elem()
for i := 0; i < srcV.NumField(); i++ {
name := srcV.Type().Field(i).Name
if !reflect.DeepEqual(srcV.Field(i).Interface(), copiedV.Field(i).Interface()) {
t.Errorf("field %s not copied: src=%#v copy=%#v",
name, srcV.Field(i).Interface(), copiedV.Field(i).Interface())
}
}
for i := 0; i < srcV.NumField(); i++ {
f := srcV.Field(i)
if f.Kind() != reflect.Slice || f.Len() == 0 {
continue
}
name := srcV.Type().Field(i).Name
fillValue(t, f.Index(0), name+"-mutated", &seed)
if reflect.DeepEqual(f.Interface(), copiedV.Field(i).Interface()) {
t.Errorf("field %s shares memory with the copy", name)
}
}
}
// fillValue sets v to a deterministic non-zero value derived from its field
// path. Kinds it does not handle fail the test loudly, so the filler is
// extended together with the struct instead of silently under-testing new
// fields.
func fillValue(t *testing.T, v reflect.Value, path string, seed *int) {
t.Helper()
switch v.Kind() {
case reflect.String:
v.SetString(path)
case reflect.Bool:
v.SetBool(true)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
*seed++
v.SetInt(int64(*seed))
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
*seed++
v.SetUint(uint64(*seed))
case reflect.Float32, reflect.Float64:
*seed++
v.SetFloat(float64(*seed))
case reflect.Slice:
s := reflect.MakeSlice(v.Type(), 2, 2)
fillValue(t, s.Index(0), path+"[0]", seed)
fillValue(t, s.Index(1), path+"[1]", seed)
v.Set(s)
case reflect.Struct:
settable := 0
for i := 0; i < v.NumField(); i++ {
f := v.Field(i)
if !f.CanSet() {
continue
}
settable++
fillValue(t, f, path+"."+v.Type().Field(i).Name, seed)
}
if settable == 0 {
t.Fatalf("struct %s at %s has no settable fields — extend fillValue to construct it", v.Type(), path)
}
default:
t.Fatalf("unsupported kind %s at %s — extend fillValue", v.Kind(), path)
}
}

View File

@@ -25,6 +25,7 @@ type Peer struct {
IP netip.Addr
IPv6 netip.Addr
RequiresApproval bool
ExtraDNSLabels []string
Meta PeerSystemMeta
ProxyMeta ProxyMeta
Location PeerLocation
@@ -37,14 +38,15 @@ type ProxyMeta struct {
// PeerSystemMeta is the slim twin of peer.PeerSystemMeta.
type PeerSystemMeta struct {
WtVersion string
GoOS string
OSVersion string
KernelVersion string
NetworkAddresses []NetworkAddress
Files []File
Capabilities []int32
Flags Flags
WtVersion string
GoOS string
OSVersion string
KernelVersion string
NetworkAddresses []NetworkAddress
Files []File
Capabilities []int32
Flags Flags
SyncMessageVersion int
}
// Flags is the slim twin of peer.Flags.
@@ -101,6 +103,18 @@ func (p *Peer) GetLastLogin() time.Time {
return time.Time{}
}
// SessionExpiresAt mirrors peer.Peer.SessionExpiresAt.
func (p *Peer) SessionExpiresAt(accountExpirationEnabled bool, expiresIn time.Duration) time.Time {
if !accountExpirationEnabled || !p.AddedWithSSOLogin() || !p.LoginExpirationEnabled {
return time.Time{}
}
last := p.GetLastLogin()
if last.IsZero() {
return time.Time{}
}
return last.Add(expiresIn).UTC()
}
func (p *Peer) LoginExpired(expiresIn time.Duration) (bool, time.Duration) {
if !p.AddedWithSSOLogin() || !p.LoginExpirationEnabled {
return false, 0

View File

@@ -0,0 +1,111 @@
package networkmap
import (
"context"
"fmt"
"strings"
"github.com/hashicorp/go-multierror"
"github.com/miekg/dns"
log "github.com/sirupsen/logrus"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/shared/management/networkmap/nmdata"
)
const peersZoneRecordTTL = 300
// PeersCustomZone builds the peers DNS zone from twin peer rows. It is the
// single source of the zone-record logic; Account.GetPeersCustomZone delegates
// here via twins.
func PeersCustomZone(ctx context.Context, accountID string, dnsDomain string, peers map[string]*nmdata.Peer, ipv6AllowedPeers map[string]struct{}) nmdata.CustomZone {
var merr *multierror.Error
if dnsDomain == "" {
log.WithContext(ctx).Error("no dns domain is set, returning empty zone")
return nmdata.CustomZone{}
}
customZone := nmdata.CustomZone{
Domain: dns.Fqdn(dnsDomain),
Records: make([]nmdata.SimpleRecord, 0, len(peers)),
}
domainSuffix := "." + dnsDomain
var sb strings.Builder
for _, peer := range peers {
if peer == nil {
continue
}
if peer.DNSLabel == "" {
merr = multierror.Append(merr, fmt.Errorf("peer %s has an empty DNS label", peer.ID))
continue
}
sb.Grow(len(peer.DNSLabel) + len(domainSuffix))
sb.WriteString(peer.DNSLabel)
sb.WriteString(domainSuffix)
fqdn := sb.String()
customZone.Records = append(customZone.Records, nmdata.SimpleRecord{
Name: fqdn,
Type: int(dns.TypeA),
Class: nbdns.DefaultClass,
TTL: peersZoneRecordTTL,
RData: peer.IP.String(),
})
// Only advertise AAAA for peers that have a valid IPv6, whose client supports it,
// and that belong to an IPv6-enabled group. Old clients don't configure v6 on their
// WireGuard interface, so resolving their AAAA causes connections to hang.
// Capability changes (client upgrade/downgrade, --disable-ipv6 toggle) propagate
// to other peers via SyncPeer/LoginPeer regardless of version change, so AAAA
// records refresh when a peer first reports the IPv6 overlay capability.
_, peerAllowed := ipv6AllowedPeers[peer.ID]
hasIPv6 := peer.IPv6.IsValid() && peer.SupportsIPv6() && peerAllowed
if hasIPv6 {
customZone.Records = append(customZone.Records, nmdata.SimpleRecord{
Name: fqdn,
Type: int(dns.TypeAAAA),
Class: nbdns.DefaultClass,
TTL: peersZoneRecordTTL,
RData: peer.IPv6.String(),
})
}
sb.Reset()
for _, extraLabel := range peer.ExtraDNSLabels {
sb.Grow(len(extraLabel) + len(domainSuffix))
sb.WriteString(extraLabel)
sb.WriteString(domainSuffix)
extraFqdn := sb.String()
customZone.Records = append(customZone.Records, nmdata.SimpleRecord{
Name: extraFqdn,
Type: int(dns.TypeA),
Class: nbdns.DefaultClass,
TTL: peersZoneRecordTTL,
RData: peer.IP.String(),
})
if hasIPv6 {
customZone.Records = append(customZone.Records, nmdata.SimpleRecord{
Name: extraFqdn,
Type: int(dns.TypeAAAA),
Class: nbdns.DefaultClass,
TTL: peersZoneRecordTTL,
RData: peer.IPv6.String(),
})
}
sb.Reset()
}
}
go func() {
if merr != nil {
log.WithContext(ctx).Errorf("error generating custom zone for account %s: %v", accountID, merr)
}
}()
return customZone
}