Compare commits

...

4 Commits

Author SHA1 Message Date
Zoltán Papp
55177dae77 Regenerate proto files with protoc v7.34.1 2026-04-14 17:57:25 +02:00
Zoltán Papp
02958887bc [management] Fix peer update test for MetricsConfig in NetbirdConfig
Update TestUpdateAccountPeers assertions: NetbirdConfig is no longer
nil in peer update responses since it now carries MetricsConfig even
when STUN/TURN config is absent.
2026-04-14 17:53:01 +02:00
Zoltán Papp
10bb6cc700 Remove log line 2026-04-14 17:22:40 +02:00
Zoltán Papp
99505b6bb2 [management, client] Add management-controlled client metrics push
Allow enabling/disabling client metrics push from the dashboard via
account settings instead of requiring env vars on every client.

- Add MetricsConfig proto message to NetbirdConfig
- Add MetricsPushEnabled to account Settings (DB-persisted)
- Expose metrics_push_enabled in OpenAPI and dashboard API handler
- Populate MetricsConfig in sync and login responses
- Client dynamically starts/stops push based on management config
- NB_METRICS_PUSH_ENABLED env var overrides management when explicitly set
- Add activity events for metrics push enable/disable
2026-04-14 17:09:24 +02:00
16 changed files with 503 additions and 300 deletions

View File

