Compare commits

...

11 Commits

Author SHA1 Message Date
crn4
f357db6ffa Merge branch 'main' into nmap/compaction-deploy 2026-01-22 19:01:58 +01:00
crn4
0297976875 compaction of components 2026-01-22 19:01:18 +01:00
crn4
b32cf57ed1 minor fixes on diffs 2026-01-21 17:12:34 +01:00
crn4
ad025e452b Merge branch 'main' into nmap/compaction-deploy 2026-01-21 16:14:18 +01:00
crn4
b533c173a4 minor fixes on diffs 2026-01-21 16:13:52 +01:00
crn4
9ccae8447b Merge branch 'main' into nmap/compaction-deploy 2026-01-20 19:50:51 +01:00
crn4
7941a91fcb diff fixes 2026-01-20 19:50:23 +01:00
crn4
88dde22f07 small log 2026-01-20 15:20:34 +01:00
crn4
a23d538d09 opt over ssh users 2026-01-20 15:11:49 +01:00
crn4
6b15d14ef0 Merge branch 'main' into nmap/compaction-deploy 2026-01-20 14:13:01 +01:00
crn4
7598efe320 components network map for deploy - comparison with legacy 2026-01-20 14:12:13 +01:00
7 changed files with 2913 additions and 1 deletions

View File

