Compare commits

...

5 Commits

Author SHA1 Message Date
crn4
21e5e6ddff cached compaction 2025-12-05 00:29:58 +01:00
crn4
10fb18736b benchmark on both maps was added 2025-12-05 00:29:58 +01:00
crn4
942abeca0c select nmap based on peer version 2025-12-05 00:29:58 +01:00
crn4
e184a43e8a firewallrules compacted 2025-12-05 00:29:58 +01:00
crn4
f33f84299f routes compacted 2025-12-05 00:29:58 +01:00
7 changed files with 842 additions and 7 deletions

View File

@@ -36,6 +36,10 @@ import (
"github.com/netbirdio/netbird/util"
)
const (
compactNetworkMapMinVersion = "v0.61.0" // TODO change to real version
)
type Controller struct {
repo Repository
metrics *metrics
@@ -478,6 +482,12 @@ func (c *Controller) getPeerNetworkMapExp(
Network: &types.Network{},
}
}
peer := account.GetPeer(peerId)
if peer != nil && supportsCompactNetworkMap(peer) {
return account.GetPeerNetworkMapCompactExp(ctx, peerId, customZone, validatedPeers, metrics)
}
return account.GetPeerNetworkMapExp(ctx, peerId, customZone, validatedPeers, metrics)
}
@@ -618,6 +628,19 @@ func (c *Controller) StartWarmup(ctx context.Context) {
}
func supportsCompactNetworkMap(peer *nbpeer.Peer) bool {
if peer.Meta.WtVersion == "development" || peer.Meta.WtVersion == "dev" {
return true
}
peerVersion := semver.Canonical("v" + peer.Meta.WtVersion)
if peerVersion == "" {
return false
}
return semver.Compare(peerVersion, compactNetworkMapMinVersion) >= 0
}
// 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 {

View File

@@ -40,6 +40,10 @@ type FirewallRule struct {
// PortRange represents the range of ports for a firewall rule
PortRange RulePortRange
PeerIPs []string
Ports []string
PortRanges []RulePortRange
}
// Equal checks if two firewall rules are equal.

View File