@@ -291,6 +291,10 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
c.clientMetrics.RecordLoginDuration(engineCtx, time.Since(loginStarted), true) c.clientMetrics.RecordLoginDuration(engineCtx, time.Since(loginStarted), true)
c.statusRecorder.MarkManagementConnected() c.statusRecorder.MarkManagementConnected()
if metricsConfig := loginResp.GetNetbirdConfig().GetMetrics(); metricsConfig != nil {
c.clientMetrics.UpdatePushFromMgm(c.ctx, metricsConfig.GetEnabled())
}
localPeerState := peer.LocalPeerState{ localPeerState := peer.LocalPeerState{
IP: loginResp.GetPeerConfig().GetAddress(), IP: loginResp.GetPeerConfig().GetAddress(),
PubKey: myPrivateKey.PublicKey().String(), PubKey: myPrivateKey.PublicKey().String(),

View File

@@ -888,6 +888,8 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
return fmt.Errorf("handle the flow configuration: %w", err) return fmt.Errorf("handle the flow configuration: %w", err)
} }
e.handleMetricsUpdate(wCfg.GetMetrics())
if err := e.PopulateNetbirdConfig(wCfg, nil); err != nil { if err := e.PopulateNetbirdConfig(wCfg, nil); err != nil {
log.Warnf("Failed to update DNS server config: %v", err) log.Warnf("Failed to update DNS server config: %v", err)
} }
@@ -964,6 +966,14 @@ func (e *Engine) handleFlowUpdate(config *mgmProto.FlowConfig) error {
return e.flowManager.Update(flowConfig) return e.flowManager.Update(flowConfig)
} }
func (e *Engine) handleMetricsUpdate(config *mgmProto.MetricsConfig) {
if config == nil {
return
}
log.Infof("received metrics configuration from management: enabled=%v", config.GetEnabled())
e.clientMetrics.UpdatePushFromMgm(e.ctx, config.GetEnabled())
}
func toFlowLoggerConfig(config *mgmProto.FlowConfig) (*nftypes.FlowConfig, error) { func toFlowLoggerConfig(config *mgmProto.FlowConfig) (*nftypes.FlowConfig, error) {
if config.GetInterval() == nil { if config.GetInterval() == nil {
return nil, errors.New("flow interval is nil") return nil, errors.New("flow interval is nil")

View File

@@ -60,6 +60,13 @@ func getMetricsInterval() time.Duration {
return interval return interval
} }
// isMetricsPushEnvSet returns true if NB_METRICS_PUSH_ENABLED is explicitly set (to any value).
// When set, the env var takes full precedence over management server configuration.
func isMetricsPushEnvSet() bool {
_, set := os.LookupEnv(EnvMetricsPushEnabled)
return set
}
func isForceSending() bool { func isForceSending() bool {
force, _ := strconv.ParseBool(os.Getenv(EnvMetricsForceSending)) force, _ := strconv.ParseBool(os.Getenv(EnvMetricsForceSending))
return force return force

View File

@@ -169,7 +169,7 @@ func (c *ClientMetrics) Export(w io.Writer) error {
return c.impl.Export(w) return c.impl.Export(w)
} }
// StartPush starts periodic pushing of metrics with the given configuration // StartPush starts periodic pushing of metrics with the given configuration.
// Precedence: PushConfig.ServerAddress > remote config server_url // Precedence: PushConfig.ServerAddress > remote config server_url
func (c *ClientMetrics) StartPush(ctx context.Context, config PushConfig) { func (c *ClientMetrics) StartPush(ctx context.Context, config PushConfig) {
if c == nil { if c == nil {
@@ -184,6 +184,53 @@ func (c *ClientMetrics) StartPush(ctx context.Context, config PushConfig) {
return return
} }
c.startPushLocked(ctx, config)
}
// StopPush stops the periodic metrics push.
func (c *ClientMetrics) StopPush() {
if c == nil {
return
}
c.pushMu.Lock()
defer c.pushMu.Unlock()
c.stopPushLocked()
}
// UpdatePushFromMgm updates metrics push based on management server configuration.
// If NB_METRICS_PUSH_ENABLED is explicitly set (true or false), management config is ignored.
// When unset, management controls whether push is enabled.
func (c *ClientMetrics) UpdatePushFromMgm(ctx context.Context, enabled bool) {
if c == nil {
return
}
if isMetricsPushEnvSet() {
log.Debugf("ignoring management config, env var is explicitly set: %s", EnvMetricsPushEnabled)
return
}
c.pushMu.Lock()
defer c.pushMu.Unlock()
if enabled {
if c.push != nil {
return
}
log.Infof("enabled metrics push by management")
c.startPushLocked(ctx, PushConfigFromEnv())
} else {
if c.push == nil {
return
}
log.Infof("disabled metrics push by management")
c.stopPushLocked()
}
}
// startPushLocked starts push. Caller must hold pushMu.
func (c *ClientMetrics) startPushLocked(ctx context.Context, config PushConfig) {
c.mu.RLock() c.mu.RLock()
agentVersion := c.agentInfo.Version agentVersion := c.agentInfo.Version
peerID := c.agentInfo.peerID peerID := c.agentInfo.peerID
@@ -208,12 +255,8 @@ func (c *ClientMetrics) StartPush(ctx context.Context, config PushConfig) {
c.push = push c.push = push
} }
func (c *ClientMetrics) StopPush() { // stopPushLocked stops push. Caller must hold pushMu.
if c == nil { func (c *ClientMetrics) stopPushLocked() {
return
}
c.pushMu.Lock()
defer c.pushMu.Unlock()
if c.push == nil { if c.push == nil {
return return
} }

View File

@@ -22,9 +22,16 @@ import (
"github.com/netbirdio/netbird/shared/sshauth" "github.com/netbirdio/netbird/shared/sshauth"
) )
func toNetbirdConfig(config *nbconfig.Config, turnCredentials *Token, relayToken *Token, extraSettings *types.ExtraSettings) *proto.NetbirdConfig { func toNetbirdConfig(config *nbconfig.Config, turnCredentials *Token, relayToken *Token, extraSettings *types.ExtraSettings, settings *types.Settings) *proto.NetbirdConfig {
if config == nil { if config == nil {
return nil if settings == nil {
return nil
}
return &proto.NetbirdConfig{
Metrics: &proto.MetricsConfig{
Enabled: settings.MetricsPushEnabled,
},
}
} }
var stuns []*proto.HostConfig var stuns []*proto.HostConfig
@@ -85,6 +92,12 @@ func toNetbirdConfig(config *nbconfig.Config, turnCredentials *Token, relayToken
Relay: relayCfg, Relay: relayCfg,
} }
if settings != nil {
nbConfig.Metrics = &proto.MetricsConfig{
Enabled: settings.MetricsPushEnabled,
}
}
return nbConfig return nbConfig
} }
@@ -125,7 +138,7 @@ func ToSyncResponse(ctx context.Context, config *nbconfig.Config, httpConfig *nb
Checks: toProtocolChecks(ctx, checks), Checks: toProtocolChecks(ctx, checks),
} }
nbConfig := toNetbirdConfig(config, turnCredentials, relayCredentials, extraSettings) nbConfig := toNetbirdConfig(config, turnCredentials, relayCredentials, extraSettings, settings)
extendedConfig := integrationsConfig.ExtendNetBirdConfig(peer.ID, peerGroups, nbConfig, extraSettings) extendedConfig := integrationsConfig.ExtendNetBirdConfig(peer.ID, peerGroups, nbConfig, extraSettings)
response.NetbirdConfig = extendedConfig response.NetbirdConfig = extendedConfig

View File

@@ -820,7 +820,7 @@ func (s *Server) prepareLoginResponse(ctx context.Context, peer *nbpeer.Peer, ne
// if peer has reached this point then it has logged in // if peer has reached this point then it has logged in
loginResp := &proto.LoginResponse{ loginResp := &proto.LoginResponse{
NetbirdConfig: toNetbirdConfig(s.config, nil, relayToken, nil), NetbirdConfig: toNetbirdConfig(s.config, nil, relayToken, nil, settings),
PeerConfig: toPeerConfig(peer, netMap.Network, s.networkMapController.GetDNSDomain(settings), settings, s.config.HttpConfig, s.config.DeviceAuthorizationFlow, netMap.EnableSSH), PeerConfig: toPeerConfig(peer, netMap.Network, s.networkMapController.GetDNSDomain(settings), settings, s.config.HttpConfig, s.config.DeviceAuthorizationFlow, netMap.EnableSSH),
Checks: toProtocolChecks(ctx, postureChecks), Checks: toProtocolChecks(ctx, postureChecks),
} }

View File

@@ -336,7 +336,8 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco
oldSettings.LazyConnectionEnabled != newSettings.LazyConnectionEnabled || oldSettings.LazyConnectionEnabled != newSettings.LazyConnectionEnabled ||
oldSettings.DNSDomain != newSettings.DNSDomain || oldSettings.DNSDomain != newSettings.DNSDomain ||
oldSettings.AutoUpdateVersion != newSettings.AutoUpdateVersion || oldSettings.AutoUpdateVersion != newSettings.AutoUpdateVersion ||
oldSettings.AutoUpdateAlways != newSettings.AutoUpdateAlways { oldSettings.AutoUpdateAlways != newSettings.AutoUpdateAlways ||
oldSettings.MetricsPushEnabled != newSettings.MetricsPushEnabled {
updateAccountPeers = true updateAccountPeers = true
} }
@@ -379,6 +380,7 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco
am.handleAutoUpdateVersionSettings(ctx, oldSettings, newSettings, userID, accountID) am.handleAutoUpdateVersionSettings(ctx, oldSettings, newSettings, userID, accountID)
am.handleAutoUpdateAlwaysSettings(ctx, oldSettings, newSettings, userID, accountID) am.handleAutoUpdateAlwaysSettings(ctx, oldSettings, newSettings, userID, accountID)
am.handlePeerExposeSettings(ctx, oldSettings, newSettings, userID, accountID) am.handlePeerExposeSettings(ctx, oldSettings, newSettings, userID, accountID)
am.handleMetricsPushSettings(ctx, oldSettings, newSettings, userID, accountID)
if err = am.handleInactivityExpirationSettings(ctx, oldSettings, newSettings, userID, accountID); err != nil { if err = am.handleInactivityExpirationSettings(ctx, oldSettings, newSettings, userID, accountID); err != nil {
return nil, err return nil, err
} }
@@ -458,6 +460,16 @@ func (am *DefaultAccountManager) handleLazyConnectionSettings(ctx context.Contex
} }
} }
func (am *DefaultAccountManager) handleMetricsPushSettings(ctx context.Context, oldSettings, newSettings *types.Settings, userID, accountID string) {
if oldSettings.MetricsPushEnabled != newSettings.MetricsPushEnabled {
if newSettings.MetricsPushEnabled {
am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountMetricsPushEnabled, nil)
} else {
am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountMetricsPushDisabled, nil)
}
}
}
func (am *DefaultAccountManager) handlePeerLoginExpirationSettings(ctx context.Context, oldSettings, newSettings *types.Settings, userID, accountID string) { func (am *DefaultAccountManager) handlePeerLoginExpirationSettings(ctx context.Context, oldSettings, newSettings *types.Settings, userID, accountID string) {
if oldSettings.PeerLoginExpirationEnabled != newSettings.PeerLoginExpirationEnabled { if oldSettings.PeerLoginExpirationEnabled != newSettings.PeerLoginExpirationEnabled {
event := activity.AccountPeerLoginExpirationEnabled event := activity.AccountPeerLoginExpirationEnabled

View File

@@ -225,6 +225,11 @@ const (
// AccountAutoUpdateAlwaysDisabled indicates that a user disabled always auto-update for the account // AccountAutoUpdateAlwaysDisabled indicates that a user disabled always auto-update for the account
AccountAutoUpdateAlwaysDisabled Activity = 117 AccountAutoUpdateAlwaysDisabled Activity = 117
// AccountMetricsPushEnabled indicates that a user enabled metrics push for the account
AccountMetricsPushEnabled Activity = 121
// AccountMetricsPushDisabled indicates that a user disabled metrics push for the account
AccountMetricsPushDisabled Activity = 122
// DomainAdded indicates that a user added a custom domain // DomainAdded indicates that a user added a custom domain
DomainAdded Activity = 118 DomainAdded Activity = 118
// DomainDeleted indicates that a user deleted a custom domain // DomainDeleted indicates that a user deleted a custom domain
@@ -379,6 +384,9 @@ var activityMap = map[Activity]Code{
AccountPeerExposeEnabled: {"Account peer expose enabled", "account.setting.peer.expose.enable"}, AccountPeerExposeEnabled: {"Account peer expose enabled", "account.setting.peer.expose.enable"},
AccountPeerExposeDisabled: {"Account peer expose disabled", "account.setting.peer.expose.disable"}, AccountPeerExposeDisabled: {"Account peer expose disabled", "account.setting.peer.expose.disable"},
AccountMetricsPushEnabled: {"Account metrics push enabled", "account.setting.metrics.push.enable"},
AccountMetricsPushDisabled: {"Account metrics push disabled", "account.setting.metrics.push.disable"},
DomainAdded: {"Domain added", "domain.add"}, DomainAdded: {"Domain added", "domain.add"},
DomainDeleted: {"Domain deleted", "domain.delete"}, DomainDeleted: {"Domain deleted", "domain.delete"},
DomainValidated: {"Domain validated", "domain.validate"}, DomainValidated: {"Domain validated", "domain.validate"},

View File

@@ -228,6 +228,9 @@ func (h *handler) updateAccountRequestSettings(req api.PutApiAccountsAccountIdJS
if req.Settings.AutoUpdateAlways != nil { if req.Settings.AutoUpdateAlways != nil {
returnSettings.AutoUpdateAlways = *req.Settings.AutoUpdateAlways returnSettings.AutoUpdateAlways = *req.Settings.AutoUpdateAlways
} }
if req.Settings.MetricsPushEnabled != nil {
returnSettings.MetricsPushEnabled = *req.Settings.MetricsPushEnabled
}
return returnSettings, nil return returnSettings, nil
} }
@@ -352,6 +355,7 @@ func toAccountResponse(accountID string, settings *types.Settings, meta *types.A
DnsDomain: &settings.DNSDomain, DnsDomain: &settings.DNSDomain,
AutoUpdateVersion: &settings.AutoUpdateVersion, AutoUpdateVersion: &settings.AutoUpdateVersion,
AutoUpdateAlways: &settings.AutoUpdateAlways, AutoUpdateAlways: &settings.AutoUpdateAlways,
MetricsPushEnabled: &settings.MetricsPushEnabled,
EmbeddedIdpEnabled: &settings.EmbeddedIdpEnabled, EmbeddedIdpEnabled: &settings.EmbeddedIdpEnabled,
LocalAuthDisabled: &settings.LocalAuthDisabled, LocalAuthDisabled: &settings.LocalAuthDisabled,
} }

View File

@@ -123,6 +123,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
DnsDomain: sr(""), DnsDomain: sr(""),
AutoUpdateAlways: br(false), AutoUpdateAlways: br(false),
AutoUpdateVersion: sr(""), AutoUpdateVersion: sr(""),
MetricsPushEnabled: br(false),
EmbeddedIdpEnabled: br(false), EmbeddedIdpEnabled: br(false),
LocalAuthDisabled: br(false), LocalAuthDisabled: br(false),
}, },
@@ -149,6 +150,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
DnsDomain: sr(""), DnsDomain: sr(""),
AutoUpdateAlways: br(false), AutoUpdateAlways: br(false),
AutoUpdateVersion: sr(""), AutoUpdateVersion: sr(""),
MetricsPushEnabled: br(false),
EmbeddedIdpEnabled: br(false), EmbeddedIdpEnabled: br(false),
LocalAuthDisabled: br(false), LocalAuthDisabled: br(false),
}, },
@@ -175,6 +177,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
DnsDomain: sr(""), DnsDomain: sr(""),
AutoUpdateAlways: br(false), AutoUpdateAlways: br(false),
AutoUpdateVersion: sr("latest"), AutoUpdateVersion: sr("latest"),
MetricsPushEnabled: br(false),
EmbeddedIdpEnabled: br(false), EmbeddedIdpEnabled: br(false),
LocalAuthDisabled: br(false), LocalAuthDisabled: br(false),
}, },
@@ -201,6 +204,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
DnsDomain: sr(""), DnsDomain: sr(""),
AutoUpdateAlways: br(false), AutoUpdateAlways: br(false),
AutoUpdateVersion: sr(""), AutoUpdateVersion: sr(""),
MetricsPushEnabled: br(false),
EmbeddedIdpEnabled: br(false), EmbeddedIdpEnabled: br(false),
LocalAuthDisabled: br(false), LocalAuthDisabled: br(false),
}, },
@@ -227,6 +231,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
DnsDomain: sr(""), DnsDomain: sr(""),
AutoUpdateAlways: br(false), AutoUpdateAlways: br(false),
AutoUpdateVersion: sr(""), AutoUpdateVersion: sr(""),
MetricsPushEnabled: br(false),
EmbeddedIdpEnabled: br(false), EmbeddedIdpEnabled: br(false),
LocalAuthDisabled: br(false), LocalAuthDisabled: br(false),
}, },
@@ -253,6 +258,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
DnsDomain: sr(""), DnsDomain: sr(""),
AutoUpdateAlways: br(false), AutoUpdateAlways: br(false),
AutoUpdateVersion: sr(""), AutoUpdateVersion: sr(""),
MetricsPushEnabled: br(false),
EmbeddedIdpEnabled: br(false), EmbeddedIdpEnabled: br(false),
LocalAuthDisabled: br(false), LocalAuthDisabled: br(false),
}, },