@@ -63,6 +63,8 @@ type Controller struct {
expNewNetworkMap bool
expNewNetworkMapAIDs map[string]struct{}
compactedNetworkMap bool
}
type bufferUpdate struct {
@@ -85,6 +87,12 @@ func NewController(ctx context.Context, store store.Store, metrics telemetry.App
newNetworkMapBuilder = false
}
compactedNetworkMap, err := strconv.ParseBool(os.Getenv(types.EnvNewNetworkMapCompacted))
if err != nil {
log.WithContext(ctx).Warnf("failed to parse %s, using default value false: %v", types.EnvNewNetworkMapCompacted, err)
compactedNetworkMap = false
}
ids := strings.Split(os.Getenv(network_map.EnvNewNetworkMapAccounts), ",")
expIDs := make(map[string]struct{}, len(ids))
for _, id := range ids {
@@ -108,6 +116,8 @@ func NewController(ctx context.Context, store store.Store, metrics telemetry.App
holder: types.NewHolder(),
expNewNetworkMap: newNetworkMapBuilder,
expNewNetworkMapAIDs: expIDs,
compactedNetworkMap: compactedNetworkMap,
}
}
@@ -233,6 +243,9 @@ func (c *Controller) sendUpdateAccountPeers(ctx context.Context, accountID strin
remotePeerNetworkMap = c.getPeerNetworkMapExp(ctx, p.AccountID, p.ID, approvedPeersMap, peersCustomZone, accountZones, c.accountManagerMetrics)
} else {
remotePeerNetworkMap = account.GetPeerNetworkMap(ctx, p.ID, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, c.accountManagerMetrics, groupIDToUserIDs)
if c.compactedNetworkMap {
account.ShadowCompareNetworkMap(ctx, p.ID, remotePeerNetworkMap, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, groupIDToUserIDs, c.accountManagerMetrics)
}
}
c.metrics.CountCalcPeerNetworkMapDuration(time.Since(start))
@@ -354,6 +367,9 @@ func (c *Controller) UpdateAccountPeer(ctx context.Context, accountId string, pe
remotePeerNetworkMap = c.getPeerNetworkMapExp(ctx, peer.AccountID, peer.ID, approvedPeersMap, peersCustomZone, accountZones, c.accountManagerMetrics)
} else {
remotePeerNetworkMap = account.GetPeerNetworkMap(ctx, peerId, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, c.accountManagerMetrics, groupIDToUserIDs)
if c.compactedNetworkMap {
account.ShadowCompareNetworkMap(ctx, peerId, remotePeerNetworkMap, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, groupIDToUserIDs, c.accountManagerMetrics)
}
}
proxyNetworkMap, ok := proxyNetworkMaps[peer.ID]
@@ -469,7 +485,11 @@ func (c *Controller) GetValidatedPeerWithMap(ctx context.Context, isRequiresAppr
} else {
resourcePolicies := account.GetResourcePoliciesMap()
routers := account.GetResourceRoutersMap()
networkMap = account.GetPeerNetworkMap(ctx, peer.ID, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, c.accountManagerMetrics, account.GetActiveGroupUsers())
groupIDToUserIDs := account.GetActiveGroupUsers()
networkMap = account.GetPeerNetworkMap(ctx, peer.ID, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, c.accountManagerMetrics, groupIDToUserIDs)
if c.compactedNetworkMap {
account.ShadowCompareNetworkMap(ctx, peer.ID, networkMap, peersCustomZone, accountZones, approvedPeersMap, resourcePolicies, routers, groupIDToUserIDs, c.accountManagerMetrics)
}
}
proxyNetworkMap, ok := proxyNetworkMaps[peer.ID]
@@ -843,6 +863,9 @@ func (c *Controller) GetNetworkMap(ctx context.Context, peerID string) (*types.N
resourcePolicies := account.GetResourcePoliciesMap()
routers := account.GetResourceRoutersMap()
networkMap = account.GetPeerNetworkMap(ctx, peer.ID, peersCustomZone, accountZones, validatedPeers, resourcePolicies, routers, nil, account.GetActiveGroupUsers())
if c.compactedNetworkMap {
account.ShadowCompareNetworkMap(ctx, peer.ID, networkMap, peersCustomZone, accountZones, validatedPeers, resourcePolicies, routers, account.GetActiveGroupUsers(), c.accountManagerMetrics)
}
}
proxyNetworkMap, ok := proxyNetworkMaps[peer.ID]

View File

@@ -14,6 +14,10 @@ type AccountManagerMetrics struct {
getPeerNetworkMapDurationMs metric.Float64Histogram
networkMapObjectCount metric.Int64Histogram
peerMetaUpdateCount metric.Int64Counter
shadowLegacySizeBytes metric.Int64Histogram
shadowComponentsSizeBytes metric.Int64Histogram
shadowSavingsPercent metric.Int64Histogram
}
// NewAccountManagerMetrics creates an instance of AccountManagerMetrics
@@ -55,12 +59,46 @@ func NewAccountManagerMetrics(ctx context.Context, meter metric.Meter) (*Account
return nil, err
}
shadowLegacySizeBytes, err := meter.Int64Histogram("management.account.shadow.legacy.size.bytes",
metric.WithUnit("bytes"),
metric.WithExplicitBucketBoundaries(
1024, 5120, 10240, 51200, 102400, 512000, 1048576, 5242880, 10485760,
),
metric.WithDescription("Size of legacy network map in bytes"))
if err != nil {
return nil, err
}
shadowComponentsSizeBytes, err := meter.Int64Histogram("management.account.shadow.components.size.bytes",
metric.WithUnit("bytes"),
metric.WithExplicitBucketBoundaries(
1024, 5120, 10240, 51200, 102400, 512000, 1048576, 5242880, 10485760,
),
metric.WithDescription("Size of components-based network map in bytes"))
if err != nil {
return nil, err
}
shadowSavingsPercent, err := meter.Int64Histogram("management.account.shadow.savings.percent",
metric.WithUnit("percent"),
metric.WithExplicitBucketBoundaries(
0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100,
),
metric.WithDescription("Percentage of bandwidth savings with components-based network map"))
if err != nil {
return nil, err
}
return &AccountManagerMetrics{
ctx: ctx,
getPeerNetworkMapDurationMs: getPeerNetworkMapDurationMs,
updateAccountPeersDurationMs: updateAccountPeersDurationMs,
networkMapObjectCount: networkMapObjectCount,
peerMetaUpdateCount: peerMetaUpdateCount,
shadowLegacySizeBytes: shadowLegacySizeBytes,
shadowComponentsSizeBytes: shadowComponentsSizeBytes,
shadowSavingsPercent: shadowSavingsPercent,
}, nil
}
@@ -84,3 +122,18 @@ func (metrics *AccountManagerMetrics) CountNetworkMapObjects(count int64) {
func (metrics *AccountManagerMetrics) CountPeerMetUpdate() {
metrics.peerMetaUpdateCount.Add(metrics.ctx, 1)
}
// CountShadowLegacySize records the size of legacy network map in bytes
func (metrics *AccountManagerMetrics) CountShadowLegacySize(bytes int64) {
metrics.shadowLegacySizeBytes.Record(metrics.ctx, bytes)
}
// CountShadowComponentsSize records the size of components-based network map in bytes
func (metrics *AccountManagerMetrics) CountShadowComponentsSize(bytes int64) {
metrics.shadowComponentsSizeBytes.Record(metrics.ctx, bytes)
}
// CountShadowSavingsPercent records the percentage of bandwidth savings
func (metrics *AccountManagerMetrics) CountShadowSavingsPercent(percent int64) {
metrics.shadowSavingsPercent.Record(metrics.ctx, percent)
}

View File

@@ -0,0 +1,529 @@
package types
import (
"context"
"slices"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/management/internals/modules/zones"
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/route"
)
func (a *Account) GetPeerNetworkMapComponents(
ctx context.Context,
peerID string,
peersCustomZone nbdns.CustomZone,
accountZones []*zones.Zone,
validatedPeersMap map[string]struct{},
resourcePolicies map[string][]*Policy,
routers map[string]map[string]*routerTypes.NetworkRouter,
groupIDToUserIDs map[string][]string,
) *NetworkMapComponents {
peer := a.Peers[peerID]
if peer == nil {
return nil
}
if _, ok := validatedPeersMap[peerID]; !ok {
return nil
}
components := &NetworkMapComponents{
PeerID: peerID,
Serial: a.Network.Serial,
Network: a.Network.Copy(),
NameServerGroups: make([]*nbdns.NameServerGroup, 0),
CustomZoneDomain: peersCustomZone.Domain,
ResourcePoliciesMap: make(map[string][]*Policy),
RoutersMap: make(map[string]map[string]*routerTypes.NetworkRouter),
NetworkResources: make([]*resourceTypes.NetworkResource, 0),
PostureFailedPeers: make(map[string]map[string]struct{}, len(a.PostureChecks)),
RouterPeers: make(map[string]*nbpeer.Peer),
}
components.AccountSettings = &AccountSettingsInfo{
PeerLoginExpirationEnabled: a.Settings.PeerLoginExpirationEnabled,
PeerLoginExpiration: a.Settings.PeerLoginExpiration,
PeerInactivityExpirationEnabled: a.Settings.PeerInactivityExpirationEnabled,
PeerInactivityExpiration: a.Settings.PeerInactivityExpiration,
}
components.DNSSettings = &a.DNSSettings
relevantPeers, relevantGroups, relevantPolicies, relevantRoutes, sshReqs := a.getPeersGroupsPoliciesRoutes(ctx, peerID, peer.SSHEnabled, validatedPeersMap, &components.PostureFailedPeers)
if len(sshReqs.neededGroupIDs) > 0 {
components.GroupIDToUserIDs = filterGroupIDToUserIDs(groupIDToUserIDs, sshReqs.neededGroupIDs)
}
if sshReqs.needAllowedUserIDs {
components.AllowedUserIDs = a.getAllowedUserIDs()
}
components.Peers = relevantPeers
components.Groups = relevantGroups
components.Policies = relevantPolicies
components.Routes = relevantRoutes
components.AllDNSRecords = filterDNSRecordsByPeers(peersCustomZone.Records, relevantPeers)
peerGroups := a.GetPeerGroups(peerID)
components.AccountZones = filterPeerAppliedZones(ctx, accountZones, peerGroups)
for _, nsGroup := range a.NameServerGroups {
if nsGroup.Enabled {
for _, gID := range nsGroup.Groups {
if _, found := relevantGroups[gID]; found {
components.NameServerGroups = append(components.NameServerGroups, nsGroup)
break
}
}
}
}
for _, resource := range a.NetworkResources {
if !resource.Enabled {
continue
}
policies, exists := resourcePolicies[resource.ID]
if !exists {
continue
}
addSourcePeers := false
networkRoutingPeers, routerExists := routers[resource.NetworkID]
if routerExists {
if _, ok := networkRoutingPeers[peerID]; ok {
addSourcePeers = true
}
}
for _, policy := range policies {
if addSourcePeers {
var peers []string
if policy.Rules[0].SourceResource.Type == ResourceTypePeer && policy.Rules[0].SourceResource.ID != "" {
peers = []string{policy.Rules[0].SourceResource.ID}
} else {
peers = a.getUniquePeerIDsFromGroupsIDs(ctx, policy.SourceGroups())
}
for _, pID := range a.getPostureValidPeersSaveFailed(peers, policy.SourcePostureChecks, validatedPeersMap, &components.PostureFailedPeers) {
if _, exists := components.Peers[pID]; !exists {
components.Peers[pID] = a.GetPeer(pID)
}
}
} else {
peerInSources := false
if policy.Rules[0].SourceResource.Type == ResourceTypePeer && policy.Rules[0].SourceResource.ID != "" {
peerInSources = policy.Rules[0].SourceResource.ID == peerID
} else {
for _, groupID := range policy.SourceGroups() {
if group := a.GetGroup(groupID); group != nil && slices.Contains(group.Peers, peerID) {
peerInSources = true
break
}
}
}
if !peerInSources {
continue
}
isValid, pname := a.validatePostureChecksOnPeerGetFailed(ctx, policy.SourcePostureChecks, peerID)
if !isValid && len(pname) > 0 {
if _, ok := (*components).PostureFailedPeers[pname]; !ok {
(*components).PostureFailedPeers[pname] = make(map[string]struct{})
}
(*components).PostureFailedPeers[pname][peer.ID] = struct{}{}
continue
}
addSourcePeers = true
}
for _, rule := range policy.Rules {
for _, srcGroupID := range rule.Sources {
if g := a.Groups[srcGroupID]; g != nil {
if _, exists := components.Groups[srcGroupID]; !exists {
components.Groups[srcGroupID] = g
}
}
}
for _, dstGroupID := range rule.Destinations {
if g := a.Groups[dstGroupID]; g != nil {
if _, exists := components.Groups[dstGroupID]; !exists {
components.Groups[dstGroupID] = g
}
}
}
}
components.ResourcePoliciesMap[resource.ID] = policies
}
components.RoutersMap[resource.NetworkID] = networkRoutingPeers
for peerIDKey := range networkRoutingPeers {
if p := a.Peers[peerIDKey]; p != nil {
if _, exists := components.RouterPeers[peerIDKey]; !exists {
components.RouterPeers[peerIDKey] = p
}
if _, exists := components.Peers[peerIDKey]; !exists {
if _, validated := validatedPeersMap[peerIDKey]; validated {
components.Peers[peerIDKey] = p
}
}
}
}
if addSourcePeers {
components.NetworkResources = append(components.NetworkResources, resource)
}
}
filterGroupPeers(&components.Groups, components.Peers)
filterPostureFailedPeers(&components.PostureFailedPeers, components.Policies, components.ResourcePoliciesMap, components.Peers)
return components
}
type sshRequirements struct {
neededGroupIDs map[string]struct{}
needAllowedUserIDs bool
}
func (a *Account) getPeersGroupsPoliciesRoutes(
ctx context.Context,
peerID string,
peerSSHEnabled bool,
validatedPeersMap map[string]struct{},
postureFailedPeers *map[string]map[string]struct{},
) (map[string]*nbpeer.Peer, map[string]*Group, []*Policy, []*route.Route, sshRequirements) {
relevantPeerIDs := make(map[string]*nbpeer.Peer, len(a.Peers)/4)
relevantGroupIDs := make(map[string]*Group, len(a.Groups)/4)
relevantPolicies := make([]*Policy, 0, len(a.Policies))
relevantRoutes := make([]*route.Route, 0, len(a.Routes))
sshReqs := sshRequirements{neededGroupIDs: make(map[string]struct{})}
relevantPeerIDs[peerID] = a.GetPeer(peerID)
for groupID, group := range a.Groups {
if slices.Contains(group.Peers, peerID) {
relevantGroupIDs[groupID] = a.GetGroup(groupID)
}
}
routeAccessControlGroups := make(map[string]struct{})
for _, r := range a.Routes {
for _, groupID := range r.Groups {
relevantGroupIDs[groupID] = a.GetGroup(groupID)
}
for _, groupID := range r.PeerGroups {
relevantGroupIDs[groupID] = a.GetGroup(groupID)
}
if r.Enabled {
for _, groupID := range r.AccessControlGroups {
relevantGroupIDs[groupID] = a.GetGroup(groupID)
routeAccessControlGroups[groupID] = struct{}{}
}
}
relevantRoutes = append(relevantRoutes, r)
}
for _, policy := range a.Policies {
if !policy.Enabled {
continue
}
policyRelevant := false
for _, rule := range policy.Rules {
if !rule.Enabled {
continue
}
if len(routeAccessControlGroups) > 0 {
for _, destGroupID := range rule.Destinations {
if _, needed := routeAccessControlGroups[destGroupID]; needed {
policyRelevant = true
for _, srcGroupID := range rule.Sources {
relevantGroupIDs[srcGroupID] = a.GetGroup(srcGroupID)
}
for _, dstGroupID := range rule.Destinations {
relevantGroupIDs[dstGroupID] = a.GetGroup(dstGroupID)
}
break
}
}
}
var sourcePeers, destinationPeers []string
var peerInSources, peerInDestinations bool
if rule.SourceResource.Type == ResourceTypePeer && rule.SourceResource.ID != "" {
sourcePeers = []string{rule.SourceResource.ID}
if rule.SourceResource.ID == peerID {
peerInSources = true
}
} else {
sourcePeers, peerInSources = a.getPeersFromGroups(ctx, rule.Sources, peerID, policy.SourcePostureChecks, validatedPeersMap, postureFailedPeers)
}
if rule.DestinationResource.Type == ResourceTypePeer && rule.DestinationResource.ID != "" {
destinationPeers = []string{rule.DestinationResource.ID}
if rule.DestinationResource.ID == peerID {
peerInDestinations = true
}
} else {
destinationPeers, peerInDestinations = a.getPeersFromGroups(ctx, rule.Destinations, peerID, nil, validatedPeersMap, postureFailedPeers)
}
if peerInSources {
policyRelevant = true
for _, pid := range destinationPeers {
relevantPeerIDs[pid] = a.GetPeer(pid)
}
for _, dstGroupID := range rule.Destinations {
relevantGroupIDs[dstGroupID] = a.GetGroup(dstGroupID)
}
}
if peerInDestinations {
policyRelevant = true
for _, pid := range sourcePeers {
relevantPeerIDs[pid] = a.GetPeer(pid)
}
for _, srcGroupID := range rule.Sources {
relevantGroupIDs[srcGroupID] = a.GetGroup(srcGroupID)
}
if rule.Protocol == PolicyRuleProtocolNetbirdSSH {
switch {
case len(rule.AuthorizedGroups) > 0:
for groupID := range rule.AuthorizedGroups {
sshReqs.neededGroupIDs[groupID] = struct{}{}
}
case rule.AuthorizedUser != "":
default:
sshReqs.needAllowedUserIDs = true
}
} else if policyRuleImpliesLegacySSH(rule) && peerSSHEnabled {
sshReqs.needAllowedUserIDs = true
}
}
}
if policyRelevant {
relevantPolicies = append(relevantPolicies, policy)
}
}
return relevantPeerIDs, relevantGroupIDs, relevantPolicies, relevantRoutes, sshReqs
}
func (a *Account) getPeersFromGroups(ctx context.Context, groups []string, peerID string, sourcePostureChecksIDs []string,
validatedPeersMap map[string]struct{}, postureFailedPeers *map[string]map[string]struct{}) ([]string, bool) {
peerInGroups := false
filteredPeerIDs := make([]string, 0, len(a.Peers))
seenPeerIds := make(map[string]struct{}, len(groups))
for _, gid := range groups {
group := a.GetGroup(gid)
if group == nil {
continue
}
if group.IsGroupAll() || len(groups) == 1 {
filteredPeerIDs = filteredPeerIDs[:0]
seenPeerIds = make(map[string]struct{}, len(group.Peers))
peerInGroups = false
for _, pid := range group.Peers {
peer, ok := a.Peers[pid]
if !ok || peer == nil {
continue
}
if _, ok := validatedPeersMap[peer.ID]; !ok {
continue
}
isValid, pname := a.validatePostureChecksOnPeerGetFailed(ctx, sourcePostureChecksIDs, peer.ID)
if !isValid && len(pname) > 0 {
if _, ok := (*postureFailedPeers)[pname]; !ok {
(*postureFailedPeers)[pname] = make(map[string]struct{})
}
(*postureFailedPeers)[pname][peer.ID] = struct{}{}
continue
}
if peer.ID == peerID {
peerInGroups = true
continue
}
filteredPeerIDs = append(filteredPeerIDs, peer.ID)
}
return filteredPeerIDs, peerInGroups
}
for _, pid := range group.Peers {
if _, seen := seenPeerIds[pid]; seen {
continue
}
seenPeerIds[pid] = struct{}{}
peer, ok := a.Peers[pid]
if !ok || peer == nil {
continue
}
if _, ok := validatedPeersMap[peer.ID]; !ok {
continue
}
isValid, pname := a.validatePostureChecksOnPeerGetFailed(ctx, sourcePostureChecksIDs, peer.ID)
if !isValid && len(pname) > 0 {
if _, ok := (*postureFailedPeers)[pname]; !ok {
(*postureFailedPeers)[pname] = make(map[string]struct{})
}
(*postureFailedPeers)[pname][peer.ID] = struct{}{}
continue
}
if peer.ID == peerID {
peerInGroups = true
continue
}
filteredPeerIDs = append(filteredPeerIDs, peer.ID)
}
}
return filteredPeerIDs, peerInGroups
}
func (a *Account) validatePostureChecksOnPeerGetFailed(ctx context.Context, sourcePostureChecksID []string, peerID string) (bool, string) {
peer, ok := a.Peers[peerID]
if !ok && peer == nil {
return false, ""
}
for _, postureChecksID := range sourcePostureChecksID {
postureChecks := a.GetPostureChecks(postureChecksID)
if postureChecks == nil {
continue
}
for _, check := range postureChecks.GetChecks() {
isValid, _ := check.Check(ctx, *peer)
if !isValid {
return false, postureChecksID
}
}
}
return true, ""
}
func (a *Account) getPostureValidPeersSaveFailed(inputPeers []string, postureChecksIDs []string, validatedPeersMap map[string]struct{}, postureFailedPeers *map[string]map[string]struct{}) []string {
var dest []string
for _, peerID := range inputPeers {
if _, validated := validatedPeersMap[peerID]; !validated {
continue
}
valid, pname := a.validatePostureChecksOnPeerGetFailed(context.Background(), postureChecksIDs, peerID)
if valid {
dest = append(dest, peerID)
continue
}
if _, ok := (*postureFailedPeers)[pname]; !ok {
(*postureFailedPeers)[pname] = make(map[string]struct{})
}
(*postureFailedPeers)[pname][peerID] = struct{}{}
}
return dest
}
func filterGroupPeers(groups *map[string]*Group, peers map[string]*nbpeer.Peer) {
for groupID, groupInfo := range *groups {
filteredPeers := make([]string, 0, len(groupInfo.Peers))
for _, pid := range groupInfo.Peers {
if _, exists := peers[pid]; exists {
filteredPeers = append(filteredPeers, pid)
}
}
if len(filteredPeers) == 0 {
delete(*groups, groupID)
} else if len(filteredPeers) != len(groupInfo.Peers) {
ng := groupInfo.Copy()
ng.Peers = filteredPeers
(*groups)[groupID] = ng
}
}
}
func filterPostureFailedPeers(postureFailedPeers *map[string]map[string]struct{}, policies []*Policy, resourcePoliciesMap map[string][]*Policy, peers map[string]*nbpeer.Peer) {
if len(*postureFailedPeers) == 0 {
return
}
referencedPostureChecks := make(map[string]struct{})
for _, policy := range policies {
for _, checkID := range policy.SourcePostureChecks {
referencedPostureChecks[checkID] = struct{}{}
}
}
for _, resPolicies := range resourcePoliciesMap {
for _, policy := range resPolicies {
for _, checkID := range policy.SourcePostureChecks {
referencedPostureChecks[checkID] = struct{}{}
}
}
}
for checkID, failedPeers := range *postureFailedPeers {
if _, referenced := referencedPostureChecks[checkID]; !referenced {
delete(*postureFailedPeers, checkID)
continue
}
for peerID := range failedPeers {
if _, exists := peers[peerID]; !exists {
delete(failedPeers, peerID)
}
}
if len(failedPeers) == 0 {
delete(*postureFailedPeers, checkID)
}
}
}
func filterDNSRecordsByPeers(records []nbdns.SimpleRecord, peers map[string]*nbpeer.Peer) []nbdns.SimpleRecord {
if len(records) == 0 || len(peers) == 0 {
return nil
}
peerIPs := make(map[string]struct{}, len(peers))
for _, peer := range peers {
if peer != nil {
peerIPs[peer.IP.String()] = struct{}{}
}
}
filteredRecords := make([]nbdns.SimpleRecord, 0, len(records))
for _, record := range records {
if _, exists := peerIPs[record.RData]; exists {
filteredRecords = append(filteredRecords, record)
}
}
return filteredRecords
}
func filterGroupIDToUserIDs(fullMap map[string][]string, neededGroupIDs map[string]struct{}) map[string][]string {
if len(neededGroupIDs) == 0 {
return nil
}
filtered := make(map[string][]string, len(neededGroupIDs))
for groupID := range neededGroupIDs {
if users, ok := fullMap[groupID]; ok {
filtered[groupID] = users
}
}
return filtered
}

View File

@@ -0,0 +1,841 @@
package types
import (
"context"
"encoding/json"
"fmt"
"net"
"net/netip"
"os"
"path/filepath"
"sort"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/netbirdio/netbird/dns"
nbdns "github.com/netbirdio/netbird/dns"
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
networkTypes "github.com/netbirdio/netbird/management/server/networks/types"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/posture"
"github.com/netbirdio/netbird/route"
)
func TestNetworkMapComponents_CompareWithLegacy(t *testing.T) {
account := createTestAccount()
ctx := context.Background()
peerID := testingPeerID
validatedPeersMap := make(map[string]struct{})
for i := range numPeers {
pid := fmt.Sprintf("peer-%d", i)
if pid == offlinePeerID {
continue
}
validatedPeersMap[pid] = struct{}{}
}
peersCustomZone := nbdns.CustomZone{}
resourcePolicies := account.GetResourcePoliciesMap()
routers := account.GetResourceRoutersMap()
groupIDToUserIDs := account.GetActiveGroupUsers()
legacyNetworkMap := account.GetPeerNetworkMap(
ctx,
peerID,
peersCustomZone,
nil,
validatedPeersMap,
resourcePolicies,
routers,
nil,
groupIDToUserIDs,
)
components := account.GetPeerNetworkMapComponents(
ctx,
peerID,
peersCustomZone,
nil,
validatedPeersMap,
resourcePolicies,
routers,
groupIDToUserIDs,
)
if components == nil {
t.Fatal("GetPeerNetworkMapComponents returned nil")
}
newNetworkMap := CalculateNetworkMapFromComponents(ctx, components)
if newNetworkMap == nil {
t.Fatal("CalculateNetworkMapFromComponents returned nil")
}
compareNetworkMaps(t, legacyNetworkMap, newNetworkMap)
}
func TestNetworkMapComponents_GoldenFileComparison(t *testing.T) {
account := createTestAccount()
ctx := context.Background()
peerID := testingPeerID
validatedPeersMap := make(map[string]struct{})
for i := range numPeers {
pid := fmt.Sprintf("peer-%d", i)
if pid == offlinePeerID {
continue
}
validatedPeersMap[pid] = struct{}{}
}
peersCustomZone := nbdns.CustomZone{}
resourcePolicies := account.GetResourcePoliciesMap()
routers := account.GetResourceRoutersMap()
groupIDToUserIDs := account.GetActiveGroupUsers()
legacyNetworkMap := account.GetPeerNetworkMap(
ctx,
peerID,
peersCustomZone,
nil,
validatedPeersMap,
resourcePolicies,
routers,
nil,
groupIDToUserIDs,
)
components := account.GetPeerNetworkMapComponents(
ctx,
peerID,
peersCustomZone,
nil,
validatedPeersMap,
resourcePolicies,
routers,
groupIDToUserIDs,
)
require.NotNil(t, components, "GetPeerNetworkMapComponents returned nil")
newNetworkMap := CalculateNetworkMapFromComponents(ctx, components)
require.NotNil(t, newNetworkMap, "CalculateNetworkMapFromComponents returned nil")
normalizeAndSortNetworkMap(legacyNetworkMap)
normalizeAndSortNetworkMap(newNetworkMap)
componentsJSON, err := json.MarshalIndent(components, "", " ")
require.NoError(t, err, "error marshaling components to JSON")
legacyJSON, err := json.MarshalIndent(legacyNetworkMap, "", " ")
require.NoError(t, err, "error marshaling legacy network map to JSON")
newJSON, err := json.MarshalIndent(newNetworkMap, "", " ")
require.NoError(t, err, "error marshaling new network map to JSON")
goldenDir := filepath.Join("testdata", "comparison")
err = os.MkdirAll(goldenDir, 0755)
require.NoError(t, err)
legacyGoldenPath := filepath.Join(goldenDir, "legacy_networkmap.json")
err = os.WriteFile(legacyGoldenPath, legacyJSON, 0644)
require.NoError(t, err, "error writing legacy golden file")
newGoldenPath := filepath.Join(goldenDir, "components_networkmap.json")
err = os.WriteFile(newGoldenPath, newJSON, 0644)
require.NoError(t, err, "error writing components golden file")
componentsPath := filepath.Join(goldenDir, "components.json")
err = os.WriteFile(componentsPath, componentsJSON, 0644)
require.NoError(t, err, "error writing components golden file")
require.JSONEq(t, string(legacyJSON), string(newJSON),
"NetworkMaps from legacy and components approaches do not match.\n"+
"Legacy JSON saved to: %s\n"+
"Components JSON saved to: %s",
legacyGoldenPath, newGoldenPath)
t.Logf("✅ NetworkMaps are identical")
t.Logf(" Legacy NetworkMap: %s", legacyGoldenPath)
t.Logf(" Components NetworkMap: %s", newGoldenPath)
}
func normalizeAndSortNetworkMap(nm *NetworkMap) {
if nm == nil {
return
}
sort.Slice(nm.Peers, func(i, j int) bool {
return nm.Peers[i].ID < nm.Peers[j].ID
})
sort.Slice(nm.OfflinePeers, func(i, j int) bool {
return nm.OfflinePeers[i].ID < nm.OfflinePeers[j].ID
})
sort.Slice(nm.Routes, func(i, j int) bool {
return string(nm.Routes[i].ID) < string(nm.Routes[j].ID)
})
sort.Slice(nm.FirewallRules, func(i, j int) bool {
if nm.FirewallRules[i].PeerIP != nm.FirewallRules[j].PeerIP {
return nm.FirewallRules[i].PeerIP < nm.FirewallRules[j].PeerIP
}
if nm.FirewallRules[i].Direction != nm.FirewallRules[j].Direction {
return nm.FirewallRules[i].Direction < nm.FirewallRules[j].Direction
}
if nm.FirewallRules[i].Protocol != nm.FirewallRules[j].Protocol {
return nm.FirewallRules[i].Protocol < nm.FirewallRules[j].Protocol
}
if nm.FirewallRules[i].Port != nm.FirewallRules[j].Port {
return nm.FirewallRules[i].Port < nm.FirewallRules[j].Port
}
return nm.FirewallRules[i].PolicyID < nm.FirewallRules[j].PolicyID
})
for i := range nm.RoutesFirewallRules {
sort.Strings(nm.RoutesFirewallRules[i].SourceRanges)
}
sort.Slice(nm.RoutesFirewallRules, func(i, j int) bool {
if nm.RoutesFirewallRules[i].Destination != nm.RoutesFirewallRules[j].Destination {
return nm.RoutesFirewallRules[i].Destination < nm.RoutesFirewallRules[j].Destination
}
minLen := len(nm.RoutesFirewallRules[i].SourceRanges)
if len(nm.RoutesFirewallRules[j].SourceRanges) < minLen {
minLen = len(nm.RoutesFirewallRules[j].SourceRanges)
}
for k := 0; k < minLen; k++ {
if nm.RoutesFirewallRules[i].SourceRanges[k] != nm.RoutesFirewallRules[j].SourceRanges[k] {
return nm.RoutesFirewallRules[i].SourceRanges[k] < nm.RoutesFirewallRules[j].SourceRanges[k]
}
}
if len(nm.RoutesFirewallRules[i].SourceRanges) != len(nm.RoutesFirewallRules[j].SourceRanges) {
return len(nm.RoutesFirewallRules[i].SourceRanges) < len(nm.RoutesFirewallRules[j].SourceRanges)
}
if string(nm.RoutesFirewallRules[i].RouteID) != string(nm.RoutesFirewallRules[j].RouteID) {
return string(nm.RoutesFirewallRules[i].RouteID) < string(nm.RoutesFirewallRules[j].RouteID)
}
if nm.RoutesFirewallRules[i].PolicyID != nm.RoutesFirewallRules[j].PolicyID {
return nm.RoutesFirewallRules[i].PolicyID < nm.RoutesFirewallRules[j].PolicyID
}
if nm.RoutesFirewallRules[i].Port != nm.RoutesFirewallRules[j].Port {
return nm.RoutesFirewallRules[i].Port < nm.RoutesFirewallRules[j].Port
}
return nm.RoutesFirewallRules[i].Protocol < nm.RoutesFirewallRules[j].Protocol
})
if nm.DNSConfig.CustomZones != nil {
for i := range nm.DNSConfig.CustomZones {
sort.Slice(nm.DNSConfig.CustomZones[i].Records, func(a, b int) bool {
return nm.DNSConfig.CustomZones[i].Records[a].Name < nm.DNSConfig.CustomZones[i].Records[b].Name
})
}
}
if len(nm.DNSConfig.NameServerGroups) != 0 {
sort.Slice(nm.DNSConfig.NameServerGroups, func(a, b int) bool {
return nm.DNSConfig.NameServerGroups[a].Name < nm.DNSConfig.NameServerGroups[b].Name
})
}
}
func compareNetworkMaps(t *testing.T, legacy, new *NetworkMap) {
t.Helper()
if legacy.Network.Serial != new.Network.Serial {
t.Errorf("Network Serial mismatch: legacy=%d, new=%d", legacy.Network.Serial, new.Network.Serial)
}
if len(legacy.Peers) != len(new.Peers) {
t.Errorf("Peers count mismatch: legacy=%d, new=%d", len(legacy.Peers), len(new.Peers))
}
legacyPeerIDs := make(map[string]bool)
for _, p := range legacy.Peers {
legacyPeerIDs[p.ID] = true
}
for _, p := range new.Peers {
if !legacyPeerIDs[p.ID] {
t.Errorf("New NetworkMap contains peer %s not in legacy", p.ID)
}
}
if len(legacy.OfflinePeers) != len(new.OfflinePeers) {
t.Errorf("OfflinePeers count mismatch: legacy=%d, new=%d", len(legacy.OfflinePeers), len(new.OfflinePeers))
}
if len(legacy.FirewallRules) != len(new.FirewallRules) {
t.Logf("FirewallRules count mismatch: legacy=%d, new=%d", len(legacy.FirewallRules), len(new.FirewallRules))
}
if len(legacy.Routes) != len(new.Routes) {
t.Logf("Routes count mismatch: legacy=%d, new=%d", len(legacy.Routes), len(new.Routes))
}
if len(legacy.RoutesFirewallRules) != len(new.RoutesFirewallRules) {
t.Logf("RoutesFirewallRules count mismatch: legacy=%d, new=%d", len(legacy.RoutesFirewallRules), len(new.RoutesFirewallRules))
}
if legacy.DNSConfig.ServiceEnable != new.DNSConfig.ServiceEnable {
t.Errorf("DNSConfig.ServiceEnable mismatch: legacy=%v, new=%v", legacy.DNSConfig.ServiceEnable, new.DNSConfig.ServiceEnable)
}
}
const (
numPeers = 100
devGroupID = "group-dev"
opsGroupID = "group-ops"
allGroupID = "group-all"
routeID = route.ID("route-main")
routeHA1ID = route.ID("route-ha-1")
routeHA2ID = route.ID("route-ha-2")
policyIDDevOps = "policy-dev-ops"
policyIDAll = "policy-all"
policyIDPosture = "policy-posture"
policyIDDrop = "policy-drop"
postureCheckID = "posture-check-ver"
networkResourceID = "res-database"
networkID = "net-database"
networkRouterID = "router-database"
nameserverGroupID = "ns-group-main"
testingPeerID = "peer-60"
expiredPeerID = "peer-98"
offlinePeerID = "peer-99"
routingPeerID = "peer-95"
testAccountID = "account-comparison-test"
)
func createTestAccount() *Account {
peers := make(map[string]*nbpeer.Peer)
devGroupPeers, opsGroupPeers, allGroupPeers := []string{}, []string{}, []string{}
for i := range numPeers {
peerID := fmt.Sprintf("peer-%d", i)
ip := net.IP{100, 64, 0, byte(i + 1)}
wtVersion := "0.25.0"
if i%2 == 0 {
wtVersion = "0.40.0"
}
p := &nbpeer.Peer{
ID: peerID, IP: ip, Key: fmt.Sprintf("key-%s", peerID), DNSLabel: fmt.Sprintf("peer%d", i+1),
Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now()},
UserID: "user-admin", Meta: nbpeer.PeerSystemMeta{WtVersion: wtVersion, GoOS: "linux"},
}
if peerID == expiredPeerID {
p.LoginExpirationEnabled = true
pastTimestamp := time.Now().Add(-2 * time.Hour)
p.LastLogin = &pastTimestamp
}
peers[peerID] = p
allGroupPeers = append(allGroupPeers, peerID)
if i < numPeers/2 {
devGroupPeers = append(devGroupPeers, peerID)
} else {
opsGroupPeers = append(opsGroupPeers, peerID)
}
}
groups := map[string]*Group{
allGroupID: {ID: allGroupID, Name: "All", Peers: allGroupPeers},
devGroupID: {ID: devGroupID, Name: "Developers", Peers: devGroupPeers},
opsGroupID: {ID: opsGroupID, Name: "Operations", Peers: opsGroupPeers},
}
policies := []*Policy{
{
ID: policyIDAll, Name: "Default-Allow", Enabled: true,
Rules: []*PolicyRule{{
ID: policyIDAll, Name: "Allow All", Enabled: true, Action: PolicyTrafficActionAccept,
Protocol: PolicyRuleProtocolALL, Bidirectional: true,
Sources: []string{allGroupID}, Destinations: []string{allGroupID},
}},
},
{
ID: policyIDDevOps, Name: "Dev to Ops Web Access", Enabled: true,
Rules: []*PolicyRule{{
ID: policyIDDevOps, Name: "Dev -> Ops (HTTP Range)", Enabled: true, Action: PolicyTrafficActionAccept,
Protocol: PolicyRuleProtocolTCP, Bidirectional: false,
PortRanges: []RulePortRange{{Start: 8080, End: 8090}},
Sources: []string{devGroupID}, Destinations: []string{opsGroupID},
}},
},
{
ID: policyIDDrop, Name: "Drop DB traffic", Enabled: true,
Rules: []*PolicyRule{{
ID: policyIDDrop, Name: "Drop DB", Enabled: true, Action: PolicyTrafficActionDrop,
Protocol: PolicyRuleProtocolTCP, Ports: []string{"5432"}, Bidirectional: true,
Sources: []string{devGroupID}, Destinations: []string{opsGroupID},
}},
},
{
ID: policyIDPosture, Name: "Posture Check for DB Resource", Enabled: true,
SourcePostureChecks: []string{postureCheckID},
Rules: []*PolicyRule{{
ID: policyIDPosture, Name: "Allow DB Access", Enabled: true, Action: PolicyTrafficActionAccept,
Protocol: PolicyRuleProtocolALL, Bidirectional: true,
Sources: []string{opsGroupID}, DestinationResource: Resource{ID: networkResourceID},
}},
},
}
routes := map[route.ID]*route.Route{
routeID: {
ID: routeID, Network: netip.MustParsePrefix("192.168.10.0/24"),
Peer: peers["peer-75"].Key,
PeerID: "peer-75",
Description: "Route to internal resource", Enabled: true,
PeerGroups: []string{devGroupID, opsGroupID},
Groups: []string{devGroupID, opsGroupID},
AccessControlGroups: []string{devGroupID},
},
routeHA1ID: {
ID: routeHA1ID, Network: netip.MustParsePrefix("10.10.0.0/16"),
Peer: peers["peer-80"].Key,
PeerID: "peer-80",
Description: "HA Route 1", Enabled: true, Metric: 1000,
PeerGroups: []string{allGroupID},
Groups: []string{allGroupID},
AccessControlGroups: []string{allGroupID},
},
routeHA2ID: {
ID: routeHA2ID, Network: netip.MustParsePrefix("10.10.0.0/16"),
Peer: peers["peer-90"].Key,
PeerID: "peer-90",
Description: "HA Route 2", Enabled: true, Metric: 900,
PeerGroups: []string{devGroupID, opsGroupID},
Groups: []string{devGroupID, opsGroupID},
AccessControlGroups: []string{allGroupID},
},
}
account := &Account{
Id: testAccountID, Peers: peers, Groups: groups, Policies: policies, Routes: routes,
Network: &Network{
Identifier: "net-comparison-test", Net: net.IPNet{IP: net.IP{100, 64, 0, 0}, Mask: net.CIDRMask(16, 32)}, Serial: 1,
},
DNSSettings: DNSSettings{DisabledManagementGroups: []string{opsGroupID}},
NameServerGroups: map[string]*nbdns.NameServerGroup{
nameserverGroupID: {
ID: nameserverGroupID, Name: "Main NS", Enabled: true, Groups: []string{devGroupID},
NameServers: []nbdns.NameServer{{IP: netip.MustParseAddr("8.8.8.8"), NSType: nbdns.UDPNameServerType, Port: 53}},
},
},
PostureChecks: []*posture.Checks{
{ID: postureCheckID, Name: "Check version", Checks: posture.ChecksDefinition{
NBVersionCheck: &posture.NBVersionCheck{MinVersion: "0.26.0"},
}},
},
NetworkResources: []*resourceTypes.NetworkResource{
{ID: networkResourceID, NetworkID: networkID, AccountID: testAccountID, Enabled: true, Address: "db.netbird.cloud"},
},
Networks: []*networkTypes.Network{{ID: networkID, Name: "DB Network", AccountID: testAccountID}},
NetworkRouters: []*routerTypes.NetworkRouter{
{ID: networkRouterID, NetworkID: networkID, Peer: routingPeerID, Enabled: true, AccountID: testAccountID},
},
Settings: &Settings{PeerLoginExpirationEnabled: true, PeerLoginExpiration: 1 * time.Hour},
}
for _, p := range account.Policies {
p.AccountID = account.Id
}
for _, r := range account.Routes {
r.AccountID = account.Id
}
return account
}
func BenchmarkLegacyNetworkMap(b *testing.B) {
account := createTestAccount()
ctx := context.Background()
peerID := testingPeerID
validatedPeersMap := make(map[string]struct{})
for i := range numPeers {
pid := fmt.Sprintf("peer-%d", i)
if pid != offlinePeerID {
validatedPeersMap[pid] = struct{}{}
}
}
peersCustomZone := nbdns.CustomZone{}
resourcePolicies := account.GetResourcePoliciesMap()
routers := account.GetResourceRoutersMap()
groupIDToUserIDs := account.GetActiveGroupUsers()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = account.GetPeerNetworkMap(
ctx,
peerID,
peersCustomZone,
nil,
validatedPeersMap,
resourcePolicies,
routers,
nil,
groupIDToUserIDs,
)
}
}
func BenchmarkComponentsNetworkMap(b *testing.B) {
account := createTestAccount()
ctx := context.Background()
peerID := testingPeerID
validatedPeersMap := make(map[string]struct{})
for i := range numPeers {
pid := fmt.Sprintf("peer-%d", i)
if pid != offlinePeerID {
validatedPeersMap[pid] = struct{}{}
}
}
peersCustomZone := nbdns.CustomZone{}
resourcePolicies := account.GetResourcePoliciesMap()
routers := account.GetResourceRoutersMap()
groupIDToUserIDs := account.GetActiveGroupUsers()
b.ResetTimer()
for i := 0; i < b.N; i++ {
components := account.GetPeerNetworkMapComponents(
ctx,
peerID,
peersCustomZone,
nil,
validatedPeersMap,
resourcePolicies,
routers,
groupIDToUserIDs,
)
_ = CalculateNetworkMapFromComponents(ctx, components)
}
}
func BenchmarkComponentsCreation(b *testing.B) {
account := createTestAccount()
ctx := context.Background()
peerID := testingPeerID
validatedPeersMap := make(map[string]struct{})
for i := range numPeers {
pid := fmt.Sprintf("peer-%d", i)
if pid != offlinePeerID {
validatedPeersMap[pid] = struct{}{}
}
}
peersCustomZone := nbdns.CustomZone{}
resourcePolicies := account.GetResourcePoliciesMap()
routers := account.GetResourceRoutersMap()
groupIDToUserIDs := account.GetActiveGroupUsers()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = account.GetPeerNetworkMapComponents(
ctx,
peerID,
peersCustomZone,
nil,
validatedPeersMap,
resourcePolicies,
routers,
groupIDToUserIDs,
)
}
}
func BenchmarkCalculationFromComponents(b *testing.B) {
account := createTestAccount()
ctx := context.Background()
peerID := testingPeerID
validatedPeersMap := make(map[string]struct{})
for i := range numPeers {
pid := fmt.Sprintf("peer-%d", i)
if pid != offlinePeerID {
validatedPeersMap[pid] = struct{}{}
}
}
peersCustomZone := nbdns.CustomZone{}
resourcePolicies := account.GetResourcePoliciesMap()
routers := account.GetResourceRoutersMap()
groupIDToUserIDs := account.GetActiveGroupUsers()
components := account.GetPeerNetworkMapComponents(
ctx,
peerID,
peersCustomZone,
nil,
validatedPeersMap,
resourcePolicies,
routers,
groupIDToUserIDs,
)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = CalculateNetworkMapFromComponents(ctx, components)
}
}
func TestGetPeerNetworkMap_ProdAccount_CompareImplementations(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
ctx := context.Background()
testAccount := loadProdAccountFromJSON(t)
testingPeerID := "cq3526bl0ubs73bbtpbg"
require.Contains(t, testAccount.Peers, testingPeerID, "Testing peer should exist in account")
validatedPeersMap := make(map[string]struct{})
for peerID := range testAccount.Peers {
validatedPeersMap[peerID] = struct{}{}
}
resourcePolicies := testAccount.GetResourcePoliciesMap()
routers := testAccount.GetResourceRoutersMap()
groupIDToUserIDs := testAccount.GetActiveGroupUsers()
legacyNetworkMap := testAccount.GetPeerNetworkMap(ctx, testingPeerID, dns.CustomZone{}, nil, validatedPeersMap, resourcePolicies, routers, nil, groupIDToUserIDs)
require.NotNil(t, legacyNetworkMap, "GetPeerNetworkMap returned nil")
components := testAccount.GetPeerNetworkMapComponents(ctx, testingPeerID, dns.CustomZone{}, nil, validatedPeersMap, resourcePolicies, routers, groupIDToUserIDs)
require.NotNil(t, components, "GetPeerNetworkMapComponents returned nil")
newNetworkMap := CalculateNetworkMapFromComponents(ctx, components)
require.NotNil(t, newNetworkMap, "CalculateNetworkMapFromComponents returned nil")
normalizeAndSortNetworkMap(legacyNetworkMap)
normalizeAndSortNetworkMap(newNetworkMap)
componentsJSON, err := json.MarshalIndent(components, "", " ")
require.NoError(t, err, "error marshaling components to JSON")
legacyJSON, err := json.MarshalIndent(legacyNetworkMap, "", " ")
require.NoError(t, err, "error marshaling legacy network map to JSON")
newJSON, err := json.MarshalIndent(newNetworkMap, "", " ")
require.NoError(t, err, "error marshaling new network map to JSON")
outputDir := filepath.Join("testdata", fmt.Sprintf("compare_peer_%s", testingPeerID))
err = os.MkdirAll(outputDir, 0755)
require.NoError(t, err)
legacyFilePath := filepath.Join(outputDir, "legacy_networkmap.json")
err = os.WriteFile(legacyFilePath, legacyJSON, 0644)
require.NoError(t, err)
componentsPath := filepath.Join(outputDir, "components.json")
err = os.WriteFile(componentsPath, componentsJSON, 0644)
require.NoError(t, err)
newFilePath := filepath.Join(outputDir, "components_networkmap.json")
err = os.WriteFile(newFilePath, newJSON, 0644)
require.NoError(t, err)
t.Logf("Files saved to:\n Legacy NetworkMap: %s\n Components: %s\n Components NetworkMap: %s",
legacyFilePath, componentsPath, newFilePath)
require.JSONEq(t, string(legacyJSON), string(newJSON),
"NetworkMaps from legacy and components approaches do not match for peer %s.\n"+
"Legacy JSON saved to: %s\n"+
"Components JSON saved to: %s\n"+
"Components NetworkMap saved to: %s",
testingPeerID, legacyFilePath, componentsPath, newFilePath)
t.Logf("✅ NetworkMaps are identical for peer %s", testingPeerID)
}
func TestGetPeerNetworkMap_ProdAccount_AllPeers(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
ctx := context.Background()
testAccount := loadProdAccountFromJSON(t)
validatedPeersMap := make(map[string]struct{})
for peerID := range testAccount.Peers {
validatedPeersMap[peerID] = struct{}{}
}
resourcePolicies := testAccount.GetResourcePoliciesMap()
routers := testAccount.GetResourceRoutersMap()
groupIDToUserIDs := testAccount.GetActiveGroupUsers()
var failedPeers []string
for peerID := range testAccount.Peers {
legacyNetworkMap := testAccount.GetPeerNetworkMap(ctx, peerID, dns.CustomZone{}, nil, validatedPeersMap, resourcePolicies, routers, nil, groupIDToUserIDs)
require.NotNil(t, legacyNetworkMap, "GetPeerNetworkMap returned nil for peer %s", peerID)
components := testAccount.GetPeerNetworkMapComponents(ctx, peerID, dns.CustomZone{}, nil, validatedPeersMap, resourcePolicies, routers, groupIDToUserIDs)
require.NotNil(t, components, "GetPeerNetworkMapComponents returned nil for peer %s", peerID)
newNetworkMap := CalculateNetworkMapFromComponents(ctx, components)
require.NotNil(t, newNetworkMap, "CalculateNetworkMapFromComponents returned nil for peer %s", peerID)
normalizeAndSortNetworkMap(legacyNetworkMap)
normalizeAndSortNetworkMap(newNetworkMap)
legacyJSON, err := json.Marshal(legacyNetworkMap)
require.NoError(t, err, "error marshaling legacy network map for peer %s", peerID)
newJSON, err := json.Marshal(newNetworkMap)
require.NoError(t, err, "error marshaling new network map for peer %s", peerID)
if string(legacyJSON) != string(newJSON) {
failedPeers = append(failedPeers, peerID)
outputDir := filepath.Join("testdata", fmt.Sprintf("failed_peer_%s", peerID))
require.NoError(t, os.MkdirAll(outputDir, 0755))
legacyIndented, _ := json.MarshalIndent(legacyNetworkMap, "", " ")
newIndented, _ := json.MarshalIndent(newNetworkMap, "", " ")
componentsIndented, _ := json.MarshalIndent(components, "", " ")
_ = os.WriteFile(filepath.Join(outputDir, "legacy_networkmap.json"), legacyIndented, 0644)
_ = os.WriteFile(filepath.Join(outputDir, "components_networkmap.json"), newIndented, 0644)
_ = os.WriteFile(filepath.Join(outputDir, "components.json"), componentsIndented, 0644)
t.Errorf("NetworkMap mismatch for peer %s. Files saved to %s", peerID, outputDir)
}
}
require.Empty(t, failedPeers, "NetworkMap comparison failed for %d peers: %v", len(failedPeers), failedPeers)
t.Logf("✅ NetworkMaps are identical for all %d peers", len(testAccount.Peers))
}
func loadProdAccountFromJSON(t testing.TB) *Account {
t.Helper()
testDataPath := filepath.Join("testdata", "account_cnlf3j3l0ubs738o5d4g.json")
data, err := os.ReadFile(testDataPath)
require.NoError(t, err, "Failed to read prod account JSON file")
var account Account
err = json.Unmarshal(data, &account)
require.NoError(t, err, "Failed to unmarshal prod account")
if account.Groups == nil {
account.Groups = make(map[string]*Group)
}
if account.Peers == nil {
account.Peers = make(map[string]*nbpeer.Peer)
}
if account.Policies == nil {
account.Policies = []*Policy{}
}
return &account
}
func BenchmarkGetPeerNetworkMapCompactCached(b *testing.B) {
account := loadProdAccountFromJSON(b)
ctx := context.Background()
validatedPeersMap := make(map[string]struct{}, len(account.Peers))
for _, peer := range account.Peers {
validatedPeersMap[peer.ID] = struct{}{}
}
dnsDomain := account.Settings.DNSDomain
customZone := account.GetPeersCustomZone(ctx, dnsDomain)
builder := NewNetworkMapBuilder(account, validatedPeersMap)
testingPeerID := "d3knp53l0ubs738a3n6g"
regularNm := builder.GetPeerNetworkMap(ctx, testingPeerID, customZone, nil, validatedPeersMap, nil)
regularJSON, err := json.Marshal(regularNm)
require.NoError(b, err)
resourcePolicies := account.GetResourcePoliciesMap()
routers := account.GetResourceRoutersMap()
agUsers := account.GetActiveGroupUsers()
components := account.GetPeerNetworkMapComponents(ctx, testingPeerID, customZone, nil, validatedPeersMap, resourcePolicies, routers, agUsers)
componentsJSON, err := json.Marshal(components)
require.NoError(b, err)
regularSize := len(regularJSON)
componentsSize := len(componentsJSON)
componentsSavingsPercent := 100 - int(float64(componentsSize)/float64(regularSize)*100)
b.ReportMetric(float64(regularSize), "regular_bytes")
b.ReportMetric(float64(componentsSize), "components_bytes")
b.ReportMetric(float64(componentsSavingsPercent), "components_savings_%")
b.Logf("========== Network Map Size Comparison ==========")
b.Logf("Regular network map: %d bytes", regularSize)
b.Logf("Components: %d bytes (-%d%%)", componentsSize, componentsSavingsPercent)
b.Logf("")
b.Logf("Bandwidth savings (Components): %d bytes saved (%d%%)", regularSize-componentsSize, componentsSavingsPercent)
b.Logf("=================================================")
b.Run("Legacy", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = account.GetPeerNetworkMap(ctx, testingPeerID, customZone, nil, validatedPeersMap, resourcePolicies, routers, nil, agUsers)
}
})
b.Run("ComponentsNetworkMap", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
components := account.GetPeerNetworkMapComponents(
ctx,
testingPeerID,
customZone,
nil,
validatedPeersMap,
resourcePolicies,
routers,
agUsers,
)
_ = CalculateNetworkMapFromComponents(ctx, components)
}
})
b.Run("ComponentsCreation", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = account.GetPeerNetworkMapComponents(
ctx,
testingPeerID,
customZone,
nil,
validatedPeersMap,
resourcePolicies,
routers,
agUsers,
)
}
})
b.Run("CalculationFromComponents", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = CalculateNetworkMapFromComponents(ctx, components)
}
})
}