@@ -2,6 +2,7 @@ package types
import (
"encoding/binary"
"fmt"
"math/rand"
"net"
"sync"
@@ -49,6 +50,135 @@ func (nm *NetworkMap) Merge(other *NetworkMap) {
nm.ForwardingRules = util.MergeUnique(nm.ForwardingRules, other.ForwardingRules)
}
func (nm *NetworkMap) UncompactRoutes() {
peers := make(map[string]*nbpeer.Peer, len(nm.Peers)+len(nm.OfflinePeers))
for _, p := range nm.Peers {
peers[p.ID] = p
}
uncompactedRoutes := make([]*route.Route, 0)
for _, compactRoute := range nm.Routes {
if len(compactRoute.ApplicablePeerIDs) == 0 {
uncompactedRoutes = append(uncompactedRoutes, compactRoute.Copy())
continue
}
for _, peerID := range compactRoute.ApplicablePeerIDs {
expandedRoute := compactRoute.Copy()
expandedRoute.ID = route.ID(string(compactRoute.ID) + ":" + peerID)
peer := peers[peerID]
if peer == nil {
continue
}
expandedRoute.Peer = peer.Key
expandedRoute.PeerID = peerID
uncompactedRoutes = append(uncompactedRoutes, expandedRoute)
}
}
nm.Routes = uncompactedRoutes
}
func (nm *NetworkMap) UncompactFirewallRules() {
uncompactedRules := make([]*FirewallRule, 0, len(nm.FirewallRules)*2)
for _, compactRule := range nm.FirewallRules {
if len(compactRule.PeerIPs) == 0 {
uncompactedRules = append(uncompactedRules, compactRule)
continue
}
for _, peerIP := range compactRule.PeerIPs {
if len(compactRule.Ports) > 0 {
for _, port := range compactRule.Ports {
expandedRule := &FirewallRule{
PolicyID: compactRule.PolicyID,
PeerIP: peerIP,
Direction: compactRule.Direction,
Action: compactRule.Action,
Protocol: compactRule.Protocol,
Port: port,
}
uncompactedRules = append(uncompactedRules, expandedRule)
}
} else if len(compactRule.PortRanges) > 0 {
for _, portRange := range compactRule.PortRanges {
expandedRule := &FirewallRule{
PolicyID: compactRule.PolicyID,
PeerIP: peerIP,
Direction: compactRule.Direction,
Action: compactRule.Action,
Protocol: compactRule.Protocol,
PortRange: portRange,
}
uncompactedRules = append(uncompactedRules, expandedRule)
}
} else {
expandedRule := &FirewallRule{
PolicyID: compactRule.PolicyID,
PeerIP: peerIP,
Direction: compactRule.Direction,
Action: compactRule.Action,
Protocol: compactRule.Protocol,
Port: compactRule.Port,
PortRange: compactRule.PortRange,
}
uncompactedRules = append(uncompactedRules, expandedRule)
}
}
}
nm.FirewallRules = uncompactedRules
}
func (nm *NetworkMap) ValidateApplicablePeerIDs(compactNm *NetworkMap, expectedPermsMap map[string]map[string]bool) error {
if compactNm == nil {
return fmt.Errorf("compact network map is nil")
}
peerIDSet := make(map[string]struct{})
for _, peer := range nm.Peers {
peerIDSet[peer.ID] = struct{}{}
}
for _, route := range compactNm.Routes {
if len(route.ApplicablePeerIDs) == 0 {
continue
}
for _, peerID := range route.ApplicablePeerIDs {
if _, exists := peerIDSet[peerID]; !exists {
return fmt.Errorf("route %s has applicable peer ID %s that doesn't exist in peer list", route.ID, peerID)
}
}
if expectedPermsMap != nil {
expected, hasExpected := expectedPermsMap[string(route.ID)]
if hasExpected {
expectedPeerIDs := make(map[string]struct{})
for peerID, shouldAccess := range expected {
if shouldAccess {
expectedPeerIDs[peerID] = struct{}{}
}
}
if len(route.ApplicablePeerIDs) != len(expectedPeerIDs) {
return fmt.Errorf("route %s: expected %d applicable peers, got %d",
route.ID, len(expectedPeerIDs), len(route.ApplicablePeerIDs))
}
for _, peerID := range route.ApplicablePeerIDs {
if _, expected := expectedPeerIDs[peerID]; !expected {
return fmt.Errorf("route %s: peer %s should not have access but is in ApplicablePeerIDs",
route.ID, peerID)
}
}
}
}
}
return nil
}
func mergeUniquePeersByID(peers1, peers2 []*nbpeer.Peer) []*nbpeer.Peer {
result := make(map[string]*nbpeer.Peer)
for _, peer := range peers1 {

View File

@@ -32,6 +32,17 @@ func (a *Account) GetPeerNetworkMapExp(
return a.NetworkMapCache.GetPeerNetworkMap(ctx, peerID, peersCustomZone, validatedPeers, metrics)
}
func (a *Account) GetPeerNetworkMapCompactExp(
ctx context.Context,
peerID string,
peersCustomZone nbdns.CustomZone,
validatedPeers map[string]struct{},
metrics *telemetry.AccountManagerMetrics,
) *NetworkMap {
a.initNetworkMapBuilder(validatedPeers)
return a.NetworkMapCache.GetPeerNetworkMapCompact(ctx, peerID, peersCustomZone, validatedPeers, metrics)
}
func (a *Account) OnPeerAddedUpdNetworkMapCache(peerId string) error {
if a.NetworkMapCache == nil {
return nil

View File

@@ -886,6 +886,9 @@ func normalizeAndSortNetworkMap(networkMap *types.NetworkMap) {
if r1.PeerIP != r2.PeerIP {
return r1.PeerIP < r2.PeerIP
}
if r1.PolicyID != r2.PolicyID {
return r1.PolicyID < r2.PolicyID
}
if r1.Protocol != r2.Protocol {
return r1.Protocol < r2.Protocol
}
@@ -1067,3 +1070,258 @@ func createTestAccountWithEntities() *types.Account {
return account
}
func createAccountFromFile() (*types.Account, error) {
accraw := filepath.Join("testdata", "account_cnlf3j3l0ubs738o5d4g.json")
data, err := os.ReadFile(accraw)
if err != nil {
return nil, err
}
var account types.Account
err = json.Unmarshal(data, &account)
if err != nil {
return nil, err
}
return &account, nil
}
func TestGetPeerNetworkMapCompact(t *testing.T) {
account, err := createAccountFromFile()
require.NoError(t, err)
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 := types.NewNetworkMapBuilder(account, validatedPeersMap)
testingPeerID := "d3knp53l0ubs738a3n6g"
regularNm := builder.GetPeerNetworkMap(ctx, testingPeerID, customZone, validatedPeersMap, nil)
compactNm := builder.GetPeerNetworkMapCompact(ctx, testingPeerID, customZone, validatedPeersMap, nil)
compactedJSON, err := json.MarshalIndent(compactNm, "", " ")
require.NoError(t, err)
compactedBeforeUncompact := filepath.Join("testdata", "compact_before_uncompact.json")
err = os.MkdirAll(filepath.Dir(compactedBeforeUncompact), 0755)
require.NoError(t, err)
err = os.WriteFile(compactedBeforeUncompact, compactedJSON, 0644)
require.NoError(t, err)
compactNm.UncompactRoutes()
compactNm.UncompactFirewallRules()
normalizeAndSortNetworkMap(regularNm)
normalizeAndSortNetworkMap(compactNm)
regularJSON, err := json.MarshalIndent(regularNm, "", " ")
require.NoError(t, err)
regularLn := len(regularJSON)
compactLn := len(compactedJSON)
t.Logf("compacted less on %d percents", 100-int32((float32(compactLn)/float32(regularLn))*100))
regular := filepath.Join("testdata", "regular_nmap.json")
err = os.MkdirAll(filepath.Dir(regular), 0755)
require.NoError(t, err)
err = os.WriteFile(regular, regularJSON, 0644)
require.NoError(t, err)
uncompactedJSON, err := json.MarshalIndent(compactNm, "", " ")
require.NoError(t, err)
uncompacted := filepath.Join("testdata", "compacted_nmap.json")
err = os.MkdirAll(filepath.Dir(regular), 0755)
require.NoError(t, err)
err = os.WriteFile(uncompacted, uncompactedJSON, 0644)
require.NoError(t, err)
require.JSONEq(t, string(regularJSON), string(uncompactedJSON), "regular and uncompacted network maps should be equal")
}
func BenchmarkGetPeerNetworkMapCompact(b *testing.B) {
account, err := createAccountFromFile()
require.NoError(b, err)
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 := types.NewNetworkMapBuilder(account, validatedPeersMap)
testingPeerID := "d3knp53l0ubs738a3n6g"
regularNm := builder.GetPeerNetworkMap(ctx, testingPeerID, customZone, validatedPeersMap, nil)
compactNm := builder.GetPeerNetworkMapCompact(ctx, testingPeerID, customZone, validatedPeersMap, nil)
regularJSON, err := json.Marshal(regularNm)
require.NoError(b, err)
compactJSON, err := json.Marshal(compactNm)
require.NoError(b, err)
regularSize := len(regularJSON)
compactSize := len(compactJSON)
savingsPercent := 100 - int(float64(compactSize)/float64(regularSize)*100)
b.ReportMetric(float64(regularSize), "regular_bytes")
b.ReportMetric(float64(compactSize), "compact_bytes")
b.ReportMetric(float64(savingsPercent), "savings_%")
b.Logf("Regular network map: %d bytes", regularSize)
b.Logf("Compact network map: %d bytes", compactSize)
b.Logf("Data savings: %d%% (%d bytes saved)", savingsPercent, regularSize-compactSize)
b.Run("Regular", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = builder.GetPeerNetworkMap(ctx, testingPeerID, customZone, validatedPeersMap, nil)
}
})
b.Run("Compact", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = builder.GetPeerNetworkMapCompact(ctx, testingPeerID, customZone, validatedPeersMap, nil)
}
})
}
func TestGetPeerNetworkMapCompactCached(t *testing.T) {
account, err := createAccountFromFile()
require.NoError(t, err)
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 := types.NewNetworkMapBuilder(account, validatedPeersMap)
testingPeerID := "d3knp53l0ubs738a3n6g"
regularNm := builder.GetPeerNetworkMap(ctx, testingPeerID, customZone, validatedPeersMap, nil)
compactCachedNm := builder.GetPeerNetworkMapCompactCached(ctx, testingPeerID, customZone, validatedPeersMap, nil)
compactedJSON, err := json.MarshalIndent(compactCachedNm, "", " ")
require.NoError(t, err)
compactedBeforeUncompact := filepath.Join("testdata", "compact_cached_before_uncompact.json")
err = os.MkdirAll(filepath.Dir(compactedBeforeUncompact), 0755)
require.NoError(t, err)
err = os.WriteFile(compactedBeforeUncompact, compactedJSON, 0644)
require.NoError(t, err)
compactCachedNm.UncompactRoutes()
compactCachedNm.UncompactFirewallRules()
normalizeAndSortNetworkMap(regularNm)
normalizeAndSortNetworkMap(compactCachedNm)
regularJSON, err := json.MarshalIndent(regularNm, "", " ")
require.NoError(t, err)
regularLn := len(regularJSON)
compactLn := len(compactedJSON)
t.Logf("compacted less on %d percents", 100-int32((float32(compactLn)/float32(regularLn))*100))
regular := filepath.Join("testdata", "regular_nmap_cached.json")
err = os.MkdirAll(filepath.Dir(regular), 0755)
require.NoError(t, err)
err = os.WriteFile(regular, regularJSON, 0644)
require.NoError(t, err)
uncompactedJSON, err := json.MarshalIndent(compactCachedNm, "", " ")
require.NoError(t, err)
uncompacted := filepath.Join("testdata", "compacted_cached_nmap.json")
err = os.MkdirAll(filepath.Dir(regular), 0755)
require.NoError(t, err)
err = os.WriteFile(uncompacted, uncompactedJSON, 0644)
require.NoError(t, err)
require.JSONEq(t, string(regularJSON), string(uncompactedJSON), "regular and uncompacted network maps should be equal")
}
func BenchmarkGetPeerNetworkMapCompactCached(b *testing.B) {
account, err := createAccountFromFile()
require.NoError(b, err)
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 := types.NewNetworkMapBuilder(account, validatedPeersMap)
testingPeerID := "d3knp53l0ubs738a3n6g"
regularNm := builder.GetPeerNetworkMap(ctx, testingPeerID, customZone, validatedPeersMap, nil)
compactNm := builder.GetPeerNetworkMapCompact(ctx, testingPeerID, customZone, validatedPeersMap, nil)
compactCachedNm := builder.GetPeerNetworkMapCompactCached(ctx, testingPeerID, customZone, validatedPeersMap, nil)
regularJSON, err := json.Marshal(regularNm)
require.NoError(b, err)
compactJSON, err := json.Marshal(compactNm)
require.NoError(b, err)
compactCachedJSON, err := json.Marshal(compactCachedNm)
require.NoError(b, err)
regularSize := len(regularJSON)
compactSize := len(compactJSON)
compactCachedSize := len(compactCachedJSON)
savingsPercent := 100 - int(float64(compactCachedSize)/float64(regularSize)*100)
b.ReportMetric(float64(regularSize), "regular_bytes")
b.ReportMetric(float64(compactCachedSize), "compact_cached_bytes")
b.ReportMetric(float64(savingsPercent), "savings_%")
b.Logf("Regular network map: %d bytes", regularSize)
b.Logf("Compact network map: %d bytes", compactSize)
b.Logf("Compact cached network map: %d bytes", compactCachedSize)
b.Logf("Data savings: %d%% (%d bytes saved)", savingsPercent, regularSize-compactCachedSize)
b.Run("Regular", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = builder.GetPeerNetworkMap(ctx, testingPeerID, customZone, validatedPeersMap, nil)
}
})
b.Run("CompactOnDemand", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = builder.GetPeerNetworkMapCompact(ctx, testingPeerID, customZone, validatedPeersMap, nil)
}
})
b.Run("CompactCached", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = builder.GetPeerNetworkMapCompactCached(ctx, testingPeerID, customZone, validatedPeersMap, nil)
}
})
}