View File

@@ -1062,7 +1062,11 @@ func testUpdateAccountPeers(t *testing.T) {
for _, channel := range peerChannels { for _, channel := range peerChannels {
update := <-channel update := <-channel
assert.Nil(t, update.Update.NetbirdConfig) assert.NotNil(t, update.Update.NetbirdConfig)
assert.Nil(t, update.Update.NetbirdConfig.Stuns)
assert.Nil(t, update.Update.NetbirdConfig.Turns)
assert.Nil(t, update.Update.NetbirdConfig.Signal)
assert.Nil(t, update.Update.NetbirdConfig.Relay)
assert.Equal(t, tc.peers, len(update.Update.NetworkMap.RemotePeers)) assert.Equal(t, tc.peers, len(update.Update.NetworkMap.RemotePeers))
assert.Equal(t, tc.peers*2, len(update.Update.NetworkMap.FirewallRules)) assert.Equal(t, tc.peers*2, len(update.Update.NetworkMap.FirewallRules))
} }

View File

@@ -65,6 +65,9 @@ type Settings struct {
// when false, updates require user interaction from the UI // when false, updates require user interaction from the UI
AutoUpdateAlways bool `gorm:"default:false"` AutoUpdateAlways bool `gorm:"default:false"`
// MetricsPushEnabled globally enables or disables client metrics push for the account
MetricsPushEnabled bool `gorm:"default:false"`
// EmbeddedIdpEnabled indicates if the embedded identity provider is enabled. // EmbeddedIdpEnabled indicates if the embedded identity provider is enabled.
// This is a runtime-only field, not stored in the database. // This is a runtime-only field, not stored in the database.
EmbeddedIdpEnabled bool `gorm:"-"` EmbeddedIdpEnabled bool `gorm:"-"`
@@ -96,6 +99,7 @@ func (s *Settings) Copy() *Settings {
NetworkRange: s.NetworkRange, NetworkRange: s.NetworkRange,
AutoUpdateVersion: s.AutoUpdateVersion, AutoUpdateVersion: s.AutoUpdateVersion,
AutoUpdateAlways: s.AutoUpdateAlways, AutoUpdateAlways: s.AutoUpdateAlways,
MetricsPushEnabled: s.MetricsPushEnabled,
EmbeddedIdpEnabled: s.EmbeddedIdpEnabled, EmbeddedIdpEnabled: s.EmbeddedIdpEnabled,
LocalAuthDisabled: s.LocalAuthDisabled, LocalAuthDisabled: s.LocalAuthDisabled,
} }

View File

@@ -367,6 +367,10 @@ components:
description: When true, updates are installed automatically in the background. When false, updates require user interaction from the UI. description: When true, updates are installed automatically in the background. When false, updates require user interaction from the UI.
type: boolean type: boolean
example: false example: false
metrics_push_enabled:
description: Enables or disables client metrics push for all peers in the account
type: boolean
example: false
embedded_idp_enabled: embedded_idp_enabled:
description: Indicates whether the embedded identity provider (Dex) is enabled for this account. This is a read-only field. description: Indicates whether the embedded identity provider (Dex) is enabled for this account. This is a read-only field.
type: boolean type: boolean

View File

@@ -1477,6 +1477,9 @@ type AccountSettings struct {
// LocalAuthDisabled Indicates whether local (email/password) authentication is disabled. When true, users can only authenticate via external identity providers. This is a read-only field. // LocalAuthDisabled Indicates whether local (email/password) authentication is disabled. When true, users can only authenticate via external identity providers. This is a read-only field.
LocalAuthDisabled *bool `json:"local_auth_disabled,omitempty"` LocalAuthDisabled *bool `json:"local_auth_disabled,omitempty"`
// MetricsPushEnabled Enables or disables client metrics push for all peers in the account
MetricsPushEnabled *bool `json:"metrics_push_enabled,omitempty"`
// NetworkRange Allows to define a custom network range for the account in CIDR format // NetworkRange Allows to define a custom network range for the account in CIDR format
NetworkRange *string `json:"network_range,omitempty"` NetworkRange *string `json:"network_range,omitempty"`

File diff suppressed because it is too large Load Diff

View File

@@ -256,6 +256,8 @@ message NetbirdConfig {
RelayConfig relay = 4; RelayConfig relay = 4;
FlowConfig flow = 5; FlowConfig flow = 5;
MetricsConfig metrics = 6;
} }
// HostConfig describes connection properties of some server (e.g. STUN, Signal, Management) // HostConfig describes connection properties of some server (e.g. STUN, Signal, Management)
@@ -294,6 +296,10 @@ message FlowConfig {
bool dnsCollection = 8; bool dnsCollection = 8;
} }
message MetricsConfig {
bool enabled = 1;
}
// JWTConfig represents JWT authentication configuration for validating tokens. // JWTConfig represents JWT authentication configuration for validating tokens.
message JWTConfig { message JWTConfig {
string issuer = 1; string issuer = 1;