View File

@@ -0,0 +1,981 @@
package types
import (
"context"
"net"
"net/netip"
"slices"
"strconv"
"strings"
"time"
"github.com/netbirdio/netbird/client/ssh/auth"
nbdns "github.com/netbirdio/netbird/dns"
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/route"
"github.com/netbirdio/netbird/shared/management/domain"
)
type NetworkMapComponents struct {
PeerID string
Serial uint64
Network *Network
AccountSettings *AccountSettingsInfo
DNSSettings *DNSSettings
CustomZoneDomain string
Peers map[string]*nbpeer.Peer
Groups map[string]*Group
Policies []*Policy
Routes []*route.Route
NameServerGroups []*nbdns.NameServerGroup
AllDNSRecords []nbdns.SimpleRecord
AccountZones []nbdns.CustomZone
ResourcePoliciesMap map[string][]*Policy
RoutersMap map[string]map[string]*routerTypes.NetworkRouter
NetworkResources []*resourceTypes.NetworkResource
GroupIDToUserIDs map[string][]string
AllowedUserIDs map[string]struct{}
PostureFailedPeers map[string]map[string]struct{}
RouterPeers map[string]*nbpeer.Peer
}
type AccountSettingsInfo struct {
PeerLoginExpirationEnabled bool
PeerLoginExpiration time.Duration
PeerInactivityExpirationEnabled bool
PeerInactivityExpiration time.Duration
}
func (c *NetworkMapComponents) GetPeerInfo(peerID string) *nbpeer.Peer {
return c.Peers[peerID]
}
func (c *NetworkMapComponents) GetRouterPeerInfo(peerID string) *nbpeer.Peer {
return c.RouterPeers[peerID]
}
func (c *NetworkMapComponents) GetGroupInfo(groupID string) *Group {
return c.Groups[groupID]
}
func (c *NetworkMapComponents) IsPeerInGroup(peerID, groupID string) bool {
group := c.GetGroupInfo(groupID)
if group == nil {
return false
}
return slices.Contains(group.Peers, peerID)
}
func (c *NetworkMapComponents) GetPeerGroups(peerID string) map[string]struct{} {
groups := make(map[string]struct{})
for groupID, group := range c.Groups {
if slices.Contains(group.Peers, peerID) {
groups[groupID] = struct{}{}
}
}
return groups
}
func (c *NetworkMapComponents) ValidatePostureChecksOnPeer(peerID string, postureCheckIDs []string) bool {
_, exists := c.Peers[peerID]
if !exists {
return false
}
if len(postureCheckIDs) == 0 {
return true
}
for _, checkID := range postureCheckIDs {
if failedPeers, exists := c.PostureFailedPeers[checkID]; exists {
if _, failed := failedPeers[peerID]; failed {
return false
}
}
}
return true
}
type NetworkMapCalculator struct {
components *NetworkMapComponents
}
func NewNetworkMapCalculator(components *NetworkMapComponents) *NetworkMapCalculator {
return &NetworkMapCalculator{
components: components,
}
}
func CalculateNetworkMapFromComponents(ctx context.Context, components *NetworkMapComponents) *NetworkMap {
calculator := NewNetworkMapCalculator(components)
return calculator.Calculate(ctx)
}
func (calc *NetworkMapCalculator) Calculate(ctx context.Context) *NetworkMap {
targetPeerID := calc.components.PeerID
peerGroups := calc.components.GetPeerGroups(targetPeerID)
aclPeers, firewallRules, authorizedUsers, sshEnabled := calc.getPeerConnectionResources(ctx, targetPeerID)
peersToConnect, expiredPeers := calc.filterPeersByLoginExpiration(aclPeers)
routesUpdate := calc.getRoutesToSync(ctx, targetPeerID, peersToConnect, peerGroups)
routesFirewallRules := calc.getPeerRoutesFirewallRules(ctx, targetPeerID)
isRouter, networkResourcesRoutes, sourcePeers := calc.getNetworkResourcesRoutesToSync(ctx, targetPeerID)
var networkResourcesFirewallRules []*RouteFirewallRule
if isRouter {
networkResourcesFirewallRules = calc.getPeerNetworkResourceFirewallRules(ctx, targetPeerID, networkResourcesRoutes)
}
peersToConnectIncludingRouters := calc.addNetworksRoutingPeers(
networkResourcesRoutes,
targetPeerID,
peersToConnect,
expiredPeers,
isRouter,
sourcePeers,
)
dnsManagementStatus := calc.getPeerDNSManagementStatus(targetPeerID)
dnsUpdate := nbdns.Config{
ServiceEnable: dnsManagementStatus,
}
if dnsManagementStatus {
var customZones []nbdns.CustomZone
if calc.components.CustomZoneDomain != "" && len(calc.components.AllDNSRecords) > 0 {
customZones = append(customZones, nbdns.CustomZone{
Domain: calc.components.CustomZoneDomain,
Records: calc.components.AllDNSRecords,
})
}
customZones = append(customZones, calc.components.AccountZones...)
dnsUpdate.CustomZones = customZones
dnsUpdate.NameServerGroups = calc.getPeerNSGroups(targetPeerID)
}
return &NetworkMap{
Peers: peersToConnectIncludingRouters,
Network: calc.components.Network.Copy(),
Routes: append(networkResourcesRoutes, routesUpdate...),
DNSConfig: dnsUpdate,
OfflinePeers: expiredPeers,
FirewallRules: firewallRules,
RoutesFirewallRules: append(networkResourcesFirewallRules, routesFirewallRules...),
AuthorizedUsers: authorizedUsers,
EnableSSH: sshEnabled,
}
}
func (calc *NetworkMapCalculator) getPeerConnectionResources(ctx context.Context, targetPeerID string) ([]*nbpeer.Peer, []*FirewallRule, map[string]map[string]struct{}, bool) {
targetPeer := calc.components.GetPeerInfo(targetPeerID)
if targetPeer == nil {
return nil, nil, nil, false
}
generateResources, getAccumulatedResources := calc.connResourcesGenerator(ctx, targetPeer)
authorizedUsers := make(map[string]map[string]struct{})
sshEnabled := false
for _, policy := range calc.components.Policies {
if !policy.Enabled {
continue
}
for _, rule := range policy.Rules {
if !rule.Enabled {
continue
}
var sourcePeers, destinationPeers []*nbpeer.Peer
var peerInSources, peerInDestinations bool
if rule.SourceResource.Type == ResourceTypePeer && rule.SourceResource.ID != "" {
sourcePeers, peerInSources = calc.getPeerFromResource(rule.SourceResource, targetPeerID)
} else {
sourcePeers, peerInSources = calc.getAllPeersFromGroups(ctx, rule.Sources, targetPeerID, policy.SourcePostureChecks)
}
if rule.DestinationResource.Type == ResourceTypePeer && rule.DestinationResource.ID != "" {
destinationPeers, peerInDestinations = calc.getPeerFromResource(rule.DestinationResource, targetPeerID)
} else {
destinationPeers, peerInDestinations = calc.getAllPeersFromGroups(ctx, rule.Destinations, targetPeerID, nil)
}
if rule.Bidirectional {
if peerInSources {
generateResources(rule, destinationPeers, FirewallRuleDirectionIN)
}
if peerInDestinations {
generateResources(rule, sourcePeers, FirewallRuleDirectionOUT)
}
}
if peerInSources {
generateResources(rule, destinationPeers, FirewallRuleDirectionOUT)
}
if peerInDestinations {
generateResources(rule, sourcePeers, FirewallRuleDirectionIN)
}
if peerInDestinations && rule.Protocol == PolicyRuleProtocolNetbirdSSH {
sshEnabled = true
switch {
case len(rule.AuthorizedGroups) > 0:
for groupID, localUsers := range rule.AuthorizedGroups {
userIDs, ok := calc.components.GroupIDToUserIDs[groupID]
if !ok {
continue
}
if len(localUsers) == 0 {
localUsers = []string{auth.Wildcard}
}
for _, localUser := range localUsers {
if authorizedUsers[localUser] == nil {
authorizedUsers[localUser] = make(map[string]struct{})
}
for _, userID := range userIDs {
authorizedUsers[localUser][userID] = struct{}{}
}
}
}
case rule.AuthorizedUser != "":
if authorizedUsers[auth.Wildcard] == nil {
authorizedUsers[auth.Wildcard] = make(map[string]struct{})
}
authorizedUsers[auth.Wildcard][rule.AuthorizedUser] = struct{}{}
default:
authorizedUsers[auth.Wildcard] = calc.getAllowedUserIDs()
}
} else if peerInDestinations && policyRuleImpliesLegacySSH(rule) && targetPeer.SSHEnabled {
sshEnabled = true
authorizedUsers[auth.Wildcard] = calc.getAllowedUserIDs()
}
}
}
peers, fwRules := getAccumulatedResources()
return peers, fwRules, authorizedUsers, sshEnabled
}
func (calc *NetworkMapCalculator) getAllowedUserIDs() map[string]struct{} {
if calc.components.AllowedUserIDs != nil {
result := make(map[string]struct{}, len(calc.components.AllowedUserIDs))
for k, v := range calc.components.AllowedUserIDs {
result[k] = v
}
return result
}
return make(map[string]struct{})
}
func (calc *NetworkMapCalculator) connResourcesGenerator(ctx context.Context, targetPeer *nbpeer.Peer) (func(*PolicyRule, []*nbpeer.Peer, int), func() ([]*nbpeer.Peer, []*FirewallRule)) {
rulesExists := make(map[string]struct{})
peersExists := make(map[string]struct{})
rules := make([]*FirewallRule, 0)
peers := make([]*nbpeer.Peer, 0)
return func(rule *PolicyRule, groupPeers []*nbpeer.Peer, direction int) {
for _, peer := range groupPeers {
if peer == nil {
continue
}
if _, ok := peersExists[peer.ID]; !ok {
peers = append(peers, peer)
peersExists[peer.ID] = struct{}{}
}
protocol := rule.Protocol
if protocol == PolicyRuleProtocolNetbirdSSH {
protocol = PolicyRuleProtocolTCP
}
fr := FirewallRule{
PolicyID: rule.ID,
PeerIP: net.IP(peer.IP).String(),
Direction: direction,
Action: string(rule.Action),
Protocol: string(protocol),
}
ruleID := rule.ID + fr.PeerIP + strconv.Itoa(direction) +
fr.Protocol + fr.Action + strings.Join(rule.Ports, ",")
if _, ok := rulesExists[ruleID]; ok {
continue
}
rulesExists[ruleID] = struct{}{}
if len(rule.Ports) == 0 && len(rule.PortRanges) == 0 {
rules = append(rules, &fr)
continue
}
rules = append(rules, expandPortsAndRanges(fr, &PolicyRule{
ID: rule.ID,
Ports: rule.Ports,
PortRanges: rule.PortRanges,
Protocol: rule.Protocol,
Action: rule.Action,
}, targetPeer)...)
}
}, func() ([]*nbpeer.Peer, []*FirewallRule) {
return peers, rules
}
}
func (calc *NetworkMapCalculator) getAllPeersFromGroups(ctx context.Context, groups []string, peerID string, sourcePostureChecksIDs []string) ([]*nbpeer.Peer, bool) {
peerInGroups := false
uniquePeerIDs := calc.getUniquePeerIDsFromGroupsIDs(ctx, groups)
filteredPeers := make([]*nbpeer.Peer, 0, len(uniquePeerIDs))
for _, p := range uniquePeerIDs {
peerInfo := calc.components.GetPeerInfo(p)
if peerInfo == nil {
continue
}
if _, ok := calc.components.Peers[p]; !ok {
continue
}
if !calc.components.ValidatePostureChecksOnPeer(p, sourcePostureChecksIDs) {
continue
}
if p == peerID {
peerInGroups = true
continue
}
filteredPeers = append(filteredPeers, peerInfo)
}
return filteredPeers, peerInGroups
}
func (calc *NetworkMapCalculator) getUniquePeerIDsFromGroupsIDs(ctx context.Context, groups []string) []string {
peerIDs := make(map[string]struct{}, len(groups))
for _, groupID := range groups {
group := calc.components.GetGroupInfo(groupID)
if group == nil {
continue
}
if group.IsGroupAll() || len(groups) == 1 {
return group.Peers
}
for _, peerID := range group.Peers {
peerIDs[peerID] = struct{}{}
}
}
ids := make([]string, 0, len(peerIDs))
for peerID := range peerIDs {
ids = append(ids, peerID)
}
return ids
}
func (calc *NetworkMapCalculator) getPeerFromResource(resource Resource, peerID string) ([]*nbpeer.Peer, bool) {
if resource.ID == peerID {
return []*nbpeer.Peer{}, true
}
peerInfo := calc.components.GetPeerInfo(resource.ID)
if peerInfo == nil {
return []*nbpeer.Peer{}, false
}
return []*nbpeer.Peer{peerInfo}, false
}
func (calc *NetworkMapCalculator) filterPeersByLoginExpiration(aclPeers []*nbpeer.Peer) ([]*nbpeer.Peer, []*nbpeer.Peer) {
var peersToConnect []*nbpeer.Peer
var expiredPeers []*nbpeer.Peer
for _, p := range aclPeers {
expired, _ := p.LoginExpired(calc.components.AccountSettings.PeerLoginExpiration)
if calc.components.AccountSettings.PeerLoginExpirationEnabled && expired {
expiredPeers = append(expiredPeers, p)
continue
}
peersToConnect = append(peersToConnect, p)
}
return peersToConnect, expiredPeers
}
func (calc *NetworkMapCalculator) getPeerDNSManagementStatus(peerID string) bool {
peerGroups := calc.components.GetPeerGroups(peerID)
enabled := true
for _, groupID := range calc.components.DNSSettings.DisabledManagementGroups {
if _, found := peerGroups[groupID]; found {
enabled = false
break
}
}
return enabled
}
func (calc *NetworkMapCalculator) getPeerNSGroups(peerID string) []*nbdns.NameServerGroup {
groupList := calc.components.GetPeerGroups(peerID)
var peerNSGroups []*nbdns.NameServerGroup
for _, nsGroup := range calc.components.NameServerGroups {
if !nsGroup.Enabled {
continue
}
for _, gID := range nsGroup.Groups {
_, found := groupList[gID]
if found {
targetPeerInfo := calc.components.GetPeerInfo(peerID)
if targetPeerInfo != nil && !calc.peerIsNameserver(targetPeerInfo, nsGroup) {
peerNSGroups = append(peerNSGroups, nsGroup.Copy())
break
}
}
}
}
return peerNSGroups
}
func (calc *NetworkMapCalculator) peerIsNameserver(peerInfo *nbpeer.Peer, nsGroup *nbdns.NameServerGroup) bool {
for _, ns := range nsGroup.NameServers {
if peerInfo.IP.String() == ns.IP.String() {
return true
}
}
return false
}
func (calc *NetworkMapCalculator) getRoutesToSync(ctx context.Context, peerID string, aclPeers []*nbpeer.Peer, peerGroups LookupMap) []*route.Route {
routes, peerDisabledRoutes := calc.getRoutingPeerRoutes(ctx, peerID)
peerRoutesMembership := make(LookupMap)
for _, r := range append(routes, peerDisabledRoutes...) {
peerRoutesMembership[string(r.GetHAUniqueID())] = struct{}{}
}
for _, peer := range aclPeers {
activeRoutes, _ := calc.getRoutingPeerRoutes(ctx, peer.ID)
groupFilteredRoutes := calc.filterRoutesByGroups(activeRoutes, peerGroups)
filteredRoutes := calc.filterRoutesFromPeersOfSameHAGroup(groupFilteredRoutes, peerRoutesMembership)
routes = append(routes, filteredRoutes...)
}
return routes
}
func (calc *NetworkMapCalculator) getRoutingPeerRoutes(ctx context.Context, peerID string) (enabledRoutes []*route.Route, disabledRoutes []*route.Route) {
peerInfo := calc.components.GetPeerInfo(peerID)
if peerInfo == nil {
peerInfo = calc.components.GetRouterPeerInfo(peerID)
}
if peerInfo == nil {
return enabledRoutes, disabledRoutes
}
seenRoute := make(map[route.ID]struct{})
takeRoute := func(r *route.Route, id string) {
if _, ok := seenRoute[r.ID]; ok {
return
}
seenRoute[r.ID] = struct{}{}
routeObj := calc.copyRoute(r)
routeObj.Peer = peerInfo.Key
if r.Enabled {
enabledRoutes = append(enabledRoutes, routeObj)
return
}
disabledRoutes = append(disabledRoutes, routeObj)
}
for _, r := range calc.components.Routes {
for _, groupID := range r.PeerGroups {
group := calc.components.GetGroupInfo(groupID)
if group == nil {
continue
}
for _, id := range group.Peers {
if id != peerID {
continue
}
newPeerRoute := calc.copyRoute(r)
newPeerRoute.Peer = id
newPeerRoute.PeerGroups = nil
newPeerRoute.ID = route.ID(string(r.ID) + ":" + id)
takeRoute(newPeerRoute, id)
break
}
}
if r.Peer == peerID {
takeRoute(calc.copyRoute(r), peerID)
}
}
return enabledRoutes, disabledRoutes
}
func (calc *NetworkMapCalculator) copyRoute(r *route.Route) *route.Route {
var groups, accessControlGroups, peerGroups []string
var domains domain.List
if r.Groups != nil {
groups = append([]string{}, r.Groups...)
}
if r.AccessControlGroups != nil {
accessControlGroups = append([]string{}, r.AccessControlGroups...)
}
if r.PeerGroups != nil {
peerGroups = append([]string{}, r.PeerGroups...)
}
if r.Domains != nil {
domains = append(domain.List{}, r.Domains...)
}
return &route.Route{
ID: r.ID,
AccountID: r.AccountID,
Network: r.Network,
NetworkType: r.NetworkType,
Description: r.Description,
Peer: r.Peer,
PeerID: r.PeerID,
Metric: r.Metric,
Masquerade: r.Masquerade,
NetID: r.NetID,
Enabled: r.Enabled,
Groups: groups,
AccessControlGroups: accessControlGroups,
PeerGroups: peerGroups,
Domains: domains,
KeepRoute: r.KeepRoute,
SkipAutoApply: r.SkipAutoApply,
}
}
func (calc *NetworkMapCalculator) filterRoutesByGroups(routes []*route.Route, groupListMap LookupMap) []*route.Route {
var filteredRoutes []*route.Route
for _, r := range routes {
for _, groupID := range r.Groups {
_, found := groupListMap[groupID]
if found {
filteredRoutes = append(filteredRoutes, r)
break
}
}
}
return filteredRoutes
}
func (calc *NetworkMapCalculator) filterRoutesFromPeersOfSameHAGroup(routes []*route.Route, peerMemberships LookupMap) []*route.Route {
var filteredRoutes []*route.Route
for _, r := range routes {
_, found := peerMemberships[string(r.GetHAUniqueID())]
if !found {
filteredRoutes = append(filteredRoutes, r)
}
}
return filteredRoutes
}
func (calc *NetworkMapCalculator) getPeerRoutesFirewallRules(ctx context.Context, peerID string) []*RouteFirewallRule {
routesFirewallRules := make([]*RouteFirewallRule, 0)
enabledRoutes, _ := calc.getRoutingPeerRoutes(ctx, peerID)
for _, r := range enabledRoutes {
if len(r.AccessControlGroups) == 0 {
defaultPermit := calc.getDefaultPermit(r)
routesFirewallRules = append(routesFirewallRules, defaultPermit...)
continue
}
distributionPeers := calc.getDistributionGroupsPeers(r)
for _, accessGroup := range r.AccessControlGroups {
policies := calc.getAllRoutePoliciesFromGroups([]string{accessGroup})
rules := calc.getRouteFirewallRules(ctx, peerID, policies, r, distributionPeers)
routesFirewallRules = append(routesFirewallRules, rules...)
}
}
return routesFirewallRules
}
func (calc *NetworkMapCalculator) findRoute(routeID route.ID) *route.Route {
for _, r := range calc.components.Routes {
if r.ID == routeID {
return r
}
}
parts := strings.Split(string(routeID), ":")
if len(parts) > 1 {
baseRouteID := route.ID(parts[0])
for _, r := range calc.components.Routes {
if r.ID == baseRouteID {
return r
}
}
}
return nil
}
func (calc *NetworkMapCalculator) getDefaultPermit(r *route.Route) []*RouteFirewallRule {
var rules []*RouteFirewallRule
sources := []string{"0.0.0.0/0"}
if r.Network.Addr().Is6() {
sources = []string{"::/0"}
}
rule := RouteFirewallRule{
SourceRanges: sources,
Action: string(PolicyTrafficActionAccept),
Destination: r.Network.String(),
Protocol: string(PolicyRuleProtocolALL),
Domains: r.Domains,
IsDynamic: r.IsDynamic(),
RouteID: r.ID,
}
rules = append(rules, &rule)
if r.IsDynamic() {
ruleV6 := rule
ruleV6.SourceRanges = []string{"::/0"}
rules = append(rules, &ruleV6)
}
return rules
}
func (calc *NetworkMapCalculator) getDistributionGroupsPeers(r *route.Route) map[string]struct{} {
distPeers := make(map[string]struct{})
for _, id := range r.Groups {
group := calc.components.GetGroupInfo(id)
if group == nil {
continue
}
for _, pID := range group.Peers {
distPeers[pID] = struct{}{}
}
}
return distPeers
}
func (calc *NetworkMapCalculator) getAllRoutePoliciesFromGroups(accessControlGroups []string) []*Policy {
routePolicies := make([]*Policy, 0)
for _, groupID := range accessControlGroups {
for _, policy := range calc.components.Policies {
for _, rule := range policy.Rules {
for _, destGroupID := range rule.Destinations {
if destGroupID == groupID {
routePolicies = append(routePolicies, policy)
break
}
}
}
}
}
return routePolicies
}
func (calc *NetworkMapCalculator) getRouteFirewallRules(ctx context.Context, peerID string, policies []*Policy, route *route.Route, distributionPeers map[string]struct{}) []*RouteFirewallRule {
var fwRules []*RouteFirewallRule
for _, policy := range policies {
if !policy.Enabled {
continue
}
for _, rule := range policy.Rules {
if !rule.Enabled {
continue
}
rulePeers := calc.getRulePeers(rule, policy.SourcePostureChecks, peerID, distributionPeers)
rules := generateRouteFirewallRules(ctx, route, rule, rulePeers, FirewallRuleDirectionIN)
fwRules = append(fwRules, rules...)
}
}
return fwRules
}
func (calc *NetworkMapCalculator) getRulePeers(rule *PolicyRule, postureChecks []string, peerID string, distributionPeers map[string]struct{}) []*nbpeer.Peer {
distPeersWithPolicy := make(map[string]struct{})
for _, id := range rule.Sources {
group := calc.components.GetGroupInfo(id)
if group == nil {
continue
}
for _, pID := range group.Peers {
if pID == peerID {
continue
}
_, distPeer := distributionPeers[pID]
_, valid := calc.components.Peers[pID]
if distPeer && valid && calc.components.ValidatePostureChecksOnPeer(pID, postureChecks) {
distPeersWithPolicy[pID] = struct{}{}
}
}
}
if rule.SourceResource.Type == ResourceTypePeer && rule.SourceResource.ID != "" {
_, distPeer := distributionPeers[rule.SourceResource.ID]
_, valid := calc.components.Peers[rule.SourceResource.ID]
if distPeer && valid && calc.components.ValidatePostureChecksOnPeer(rule.SourceResource.ID, postureChecks) {
distPeersWithPolicy[rule.SourceResource.ID] = struct{}{}
}
}
distributionGroupPeers := make([]*nbpeer.Peer, 0, len(distPeersWithPolicy))
for pID := range distPeersWithPolicy {
peerInfo := calc.components.GetPeerInfo(pID)
if peerInfo == nil {
continue
}
distributionGroupPeers = append(distributionGroupPeers, peerInfo)
}
return distributionGroupPeers
}
func (calc *NetworkMapCalculator) getNetworkResourcesRoutesToSync(ctx context.Context, peerID string) (bool, []*route.Route, map[string]struct{}) {
var isRoutingPeer bool
var routes []*route.Route
allSourcePeers := make(map[string]struct{})
for _, resource := range calc.components.NetworkResources {
if !resource.Enabled {
continue
}
var addSourcePeers bool
networkRoutingPeers, exists := calc.components.RoutersMap[resource.NetworkID]
if exists {
if router, ok := networkRoutingPeers[peerID]; ok {
isRoutingPeer, addSourcePeers = true, true
routes = append(routes, calc.getNetworkResourcesRoutes(resource, peerID, router)...)
}
}
addedResourceRoute := false
for _, policy := range calc.components.ResourcePoliciesMap[resource.ID] {
var peers []string
if policy.Rules[0].SourceResource.Type == ResourceTypePeer && policy.Rules[0].SourceResource.ID != "" {
peers = []string{policy.Rules[0].SourceResource.ID}
} else {
peers = calc.getUniquePeerIDsFromGroupsIDs(ctx, policy.SourceGroups())
}
if addSourcePeers {
for _, pID := range calc.getPostureValidPeers(peers, policy.SourcePostureChecks) {
allSourcePeers[pID] = struct{}{}
}
} else if calc.peerInSlice(peerID, peers) && calc.components.ValidatePostureChecksOnPeer(peerID, policy.SourcePostureChecks) {
for peerId, router := range networkRoutingPeers {
routes = append(routes, calc.getNetworkResourcesRoutes(resource, peerId, router)...)
}
addedResourceRoute = true
}
if addedResourceRoute {
break
}
}
}
return isRoutingPeer, routes, allSourcePeers
}
func (calc *NetworkMapCalculator) getNetworkResourcesRoutes(resource *resourceTypes.NetworkResource, peerID string, router *routerTypes.NetworkRouter) []*route.Route {
resourceAppliedPolicies := calc.components.ResourcePoliciesMap[resource.ID]
var routes []*route.Route
if len(resourceAppliedPolicies) > 0 {
peerInfo := calc.components.GetPeerInfo(peerID)
if peerInfo != nil {
routes = append(routes, calc.networkResourceToRoute(resource, peerInfo, router))
}
}
return routes
}
func (calc *NetworkMapCalculator) networkResourceToRoute(resource *resourceTypes.NetworkResource, peer *nbpeer.Peer, router *routerTypes.NetworkRouter) *route.Route {
r := &route.Route{
ID: route.ID(resource.ID + ":" + peer.ID),
AccountID: resource.AccountID,
Peer: peer.Key,
PeerID: peer.ID,
Metric: router.Metric,
Masquerade: router.Masquerade,
Enabled: resource.Enabled,
KeepRoute: true,
NetID: route.NetID(resource.Name),
Description: resource.Description,
}
if resource.Type == resourceTypes.Host || resource.Type == resourceTypes.Subnet {
r.Network = resource.Prefix
r.NetworkType = route.IPv4Network
if resource.Prefix.Addr().Is6() {
r.NetworkType = route.IPv6Network
}
}
if resource.Type == resourceTypes.Domain {
domainList, err := domain.FromStringList([]string{resource.Domain})
if err == nil {
r.Domains = domainList
r.NetworkType = route.DomainNetwork
r.Network = netip.PrefixFrom(netip.AddrFrom4([4]byte{192, 0, 2, 0}), 32)
}
}
return r
}
func (calc *NetworkMapCalculator) getPostureValidPeers(inputPeers []string, postureChecksIDs []string) []string {
var dest []string
for _, peerID := range inputPeers {
if calc.components.ValidatePostureChecksOnPeer(peerID, postureChecksIDs) {
dest = append(dest, peerID)
}
}
return dest
}
func (calc *NetworkMapCalculator) peerInSlice(peerID string, peers []string) bool {
for _, p := range peers {
if p == peerID {
return true
}
}
return false
}
func (calc *NetworkMapCalculator) getPeerNetworkResourceFirewallRules(ctx context.Context, peerID string, routes []*route.Route) []*RouteFirewallRule {
routesFirewallRules := make([]*RouteFirewallRule, 0)
peerInfo := calc.components.GetPeerInfo(peerID)
if peerInfo == nil {
return routesFirewallRules
}
for _, r := range routes {
if r.Peer != peerInfo.Key {
continue
}
resourceID := string(r.GetResourceID())
resourcePolicies := calc.components.ResourcePoliciesMap[resourceID]
distributionPeers := calc.getPoliciesSourcePeers(resourcePolicies)
rules := calc.getRouteFirewallRules(ctx, peerID, resourcePolicies, r, distributionPeers)
for _, rule := range rules {
if len(rule.SourceRanges) > 0 {
routesFirewallRules = append(routesFirewallRules, rule)
}
}
}
return routesFirewallRules
}
func (calc *NetworkMapCalculator) getPoliciesSourcePeers(policies []*Policy) map[string]struct{} {
sourcePeers := make(map[string]struct{})
for _, policy := range policies {
for _, rule := range policy.Rules {
for _, sourceGroup := range rule.Sources {
group := calc.components.GetGroupInfo(sourceGroup)
if group == nil {
continue
}
for _, peer := range group.Peers {
sourcePeers[peer] = struct{}{}
}
}
if rule.SourceResource.Type == ResourceTypePeer && rule.SourceResource.ID != "" {
sourcePeers[rule.SourceResource.ID] = struct{}{}
}
}
}
return sourcePeers
}
func (calc *NetworkMapCalculator) addNetworksRoutingPeers(
networkResourcesRoutes []*route.Route,
peerID string,
peersToConnect []*nbpeer.Peer,
expiredPeers []*nbpeer.Peer,
isRouter bool,
sourcePeers map[string]struct{},
) []*nbpeer.Peer {
networkRoutesPeers := make(map[string]struct{}, len(networkResourcesRoutes))
for _, r := range networkResourcesRoutes {
networkRoutesPeers[r.PeerID] = struct{}{}
}
delete(sourcePeers, peerID)
delete(networkRoutesPeers, peerID)
for _, existingPeer := range peersToConnect {
delete(sourcePeers, existingPeer.ID)
delete(networkRoutesPeers, existingPeer.ID)
}
for _, expPeer := range expiredPeers {
delete(sourcePeers, expPeer.ID)
delete(networkRoutesPeers, expPeer.ID)
}
missingPeers := make(map[string]struct{}, len(sourcePeers)+len(networkRoutesPeers))
if isRouter {
for p := range sourcePeers {
missingPeers[p] = struct{}{}
}
}
for p := range networkRoutesPeers {
missingPeers[p] = struct{}{}
}
for p := range missingPeers {
peerInfo := calc.components.GetPeerInfo(p)
if peerInfo == nil {
peerInfo = calc.components.GetRouterPeerInfo(p)
}
if peerInfo != nil {
peersToConnect = append(peersToConnect, peerInfo)
}
}
return peersToConnect
}

