diff --git a/management/internals/controllers/network_map/controller/controller.go b/management/internals/controllers/network_map/controller/controller.go index 3f58883e2..555d82cce 100644 --- a/management/internals/controllers/network_map/controller/controller.go +++ b/management/internals/controllers/network_map/controller/controller.go @@ -18,6 +18,7 @@ import ( "github.com/netbirdio/netbird/management/internals/controllers/network_map" "github.com/netbirdio/netbird/management/internals/controllers/network_map/controller/cache" "github.com/netbirdio/netbird/management/internals/modules/peers/ephemeral" + networkmapdb "github.com/netbirdio/netbird/management/internals/network_map_db" "github.com/netbirdio/netbird/management/internals/server/config" "github.com/netbirdio/netbird/management/internals/shared/grpc" "github.com/netbirdio/netbird/management/server/account" @@ -30,6 +31,8 @@ import ( "github.com/netbirdio/netbird/management/server/telemetry" "github.com/netbirdio/netbird/management/server/types" sharedgrpc "github.com/netbirdio/netbird/shared/management/grpc" + "github.com/netbirdio/netbird/shared/management/networkmap" + "github.com/netbirdio/netbird/shared/management/networkmap/nmdata" "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/util" @@ -61,6 +64,8 @@ type Controller struct { serverSupportedSyncMessageVersion sharedgrpc.SyncMessageVersion perAccountServerSupportedSyncMessageVersions map[string]sharedgrpc.SyncMessageVersion + + nmdataStore networkmapdb.NetworkMapDBStore } type bufferUpdate struct { @@ -78,7 +83,7 @@ type bufferAffectedUpdate struct { var _ network_map.Controller = (*Controller)(nil) -func NewController(ctx context.Context, store store.Store, metrics telemetry.AppMetrics, peersUpdateManager network_map.PeersUpdateManager, requestBuffer account.RequestBuffer, integratedPeerValidator integrated_validator.IntegratedValidator, settingsManager settings.Manager, dnsDomain string, proxyController port_forwarding.Controller, ephemeralPeersManager ephemeral.Manager, config *config.Config) *Controller { +func NewController(ctx context.Context, store store.Store, metrics telemetry.AppMetrics, peersUpdateManager network_map.PeersUpdateManager, requestBuffer account.RequestBuffer, integratedPeerValidator integrated_validator.IntegratedValidator, settingsManager settings.Manager, dnsDomain string, proxyController port_forwarding.Controller, ephemeralPeersManager ephemeral.Manager, config *config.Config, nmdataStore networkmapdb.NetworkMapDBStore) *Controller { nMetrics, err := newMetrics(metrics.UpdateChannelMetrics()) if err != nil { log.Fatal(fmt.Errorf("error creating metrics: %w", err)) @@ -99,6 +104,7 @@ func NewController(ctx context.Context, store store.Store, metrics telemetry.App EphemeralPeersManager: ephemeralPeersManager, serverSupportedSyncMessageVersion: sharedgrpc.SyncMessageVersionFromConfig(config.HighestSupportedSyncMessageVersion), perAccountServerSupportedSyncMessageVersions: sharedgrpc.SyncMessageVersionsFromMap(config.PerAccountHighestSupportedSyncMessageVersion), + nmdataStore: nmdataStore, } } @@ -147,6 +153,11 @@ func (c *Controller) CountStreams() int { func (c *Controller) sendUpdateAccountPeers(ctx context.Context, accountID string, reason types.UpdateReason) error { log.WithContext(ctx).Tracef("updating peers for account %s from %s", accountID, util.GetCallerName()) + + if nmData := c.getNetworkMapData(ctx, accountID); nmData != nil { + return c.sendUpdateAccountPeersFromData(ctx, accountID, reason, nmData) + } + account, err := c.requestBuffer.GetAccountWithBackpressure(ctx, accountID) if err != nil { return fmt.Errorf("failed to get account: %v", err) @@ -254,7 +265,7 @@ func (c *Controller) sendUpdateAccountPeers(ctx context.Context, accountID strin // proxyNetworkMap rides the envelope as a ProxyPatch sidecar; // the client merges it into Calculate()'s output the same // way the legacy server did via NetworkMap.Merge. - update = grpc.ToComponentSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, p, nil, nil, components, proxyNetworkMap, dnsDomain, postureChecks, account.Settings, extraSetting, maps.Keys(peerGroups), dnsFwdPort) + update = grpc.ToComponentSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, types.TwinPeer(p), nil, nil, components, proxyNetworkMap, dnsDomain, postureChecks, types.TwinAccountSettings(account.Settings), extraSetting, maps.Keys(peerGroups), dnsFwdPort) c.metrics.CountToComponentSyncResponseDuration(time.Since(start)) c.peersUpdateManager.SendUpdate(ctx, p.ID, &network_map.UpdateMessage{ @@ -275,7 +286,7 @@ func (c *Controller) sendUpdateAccountPeers(ctx context.Context, accountID strin } start = time.Now() - update = grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, p, nil, nil, nmap, dnsDomain, postureChecks, dnsCache, account.Settings, extraSetting, maps.Keys(peerGroups), dnsFwdPort) + update = grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, types.TwinPeer(p), nil, nil, nmap, dnsDomain, postureChecks, dnsCache, types.TwinAccountSettings(account.Settings), extraSetting, maps.Keys(peerGroups), dnsFwdPort) c.metrics.CountToSyncResponseDuration(time.Since(start)) c.peersUpdateManager.SendUpdate(ctx, p.ID, &network_map.UpdateMessage{ @@ -293,6 +304,247 @@ func (c *Controller) sendUpdateAccountPeers(ctx context.Context, accountID strin return nil } +// sendUpdateAccountPeersFromData is the account-free variant of +// sendUpdateAccountPeers: everything is computed from the network-map DB +// store's twin data; only extra settings and validated peers are resolved at +// runtime. Proxy network maps and policy injection, private-service zones, +// group-to-user SSH mappings and forced routing-peer DNS resolution have no +// DB-backed source yet and are omitted. +func (c *Controller) sendUpdateAccountPeersFromData(ctx context.Context, accountID string, reason types.UpdateReason, nmData *networkmap.NetworkMapData) error { + peersToUpdate := c.connectedPeersFromData(nmData, nil) + if len(peersToUpdate) == 0 { + return nil + } + return c.sendUpdatesFromData(ctx, accountID, nmData, peersToUpdate, &reason) +} + +// sendUpdateForAffectedPeersFromData is the account-free variant of +// sendUpdateForAffectedPeers. +func (c *Controller) sendUpdateForAffectedPeersFromData(ctx context.Context, accountID string, peerIDs []string, nmData *networkmap.NetworkMapData) error { + affected := make(map[string]struct{}, len(peerIDs)) + for _, id := range peerIDs { + affected[id] = struct{}{} + } + + peersToUpdate := c.connectedPeersFromData(nmData, affected) + if len(peersToUpdate) == 0 { + log.WithContext(ctx).Tracef("sendUpdateForAffectedPeersFromData: no peers to update (affected peers not found in data or no channels)") + return nil + } + + log.WithContext(ctx).Tracef("sendUpdateForAffectedPeersFromData: sending network map to %d connected peers", len(peersToUpdate)) + + return c.sendUpdatesFromData(ctx, accountID, nmData, peersToUpdate, nil) +} + +func (c *Controller) connectedPeersFromData(nmData *networkmap.NetworkMapData, affected map[string]struct{}) []*nmdata.Peer { + var result []*nmdata.Peer + for _, peer := range nmData.Peers { + if affected != nil { + if _, ok := affected[peer.ID]; !ok { + continue + } + } + if c.peersUpdateManager.HasChannel(peer.ID) { + result = append(result, peer) + } + } + return result +} + +func (c *Controller) sendUpdatesFromData(ctx context.Context, accountID string, nmData *networkmap.NetworkMapData, peersToUpdate []*nmdata.Peer, reason *types.UpdateReason) error { + globalStart := time.Now() + + extraSettings, err := c.settingsManager.GetExtraSettings(ctx, accountID) + if err != nil { + return fmt.Errorf("failed to get flow enabled status: %v", err) + } + + dnsCache := &cache.DNSConfigCache{} + dnsDomain := c.getDNSDomainFromData(nmData.AccountSettings) + peersCustomZone := networkmap.PeersCustomZone(ctx, accountID, dnsDomain, nmData.Peers, ipv6AllowedPeersFromData(nmData)) + + dnsFwdPort := computeForwarderPortFromData(nmData.Peers, network_map.DnsForwarderPortMinVersion) + + var wg sync.WaitGroup + semaphore := make(chan struct{}, 10) + + for _, peer := range peersToUpdate { + if reason != nil && c.accountManagerMetrics != nil { + c.accountManagerMetrics.CountNmapTriggered(string(reason.Resource), string(reason.Operation)) + } + + wg.Add(1) + semaphore <- struct{}{} + go func(p *nmdata.Peer) { + defer wg.Done() + defer func() { <-semaphore }() + + start := time.Now() + + postureChecks := peerPostureChecksFromData(nmData, p.ID) + + c.metrics.CountCalcPostureChecksDuration(time.Since(start)) + start = time.Now() + + peerGroups := maps.Keys(nmData.GetPeerGroups(p.ID)) + var update *proto.SyncResponse + + commonSyncMessageVersion := sharedgrpc.HighestCommonSyncMessageVersion( + c.perAccountOrGlobalSupportedSyncMessageVersions(accountID), + sharedgrpc.SyncMessageVersionFromConfig(&p.Meta.SyncMessageVersion)) + + log.WithContext(ctx). + WithFields(log.Fields{ + "sync_message_version": commonSyncMessageVersion, + "server_sync_message_version": c.perAccountOrGlobalSupportedSyncMessageVersions(accountID), + "peer_sync_message_version": sharedgrpc.SyncMessageVersionFromConfig(&p.Meta.SyncMessageVersion), + }).Debug("common highest sync message version") + + if commonSyncMessageVersion == sharedgrpc.ComponentNetworkMap { + components := nmData.GetPeerNetworkMapComponents(p.ID, peersCustomZone) + + c.metrics.CountCalcPeerNetworkMapDuration(time.Since(start)) + + start = time.Now() + update = grpc.ToComponentSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, p, nil, nil, components, nil, dnsDomain, postureChecks, nmData.AccountSettings, extraSettings, peerGroups, dnsFwdPort) + c.metrics.CountToComponentSyncResponseDuration(time.Since(start)) + + c.peersUpdateManager.SendUpdate(ctx, p.ID, &network_map.UpdateMessage{ + Update: update, + MessageType: network_map.MessageTypeNetworkMap, + }) + + return + } + + nmap := networkMapFromData(ctx, nmData, p.ID, peersCustomZone) + + c.metrics.CountCalcPeerNetworkMapDuration(time.Since(start)) + + start = time.Now() + update = grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, p, nil, nil, nmap, dnsDomain, postureChecks, dnsCache, nmData.AccountSettings, extraSettings, peerGroups, dnsFwdPort) + c.metrics.CountToSyncResponseDuration(time.Since(start)) + + c.peersUpdateManager.SendUpdate(ctx, p.ID, &network_map.UpdateMessage{ + Update: update, + MessageType: network_map.MessageTypeNetworkMap, + }) + }(peer) + } + + wg.Wait() + if c.accountManagerMetrics != nil { + c.accountManagerMetrics.CountUpdateAccountPeersDuration(time.Since(globalStart)) + } + + return nil +} + +func (c *Controller) getNetworkMapData(ctx context.Context, accountID string) *networkmap.NetworkMapData { + if c.nmdataStore == nil { + return nil + } + + nmData, err := c.nmdataStore.GetNetworkMapData(ctx, accountID) + if err != nil { + log.WithContext(ctx).Errorf("failed to get network map data for account %s, falling back to account-based computation: %v", accountID, err) + return nil + } + + return nmData +} + +func (c *Controller) getDNSDomainFromData(settings *nmdata.AccountSettingsInfo) string { + if settings == nil || settings.DNSDomain == "" { + return c.dnsDomain + } + return settings.DNSDomain +} + +func ipv6AllowedPeersFromData(nmData *networkmap.NetworkMapData) map[string]struct{} { + result := make(map[string]struct{}) + if nmData.AccountSettings != nil { + for _, groupID := range nmData.AccountSettings.IPv6EnabledGroups { + group := nmData.Groups[groupID] + if group == nil { + continue + } + for _, peerID := range group.Peers { + result[peerID] = struct{}{} + } + } + } + for id, p := range nmData.Peers { + if p != nil && p.ProxyMeta.Embedded { + result[id] = struct{}{} + } + } + return result +} + +func networkMapFromData(ctx context.Context, nmData *networkmap.NetworkMapData, peerID string, peersCustomZone nmdata.CustomZone) *types.NetworkMap { + components := nmData.GetPeerNetworkMapComponents(peerID, peersCustomZone) + if components.IsEmpty() { + return &types.NetworkMap{Network: components.Network} + } + return types.CalculateNetworkMapFromComponents(ctx, components) +} + +// peerPostureChecksFromData mirrors getPeerPostureChecks on the twin store. The +// sync response only encodes process-check file paths, so only ProcessCheck is +// converted back to the server posture type. +func peerPostureChecksFromData(nmData *networkmap.NetworkMapData, peerID string) []*posture.Checks { + if len(nmData.PostureChecks) == 0 { + return nil + } + + peerPostureChecks := make(map[string]*posture.Checks) + for _, policy := range nmData.Policies { + if policy == nil || !policy.Enabled || len(policy.SourcePostureChecks) == 0 { + continue + } + if !isPeerInPolicySourceGroupsFromData(nmData, peerID, policy) { + continue + } + for _, checkID := range policy.SourcePostureChecks { + twin := nmData.PostureChecks[checkID] + if twin == nil { + continue + } + peerPostureChecks[checkID] = postureChecksFromTwin(twin) + } + } + + return maps.Values(peerPostureChecks) +} + +func isPeerInPolicySourceGroupsFromData(nmData *networkmap.NetworkMapData, peerID string, policy *nmdata.Policy) bool { + for _, rule := range policy.Rules { + if rule == nil || !rule.Enabled { + continue + } + for _, groupID := range rule.Sources { + if group := nmData.Groups[groupID]; group != nil && slices.Contains(group.Peers, peerID) { + return true + } + } + } + return false +} + +func postureChecksFromTwin(twin *nmdata.PostureChecks) *posture.Checks { + checks := &posture.Checks{ID: twin.ID} + if twin.Checks.ProcessCheck != nil { + processes := make([]posture.Process, 0, len(twin.Checks.ProcessCheck.Processes)) + for _, p := range twin.Checks.ProcessCheck.Processes { + processes = append(processes, posture.Process{LinuxPath: p.LinuxPath, MacPath: p.MacPath, WindowsPath: p.WindowsPath}) + } + checks.Checks.ProcessCheck = &posture.ProcessCheck{Processes: processes} + } + return checks +} + func (c *Controller) perAccountOrGlobalSupportedSyncMessageVersions(accountId string) sharedgrpc.SyncMessageVersion { if perAccount, ok := c.perAccountServerSupportedSyncMessageVersions[accountId]; ok { return perAccount @@ -325,6 +577,10 @@ func (c *Controller) sendUpdateForAffectedPeers(ctx context.Context, accountID s return nil } + if nmData := c.getNetworkMapData(ctx, accountID); nmData != nil { + return c.sendUpdateForAffectedPeersFromData(ctx, accountID, peerIDs, nmData) + } + account, err := c.requestBuffer.GetAccountWithBackpressure(ctx, accountID) if err != nil { return fmt.Errorf("failed to get account: %v", err) @@ -426,7 +682,7 @@ func (c *Controller) sendUpdateForAffectedPeers(ctx context.Context, accountID s // proxyNetworkMap rides the envelope as a ProxyPatch sidecar; // the client merges it into Calculate()'s output the same // way the legacy server did via NetworkMap.Merge. - update = grpc.ToComponentSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, p, nil, nil, components, proxyNetworkMap, dnsDomain, postureChecks, account.Settings, extraSetting, maps.Keys(peerGroups), dnsFwdPort) + update = grpc.ToComponentSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, types.TwinPeer(p), nil, nil, components, proxyNetworkMap, dnsDomain, postureChecks, types.TwinAccountSettings(account.Settings), extraSetting, maps.Keys(peerGroups), dnsFwdPort) c.metrics.CountToComponentSyncResponseDuration(time.Since(start)) c.peersUpdateManager.SendUpdate(ctx, p.ID, &network_map.UpdateMessage{ @@ -447,7 +703,7 @@ func (c *Controller) sendUpdateForAffectedPeers(ctx context.Context, accountID s } start = time.Now() - update = grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, p, nil, nil, nmap, dnsDomain, postureChecks, dnsCache, account.Settings, extraSetting, maps.Keys(peerGroups), dnsFwdPort) + update = grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, types.TwinPeer(p), nil, nil, nmap, dnsDomain, postureChecks, dnsCache, types.TwinAccountSettings(account.Settings), extraSetting, maps.Keys(peerGroups), dnsFwdPort) c.metrics.CountToSyncResponseDuration(time.Since(start)) c.peersUpdateManager.SendUpdate(ctx, p.ID, &network_map.UpdateMessage{ @@ -564,7 +820,7 @@ func (c *Controller) UpdateAccountPeer(ctx context.Context, accountId string, pe // proxyNetworkMap rides the envelope as a ProxyPatch sidecar; // the client merges it into Calculate()'s output the same // way the legacy server did via NetworkMap.Merge. - update = grpc.ToComponentSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, peer, nil, nil, components, proxyNetworkMap, dnsDomain, postureChecks, account.Settings, extraSettings, maps.Keys(peerGroups), dnsFwdPort) + update = grpc.ToComponentSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, types.TwinPeer(peer), nil, nil, components, proxyNetworkMap, dnsDomain, postureChecks, types.TwinAccountSettings(account.Settings), extraSettings, maps.Keys(peerGroups), dnsFwdPort) c.peersUpdateManager.SendUpdate(ctx, peer.ID, &network_map.UpdateMessage{ Update: update, @@ -581,7 +837,7 @@ func (c *Controller) UpdateAccountPeer(ctx context.Context, accountId string, pe nmap.Merge(proxyNetworkMap) } - update = grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, peer, nil, nil, nmap, dnsDomain, postureChecks, dnsCache, account.Settings, extraSettings, maps.Keys(peerGroups), dnsFwdPort) + update = grpc.ToSyncResponse(ctx, nil, c.config.HttpConfig, c.config.DeviceAuthorizationFlow, types.TwinPeer(peer), nil, nil, nmap, dnsDomain, postureChecks, dnsCache, types.TwinAccountSettings(account.Settings), extraSettings, maps.Keys(peerGroups), dnsFwdPort) c.peersUpdateManager.SendUpdate(ctx, peer.ID, &network_map.UpdateMessage{ Update: update, @@ -908,20 +1164,36 @@ func (c *Controller) StartWarmup(ctx context.Context) { // computeForwarderPort checks if all peers in the account have updated to a specific version or newer. // If all peers have the required version, it returns the new well-known port (22054), otherwise returns 0. func computeForwarderPort(peers []*nbpeer.Peer, requiredVersion string) int64 { - if len(peers) == 0 { + versions := make([]string, 0, len(peers)) + for _, peer := range peers { + versions = append(versions, peer.Meta.WtVersion) + } + return computeForwarderPortFromVersions(versions, requiredVersion) +} + +func computeForwarderPortFromData(peers map[string]*nmdata.Peer, requiredVersion string) int64 { + versions := make([]string, 0, len(peers)) + for _, peer := range peers { + versions = append(versions, peer.Meta.WtVersion) + } + return computeForwarderPortFromVersions(versions, requiredVersion) +} + +func computeForwarderPortFromVersions(wtVersions []string, requiredVersion string) int64 { + if len(wtVersions) == 0 { return int64(network_map.OldForwarderPort) } reqVer := semver.Canonical(requiredVersion) // Check if all peers have the required version or newer - for _, peer := range peers { + for _, wtVersion := range wtVersions { // Development version is always supported - if version.IsDevelopmentVersion(peer.Meta.WtVersion) { + if version.IsDevelopmentVersion(wtVersion) { continue } - peerVersion := semver.Canonical("v" + peer.Meta.WtVersion) + peerVersion := semver.Canonical("v" + wtVersion) if peerVersion == "" { // If any peer doesn't have version info, return 0 return int64(network_map.OldForwarderPort) diff --git a/management/internals/network_map_db/pgsql/account_settings.go b/management/internals/network_map_db/pgsql/account_settings.go index 5aebbac0d..92364dc1e 100644 --- a/management/internals/network_map_db/pgsql/account_settings.go +++ b/management/internals/network_map_db/pgsql/account_settings.go @@ -3,6 +3,7 @@ package networkmap_pgsql import ( "context" "database/sql" + "encoding/json" "time" "github.com/jackc/pgx/v5" @@ -14,7 +15,14 @@ const ( select settings_peer_login_expiration_enabled as peer_login_expiration_enabled, settings_peer_login_expiration as peer_login_expiration, settings_peer_inactivity_expiration_enabled as peer_inactivity_expiration_enabled, - settings_peer_inactivity_expiration as peer_inactivity_expiration + settings_peer_inactivity_expiration as peer_inactivity_expiration, + settings_dns_domain as dns_domain, + settings_ipv6_enabled_groups as ipv6_enabled_groups, + settings_routing_peer_dns_resolution_enabled as routing_peer_dns_resolution_enabled, + settings_lazy_connection_enabled as lazy_connection_enabled, + settings_auto_update_version as auto_update_version, + settings_auto_update_always as auto_update_always, + settings_metrics_push_enabled as metrics_push_enabled from accounts where id=$1 ` @@ -40,12 +48,25 @@ func GetAccountSettingsViaPgxConnection(ctx context.Context, con *pgx.Conn, acco return nmdata.AccountSettingsInfo{}, err } - return nmdata.AccountSettingsInfo{ + settingsInfo := nmdata.AccountSettingsInfo{ PeerLoginExpirationEnabled: settings.PeerLoginExpirationEnabled.Bool, PeerLoginExpiration: time.Duration(settings.PeerLoginExpiration.Int64), - PeerInactivityExpirationEnabled: settings.PeerLoginExpirationEnabled.Bool, + PeerInactivityExpirationEnabled: settings.PeerInactivityExpirationEnabled.Bool, PeerInactivityExpiration: time.Duration(settings.PeerInactivityExpiration.Int64), - }, nil + DNSDomain: settings.DNSDomain.String, + RoutingPeerDNSResolutionEnabled: settings.RoutingPeerDNSResolutionEnabled.Bool, + LazyConnectionEnabled: settings.LazyConnectionEnabled.Bool, + AutoUpdateVersion: settings.AutoUpdateVersion.String, + AutoUpdateAlways: settings.AutoUpdateAlways.Bool, + MetricsPushEnabled: settings.MetricsPushEnabled.Bool, + } + if settings.IPv6EnabledGroups != nil { + if err := json.Unmarshal(settings.IPv6EnabledGroups, &settingsInfo.IPv6EnabledGroups); err != nil { + return nmdata.AccountSettingsInfo{}, err + } + } + + return settingsInfo, nil } type account struct { @@ -53,4 +74,11 @@ type account struct { PeerLoginExpiration sql.NullInt64 PeerInactivityExpirationEnabled sql.NullBool PeerInactivityExpiration sql.NullInt64 + DNSDomain sql.NullString + IPv6EnabledGroups json.RawMessage + RoutingPeerDNSResolutionEnabled sql.NullBool + LazyConnectionEnabled sql.NullBool + AutoUpdateVersion sql.NullString + AutoUpdateAlways sql.NullBool + MetricsPushEnabled sql.NullBool } diff --git a/management/internals/network_map_db/pgsql/peer.go b/management/internals/network_map_db/pgsql/peer.go index 7d62df4ba..2676097a6 100644 --- a/management/internals/network_map_db/pgsql/peer.go +++ b/management/internals/network_map_db/pgsql/peer.go @@ -13,9 +13,9 @@ import ( const ( GetPeersQuery = ` - select id, key, ssh_key, dns_label, user_id, ssh_enabled, login_expiration_enabled, last_login, ip, ipv6, + select id, key, ssh_key, dns_label, extra_dns_labels, user_id, ssh_enabled, login_expiration_enabled, last_login, ip, ipv6, peer_status_requires_approval, proxy_meta_embedded, proxy_meta_cluster, - meta_wt_version, meta_go_os, meta_os_version, meta_kernel_version, meta_network_addresses, meta_files, meta_capabilities, meta_flags, + meta_wt_version, meta_go_os, meta_os_version, meta_kernel_version, meta_network_addresses, meta_files, meta_capabilities, meta_flags, meta_sync_message_version, location_country_code, location_city_name, location_connection_ip from peers where account_id = $1 @@ -60,6 +60,9 @@ func GetPeersViaPgxConnection(ctx context.Context, con *pgx.Conn, accountId stri if p.MetaWtVersion.Valid { dp.Meta.WtVersion = p.MetaWtVersion.String } + if p.MetaSyncMessageVersion.Valid { + dp.Meta.SyncMessageVersion = int(p.MetaSyncMessageVersion.Int64) + } if p.MetaGoOS.Valid { dp.Meta.GoOS = p.MetaGoOS.String } @@ -105,6 +108,8 @@ func GetPeersViaPgxConnection(ctx context.Context, con *pgx.Conn, accountId stri return toret, nil, err } } + + toret = append(toret, dp) } return toret, clusterToPeerIdx, nil @@ -116,6 +121,7 @@ type peer struct { Key sql.NullString SSHKey sql.NullString DNSLabel sql.NullString + ExtraDNSLabels json.RawMessage UserID sql.NullString LastLogin sql.NullTime SSHEnabled sql.NullBool @@ -134,6 +140,7 @@ type peer struct { MetaGoOS sql.NullString `nmap:"skip"` MetaOSVersion sql.NullString `nmap:"skip"` MetaKernelVersion sql.NullString `nmap:"skip"` + MetaSyncMessageVersion sql.NullInt64 `nmap:"skip"` LocationCountryCode sql.NullString `nmap:"skip"` LocationCityName sql.NullString `nmap:"skip"` } diff --git a/management/internals/server/boot.go b/management/internals/server/boot.go index 1c78af9d0..5bc755754 100644 --- a/management/internals/server/boot.go +++ b/management/internals/server/boot.go @@ -7,6 +7,7 @@ import ( "crypto/tls" "net/http" "net/netip" + "os" "slices" "time" @@ -24,13 +25,15 @@ import ( "github.com/netbirdio/netbird/encryption" "github.com/netbirdio/netbird/formatter/hook" + "github.com/netbirdio/netbird/management/internals/modules/agentnetwork" "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/accesslogs" accesslogsmanager "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/accesslogs/manager" rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service" + networkmapdb "github.com/netbirdio/netbird/management/internals/network_map_db" + networkmap_pgsql "github.com/netbirdio/netbird/management/internals/network_map_db/pgsql" nbgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc" "github.com/netbirdio/netbird/management/server/activity" activitystore "github.com/netbirdio/netbird/management/server/activity/store" - "github.com/netbirdio/netbird/management/internals/modules/agentnetwork" nbcache "github.com/netbirdio/netbird/management/server/cache" nbContext "github.com/netbirdio/netbird/management/server/context" nbhttp "github.com/netbirdio/netbird/management/server/http" @@ -99,6 +102,22 @@ func (s *BaseServer) Store() store.Store { }) } +func (s *BaseServer) NetworkMapStore() networkmapdb.NetworkMapDBStore { + return Create(s, func() networkmapdb.NetworkMapDBStore { + dsn := os.Getenv("NETBIRD_NMAP_STORE_DSN") // Todo: this needs to be hoocked up properly + if dsn == "" { + return nil + } + + store, err := networkmap_pgsql.NewPostgresqlStore(context.Background(), dsn) + if err != nil { + log.Fatalf("failed to create network map store: %v", err) + } + + return store + }) +} + func (s *BaseServer) EventStore() activity.Store { return Create(s, func() activity.Store { var err error diff --git a/management/internals/server/controllers.go b/management/internals/server/controllers.go index 1b2556809..a9293d266 100644 --- a/management/internals/server/controllers.go +++ b/management/internals/server/controllers.go @@ -123,7 +123,7 @@ func (s *BaseServer) EphemeralManager() ephemeral.Manager { func (s *BaseServer) NetworkMapController() network_map.Controller { return Create(s, func() network_map.Controller { - return nmapcontroller.NewController(context.Background(), s.Store(), s.Metrics(), s.PeersUpdateManager(), s.AccountRequestBuffer(), s.IntegratedValidator(), s.SettingsManager(), s.DNSDomain(), s.ProxyController(), s.EphemeralManager(), s.Config) + return nmapcontroller.NewController(context.Background(), s.Store(), s.Metrics(), s.PeersUpdateManager(), s.AccountRequestBuffer(), s.IntegratedValidator(), s.SettingsManager(), s.DNSDomain(), s.ProxyController(), s.EphemeralManager(), s.Config, s.NetworkMapStore()) }) } diff --git a/management/internals/shared/grpc/components_envelope_response.go b/management/internals/shared/grpc/components_envelope_response.go index cb6a80ed9..4d8cab732 100644 --- a/management/internals/shared/grpc/components_envelope_response.go +++ b/management/internals/shared/grpc/components_envelope_response.go @@ -7,7 +7,6 @@ import ( "github.com/netbirdio/netbird/client/ssh/auth" nbconfig "github.com/netbirdio/netbird/management/internals/server/config" - nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/management/server/types" sharedgrpc "github.com/netbirdio/netbird/shared/management/grpc" @@ -32,14 +31,14 @@ func ToComponentSyncResponse( config *nbconfig.Config, httpConfig *nbconfig.HttpServerConfig, deviceFlowConfig *nbconfig.DeviceAuthorizationFlow, - peer *nbpeer.Peer, + peer *nmdata.Peer, turnCredentials *Token, relayCredentials *Token, components *types.NetworkMapComponents, proxyPatch *types.NetworkMap, dnsName string, checks []*posture.Checks, - settings *types.Settings, + settings *nmdata.AccountSettingsInfo, extraSettings *types.ExtraSettings, peerGroups []string, dnsFwdPort int64, @@ -146,7 +145,7 @@ func toProxyPatch(nm *types.NetworkMap, dnsName string, includeIPv6, useSourcePr // // The full SSH AuthorizedUsers map is still produced by the client when it // runs Calculate() over the envelope. -func computeSSHEnabledForPeer(c *types.NetworkMapComponents, peer *nbpeer.Peer) bool { +func computeSSHEnabledForPeer(c *types.NetworkMapComponents, peer *nmdata.Peer) bool { if c == nil || peer == nil { return false } @@ -171,7 +170,7 @@ func computeSSHEnabledForPeer(c *types.NetworkMapComponents, peer *nbpeer.Peer) // ruleEnablesSSHForPeer returns true when rule is active, targets peer, and // either explicitly authorises SSH or covers the legacy TCP/22 path while the // peer itself has SSH enabled locally. -func ruleEnablesSSHForPeer(c *types.NetworkMapComponents, rule *nmdata.PolicyRule, peer *nbpeer.Peer) bool { +func ruleEnablesSSHForPeer(c *types.NetworkMapComponents, rule *nmdata.PolicyRule, peer *nmdata.Peer) bool { if rule == nil || !rule.Enabled { return false } diff --git a/management/internals/shared/grpc/components_envelope_response_test.go b/management/internals/shared/grpc/components_envelope_response_test.go index 34ef66fdf..039cb73f4 100644 --- a/management/internals/shared/grpc/components_envelope_response_test.go +++ b/management/internals/shared/grpc/components_envelope_response_test.go @@ -5,7 +5,6 @@ import ( "github.com/stretchr/testify/assert" - nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/shared/management/networkmap/nmdata" ) @@ -18,10 +17,10 @@ func TestComputeSSHEnabledForPeer(t *testing.T) { const targetPeerID = "target" const targetGroupID = "g_dst" - mkComponents := func(rule *nmdata.PolicyRule, sshEnabled bool) (*types.NetworkMapComponents, *nbpeer.Peer) { - peer := &nbpeer.Peer{ID: targetPeerID, SSHEnabled: sshEnabled} + mkComponents := func(rule *nmdata.PolicyRule, sshEnabled bool) (*types.NetworkMapComponents, *nmdata.Peer) { + peer := &nmdata.Peer{ID: targetPeerID, SSHEnabled: sshEnabled} return &types.NetworkMapComponents{ - Peers: map[string]*nmdata.Peer{targetPeerID: types.TwinPeer(peer)}, + Peers: map[string]*nmdata.Peer{targetPeerID: peer}, Groups: map[string]*nmdata.Group{targetGroupID: {Name: "dst", Peers: []string{targetPeerID}}}, Policies: []*nmdata.Policy{{ ID: "p", @@ -156,7 +155,7 @@ func TestComputeSSHEnabledForPeer(t *testing.T) { // belt-and-suspenders presence guard mirroring Calculate's // getAllPeersFromGroups invariant. func TestComputeSSHEnabledForPeer_TargetMissingFromComponents(t *testing.T) { - peer := &nbpeer.Peer{ID: "missing", SSHEnabled: true} + peer := &nmdata.Peer{ID: "missing", SSHEnabled: true} c := &types.NetworkMapComponents{ Peers: map[string]*nmdata.Peer{}, // target peer NOT present Groups: map[string]*nmdata.Group{ @@ -179,6 +178,6 @@ func TestComputeSSHEnabledForPeer_TargetMissingFromComponents(t *testing.T) { // exported indirectly via ToComponentSyncResponse and may receive nil // components on graceful-degrade paths. func TestComputeSSHEnabledForPeer_NilInputs(t *testing.T) { - assert.False(t, computeSSHEnabledForPeer(nil, &nbpeer.Peer{ID: "x"})) + assert.False(t, computeSSHEnabledForPeer(nil, &nmdata.Peer{ID: "x"})) assert.False(t, computeSSHEnabledForPeer(&types.NetworkMapComponents{}, nil)) } diff --git a/management/internals/shared/grpc/conversion.go b/management/internals/shared/grpc/conversion.go index debeb8482..9a27e88f3 100644 --- a/management/internals/shared/grpc/conversion.go +++ b/management/internals/shared/grpc/conversion.go @@ -18,7 +18,6 @@ import ( "github.com/netbirdio/netbird/management/internals/controllers/network_map/controller/cache" nbconfig "github.com/netbirdio/netbird/management/internals/server/config" - nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/shared/management/networkmap" @@ -48,7 +47,7 @@ func init() { // nil when no server config is set (the fan-out network-map path) because clients treat any // non-nil config as authoritative: a config without a relay section is interpreted as relay // disabled and wipes the clients' relay URLs. -func toNetbirdConfig(config *nbconfig.Config, turnCredentials *Token, relayToken *Token, extraSettings *types.ExtraSettings, settings *types.Settings) *proto.NetbirdConfig { +func toNetbirdConfig(config *nbconfig.Config, turnCredentials *Token, relayToken *Token, extraSettings *types.ExtraSettings, settings *nmdata.AccountSettingsInfo) *proto.NetbirdConfig { if config == nil { return nil } @@ -120,7 +119,7 @@ func toNetbirdConfig(config *nbconfig.Config, turnCredentials *Token, relayToken return nbConfig } -func toPeerConfig(peer *nbpeer.Peer, network *nmdata.Network, dnsName string, settings *types.Settings, httpConfig *nbconfig.HttpServerConfig, deviceFlowConfig *nbconfig.DeviceAuthorizationFlow, enableSSH bool, forceRoutingPeerDNS bool) *proto.PeerConfig { +func toPeerConfig(peer *nmdata.Peer, network *nmdata.Network, dnsName string, settings *nmdata.AccountSettingsInfo, httpConfig *nbconfig.HttpServerConfig, deviceFlowConfig *nbconfig.DeviceAuthorizationFlow, enableSSH bool, forceRoutingPeerDNS bool) *proto.PeerConfig { netmask, _ := network.Net.Mask.Size() fqdn := peer.FQDN(dnsName) @@ -155,7 +154,7 @@ func toPeerConfig(peer *nbpeer.Peer, network *nmdata.Network, dnsName string, se return peerConfig } -func ToSyncResponse(ctx context.Context, config *nbconfig.Config, httpConfig *nbconfig.HttpServerConfig, deviceFlowConfig *nbconfig.DeviceAuthorizationFlow, peer *nbpeer.Peer, turnCredentials *Token, relayCredentials *Token, networkMap *types.NetworkMap, dnsName string, checks []*posture.Checks, dnsCache *cache.DNSConfigCache, settings *types.Settings, extraSettings *types.ExtraSettings, peerGroups []string, dnsFwdPort int64) *proto.SyncResponse { +func ToSyncResponse(ctx context.Context, config *nbconfig.Config, httpConfig *nbconfig.HttpServerConfig, deviceFlowConfig *nbconfig.DeviceAuthorizationFlow, peer *nmdata.Peer, turnCredentials *Token, relayCredentials *Token, networkMap *types.NetworkMap, dnsName string, checks []*posture.Checks, dnsCache *cache.DNSConfigCache, settings *nmdata.AccountSettingsInfo, extraSettings *types.ExtraSettings, peerGroups []string, dnsFwdPort int64) *proto.SyncResponse { // IPv6 data in AllowedIPs and SourcePrefixes wildcard expansion depends on // whether the target peer supports IPv6. Routes and firewall rules are already // filtered at the source (network map builder). diff --git a/management/internals/shared/grpc/conversion_test.go b/management/internals/shared/grpc/conversion_test.go index 402b4fd07..834063040 100644 --- a/management/internals/shared/grpc/conversion_test.go +++ b/management/internals/shared/grpc/conversion_test.go @@ -276,7 +276,7 @@ func TestToNetbirdConfig_RelayInvariant(t *testing.T) { settings := &types.Settings{MetricsPushEnabled: true} t.Run("nil server config returns nil config", func(t *testing.T) { - nbCfg := toNetbirdConfig(nil, nil, nil, nil, settings) + nbCfg := toNetbirdConfig(nil, nil, nil, nil, types.TwinAccountSettings(settings)) assert.Nil(t, nbCfg, "fan-out updates must not carry a partial NetbirdConfig even when settings are present") }) @@ -291,7 +291,7 @@ func TestToNetbirdConfig_RelayInvariant(t *testing.T) { } relayToken := &Token{Payload: "token-payload", Signature: "token-signature"} - nbCfg := toNetbirdConfig(cfg, nil, relayToken, nil, settings) + nbCfg := toNetbirdConfig(cfg, nil, relayToken, nil, types.TwinAccountSettings(settings)) require.NotNil(t, nbCfg) require.NotNil(t, nbCfg.Relay, "non-nil NetbirdConfig must include the relay section") assert.Equal(t, cfg.Relay.Addresses, nbCfg.Relay.Urls, "relay URLs should match the server config") diff --git a/management/internals/shared/grpc/server.go b/management/internals/shared/grpc/server.go index a43316ca6..220f95d6e 100644 --- a/management/internals/shared/grpc/server.go +++ b/management/internals/shared/grpc/server.go @@ -920,8 +920,8 @@ func (s *Server) prepareLoginResponse(ctx context.Context, peer *nbpeer.Peer, ne // if peer has reached this point then it has logged in loginResp := &proto.LoginResponse{ - NetbirdConfig: toNetbirdConfig(s.config, nil, relayToken, nil, settings), - PeerConfig: toPeerConfig(peer, types.TwinNetwork(network), s.networkMapController.GetDNSDomain(settings), settings, s.config.HttpConfig, s.config.DeviceAuthorizationFlow, enableSSH, false), + NetbirdConfig: toNetbirdConfig(s.config, nil, relayToken, nil, types.TwinAccountSettings(settings)), + PeerConfig: toPeerConfig(types.TwinPeer(peer), types.TwinNetwork(network), s.networkMapController.GetDNSDomain(settings), types.TwinAccountSettings(settings), s.config.HttpConfig, s.config.DeviceAuthorizationFlow, enableSSH, false), Checks: toProtocolChecks(ctx, postureChecks), } @@ -1052,9 +1052,9 @@ func (s *Server) sendInitialSync(ctx context.Context, peerKey wgtypes.Key, peer log.WithContext(ctx).Errorf("failed to build components for peer %s on initial sync: %v", peer.ID, err) return status.Errorf(codes.Internal, "failed to build initial sync envelope") } - plainResp = ToComponentSyncResponse(ctx, s.config, s.config.HttpConfig, s.config.DeviceAuthorizationFlow, freshPeer, turnToken, relayToken, components, proxyPatch, dnsName, freshPostureChecks, settings, settings.Extra, peerGroups, freshDnsFwdPort) + plainResp = ToComponentSyncResponse(ctx, s.config, s.config.HttpConfig, s.config.DeviceAuthorizationFlow, types.TwinPeer(freshPeer), turnToken, relayToken, components, proxyPatch, dnsName, freshPostureChecks, types.TwinAccountSettings(settings), settings.Extra, peerGroups, freshDnsFwdPort) } else { - plainResp = ToSyncResponse(ctx, s.config, s.config.HttpConfig, s.config.DeviceAuthorizationFlow, peer, turnToken, relayToken, networkMap, dnsName, postureChecks, nil, settings, settings.Extra, peerGroups, dnsFwdPort) + plainResp = ToSyncResponse(ctx, s.config, s.config.HttpConfig, s.config.DeviceAuthorizationFlow, types.TwinPeer(peer), turnToken, relayToken, networkMap, dnsName, postureChecks, nil, types.TwinAccountSettings(settings), settings.Extra, peerGroups, dnsFwdPort) } key, err := s.secretsManager.GetWGKey() diff --git a/management/server/account_test.go b/management/server/account_test.go index 3c0bb25da..ab2f7d861 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -3330,7 +3330,7 @@ func createManager(t testing.TB) (*DefaultAccountManager, *update_channel.PeersU updateManager := update_channel.NewPeersUpdateManager(metrics) requestBuffer := NewAccountRequestBuffer(ctx, store) - networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peers.NewManager(store, permissionsManager)), &config.Config{}) + networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peers.NewManager(store, permissionsManager)), &config.Config{}, nil) manager, err := BuildManager(ctx, &config.Config{}, store, networkMapController, job.NewJobManager(nil, store, peersManager), nil, "", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore) if err != nil { return nil, nil, err diff --git a/management/server/dns_test.go b/management/server/dns_test.go index 8917902d9..496ca7a6b 100644 --- a/management/server/dns_test.go +++ b/management/server/dns_test.go @@ -234,7 +234,7 @@ func createDNSManager(t *testing.T) (*DefaultAccountManager, error) { updateManager := update_channel.NewPeersUpdateManager(metrics) requestBuffer := NewAccountRequestBuffer(ctx, store) - networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.test", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peers.NewManager(store, permissionsManager)), &config.Config{}) + networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.test", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peers.NewManager(store, permissionsManager)), &config.Config{}, nil) return BuildManager(context.Background(), nil, store, networkMapController, job.NewJobManager(nil, store, peersManager), nil, "", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore) } diff --git a/management/server/http/testing/testing_tools/channel/channel.go b/management/server/http/testing/testing_tools/channel/channel.go index 8b05b2ddf..44408d751 100644 --- a/management/server/http/testing/testing_tools/channel/channel.go +++ b/management/server/http/testing/testing_tools/channel/channel.go @@ -96,7 +96,7 @@ func BuildApiBlackBoxWithDBState(t testing_tools.TB, sqlFile string, expectedPee } requestBuffer := server.NewAccountRequestBuffer(ctx, store) - networkMapController := controller.NewController(ctx, store, metrics, peersUpdateManager, requestBuffer, server.MockIntegratedValidator{}, settingsManager, "", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peersManager), &config.Config{}) + networkMapController := controller.NewController(ctx, store, metrics, peersUpdateManager, requestBuffer, server.MockIntegratedValidator{}, settingsManager, "", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peersManager), &config.Config{}, nil) am, err := server.BuildManager(ctx, nil, store, networkMapController, jobManager, nil, "", &activity.InMemoryEventStore{}, geoMock, false, validatorMock, metrics, proxyController, settingsManager, permissionsManager, false, cacheStore) if err != nil { t.Fatalf("Failed to create manager: %v", err) @@ -226,7 +226,7 @@ func BuildApiBlackBoxWithDBStateAndPeerChannel(t testing_tools.TB, sqlFile strin } requestBuffer := server.NewAccountRequestBuffer(ctx, store) - networkMapController := controller.NewController(ctx, store, metrics, peersUpdateManager, requestBuffer, server.MockIntegratedValidator{}, settingsManager, "", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peersManager), &config.Config{}) + networkMapController := controller.NewController(ctx, store, metrics, peersUpdateManager, requestBuffer, server.MockIntegratedValidator{}, settingsManager, "", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peersManager), &config.Config{}, nil) am, err := server.BuildManager(ctx, nil, store, networkMapController, jobManager, nil, "", &activity.InMemoryEventStore{}, geoMock, false, validatorMock, metrics, proxyController, settingsManager, permissionsManager, false, cacheStore) if err != nil { t.Fatalf("Failed to create manager: %v", err) diff --git a/management/server/identity_provider_test.go b/management/server/identity_provider_test.go index d51254c55..8c5478a41 100644 --- a/management/server/identity_provider_test.go +++ b/management/server/identity_provider_test.go @@ -92,7 +92,7 @@ func createManagerWithEmbeddedIdP(t testing.TB) (*DefaultAccountManager, *update updateManager := update_channel.NewPeersUpdateManager(metrics) requestBuffer := NewAccountRequestBuffer(ctx, testStore) - networkMapController := controller.NewController(ctx, testStore, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(testStore, peersManager), &config.Config{}) + networkMapController := controller.NewController(ctx, testStore, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(testStore, peersManager), &config.Config{}, nil) manager, err := BuildManager(ctx, &config.Config{}, testStore, networkMapController, job.NewJobManager(nil, testStore, peersManager), idpManager, "", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore) if err != nil { return nil, nil, err diff --git a/management/server/management_proto_test.go b/management/server/management_proto_test.go index 45d4ab8c9..6a065bc7a 100644 --- a/management/server/management_proto_test.go +++ b/management/server/management_proto_test.go @@ -376,7 +376,7 @@ func startManagementForTest(t *testing.T, testFile string, config *config.Config return nil, nil, "", cleanup, err } - networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), ephemeralMgr, config) + networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), ephemeralMgr, config, nil) accountManager, err := BuildManager(ctx, nil, store, networkMapController, jobManager, nil, "", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore) diff --git a/management/server/management_test.go b/management/server/management_test.go index f1d49193c..8cfdf1d3c 100644 --- a/management/server/management_test.go +++ b/management/server/management_test.go @@ -216,7 +216,7 @@ func startServer( updateManager := update_channel.NewPeersUpdateManager(metrics) requestBuffer := server.NewAccountRequestBuffer(ctx, str) - networkMapController := controller.NewController(ctx, str, metrics, updateManager, requestBuffer, server.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(str, peers.NewManager(str, permissionsManager)), config) + networkMapController := controller.NewController(ctx, str, metrics, updateManager, requestBuffer, server.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(str, peers.NewManager(str, permissionsManager)), config, nil) accountManager, err := server.BuildManager( context.Background(), diff --git a/management/server/nameserver_test.go b/management/server/nameserver_test.go index e13b0bb19..ed63818fc 100644 --- a/management/server/nameserver_test.go +++ b/management/server/nameserver_test.go @@ -803,7 +803,7 @@ func createNSManager(t *testing.T) (*DefaultAccountManager, error) { updateManager := update_channel.NewPeersUpdateManager(metrics) requestBuffer := NewAccountRequestBuffer(ctx, store) - networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peers.NewManager(store, permissionsManager)), &config.Config{}) + networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peers.NewManager(store, permissionsManager)), &config.Config{}, nil) return BuildManager(context.Background(), nil, store, networkMapController, job.NewJobManager(nil, store, peersManager), nil, "", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore) } diff --git a/management/server/peer_test.go b/management/server/peer_test.go index 293144190..b7643acb9 100644 --- a/management/server/peer_test.go +++ b/management/server/peer_test.go @@ -1181,7 +1181,7 @@ func TestToSyncResponse(t *testing.T) { } dnsCache := &cache.DNSConfigCache{} accountSettings := &types.Settings{RoutingPeerDNSResolutionEnabled: true} - response := grpc.ToSyncResponse(context.Background(), config, config.HttpConfig, config.DeviceAuthorizationFlow, peer, turnRelayToken, turnRelayToken, networkMap, dnsName, checks, dnsCache, accountSettings, nil, []string{}, int64(dnsForwarderPort)) + response := grpc.ToSyncResponse(context.Background(), config, config.HttpConfig, config.DeviceAuthorizationFlow, types.TwinPeer(peer), turnRelayToken, turnRelayToken, networkMap, dnsName, checks, dnsCache, types.TwinAccountSettings(accountSettings), nil, []string{}, int64(dnsForwarderPort)) assert.NotNil(t, response) // assert peer config @@ -1301,7 +1301,7 @@ func Test_RegisterPeerByUser(t *testing.T) { updateManager := update_channel.NewPeersUpdateManager(metrics) requestBuffer := NewAccountRequestBuffer(ctx, s) - networkMapController := controller.NewController(ctx, s, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(s, peers.NewManager(s, permissionsManager)), &config.Config{}) + networkMapController := controller.NewController(ctx, s, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(s, peers.NewManager(s, permissionsManager)), &config.Config{}, nil) am, err := BuildManager(context.Background(), nil, s, networkMapController, job.NewJobManager(nil, s, peersManager), nil, "", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore) assert.NoError(t, err) @@ -1392,7 +1392,7 @@ func Test_RegisterPeerBySetupKey(t *testing.T) { updateManager := update_channel.NewPeersUpdateManager(metrics) requestBuffer := NewAccountRequestBuffer(ctx, s) - networkMapController := controller.NewController(ctx, s, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(s, peers.NewManager(s, permissionsManager)), &config.Config{}) + networkMapController := controller.NewController(ctx, s, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(s, peers.NewManager(s, permissionsManager)), &config.Config{}, nil) am, err := BuildManager(context.Background(), nil, s, networkMapController, job.NewJobManager(nil, s, peersManager), nil, "", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore) assert.NoError(t, err) @@ -1551,7 +1551,7 @@ func Test_RegisterPeerRollbackOnFailure(t *testing.T) { updateManager := update_channel.NewPeersUpdateManager(metrics) requestBuffer := NewAccountRequestBuffer(ctx, s) - networkMapController := controller.NewController(ctx, s, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(s, peers.NewManager(s, permissionsManager)), &config.Config{}) + networkMapController := controller.NewController(ctx, s, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(s, peers.NewManager(s, permissionsManager)), &config.Config{}, nil) am, err := BuildManager(context.Background(), nil, s, networkMapController, job.NewJobManager(nil, s, peersManager), nil, "", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore) assert.NoError(t, err) @@ -1636,7 +1636,7 @@ func Test_LoginPeer(t *testing.T) { updateManager := update_channel.NewPeersUpdateManager(metrics) requestBuffer := NewAccountRequestBuffer(ctx, s) - networkMapController := controller.NewController(ctx, s, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(s, peers.NewManager(s, permissionsManager)), &config.Config{}) + networkMapController := controller.NewController(ctx, s, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(s, peers.NewManager(s, permissionsManager)), &config.Config{}, nil) am, err := BuildManager(context.Background(), nil, s, networkMapController, job.NewJobManager(nil, s, peersManager), nil, "", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore) assert.NoError(t, err) diff --git a/management/server/route_test.go b/management/server/route_test.go index aeee64908..e765d5329 100644 --- a/management/server/route_test.go +++ b/management/server/route_test.go @@ -1299,7 +1299,7 @@ func createRouterManager(t *testing.T) (*DefaultAccountManager, *update_channel. updateManager := update_channel.NewPeersUpdateManager(metrics) requestBuffer := NewAccountRequestBuffer(ctx, store) - networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peers.NewManager(store, permissionsManager)), &config.Config{}) + networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peers.NewManager(store, permissionsManager)), &config.Config{}, nil) am, err := BuildManager(context.Background(), nil, store, networkMapController, job.NewJobManager(nil, store, peersManager), nil, "", eventStore, nil, false, MockIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager, false, cacheStore) if err != nil { diff --git a/management/server/types/account.go b/management/server/types/account.go index 74ea94865..54f1f21b0 100644 --- a/management/server/types/account.go +++ b/management/server/types/account.go @@ -9,7 +9,6 @@ import ( "strings" "time" - "github.com/hashicorp/go-multierror" "github.com/miekg/dns" "github.com/rs/xid" log "github.com/sirupsen/logrus" @@ -26,6 +25,8 @@ import ( "github.com/netbirdio/netbird/management/server/util" "github.com/netbirdio/netbird/route" "github.com/netbirdio/netbird/shared/management/domain" + "github.com/netbirdio/netbird/shared/management/networkmap" + "github.com/netbirdio/netbird/shared/management/networkmap/nmdata" "github.com/netbirdio/netbird/shared/management/status" ) @@ -380,94 +381,11 @@ func peerInDistributionGroups(peerGroups LookupMap, distributionGroups []string) } func (a *Account) GetPeersCustomZone(ctx context.Context, dnsDomain string) nbdns.CustomZone { - var merr *multierror.Error - - if dnsDomain == "" { - log.WithContext(ctx).Error("no dns domain is set, returning empty zone") - return nbdns.CustomZone{} + twins := make(map[string]*nmdata.Peer, len(a.Peers)) + for id, p := range a.Peers { + twins[id] = twinPeer(p) } - - customZone := nbdns.CustomZone{ - Domain: dns.Fqdn(dnsDomain), - Records: make([]nbdns.SimpleRecord, 0, len(a.Peers)), - } - - domainSuffix := "." + dnsDomain - - ipv6AllowedPeers := a.peerIPv6AllowedSet() - - var sb strings.Builder - for _, peer := range a.Peers { - if peer.DNSLabel == "" { - merr = multierror.Append(merr, fmt.Errorf("peer %s has an empty DNS label", peer.Name)) - continue - } - - sb.Grow(len(peer.DNSLabel) + len(domainSuffix)) - sb.WriteString(peer.DNSLabel) - sb.WriteString(domainSuffix) - - fqdn := sb.String() - customZone.Records = append(customZone.Records, nbdns.SimpleRecord{ - Name: fqdn, - Type: int(dns.TypeA), - Class: nbdns.DefaultClass, - TTL: defaultTTL, - RData: peer.IP.String(), - }) - // Only advertise AAAA for peers that have a valid IPv6, whose client supports it, - // and that belong to an IPv6-enabled group. Old clients don't configure v6 on their - // WireGuard interface, so resolving their AAAA causes connections to hang. - // Capability changes (client upgrade/downgrade, --disable-ipv6 toggle) propagate - // to other peers via SyncPeer/LoginPeer regardless of version change, so AAAA - // records refresh when a peer first reports the IPv6 overlay capability. - _, peerAllowed := ipv6AllowedPeers[peer.ID] - hasIPv6 := peer.IPv6.IsValid() && peer.SupportsIPv6() && peerAllowed - if hasIPv6 { - customZone.Records = append(customZone.Records, nbdns.SimpleRecord{ - Name: fqdn, - Type: int(dns.TypeAAAA), - Class: nbdns.DefaultClass, - TTL: defaultTTL, - RData: peer.IPv6.String(), - }) - } - sb.Reset() - - for _, extraLabel := range peer.ExtraDNSLabels { - sb.Grow(len(extraLabel) + len(domainSuffix)) - sb.WriteString(extraLabel) - sb.WriteString(domainSuffix) - - extraFqdn := sb.String() - customZone.Records = append(customZone.Records, nbdns.SimpleRecord{ - Name: extraFqdn, - Type: int(dns.TypeA), - Class: nbdns.DefaultClass, - TTL: defaultTTL, - RData: peer.IP.String(), - }) - if hasIPv6 { - customZone.Records = append(customZone.Records, nbdns.SimpleRecord{ - Name: extraFqdn, - Type: int(dns.TypeAAAA), - Class: nbdns.DefaultClass, - TTL: defaultTTL, - RData: peer.IPv6.String(), - }) - } - sb.Reset() - } - - } - - go func() { - if merr != nil { - log.WithContext(ctx).Errorf("error generating custom zone for account %s: %v", a.Id, merr) - } - }() - - return customZone + return fromTwinCustomZone(networkmap.PeersCustomZone(ctx, a.Id, dnsDomain, twins, a.peerIPv6AllowedSet())) } // GetExpiredPeers returns peers that have been expired diff --git a/management/server/types/account_components.go b/management/server/types/account_components.go index f42504094..1a4f87ef0 100644 --- a/management/server/types/account_components.go +++ b/management/server/types/account_components.go @@ -104,7 +104,7 @@ func (a *Account) GetPeerNetworkMapComponents( groupIDToUserIDs map[string][]string, ) *NetworkMapComponents { nmd := a.toNetworkMapData(accountZones, validatedPeersMap, resourcePolicies, routers, groupIDToUserIDs) - components := nmd.GetPeerNetworkMapComponents(peerID, toTwinCustomZone(peersCustomZone)) + components := nmd.GetPeerNetworkMapComponents(peerID, TwinCustomZone(peersCustomZone)) if components != nil { components.ForceRoutingPeerDNSResolution = a.forcesRoutingPeerDNSResolution(peerID, routers) } diff --git a/management/server/types/account_networkmapdata.go b/management/server/types/account_networkmapdata.go index 2681f1270..1613628da 100644 --- a/management/server/types/account_networkmapdata.go +++ b/management/server/types/account_networkmapdata.go @@ -45,14 +45,7 @@ func (a *Account) toNetworkMapData( nmd.Network = TwinNetwork(a.Network) } nmd.DNSSettings = &nmdata.DNSSettings{DisabledManagementGroups: a.DNSSettings.DisabledManagementGroups} - if a.Settings != nil { - nmd.AccountSettings = &nmdata.AccountSettingsInfo{ - PeerLoginExpirationEnabled: a.Settings.PeerLoginExpirationEnabled, - PeerLoginExpiration: a.Settings.PeerLoginExpiration, - PeerInactivityExpirationEnabled: a.Settings.PeerInactivityExpirationEnabled, - PeerInactivityExpiration: a.Settings.PeerInactivityExpiration, - } - } + nmd.AccountSettings = TwinAccountSettings(a.Settings) for id, p := range a.Peers { nmd.Peers[id] = twinPeer(p) @@ -145,15 +138,17 @@ func twinPeer(p *nbpeer.Peer) *nmdata.Peer { IP: p.IP, IPv6: p.IPv6, RequiresApproval: p.Status != nil && p.Status.RequiresApproval, + ExtraDNSLabels: p.ExtraDNSLabels, ProxyMeta: nmdata.ProxyMeta{Embedded: p.ProxyMeta.Embedded}, Meta: nmdata.PeerSystemMeta{ - WtVersion: p.Meta.WtVersion, - GoOS: p.Meta.GoOS, - OSVersion: p.Meta.OSVersion, - KernelVersion: p.Meta.KernelVersion, - NetworkAddresses: networkAddresses, - Files: files, - Capabilities: p.Meta.Capabilities, + WtVersion: p.Meta.WtVersion, + GoOS: p.Meta.GoOS, + OSVersion: p.Meta.OSVersion, + KernelVersion: p.Meta.KernelVersion, + NetworkAddresses: networkAddresses, + Files: files, + Capabilities: p.Meta.Capabilities, + SyncMessageVersion: p.Meta.SyncMessageVersion, Flags: nmdata.Flags{ ServerSSHAllowed: p.Meta.Flags.ServerSSHAllowed, DisableIPv6: p.Meta.Flags.DisableIPv6, @@ -508,7 +503,50 @@ func (a *Account) buildPrivateServiceCandidates() []networkmap.PrivateServiceCan return out } -func toTwinCustomZone(z nbdns.CustomZone) nmdata.CustomZone { +// TwinAccountSettings converts real account settings to the slim nmdata twin. +// Exported for callers of the twin-based sync response builders. +func TwinAccountSettings(s *Settings) *nmdata.AccountSettingsInfo { + if s == nil { + return nil + } + return &nmdata.AccountSettingsInfo{ + PeerLoginExpirationEnabled: s.PeerLoginExpirationEnabled, + PeerLoginExpiration: s.PeerLoginExpiration, + PeerInactivityExpirationEnabled: s.PeerInactivityExpirationEnabled, + PeerInactivityExpiration: s.PeerInactivityExpiration, + DNSDomain: s.DNSDomain, + IPv6EnabledGroups: s.IPv6EnabledGroups, + RoutingPeerDNSResolutionEnabled: s.RoutingPeerDNSResolutionEnabled, + LazyConnectionEnabled: s.LazyConnectionEnabled, + AutoUpdateVersion: s.AutoUpdateVersion, + AutoUpdateAlways: s.AutoUpdateAlways, + MetricsPushEnabled: s.MetricsPushEnabled, + } +} + +func fromTwinCustomZone(z nmdata.CustomZone) nbdns.CustomZone { + records := make([]nbdns.SimpleRecord, 0, len(z.Records)) + for _, r := range z.Records { + records = append(records, nbdns.SimpleRecord{ + Name: r.Name, + Type: r.Type, + Class: r.Class, + TTL: r.TTL, + RData: r.RData, + }) + } + return nbdns.CustomZone{ + Domain: z.Domain, + Records: records, + SearchDomainDisabled: z.SearchDomainDisabled, + NonAuthoritative: z.NonAuthoritative, + } +} + +// TwinCustomZone converts a real DNS custom zone to its slim nmdata twin. +// Exported for the network-map controller's DB-store path, which feeds real +// zones into the twin-based components calculation. +func TwinCustomZone(z nbdns.CustomZone) nmdata.CustomZone { records := make([]nmdata.SimpleRecord, 0, len(z.Records)) for _, r := range z.Records { records = append(records, nmdata.SimpleRecord{ diff --git a/management/server/types/networkmap_wire_benchmark_test.go b/management/server/types/networkmap_wire_benchmark_test.go index ee9839a3f..ccec054cd 100644 --- a/management/server/types/networkmap_wire_benchmark_test.go +++ b/management/server/types/networkmap_wire_benchmark_test.go @@ -66,7 +66,7 @@ func BenchmarkNetworkMapWireEncode(b *testing.B) { // Pre-encode once so the size metric is identical for every run inside // the same scale; the b.Loop call only re-runs encode + Marshal. - legacyResp := mgmtgrpc.ToSyncResponse(ctx, nil, nil, nil, peer, nil, nil, networkMap, "netbird.cloud", nil, dnsCache, settings, nil, nil, 0) + legacyResp := mgmtgrpc.ToSyncResponse(ctx, nil, nil, nil, types.TwinPeer(peer), nil, nil, networkMap, "netbird.cloud", nil, dnsCache, types.TwinAccountSettings(settings), nil, nil, 0) legacyBytes, err := goproto.Marshal(legacyResp.NetworkMap) if err != nil { b.Fatalf("marshal legacy networkmap: %v", err) @@ -88,7 +88,7 @@ func BenchmarkNetworkMapWireEncode(b *testing.B) { b.ReportMetric(float64(len(legacyBytes)), "bytes/msg") b.ResetTimer() for range b.N { - resp := mgmtgrpc.ToSyncResponse(ctx, nil, nil, nil, peer, nil, nil, networkMap, "netbird.cloud", nil, dnsCache, settings, nil, nil, 0) + resp := mgmtgrpc.ToSyncResponse(ctx, nil, nil, nil, types.TwinPeer(peer), nil, nil, networkMap, "netbird.cloud", nil, dnsCache, types.TwinAccountSettings(settings), nil, nil, 0) if _, err := goproto.Marshal(resp.NetworkMap); err != nil { b.Fatal(err) } @@ -135,7 +135,7 @@ func BenchmarkNetworkMapWireSize(b *testing.B) { dnsCache := &cache.DNSConfigCache{} settings := &types.Settings{} - legacyResp := mgmtgrpc.ToSyncResponse(ctx, nil, nil, nil, peer, nil, nil, networkMap, "netbird.cloud", nil, dnsCache, settings, nil, nil, 0) + legacyResp := mgmtgrpc.ToSyncResponse(ctx, nil, nil, nil, types.TwinPeer(peer), nil, nil, networkMap, "netbird.cloud", nil, dnsCache, types.TwinAccountSettings(settings), nil, nil, 0) legacyBytes, err := goproto.Marshal(legacyResp.NetworkMap) if err != nil { b.Fatalf("marshal legacy networkmap: %v", err) diff --git a/management/server/types/networkmap_wire_breakdown_test.go b/management/server/types/networkmap_wire_breakdown_test.go index ac2855fa3..adf66b386 100644 --- a/management/server/types/networkmap_wire_breakdown_test.go +++ b/management/server/types/networkmap_wire_breakdown_test.go @@ -45,7 +45,7 @@ func TestNetworkMapWireBreakdown(t *testing.T) { dnsCache := &cache.DNSConfigCache{} settings := &types.Settings{} - legacyResp := mgmtgrpc.ToSyncResponse(ctx, nil, nil, nil, peer, nil, nil, networkMap, "netbird.cloud", nil, dnsCache, settings, nil, nil, 0) + legacyResp := mgmtgrpc.ToSyncResponse(ctx, nil, nil, nil, types.TwinPeer(peer), nil, nil, networkMap, "netbird.cloud", nil, dnsCache, types.TwinAccountSettings(settings), nil, nil, 0) legacyTotal := mustMarshalSize(t, legacyResp.NetworkMap) envelope := mgmtgrpc.EncodeNetworkMapEnvelope(mgmtgrpc.ComponentsEnvelopeInput{ diff --git a/shared/management/client/client_test.go b/shared/management/client/client_test.go index 570de7631..2ac2c5f35 100644 --- a/shared/management/client/client_test.go +++ b/shared/management/client/client_test.go @@ -126,7 +126,7 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) { updateManager := update_channel.NewPeersUpdateManager(metrics) requestBuffer := mgmt.NewAccountRequestBuffer(ctx, store) - networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, mgmt.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peersManger), config) + networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, mgmt.MockIntegratedValidator{}, settingsMockManager, "netbird.selfhosted", port_forwarding.NewControllerMock(), ephemeral_manager.NewEphemeralManager(store, peersManger), config, nil) accountManager, err := mgmt.BuildManager(context.Background(), config, store, networkMapController, jobManager, nil, "", eventStore, nil, false, ia, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManagerMock, false, cacheStore) if err != nil { t.Fatal(err) diff --git a/shared/management/networkmap/networkmapcompute.go b/shared/management/networkmap/networkmapcompute.go index f697305a4..c5f126228 100644 --- a/shared/management/networkmap/networkmapcompute.go +++ b/shared/management/networkmap/networkmapcompute.go @@ -65,7 +65,7 @@ func (nmd *NetworkMapData) GetPeerNetworkMapComponents(peerID string, peersCusto components.Routes = relevantRoutes components.AllDNSRecords = filterDNSRecordsByPeers(peersCustomZone.Records, relevantPeers, peer.SupportsIPv6() && peer.IPv6.IsValid()) - peerGroups := nmd.getPeerGroups(peerID) + peerGroups := nmd.GetPeerGroups(peerID) components.AccountZones = nmd.appliedZones(peerGroups) components.AccountZones = append(components.AccountZones, nmd.privateServiceZones(peerGroups)...) @@ -473,7 +473,7 @@ func (nmd *NetworkMapData) getPostureValidPeersSaveFailed(inputPeers []string, p return dest } -func (nmd *NetworkMapData) getPeerGroups(peerID string) map[string]struct{} { +func (nmd *NetworkMapData) GetPeerGroups(peerID string) map[string]struct{} { groups := make(map[string]struct{}) for groupID, group := range nmd.Groups { if slices.Contains(group.Peers, peerID) { diff --git a/shared/management/networkmap/nmdata/account_settings.go b/shared/management/networkmap/nmdata/account_settings.go index fefc3d81c..57e29e838 100644 --- a/shared/management/networkmap/nmdata/account_settings.go +++ b/shared/management/networkmap/nmdata/account_settings.go @@ -8,4 +8,11 @@ type AccountSettingsInfo struct { PeerLoginExpiration time.Duration PeerInactivityExpirationEnabled bool PeerInactivityExpiration time.Duration + DNSDomain string + IPv6EnabledGroups []string + RoutingPeerDNSResolutionEnabled bool + LazyConnectionEnabled bool + AutoUpdateVersion string + AutoUpdateAlways bool + MetricsPushEnabled bool } diff --git a/shared/management/networkmap/nmdata/peer.go b/shared/management/networkmap/nmdata/peer.go index 46a98a0c5..b50655c11 100644 --- a/shared/management/networkmap/nmdata/peer.go +++ b/shared/management/networkmap/nmdata/peer.go @@ -25,6 +25,7 @@ type Peer struct { IP netip.Addr IPv6 netip.Addr RequiresApproval bool + ExtraDNSLabels []string Meta PeerSystemMeta ProxyMeta ProxyMeta Location PeerLocation @@ -37,14 +38,15 @@ type ProxyMeta struct { // PeerSystemMeta is the slim twin of peer.PeerSystemMeta. type PeerSystemMeta struct { - WtVersion string - GoOS string - OSVersion string - KernelVersion string - NetworkAddresses []NetworkAddress - Files []File - Capabilities []int32 - Flags Flags + WtVersion string + GoOS string + OSVersion string + KernelVersion string + NetworkAddresses []NetworkAddress + Files []File + Capabilities []int32 + Flags Flags + SyncMessageVersion int } // Flags is the slim twin of peer.Flags. @@ -101,6 +103,18 @@ func (p *Peer) GetLastLogin() time.Time { return time.Time{} } +// SessionExpiresAt mirrors peer.Peer.SessionExpiresAt. +func (p *Peer) SessionExpiresAt(accountExpirationEnabled bool, expiresIn time.Duration) time.Time { + if !accountExpirationEnabled || !p.AddedWithSSOLogin() || !p.LoginExpirationEnabled { + return time.Time{} + } + last := p.GetLastLogin() + if last.IsZero() { + return time.Time{} + } + return last.Add(expiresIn).UTC() +} + func (p *Peer) LoginExpired(expiresIn time.Duration) (bool, time.Duration) { if !p.AddedWithSSOLogin() || !p.LoginExpirationEnabled { return false, 0 diff --git a/shared/management/networkmap/peers_custom_zone.go b/shared/management/networkmap/peers_custom_zone.go new file mode 100644 index 000000000..063844358 --- /dev/null +++ b/shared/management/networkmap/peers_custom_zone.go @@ -0,0 +1,111 @@ +package networkmap + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/go-multierror" + "github.com/miekg/dns" + log "github.com/sirupsen/logrus" + + nbdns "github.com/netbirdio/netbird/dns" + "github.com/netbirdio/netbird/shared/management/networkmap/nmdata" +) + +const peersZoneRecordTTL = 300 + +// PeersCustomZone builds the peers DNS zone from twin peer rows. It is the +// single source of the zone-record logic; Account.GetPeersCustomZone delegates +// here via twins. +func PeersCustomZone(ctx context.Context, accountID string, dnsDomain string, peers map[string]*nmdata.Peer, ipv6AllowedPeers map[string]struct{}) nmdata.CustomZone { + var merr *multierror.Error + + if dnsDomain == "" { + log.WithContext(ctx).Error("no dns domain is set, returning empty zone") + return nmdata.CustomZone{} + } + + customZone := nmdata.CustomZone{ + Domain: dns.Fqdn(dnsDomain), + Records: make([]nmdata.SimpleRecord, 0, len(peers)), + } + + domainSuffix := "." + dnsDomain + + var sb strings.Builder + for _, peer := range peers { + if peer == nil { + continue + } + if peer.DNSLabel == "" { + merr = multierror.Append(merr, fmt.Errorf("peer %s has an empty DNS label", peer.ID)) + continue + } + + sb.Grow(len(peer.DNSLabel) + len(domainSuffix)) + sb.WriteString(peer.DNSLabel) + sb.WriteString(domainSuffix) + + fqdn := sb.String() + customZone.Records = append(customZone.Records, nmdata.SimpleRecord{ + Name: fqdn, + Type: int(dns.TypeA), + Class: nbdns.DefaultClass, + TTL: peersZoneRecordTTL, + RData: peer.IP.String(), + }) + // Only advertise AAAA for peers that have a valid IPv6, whose client supports it, + // and that belong to an IPv6-enabled group. Old clients don't configure v6 on their + // WireGuard interface, so resolving their AAAA causes connections to hang. + // Capability changes (client upgrade/downgrade, --disable-ipv6 toggle) propagate + // to other peers via SyncPeer/LoginPeer regardless of version change, so AAAA + // records refresh when a peer first reports the IPv6 overlay capability. + _, peerAllowed := ipv6AllowedPeers[peer.ID] + hasIPv6 := peer.IPv6.IsValid() && peer.SupportsIPv6() && peerAllowed + if hasIPv6 { + customZone.Records = append(customZone.Records, nmdata.SimpleRecord{ + Name: fqdn, + Type: int(dns.TypeAAAA), + Class: nbdns.DefaultClass, + TTL: peersZoneRecordTTL, + RData: peer.IPv6.String(), + }) + } + sb.Reset() + + for _, extraLabel := range peer.ExtraDNSLabels { + sb.Grow(len(extraLabel) + len(domainSuffix)) + sb.WriteString(extraLabel) + sb.WriteString(domainSuffix) + + extraFqdn := sb.String() + customZone.Records = append(customZone.Records, nmdata.SimpleRecord{ + Name: extraFqdn, + Type: int(dns.TypeA), + Class: nbdns.DefaultClass, + TTL: peersZoneRecordTTL, + RData: peer.IP.String(), + }) + if hasIPv6 { + customZone.Records = append(customZone.Records, nmdata.SimpleRecord{ + Name: extraFqdn, + Type: int(dns.TypeAAAA), + Class: nbdns.DefaultClass, + TTL: peersZoneRecordTTL, + RData: peer.IPv6.String(), + }) + } + sb.Reset() + } + + } + + go func() { + if merr != nil { + log.WithContext(ctx).Errorf("error generating custom zone for account %s: %v", accountID, merr) + } + }() + + return customZone +}