[management] setting to return error on name collision during AddPeer (#8154)

This commit is contained in:
Pascal Fischer
2026-10-12 14:01:37 +02:00
committed by GitHub
parent 66a28fd666
commit f6cfa775f8
12 changed files with 344 additions and 28 deletions
+11
View File
@@ -459,6 +459,7 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco
am.handleAutoUpdateAlwaysSettings(ctx, oldSettings, newSettings, userID, accountID)
am.handlePeerExposeSettings(ctx, oldSettings, newSettings, userID, accountID)
am.handleMetricsPushSettings(ctx, oldSettings, newSettings, userID, accountID)
am.handlePeerHostnameCollisionSettings(ctx, oldSettings, newSettings, userID, accountID)
if err = am.handleInactivityExpirationSettings(ctx, oldSettings, newSettings, userID, accountID); err != nil {
return nil, err
}
@@ -644,6 +645,16 @@ func (am *DefaultAccountManager) handleMetricsPushSettings(ctx context.Context,
}
}
func (am *DefaultAccountManager) handlePeerHostnameCollisionSettings(ctx context.Context, oldSettings, newSettings *types.Settings, userID, accountID string) {
if oldSettings.PeerHostnameCollisionRejected != newSettings.PeerHostnameCollisionRejected {
if newSettings.PeerHostnameCollisionRejected {
am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountPeerHostnameCollisionRejectionEnabled, nil)
} else {
am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountPeerHostnameCollisionRejectionDisabled, nil)
}
}
}
func (am *DefaultAccountManager) handlePeerLoginExpirationSettings(ctx context.Context, oldSettings, newSettings *types.Settings, userID, accountID string) {
if oldSettings.PeerLoginExpirationEnabled != newSettings.PeerLoginExpirationEnabled {
event := activity.AccountPeerLoginExpirationEnabled
+9 -2
View File
@@ -287,6 +287,11 @@ const (
// CustomDomainValidationExpired indicates that an unvalidated domain registration expired.
CustomDomainValidationExpired Activity = 143
// AccountPeerHostnameCollisionRejectionEnabled indicates that a user enabled rejecting peer hostname collisions for the account
AccountPeerHostnameCollisionRejectionEnabled Activity = 144
// AccountPeerHostnameCollisionRejectionDisabled indicates that a user disabled rejecting peer hostname collisions for the account
AccountPeerHostnameCollisionRejectionDisabled Activity = 145
AccountDeleted Activity = 99999
)
@@ -461,8 +466,10 @@ var activityMap = map[Activity]Code{
AgentNetworkSettingsUpdated: {"Agent Network settings updated", "agent_network.settings.update"},
AgentNetworkSettingsDeleted: {"Agent Network settings deleted", "agent_network.settings.delete"},
AccountMetricsPushEnabled: {"Account metrics push enabled", "account.setting.metrics.push.enable"},
AccountMetricsPushDisabled: {"Account metrics push disabled", "account.setting.metrics.push.disable"},
AccountMetricsPushEnabled: {"Account metrics push enabled", "account.setting.metrics.push.enable"},
AccountMetricsPushDisabled: {"Account metrics push disabled", "account.setting.metrics.push.disable"},
AccountPeerHostnameCollisionRejectionEnabled: {"Account peer hostname collision rejection enabled", "account.setting.peer.hostname.collision.rejection.enable"},
AccountPeerHostnameCollisionRejectionDisabled: {"Account peer hostname collision rejection disabled", "account.setting.peer.hostname.collision.rejection.disable"},
DomainAdded: {"Domain added", "domain.add"},
DomainDeleted: {"Domain deleted", "domain.delete"},
@@ -286,6 +286,9 @@ func (h *handler) updateAccountRequestSettings(req api.PutApiAccountsAccountIdJS
if req.Settings.MetricsPushEnabled != nil {
returnSettings.MetricsPushEnabled = *req.Settings.MetricsPushEnabled
}
if req.Settings.PeerHostnameCollisionRejected != nil {
returnSettings.PeerHostnameCollisionRejected = *req.Settings.PeerHostnameCollisionRejected
}
if req.Settings.AgentNetworkOnly != nil {
returnSettings.AgentNetworkOnly = *req.Settings.AgentNetworkOnly
}
@@ -432,6 +435,7 @@ func toAccountResponse(accountID string, settings *types.Settings, meta *types.A
AutoUpdateAlways: &settings.AutoUpdateAlways,
Ipv6EnabledGroups: &settings.IPv6EnabledGroups,
MetricsPushEnabled: &settings.MetricsPushEnabled,
PeerHostnameCollisionRejected: &settings.PeerHostnameCollisionRejected,
AgentNetworkOnly: &settings.AgentNetworkOnly,
EmbeddedIdpEnabled: &settings.EmbeddedIdpEnabled,
LocalAuthDisabled: &settings.LocalAuthDisabled,
@@ -130,6 +130,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
AutoUpdateAlways: br(false),
AutoUpdateVersion: sr(""),
MetricsPushEnabled: br(false),
PeerHostnameCollisionRejected: br(false),
AgentNetworkOnly: br(false),
EmbeddedIdpEnabled: br(false),
LocalAuthDisabled: br(false),
@@ -159,6 +160,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
AutoUpdateAlways: br(false),
AutoUpdateVersion: sr(""),
MetricsPushEnabled: br(false),
PeerHostnameCollisionRejected: br(false),
AgentNetworkOnly: br(false),
EmbeddedIdpEnabled: br(false),
LocalAuthDisabled: br(false),
@@ -188,6 +190,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
AutoUpdateAlways: br(false),
AutoUpdateVersion: sr("latest"),
MetricsPushEnabled: br(false),
PeerHostnameCollisionRejected: br(false),
AgentNetworkOnly: br(false),
EmbeddedIdpEnabled: br(false),
LocalAuthDisabled: br(false),
@@ -217,6 +220,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
AutoUpdateAlways: br(false),
AutoUpdateVersion: sr(""),
MetricsPushEnabled: br(false),
PeerHostnameCollisionRejected: br(false),
AgentNetworkOnly: br(false),
EmbeddedIdpEnabled: br(false),
LocalAuthDisabled: br(false),
@@ -246,6 +250,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
AutoUpdateAlways: br(false),
AutoUpdateVersion: sr(""),
MetricsPushEnabled: br(false),
PeerHostnameCollisionRejected: br(false),
AgentNetworkOnly: br(false),
EmbeddedIdpEnabled: br(false),
LocalAuthDisabled: br(false),
@@ -275,6 +280,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
AutoUpdateAlways: br(false),
AutoUpdateVersion: sr(""),
MetricsPushEnabled: br(false),
PeerHostnameCollisionRejected: br(false),
AgentNetworkOnly: br(false),
EmbeddedIdpEnabled: br(false),
LocalAuthDisabled: br(false),
@@ -304,6 +310,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
AutoUpdateAlways: br(false),
AutoUpdateVersion: sr(""),
MetricsPushEnabled: br(false),
PeerHostnameCollisionRejected: br(false),
AgentNetworkOnly: br(true),
DashboardFeatures: &api.AccountDashboardFeatures{
AgentNetwork: br(true),
@@ -345,6 +352,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
AutoUpdateAlways: br(false),
AutoUpdateVersion: sr(""),
MetricsPushEnabled: br(false),
PeerHostnameCollisionRejected: br(false),
AgentNetworkOnly: br(false),
DashboardFeatures: &api.AccountDashboardFeatures{
AgentNetwork: br(true),
@@ -377,6 +385,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
AutoUpdateAlways: br(false),
AutoUpdateVersion: sr(""),
MetricsPushEnabled: br(false),
PeerHostnameCollisionRejected: br(false),
AgentNetworkOnly: br(false),
EmbeddedIdpEnabled: br(false),
LocalAuthDisabled: br(false),
+77 -24
View File
@@ -4,6 +4,7 @@ import (
"context"
"crypto/sha256"
b64 "encoding/base64"
"errors"
"fmt"
"net"
"net/netip"
@@ -14,6 +15,7 @@ import (
"github.com/rs/xid"
log "github.com/sirupsen/logrus"
"golang.org/x/exp/maps"
"gorm.io/gorm"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/management/server/idp"
@@ -288,19 +290,34 @@ func (am *DefaultAccountManager) UpdatePeer(ctx context.Context, accountID, user
var newLabel string
newLabel, err = nbdns.GetParsedDomainLabel(update.Name)
if err != nil {
newLabel = ""
} else {
_, err := transaction.GetPeerIdByLabel(ctx, store.LockingStrengthNone, accountID, newLabel)
if err == nil {
newLabel = ""
}
}
if newLabel == "" {
newLabel, err = getPeerIPDNSLabel(peer.IP, update.Name)
if settings.PeerHostnameCollisionRejected {
if err != nil {
return fmt.Errorf("failed to get free DNS label: %w", err)
return status.Errorf(status.InvalidArgument, "invalid peer name: %v", err)
}
if newLabel != peer.DNSLabel {
taken, err := peerLabelTaken(ctx, transaction, accountID, newLabel)
if err != nil {
return err
}
if taken {
return errPeerHostnameInUse(update.Name)
}
}
} else {
if err != nil {
newLabel = ""
} else {
_, err := transaction.GetPeerIdByLabel(ctx, store.LockingStrengthNone, accountID, newLabel)
if err == nil {
newLabel = ""
}
}
if newLabel == "" {
newLabel, err = getPeerIPDNSLabel(peer.IP, update.Name)
if err != nil {
return fmt.Errorf("failed to get free DNS label: %w", err)
}
}
}
peer.Name = update.Name
@@ -336,6 +353,9 @@ func (am *DefaultAccountManager) UpdatePeer(ctx context.Context, accountID, user
return transaction.SavePeer(ctx, accountID, peer)
})
if err != nil {
if peerLabelChanged && isUniqueConstraintError(err) {
return nil, errPeerHostnameInUse(peer.Name)
}
return nil, err
}
@@ -832,6 +852,14 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, accountID, setupKe
return nil, nil, nil, false, fmt.Errorf("failed getting network: %w", err)
}
hostLabel, err := nbdns.GetParsedDomainLabel(peer.Meta.Hostname)
if err != nil {
return nil, nil, nil, false, status.Errorf(status.InvalidArgument, "invalid peer hostname: %v", err)
}
rejectLabelCollision := settings.PeerHostnameCollisionRejected
labelTaken := false
var insertErr error
maxAttempts := 10
for attempt := 1; attempt <= maxAttempts; attempt++ {
netPrefix, err := netip.ParsePrefix(network.Net.String())
@@ -843,17 +871,12 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, accountID, setupKe
return nil, nil, nil, false, fmt.Errorf("failed to get free IP: %w", err)
}
var freeLabel string
if ephemeral || attempt > 1 {
freeLabel := hostLabel
if !rejectLabelCollision && (ephemeral || labelTaken) {
freeLabel, err = getPeerIPDNSLabel(freeIP, peer.Meta.Hostname)
if err != nil {
return nil, nil, nil, false, fmt.Errorf("failed to get free DNS label: %w", err)
}
} else {
freeLabel, err = nbdns.GetParsedDomainLabel(peer.Meta.Hostname)
if err != nil {
return nil, nil, nil, false, fmt.Errorf("failed to get free DNS label: %w", err)
}
}
newPeer.DNSLabel = freeLabel
newPeer.IP = freeIP
@@ -884,7 +907,7 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, accountID, setupKe
}
}
err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
insertErr = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
err = transaction.AddPeerToAccount(ctx, newPeer)
if err != nil {
return err
@@ -942,16 +965,28 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, accountID, setupKe
log.WithContext(ctx).Debugf("Peer %s added to account %s", newPeer.ID, accountID)
return nil
})
if err == nil {
if insertErr == nil {
break
}
if isUniqueConstraintError(err) {
log.WithContext(ctx).WithFields(log.Fields{"dns_label": freeLabel, "ip": freeIP}).Tracef("Failed to add peer in attempt %d, retrying: %v", attempt, err)
if isUniqueConstraintError(insertErr) {
if freeLabel == hostLabel {
labelTaken, err = peerLabelTaken(ctx, am.Store, accountID, hostLabel)
if err != nil {
return nil, nil, nil, false, err
}
if labelTaken && rejectLabelCollision {
return nil, nil, nil, false, errPeerHostnameInUse(peer.Meta.Hostname)
}
}
log.WithContext(ctx).WithFields(log.Fields{"dns_label": freeLabel, "ip": freeIP}).Tracef("Failed to add peer in attempt %d, retrying: %v", attempt, insertErr)
continue
}
return nil, nil, nil, false, fmt.Errorf("failed to add peer to database: %w", err)
return nil, nil, nil, false, fmt.Errorf("failed to add peer to database: %w", insertErr)
}
if insertErr != nil {
return nil, nil, nil, false, fmt.Errorf("add peer after %d attempts: %w", maxAttempts, insertErr)
}
if newPeer == nil {
return nil, nil, nil, false, fmt.Errorf("new peer is nil")
@@ -985,6 +1020,24 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, accountID, setupKe
return newPeer, network, postureChecks, enableSSH, nil
}
// peerLabelTaken reports whether a peer of the account already uses the DNS label.
func peerLabelTaken(ctx context.Context, s store.Store, accountID, label string) (bool, error) {
_, err := s.GetPeerIdByLabel(ctx, store.LockingStrengthNone, accountID, label)
switch {
case err == nil:
return true, nil
case errors.Is(err, gorm.ErrRecordNotFound):
return false, nil
default:
return false, fmt.Errorf("check peer DNS label: %w", err)
}
}
// errPeerHostnameInUse is a PermissionDenied error because the client treats only that gRPC code as a permanent login failure instead of retrying.
func errPeerHostnameInUse(hostname string) error {
return status.Errorf(status.PermissionDenied, "peer with hostname %q is already registered", hostname)
}
func getPeerIPDNSLabel(ip netip.Addr, peerHostName string) (string, error) {
if !ip.Is4() {
return "", fmt.Errorf("DNS label generation requires an IPv4 address, got %s", ip)
+211
View File
@@ -3050,3 +3050,214 @@ func TestResolvePeerLocation(t *testing.T) {
})
}
}
func TestAddPeer_HostnameCollisionRejected(t *testing.T) {
manager, _, err := createManager(t)
require.NoError(t, err)
account := newAccountWithId(context.Background(), "test-account", "owner", "", "", "", false)
account.Settings.PeerHostnameCollisionRejected = true
require.NoError(t, manager.Store.SaveAccount(context.Background(), account))
setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "key", types.SetupKeyReusable, time.Hour, nil, 100, "owner", false, false)
require.NoError(t, err)
ephemeralKey, err := manager.CreateSetupKey(context.Background(), account.Id, "ephemeral", types.SetupKeyReusable, time.Hour, nil, 100, "owner", true, false)
require.NoError(t, err)
addPeer := func(hostname, key string) (*nbpeer.Peer, error) {
wgKey, err := wgtypes.GenerateKey()
require.NoError(t, err)
peer := &nbpeer.Peer{
Key: wgKey.PublicKey().String(),
Meta: nbpeer.PeerSystemMeta{Hostname: hostname, OS: "linux"},
}
added, _, _, _, err := manager.AddPeer(context.Background(), "", key, "", peer, false)
return added, err
}
first, err := addPeer("my-host", setupKey.Key)
require.NoError(t, err)
assert.Equal(t, "my-host", first.DNSLabel)
_, err = addPeer("My-Host", setupKey.Key)
require.Error(t, err)
e, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, status.PermissionDenied, e.Type())
other, err := addPeer("other-host", setupKey.Key)
require.NoError(t, err)
assert.Equal(t, "other-host", other.DNSLabel)
_, err = addPeer("my-host", ephemeralKey.Key)
require.Error(t, err)
e, ok = status.FromError(err)
require.True(t, ok)
assert.Equal(t, status.PermissionDenied, e.Type())
ephemeral, err := addPeer("ephemeral-host", ephemeralKey.Key)
require.NoError(t, err)
assert.Equal(t, "ephemeral-host", ephemeral.DNSLabel)
account.Settings.PeerHostnameCollisionRejected = false
require.NoError(t, manager.Store.SaveAccountSettings(context.Background(), account.Id, account.Settings))
suffixed, err := addPeer("my-host", setupKey.Key)
require.NoError(t, err)
assert.True(t, strings.HasPrefix(suffixed.DNSLabel, "my-host-"))
}
func TestUpdatePeer_HostnameCollisionRejected(t *testing.T) {
manager, _, err := createManager(t)
require.NoError(t, err)
account := newAccountWithId(context.Background(), "test-account", "owner", "", "", "", false)
account.Settings.PeerHostnameCollisionRejected = true
require.NoError(t, manager.Store.SaveAccount(context.Background(), account))
addPeer := func(hostname string) *nbpeer.Peer {
wgKey, err := wgtypes.GenerateKey()
require.NoError(t, err)
added, _, _, _, err := manager.AddPeer(context.Background(), "", "", "owner", &nbpeer.Peer{
Key: wgKey.PublicKey().String(),
Meta: nbpeer.PeerSystemMeta{Hostname: hostname, OS: "linux"},
}, false)
require.NoError(t, err)
return added
}
first := addPeer("web-server")
second := addPeer("old-name")
update := second.Copy()
update.Name = "Web-Server"
_, err = manager.UpdatePeer(context.Background(), account.Id, "owner", update)
require.Error(t, err)
e, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, status.PermissionDenied, e.Type())
update = second.Copy()
update.Name = "api-server.example.com"
updated, err := manager.UpdatePeer(context.Background(), account.Id, "owner", update)
require.NoError(t, err)
assert.Equal(t, "api-server", updated.DNSLabel)
update = first.Copy()
update.Name = "WEB-SERVER"
updated, err = manager.UpdatePeer(context.Background(), account.Id, "owner", update)
require.NoError(t, err, "renaming to a different spelling of the own label must not collide with itself")
assert.Equal(t, "web-server", updated.DNSLabel)
}
func TestAddPeer_HostnameCollisionRejectedConcurrent(t *testing.T) {
manager, _, err := createManager(t)
require.NoError(t, err)
account := newAccountWithId(context.Background(), "test-account", "owner", "", "", "", false)
account.Settings.PeerHostnameCollisionRejected = true
require.NoError(t, manager.Store.SaveAccount(context.Background(), account))
setupKey, err := manager.CreateSetupKey(context.Background(), account.Id, "key", types.SetupKeyReusable, time.Hour, nil, 100, "owner", false, false)
require.NoError(t, err)
const attempts = 20
start := make(chan struct{})
errs := make(chan error, attempts)
var wg sync.WaitGroup
for i := 0; i < attempts; i++ {
wg.Add(1)
go func() {
defer wg.Done()
wgKey, err := wgtypes.GenerateKey()
if err != nil {
errs <- err
return
}
<-start
_, _, _, _, err = manager.AddPeer(context.Background(), "", setupKey.Key, "", &nbpeer.Peer{
Key: wgKey.PublicKey().String(),
Meta: nbpeer.PeerSystemMeta{Hostname: "same-host", OS: "linux"},
}, false)
errs <- err
}()
}
close(start)
wg.Wait()
close(errs)
succeeded := 0
for err := range errs {
if err == nil {
succeeded++
continue
}
e, ok := status.FromError(err)
require.True(t, ok, "unexpected error: %v", err)
assert.Equal(t, status.PermissionDenied, e.Type(), "unexpected error: %v", err)
}
assert.Equal(t, 1, succeeded)
peers, err := manager.Store.GetAccountPeers(context.Background(), store.LockingStrengthNone, account.Id, "", "", "")
require.NoError(t, err)
require.Len(t, peers, 1)
assert.Equal(t, "same-host", peers[0].DNSLabel)
}
func TestAddPeer_IPCollisionKeepsLabel(t *testing.T) {
for _, rejectCollision := range []bool{false, true} {
t.Run(fmt.Sprintf("collision rejected %t", rejectCollision), func(t *testing.T) {
manager, _, err := createManager(t)
require.NoError(t, err)
account := newAccountWithId(context.Background(), "test-account", "owner", "", "", "", false)
account.Settings.PeerHostnameCollisionRejected = rejectCollision
_, smallNet, err := net.ParseCIDR("100.64.0.0/26")
require.NoError(t, err)
account.Network.Net = *smallNet
require.NoError(t, manager.Store.SaveAccount(context.Background(), account))
const totalPeers = 20
for i := 0; i < totalPeers; i++ {
wgKey, err := wgtypes.GenerateKey()
require.NoError(t, err)
hostname := fmt.Sprintf("host-%d", i)
added, _, _, _, err := manager.AddPeer(context.Background(), "", "", "owner", &nbpeer.Peer{
Key: wgKey.PublicKey().String(),
Meta: nbpeer.PeerSystemMeta{Hostname: hostname, OS: "linux"},
}, false)
require.NoError(t, err)
assert.Equal(t, hostname, added.DNSLabel, "an IP collision must not change the DNS label")
}
})
}
}
func TestAddPeer_ExhaustedIPAllocationFails(t *testing.T) {
manager, _, err := createManager(t)
require.NoError(t, err)
account := newAccountWithId(context.Background(), "test-account", "owner", "", "", "", false)
_, tinyNet, err := net.ParseCIDR("100.64.0.0/30")
require.NoError(t, err)
account.Network.Net = *tinyNet
require.NoError(t, manager.Store.SaveAccount(context.Background(), account))
addPeer := func(hostname string) error {
wgKey, err := wgtypes.GenerateKey()
require.NoError(t, err)
_, _, _, _, err = manager.AddPeer(context.Background(), "", "", "owner", &nbpeer.Peer{
Key: wgKey.PublicKey().String(),
Meta: nbpeer.PeerSystemMeta{Hostname: hostname, OS: "linux"},
}, false)
return err
}
require.NoError(t, addPeer("host-1"))
require.NoError(t, addPeer("host-2"))
err = addPeer("host-3")
require.Error(t, err, "no free IP left, registration must fail")
peers, err := manager.Store.GetAccountPeers(context.Background(), store.LockingStrengthNone, account.Id, "", "", "")
require.NoError(t, err)
assert.Len(t, peers, 2)
}
@@ -746,6 +746,7 @@ func (s *SqlStore) getAccount(ctx context.Context, accountID string) (*types.Acc
settings_routing_peer_dns_resolution_enabled, settings_dns_domain, settings_network_range,
settings_network_range_v6, settings_ipv6_enabled_groups, settings_lazy_connection_enabled,
settings_local_mfa_enabled, settings_metrics_push_enabled, settings_agent_network_only,
settings_peer_hostname_collision_rejected,
settings_dashboard_features, settings_auto_update_version, settings_auto_update_always,
settings_peer_expose_enabled, settings_peer_expose_groups,
-- Embedded ExtraSettings
@@ -771,6 +772,7 @@ func (s *SqlStore) getAccount(ctx context.Context, accountID string) (*types.Acc
sLazyConnectionEnabled sql.NullBool
sLocalMFAEnabled sql.NullBool
sMetricsPushEnabled sql.NullBool
sPeerHostnameCollisionRejected sql.NullBool
sAgentNetworkOnly sql.NullBool
sDashboardFeatures sql.NullString
autoUpdateVersion sql.NullString
@@ -800,6 +802,7 @@ func (s *SqlStore) getAccount(ctx context.Context, accountID string) (*types.Acc
&sRoutingPeerDNSResolutionEnabled, &sDNSDomain, &sNetworkRange,
&sNetworkRangeV6, &sIPv6EnabledGroups, &sLazyConnectionEnabled,
&sLocalMFAEnabled, &sMetricsPushEnabled, &sAgentNetworkOnly,
&sPeerHostnameCollisionRejected,
&sDashboardFeatures, &autoUpdateVersion, &autoUpdateAlways,
&peerExposeEnabled, &peerExposeGroups,
&sExtraPeerApprovalEnabled, &sExtraUserApprovalRequired,
@@ -870,6 +873,9 @@ func (s *SqlStore) getAccount(ctx context.Context, accountID string) (*types.Acc
if sMetricsPushEnabled.Valid {
account.Settings.MetricsPushEnabled = sMetricsPushEnabled.Bool
}
if sPeerHostnameCollisionRejected.Valid {
account.Settings.PeerHostnameCollisionRejected = sPeerHostnameCollisionRejected.Bool
}
if sAgentNetworkOnly.Valid {
account.Settings.AgentNetworkOnly = sAgentNetworkOnly.Bool
}
@@ -288,7 +288,7 @@ func Test_AccountSettings_SaveAndRetrieve(t *testing.T) {
settings := types.Settings{}
numOfExportedFields, err := populateFields.PopulateAll(reflect.ValueOf(&settings).Elem())
assert.NoError(t, err)
assert.Equal(t, 27, numOfExportedFields)
assert.Equal(t, 28, numOfExportedFields)
account.Settings = &settings
err = store.SaveAccount(context.Background(), account)
+4 -1
View File
@@ -715,12 +715,15 @@ func (s *SqlStore) GetPeerIdByLabel(ctx context.Context, lockStrength LockingStr
Where("account_id = ? AND dns_label = ?", accountID, hostname).
Limit(1).
Scan(&peerID)
if result.Error != nil {
return "", result.Error
}
if peerID == "" {
return "", gorm.ErrRecordNotFound
}
return peerID, result.Error
return peerID, nil
}
// GetEmbeddedProxyPeerIDsByCluster returns peer IDs of all embedded proxy peers
+5
View File
@@ -76,6 +76,10 @@ type Settings struct {
// MetricsPushEnabled globally enables or disables client metrics push for the account
MetricsPushEnabled bool `gorm:"default:false"`
// PeerHostnameCollisionRejected rejects registering or renaming a peer to a hostname
// already used by another peer of the account instead of suffixing it
PeerHostnameCollisionRejected bool `gorm:"default:false"`
// AgentNetworkOnly limits the dashboard to the Agent Network surface for this account.
// Set for accounts created via netbird.ai signups; users can disable it later.
AgentNetworkOnly bool `gorm:"default:false"`
@@ -123,6 +127,7 @@ func (s *Settings) Copy() *Settings {
AutoUpdateAlways: s.AutoUpdateAlways,
IPv6EnabledGroups: slices.Clone(s.IPv6EnabledGroups),
MetricsPushEnabled: s.MetricsPushEnabled,
PeerHostnameCollisionRejected: s.PeerHostnameCollisionRejected,
AgentNetworkOnly: s.AgentNetworkOnly,
EmbeddedIdpEnabled: s.EmbeddedIdpEnabled,
LocalAuthDisabled: s.LocalAuthDisabled,
+4
View File
@@ -380,6 +380,10 @@ components:
description: Enables or disables client metrics push for all peers in the account
type: boolean
example: false
peer_hostname_collision_rejected:
description: When enabled, registering or renaming a peer to a hostname already used by another peer in the account fails instead of receiving an IP-based suffix. Applies to ephemeral peers as well.
type: boolean
example: false
agent_network_only:
description: Limits the dashboard to the Agent Network surface for this account. Set for accounts created via netbird.ai signups and can be disabled later. Enabling this requires dashboard_features.agent_network to be true in the same request.
type: boolean
+3
View File
@@ -1693,6 +1693,9 @@ type AccountSettings struct {
// PeerExposeGroups Limits which peer groups are allowed to expose services. If empty, all peers are allowed when peer expose is enabled.
PeerExposeGroups []string `json:"peer_expose_groups"`
// PeerHostnameCollisionRejected When enabled, registering or renaming a peer to a hostname already used by another peer in the account fails instead of receiving an IP-based suffix. Applies to ephemeral peers as well.
PeerHostnameCollisionRejected *bool `json:"peer_hostname_collision_rejected,omitempty"`
// PeerInactivityExpiration Period of time of inactivity after which peer session expires (seconds).
PeerInactivityExpiration int `json:"peer_inactivity_expiration"`