View File

@@ -0,0 +1,230 @@
package types
import (
nbdns "github.com/netbirdio/netbird/dns"
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/route"
)
type GroupCompact struct {
Name string
PeerIndexes []int
}
type NetworkMapComponentsCompact struct {
PeerID string
Network *Network
AccountSettings *AccountSettingsInfo
DNSSettings *DNSSettings
CustomZoneDomain string
AllPeers []*nbpeer.Peer
PeerIndexes []int
RouterPeerIndexes []int
Groups map[string]*GroupCompact
AllPolicies []*Policy
PolicyIndexes []int
ResourcePoliciesMap map[string][]int
Routes []*route.Route
NameServerGroups []*nbdns.NameServerGroup
AllDNSRecords []nbdns.SimpleRecord
AccountZones []nbdns.CustomZone
RoutersMap map[string]map[string]*routerTypes.NetworkRouter
NetworkResources []*resourceTypes.NetworkResource
GroupIDToUserIDs map[string][]string
AllowedUserIDs map[string]struct{}
PostureFailedPeers map[string]map[string]struct{}
}
func (c *NetworkMapComponents) ToCompact() *NetworkMapComponentsCompact {
peerToIndex := make(map[string]int)
var allPeers []*nbpeer.Peer
for id, peer := range c.Peers {
if _, exists := peerToIndex[id]; !exists {
peerToIndex[id] = len(allPeers)
allPeers = append(allPeers, peer)
}
}
for id, peer := range c.RouterPeers {
if _, exists := peerToIndex[id]; !exists {
peerToIndex[id] = len(allPeers)
allPeers = append(allPeers, peer)
}
}
peerIndexes := make([]int, 0, len(c.Peers))
for id := range c.Peers {
peerIndexes = append(peerIndexes, peerToIndex[id])
}
routerPeerIndexes := make([]int, 0, len(c.RouterPeers))
for id := range c.RouterPeers {
routerPeerIndexes = append(routerPeerIndexes, peerToIndex[id])
}
groups := make(map[string]*GroupCompact, len(c.Groups))
for id, group := range c.Groups {
peerIdxs := make([]int, 0, len(group.Peers))
for _, peerID := range group.Peers {
if idx, ok := peerToIndex[peerID]; ok {
peerIdxs = append(peerIdxs, idx)
}
}
groups[id] = &GroupCompact{
Name: group.Name,
PeerIndexes: peerIdxs,
}
}
policyToIndex := make(map[*Policy]int)
var allPolicies []*Policy
for _, policy := range c.Policies {
if _, exists := policyToIndex[policy]; !exists {
policyToIndex[policy] = len(allPolicies)
allPolicies = append(allPolicies, policy)
}
}
for _, policies := range c.ResourcePoliciesMap {
for _, policy := range policies {
if _, exists := policyToIndex[policy]; !exists {
policyToIndex[policy] = len(allPolicies)
allPolicies = append(allPolicies, policy)
}
}
}
policyIndexes := make([]int, len(c.Policies))
for i, policy := range c.Policies {
policyIndexes[i] = policyToIndex[policy]
}
var resourcePoliciesMap map[string][]int
if len(c.ResourcePoliciesMap) > 0 {
resourcePoliciesMap = make(map[string][]int, len(c.ResourcePoliciesMap))
for resID, policies := range c.ResourcePoliciesMap {
indexes := make([]int, len(policies))
for i, policy := range policies {
indexes[i] = policyToIndex[policy]
}
resourcePoliciesMap[resID] = indexes
}
}
return &NetworkMapComponentsCompact{
PeerID: c.PeerID,
Network: c.Network,
AccountSettings: c.AccountSettings,
DNSSettings: c.DNSSettings,
CustomZoneDomain: c.CustomZoneDomain,
AllPeers: allPeers,
PeerIndexes: peerIndexes,
RouterPeerIndexes: routerPeerIndexes,
Groups: groups,
AllPolicies: allPolicies,
PolicyIndexes: policyIndexes,
ResourcePoliciesMap: resourcePoliciesMap,
Routes: c.Routes,
NameServerGroups: c.NameServerGroups,
AllDNSRecords: c.AllDNSRecords,
AccountZones: c.AccountZones,
RoutersMap: c.RoutersMap,
NetworkResources: c.NetworkResources,
GroupIDToUserIDs: c.GroupIDToUserIDs,
AllowedUserIDs: c.AllowedUserIDs,
PostureFailedPeers: c.PostureFailedPeers,
}
}
func (c *NetworkMapComponentsCompact) ToFull() *NetworkMapComponents {
peers := make(map[string]*nbpeer.Peer, len(c.PeerIndexes))
for _, idx := range c.PeerIndexes {
if idx >= 0 && idx < len(c.AllPeers) {
peer := c.AllPeers[idx]
peers[peer.ID] = peer
}
}
routerPeers := make(map[string]*nbpeer.Peer, len(c.RouterPeerIndexes))
for _, idx := range c.RouterPeerIndexes {
if idx >= 0 && idx < len(c.AllPeers) {
peer := c.AllPeers[idx]
routerPeers[peer.ID] = peer
}
}
groups := make(map[string]*Group, len(c.Groups))
for id, gc := range c.Groups {
peerIDs := make([]string, 0, len(gc.PeerIndexes))
for _, idx := range gc.PeerIndexes {
if idx >= 0 && idx < len(c.AllPeers) {
peerIDs = append(peerIDs, c.AllPeers[idx].ID)
}
}
groups[id] = &Group{
ID: id,
Name: gc.Name,
Peers: peerIDs,
}
}
policies := make([]*Policy, len(c.PolicyIndexes))
for i, idx := range c.PolicyIndexes {
if idx >= 0 && idx < len(c.AllPolicies) {
policies[i] = c.AllPolicies[idx]
}
}
var resourcePoliciesMap map[string][]*Policy
if len(c.ResourcePoliciesMap) > 0 {
resourcePoliciesMap = make(map[string][]*Policy, len(c.ResourcePoliciesMap))
for resID, indexes := range c.ResourcePoliciesMap {
pols := make([]*Policy, 0, len(indexes))
for _, idx := range indexes {
if idx >= 0 && idx < len(c.AllPolicies) {
pols = append(pols, c.AllPolicies[idx])
}
}
resourcePoliciesMap[resID] = pols
}
}
return &NetworkMapComponents{
PeerID: c.PeerID,
Network: c.Network,
AccountSettings: c.AccountSettings,
DNSSettings: c.DNSSettings,
CustomZoneDomain: c.CustomZoneDomain,
Peers: peers,
RouterPeers: routerPeers,
Groups: groups,
Policies: policies,
Routes: c.Routes,
NameServerGroups: c.NameServerGroups,
AllDNSRecords: c.AllDNSRecords,
AccountZones: c.AccountZones,
ResourcePoliciesMap: resourcePoliciesMap,
RoutersMap: c.RoutersMap,
NetworkResources: c.NetworkResources,
GroupIDToUserIDs: c.GroupIDToUserIDs,
AllowedUserIDs: c.AllowedUserIDs,
PostureFailedPeers: c.PostureFailedPeers,
}
}