View File

@@ -42,9 +42,11 @@ type NetworkMapCache struct {
groupToRoutes map[string][]*route.Route
peerToRoutes map[string][]*route.Route
peerACLs map[string]*PeerACLView
peerRoutes map[string]*PeerRoutesView
peerDNS map[string]*nbdns.Config
peerACLs map[string]*PeerACLView
peerRoutes map[string]*PeerRoutesView
peerDNS map[string]*nbdns.Config
peerFirewallRulesCompact map[string][]*FirewallRule
peerRoutesCompact map[string][]*route.Route
resourceRouters map[string]map[string]*routerTypes.NetworkRouter
resourcePolicies map[string][]*Policy
@@ -93,10 +95,12 @@ func NewNetworkMapBuilder(account *Account, validatedPeers map[string]struct{})
groupToPolicies: make(map[string][]*Policy),
groupToRoutes: make(map[string][]*route.Route),
peerToRoutes: make(map[string][]*route.Route),
peerACLs: make(map[string]*PeerACLView),
peerRoutes: make(map[string]*PeerRoutesView),
peerDNS: make(map[string]*nbdns.Config),
globalResources: make(map[string]*resourceTypes.NetworkResource),
peerACLs: make(map[string]*PeerACLView),
peerRoutes: make(map[string]*PeerRoutesView),
peerDNS: make(map[string]*nbdns.Config),
peerFirewallRulesCompact: make(map[string][]*FirewallRule),
peerRoutesCompact: make(map[string][]*route.Route),
globalResources: make(map[string]*resourceTypes.NetworkResource),
acgToRoutes: make(map[string]map[route.ID]*RouteOwnerInfo),
noACGRoutes: make(map[route.ID]*RouteOwnerInfo),
},
@@ -253,6 +257,9 @@ func (b *NetworkMapBuilder) buildPeerACLView(account *Account, peerID string) {
}
b.cache.peerACLs[peerID] = view
compactedRules := compactFirewallRules(firewallRules)
b.cache.peerFirewallRulesCompact[peerID] = compactedRules
}
func (b *NetworkMapBuilder) getPeerConnectionResources(account *Account, peer *nbpeer.Peer,
@@ -659,6 +666,10 @@ func (b *NetworkMapBuilder) buildPeerRoutesView(account *Account, peerID string)
}
}
otherRouteIDs := slices.Concat(view.NetworkResourceIDs, view.InheritedRouteIDs)
compactedRoutes := b.compactRoutesForPeer(peerID, view.OwnRouteIDs, otherRouteIDs)
b.cache.peerRoutesCompact[peerID] = compactedRoutes
b.cache.peerRoutes[peerID] = view
}
@@ -1045,6 +1056,377 @@ func (b *NetworkMapBuilder) assembleNetworkMap(
}
}
func (b *NetworkMapBuilder) GetPeerNetworkMapCompactCached(
ctx context.Context, peerID string, peersCustomZone nbdns.CustomZone,
validatedPeers map[string]struct{}, metrics *telemetry.AccountManagerMetrics,
) *NetworkMap {
start := time.Now()
account := b.account.Load()
peer := account.GetPeer(peerID)
if peer == nil {
return &NetworkMap{Network: account.Network.Copy()}
}
b.cache.mu.RLock()
defer b.cache.mu.RUnlock()
aclView := b.cache.peerACLs[peerID]
routesView := b.cache.peerRoutes[peerID]
dnsConfig := b.cache.peerDNS[peerID]
if aclView == nil || routesView == nil || dnsConfig == nil {
return &NetworkMap{Network: account.Network.Copy()}
}
nm := b.assembleNetworkMapCompactCached(account, peer, aclView, routesView, dnsConfig, peersCustomZone, validatedPeers)
if metrics != nil {
objectCount := int64(len(nm.Peers) + len(nm.OfflinePeers) + len(nm.Routes) + len(nm.FirewallRules) + len(nm.RoutesFirewallRules))
metrics.CountNetworkMapObjects(objectCount)
metrics.CountGetPeerNetworkMapDuration(time.Since(start))
if objectCount > 5000 {
log.WithContext(ctx).Tracef("account: %s has a total resource count of %d objects from cache",
account.Id, objectCount)
}
}
return nm
}
func (b *NetworkMapBuilder) GetPeerNetworkMapCompact(
ctx context.Context, peerID string, peersCustomZone nbdns.CustomZone,
validatedPeers map[string]struct{}, metrics *telemetry.AccountManagerMetrics,
) *NetworkMap {
start := time.Now()
account := b.account.Load()
peer := account.GetPeer(peerID)
if peer == nil {
return &NetworkMap{Network: account.Network.Copy()}
}
b.cache.mu.RLock()
defer b.cache.mu.RUnlock()
aclView := b.cache.peerACLs[peerID]
routesView := b.cache.peerRoutes[peerID]
dnsConfig := b.cache.peerDNS[peerID]
if aclView == nil || routesView == nil || dnsConfig == nil {
return &NetworkMap{Network: account.Network.Copy()}
}
nm := b.assembleNetworkMapCompact(account, peer, aclView, routesView, dnsConfig, peersCustomZone, validatedPeers)
if metrics != nil {
objectCount := int64(len(nm.Peers) + len(nm.OfflinePeers) + len(nm.Routes) + len(nm.FirewallRules) + len(nm.RoutesFirewallRules))
metrics.CountNetworkMapObjects(objectCount)
metrics.CountGetPeerNetworkMapDuration(time.Since(start))
if objectCount > 5000 {
log.WithContext(ctx).Tracef("account: %s has a total resource count of %d objects from cache",
account.Id, objectCount)
}
}
return nm
}
func (b *NetworkMapBuilder) assembleNetworkMapCompactCached(
account *Account, peer *nbpeer.Peer, aclView *PeerACLView, routesView *PeerRoutesView,
dnsConfig *nbdns.Config, customZone nbdns.CustomZone, validatedPeers map[string]struct{},
) *NetworkMap {
var peersToConnect []*nbpeer.Peer
var expiredPeers []*nbpeer.Peer
for _, peerID := range aclView.ConnectedPeerIDs {
if _, ok := validatedPeers[peerID]; !ok {
continue
}
peerItem := b.cache.globalPeers[peerID]
if peerItem == nil {
continue
}
expired, _ := peerItem.LoginExpired(account.Settings.PeerLoginExpiration)
if account.Settings.PeerLoginExpirationEnabled && expired {
expiredPeers = append(expiredPeers, peerItem)
} else {
peersToConnect = append(peersToConnect, peerItem)
}
}
routes := b.cache.peerRoutesCompact[peer.ID]
firewallRules := b.cache.peerFirewallRulesCompact[peer.ID]
var routesFirewallRules []*RouteFirewallRule
for _, ruleID := range routesView.RouteFirewallRuleIDs {
if rule := b.cache.globalRouteRules[ruleID]; rule != nil {
routesFirewallRules = append(routesFirewallRules, rule)
}
}
finalDNSConfig := *dnsConfig
if finalDNSConfig.ServiceEnable && customZone.Domain != "" {
var zones []nbdns.CustomZone
records := filterZoneRecordsForPeers(peer, customZone, peersToConnect, expiredPeers)
zones = append(zones, nbdns.CustomZone{
Domain: customZone.Domain,
Records: records,
})
finalDNSConfig.CustomZones = zones
}
return &NetworkMap{
Peers: peersToConnect,
Network: account.Network.Copy(),
Routes: routes,
DNSConfig: finalDNSConfig,
OfflinePeers: expiredPeers,
FirewallRules: firewallRules,
RoutesFirewallRules: routesFirewallRules,
}
}
func (b *NetworkMapBuilder) assembleNetworkMapCompact(
account *Account, peer *nbpeer.Peer, aclView *PeerACLView, routesView *PeerRoutesView,
dnsConfig *nbdns.Config, customZone nbdns.CustomZone, validatedPeers map[string]struct{},
) *NetworkMap {
var peersToConnect []*nbpeer.Peer
var expiredPeers []*nbpeer.Peer
for _, peerID := range aclView.ConnectedPeerIDs {
if _, ok := validatedPeers[peerID]; !ok {
continue
}
peer := b.cache.globalPeers[peerID]
if peer == nil {
continue
}
expired, _ := peer.LoginExpired(account.Settings.PeerLoginExpiration)
if account.Settings.PeerLoginExpirationEnabled && expired {
expiredPeers = append(expiredPeers, peer)
} else {
peersToConnect = append(peersToConnect, peer)
}
}
var routes []*route.Route
for _, routeID := range routesView.OwnRouteIDs {
if route := b.cache.globalRoutes[routeID]; route != nil {
routes = append(routes, route)
}
}
otherRouteIDs := slices.Concat(routesView.NetworkResourceIDs, routesView.InheritedRouteIDs)
type crt struct {
route *route.Route
peerIds []string
}
rtfilter := make(map[string]crt)
peerId := peer.ID
for _, routeID := range otherRouteIDs {
if route := b.cache.globalRoutes[routeID]; route != nil {
rid, pid := splitRouteAndPeer(route)
if pid == peerId || len(pid) == 0 {
routes = append(routes, route)
continue
}
crt := rtfilter[rid]
crt.peerIds = append(crt.peerIds, pid)
crt.route = route.CopyClean()
rtfilter[rid] = crt
}
}
for rid, crt := range rtfilter {
crt.route.ApplicablePeerIDs = crt.peerIds
crt.route.ID = route.ID(rid)
routes = append(routes, crt.route)
}
var expandedFirewallRules []*FirewallRule
for _, ruleID := range aclView.FirewallRuleIDs {
if rule := b.cache.globalRules[ruleID]; rule != nil {
expandedFirewallRules = append(expandedFirewallRules, rule)
}
}
firewallRules := compactFirewallRules(expandedFirewallRules)
var routesFirewallRules []*RouteFirewallRule
for _, ruleID := range routesView.RouteFirewallRuleIDs {
if rule := b.cache.globalRouteRules[ruleID]; rule != nil {
routesFirewallRules = append(routesFirewallRules, rule)
}
}
finalDNSConfig := *dnsConfig
if finalDNSConfig.ServiceEnable && customZone.Domain != "" {
var zones []nbdns.CustomZone
records := filterZoneRecordsForPeers(peer, customZone, peersToConnect, expiredPeers)
zones = append(zones, nbdns.CustomZone{
Domain: customZone.Domain,
Records: records,
})
finalDNSConfig.CustomZones = zones
}
return &NetworkMap{
Peers: peersToConnect,
Network: account.Network.Copy(),
Routes: routes,
DNSConfig: finalDNSConfig,
OfflinePeers: expiredPeers,
FirewallRules: firewallRules,
RoutesFirewallRules: routesFirewallRules,
}
}
func splitRouteAndPeer(r *route.Route) (string, string) {
parts := strings.Split(string(r.ID), ":")
if len(parts) < 2 {
return string(r.ID), ""
}
return parts[0], parts[1]
}
func (b *NetworkMapBuilder) compactRoutesForPeer(peerID string, ownRouteIDs []route.ID, otherRouteIDs []route.ID) []*route.Route {
var routes []*route.Route
for _, routeID := range ownRouteIDs {
if rt := b.cache.globalRoutes[routeID]; rt != nil {
routes = append(routes, rt)
}
}
type crt struct {
route *route.Route
peerIds []string
}
rtfilter := make(map[string]crt)
for _, routeID := range otherRouteIDs {
if rt := b.cache.globalRoutes[routeID]; rt != nil {
rid, pid := splitRouteAndPeer(rt)
if pid == peerID || len(pid) == 0 {
routes = append(routes, rt)
continue
}
crt := rtfilter[rid]
crt.peerIds = append(crt.peerIds, pid)
crt.route = rt.CopyClean()
rtfilter[rid] = crt
}
}
for rid, crt := range rtfilter {
crt.route.ApplicablePeerIDs = crt.peerIds
crt.route.ID = route.ID(rid)
routes = append(routes, crt.route)
}
return routes
}
func compactFirewallRules(expandedRules []*FirewallRule) []*FirewallRule {
type peerKey struct {
PolicyID string
PeerIP string
Direction int
Action string
Protocol string
}
peerGroups := make(map[peerKey]struct {
ports []string
portRanges []RulePortRange
})
for _, rule := range expandedRules {
key := peerKey{
PolicyID: rule.PolicyID,
PeerIP: rule.PeerIP,
Direction: rule.Direction,
Action: rule.Action,
Protocol: rule.Protocol,
}
group := peerGroups[key]
if rule.Port != "" {
group.ports = append(group.ports, rule.Port)
}
if rule.PortRange.Start != 0 || rule.PortRange.End != 0 {
group.portRanges = append(group.portRanges, rule.PortRange)
}
peerGroups[key] = group
}
type ruleKey struct {
PolicyID string
Direction int
Action string
Protocol string
PortsSig string
RangesSig string
}
ruleGroups := make(map[ruleKey]struct {
peerIPs []string
ports []string
portRanges []RulePortRange
})
for pKey, pGroup := range peerGroups {
portsSig := strings.Join(pGroup.ports, ",")
rangesSig := fmt.Sprintf("%v", pGroup.portRanges)
rKey := ruleKey{
PolicyID: pKey.PolicyID,
Direction: pKey.Direction,
Action: pKey.Action,
Protocol: pKey.Protocol,
PortsSig: portsSig,
RangesSig: rangesSig,
}
group := ruleGroups[rKey]
group.peerIPs = append(group.peerIPs, pKey.PeerIP)
if len(group.ports) == 0 {
group.ports = pGroup.ports
}
if len(group.portRanges) == 0 {
group.portRanges = pGroup.portRanges
}
ruleGroups[rKey] = group
}
compactRules := make([]*FirewallRule, 0, len(ruleGroups))
for rKey, group := range ruleGroups {
compactRule := &FirewallRule{
PolicyID: rKey.PolicyID,
Direction: rKey.Direction,
Action: rKey.Action,
Protocol: rKey.Protocol,
PeerIPs: group.peerIPs,
Ports: group.ports,
PortRanges: group.portRanges,
}
compactRules = append(compactRules, compactRule)
}
return compactRules
}
func (b *NetworkMapBuilder) generateFirewallRuleID(rule *FirewallRule) string {
var s strings.Builder
s.WriteString(fw)

View File

@@ -109,6 +109,9 @@ type Route struct {
AccessControlGroups []string `gorm:"serializer:json"`
// SkipAutoApply indicates if this exit node route (0.0.0.0/0) should skip auto-application for client routing
SkipAutoApply bool
// ApplicablePeerIDs is used in compact network maps to indicate which peers this route applies to
// When populated, client should use these IDs to reference peers from the Peers array instead of using Peer/PeerID/Groups
ApplicablePeerIDs []string `gorm:"-"`
}
// EventMeta returns activity event meta related to the route
@@ -144,6 +147,30 @@ func (r *Route) Copy() *Route {
return route
}
// CopyClean copies a route object without the peer-specific part of the ID
// and peer data
func (r *Route) CopyClean() *Route {
cleanId := strings.Split(string(r.ID), ":")[0]
route := &Route{
ID: ID(cleanId),
AccountID: r.AccountID,
Description: r.Description,
NetID: r.NetID,
Network: r.Network,
Domains: slices.Clone(r.Domains),
KeepRoute: r.KeepRoute,
NetworkType: r.NetworkType,
PeerGroups: slices.Clone(r.PeerGroups),
Metric: r.Metric,
Masquerade: r.Masquerade,
Enabled: r.Enabled,
Groups: slices.Clone(r.Groups),
AccessControlGroups: slices.Clone(r.AccessControlGroups),
SkipAutoApply: r.SkipAutoApply,
}
return route
}
// Equal compares one route with the other
func (r *Route) Equal(other *Route) bool {
if r == nil && other == nil {