View File

@@ -0,0 +1,255 @@
package types
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
log "github.com/sirupsen/logrus"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/management/internals/modules/zones"
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
"github.com/netbirdio/netbird/management/server/telemetry"
)
var (
shadowOutputDir = "netbird-shadow-compare"
EnvNewNetworkMapCompacted = "NB_NETWORK_MAP_COMPACTED"
)
func (a *Account) ShadowCompareNetworkMap(
ctx context.Context,
peerID string,
legacyNetworkMap *NetworkMap,
peersCustomZone nbdns.CustomZone,
accountZones []*zones.Zone,
validatedPeersMap map[string]struct{},
resourcePolicies map[string][]*Policy,
routers map[string]map[string]*routerTypes.NetworkRouter,
groupIDToUserIDs map[string][]string,
metrics *telemetry.AccountManagerMetrics,
) {
go func() {
defer func() {
if r := recover(); r != nil {
log.WithContext(ctx).Errorf("shadow comparison panic for peer %s: %v", peerID, r)
}
}()
components := a.GetPeerNetworkMapComponents(
ctx,
peerID,
peersCustomZone,
accountZones,
validatedPeersMap,
resourcePolicies,
routers,
groupIDToUserIDs,
)
if components == nil {
log.WithContext(ctx).Warnf("shadow comparison: components nil for peer %s", peerID)
return
}
componentsNetworkMap := CalculateNetworkMapFromComponents(ctx, components)
if componentsNetworkMap == nil {
log.WithContext(ctx).Warnf("shadow comparison: components network map nil for peer %s", peerID)
return
}
diff := compareNetworkMapCounts(legacyNetworkMap, componentsNetworkMap)
legacyBytes, componentsBytes := measureSizes(legacyNetworkMap, components)
savingsPercent := 0
if legacyBytes > 0 {
savingsPercent = 100 - int(float64(componentsBytes)*100/float64(legacyBytes))
}
if metrics != nil {
metrics.CountShadowLegacySize(int64(legacyBytes))
metrics.CountShadowComponentsSize(int64(componentsBytes))
metrics.CountShadowSavingsPercent(int64(savingsPercent))
}
if diff.HasDifferences() {
log.WithContext(ctx).Warnf(
"shadow comparison MISMATCH for account %s and peer %s: %s", a.Id, peerID, diff.String())
saveMismatchedMaps(ctx, a.Id, peerID, legacyNetworkMap, componentsNetworkMap, diff)
} else {
log.WithContext(ctx).Tracef("shadow comparison MATCH for account %s and peer %s", a.Id, peerID)
}
}()
}
func measureSizes(networkMap *NetworkMap, components *NetworkMapComponents) (legacyBytes, compactBytes int) {
if networkMap != nil {
if data, err := json.Marshal(networkMap); err == nil {
legacyBytes = len(data)
}
}
if components != nil {
compact := components.ToCompact()
if data, err := json.Marshal(compact); err == nil {
compactBytes = len(data)
}
}
return
}
type NetworkMapDiff struct {
PeersLegacy int
PeersComponents int
OfflinePeersLegacy int
OfflinePeersComponents int
RoutesLegacy int
RoutesComponents int
FirewallRulesLegacy int
FirewallRulesComponents int
RouteFWRulesLegacy int
RouteFWRulesComponents int
ForwardingRulesLegacy int
ForwardingRulesComponents int
DNSZonesLegacy int
DNSZonesComponents int
DNSNSGroupsLegacy int
DNSNSGroupsComponents int
EnableSSHLegacy bool
EnableSSHComponents bool
AuthUsersLegacy int
AuthUsersComponents int
}
func (d *NetworkMapDiff) HasDifferences() bool {
return d.PeersLegacy != d.PeersComponents ||
d.OfflinePeersLegacy != d.OfflinePeersComponents ||
d.RoutesLegacy != d.RoutesComponents ||
d.FirewallRulesLegacy != d.FirewallRulesComponents ||
d.RouteFWRulesLegacy != d.RouteFWRulesComponents ||
d.ForwardingRulesLegacy != d.ForwardingRulesComponents ||
d.DNSZonesLegacy != d.DNSZonesComponents ||
d.DNSNSGroupsLegacy != d.DNSNSGroupsComponents ||
d.EnableSSHLegacy != d.EnableSSHComponents ||
d.AuthUsersLegacy != d.AuthUsersComponents
}
func (d *NetworkMapDiff) String() string {
var diffs []string
if d.PeersLegacy != d.PeersComponents {
diffs = append(diffs, fmt.Sprintf("Peers: %d vs %d", d.PeersLegacy, d.PeersComponents))
}
if d.OfflinePeersLegacy != d.OfflinePeersComponents {
diffs = append(diffs, fmt.Sprintf("OfflinePeers: %d vs %d", d.OfflinePeersLegacy, d.OfflinePeersComponents))
}
if d.RoutesLegacy != d.RoutesComponents {
diffs = append(diffs, fmt.Sprintf("Routes: %d vs %d", d.RoutesLegacy, d.RoutesComponents))
}
if d.FirewallRulesLegacy != d.FirewallRulesComponents {
diffs = append(diffs, fmt.Sprintf("FirewallRules: %d vs %d", d.FirewallRulesLegacy, d.FirewallRulesComponents))
}
if d.RouteFWRulesLegacy != d.RouteFWRulesComponents {
diffs = append(diffs, fmt.Sprintf("RoutesFirewallRules: %d vs %d", d.RouteFWRulesLegacy, d.RouteFWRulesComponents))
}
if d.ForwardingRulesLegacy != d.ForwardingRulesComponents {
diffs = append(diffs, fmt.Sprintf("ForwardingRules: %d vs %d", d.ForwardingRulesLegacy, d.ForwardingRulesComponents))
}
if d.DNSZonesLegacy != d.DNSZonesComponents {
diffs = append(diffs, fmt.Sprintf("DNSZones: %d vs %d", d.DNSZonesLegacy, d.DNSZonesComponents))
}
if d.DNSNSGroupsLegacy != d.DNSNSGroupsComponents {
diffs = append(diffs, fmt.Sprintf("DNSNSGroups: %d vs %d", d.DNSNSGroupsLegacy, d.DNSNSGroupsComponents))
}
if d.EnableSSHLegacy != d.EnableSSHComponents {
diffs = append(diffs, fmt.Sprintf("EnableSSH: %v vs %v", d.EnableSSHLegacy, d.EnableSSHComponents))
}
if d.AuthUsersLegacy != d.AuthUsersComponents {
diffs = append(diffs, fmt.Sprintf("AuthorizedUsers: %d vs %d", d.AuthUsersLegacy, d.AuthUsersComponents))
}
if len(diffs) == 0 {
return "no differences"
}
result := ""
for i, d := range diffs {
if i > 0 {
result += ", "
}
result += d
}
return result
}
func compareNetworkMapCounts(legacy, components *NetworkMap) NetworkMapDiff {
diff := NetworkMapDiff{}
if legacy != nil {
diff.PeersLegacy = len(legacy.Peers)
diff.OfflinePeersLegacy = len(legacy.OfflinePeers)
diff.RoutesLegacy = len(legacy.Routes)
diff.FirewallRulesLegacy = len(legacy.FirewallRules)
diff.RouteFWRulesLegacy = len(legacy.RoutesFirewallRules)
diff.ForwardingRulesLegacy = len(legacy.ForwardingRules)
diff.DNSZonesLegacy = len(legacy.DNSConfig.CustomZones)
diff.DNSNSGroupsLegacy = len(legacy.DNSConfig.NameServerGroups)
diff.EnableSSHLegacy = legacy.EnableSSH
diff.AuthUsersLegacy = len(legacy.AuthorizedUsers)
}
if components != nil {
diff.PeersComponents = len(components.Peers)
diff.OfflinePeersComponents = len(components.OfflinePeers)
diff.RoutesComponents = len(components.Routes)
diff.FirewallRulesComponents = len(components.FirewallRules)
diff.RouteFWRulesComponents = len(components.RoutesFirewallRules)
diff.ForwardingRulesComponents = len(components.ForwardingRules)
diff.DNSZonesComponents = len(components.DNSConfig.CustomZones)
diff.DNSNSGroupsComponents = len(components.DNSConfig.NameServerGroups)
diff.EnableSSHComponents = components.EnableSSH
diff.AuthUsersComponents = len(components.AuthorizedUsers)
}
return diff
}
func saveMismatchedMaps(ctx context.Context, accountID, peerID string, legacy, components *NetworkMap, diff NetworkMapDiff) {
outputDir := filepath.Join(shadowOutputDir, accountID, peerID)
if entries, err := os.ReadDir(outputDir); err == nil && len(entries) > 0 {
return
}
if err := os.MkdirAll(outputDir, 0755); err != nil {
log.WithContext(ctx).Errorf("failed to create shadow output dir: %v", err)
return
}
legacyPath := filepath.Join(outputDir, "legacy.json")
componentsPath := filepath.Join(outputDir, "components.json")
diffPath := filepath.Join(outputDir, "diff.json")
if legacyJSON, err := json.MarshalIndent(legacy, "", " "); err == nil {
if err := os.WriteFile(legacyPath, legacyJSON, 0644); err != nil {
log.WithContext(ctx).Errorf("failed to write legacy map: %v", err)
}
}
if componentsJSON, err := json.MarshalIndent(components, "", " "); err == nil {
if err := os.WriteFile(componentsPath, componentsJSON, 0644); err != nil {
log.WithContext(ctx).Errorf("failed to write components map: %v", err)
}
}
if diffJSON, err := json.MarshalIndent(diff, "", " "); err == nil {
if err := os.WriteFile(diffPath, diffJSON, 0644); err != nil {
log.WithContext(ctx).Errorf("failed to write diff: %v", err)
}
}
log.WithContext(ctx).Infof("shadow comparison mismatch saved to %s", outputDir)
}