Repository: netbirdio/netbird
Pull request #3798: [management] fix a bug with missed extra dns labels for a new peer
Author: login: crn4
Created: 2025-05-07T06:59:51Z | Merged: 2025-05-14T15:50:21Z

## Describe your changes
during a registration of a new peer we miss to add extra dns labels to peer object
## Issue ticket number and link

## Stack

<!-- branch-stack -->

### Checklist
- [X] Is it a bug fix
- [ ] Is a typo/documentation fix
- [ ] Is a feature enhancement
- [ ] It is a refactor
- [ ] Created tests that fail without the change (if possible)
- [ ] Extended the README / documentation, if necessary



Diff:
diff --git a/management/domain/validate.go b/management/domain/validate.go
index a42aebe6fb3..bf2af7116fc 100644
--- a/management/domain/validate.go
+++ b/management/domain/validate.go
@@ -8,6 +8,8 @@ import (
 
 const maxDomains = 32
 
+var domainRegex = regexp.MustCompile(`^(?:\*\.)?(?:(?:xn--)?[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?\.)*(?:xn--)?[a-zA-Z0-9](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?$`)
+
 // ValidateDomains checks if each domain in the list is valid and returns a punycode-encoded DomainList.
 func ValidateDomains(domains []string) (List, error) {
 	if len(domains) == 0 {
@@ -17,8 +19,6 @@ func ValidateDomains(domains []string) (List, error) {
 		return nil, fmt.Errorf("domains list exceeds maximum allowed domains: %d", maxDomains)
 	}
 
-	domainRegex := regexp.MustCompile(`^(?:\*\.)?(?:(?:xn--)?[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?\.)*(?:xn--)?[a-zA-Z0-9](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?$`)
-
 	var domainList List
 
 	for _, d := range domains {
@@ -37,27 +37,20 @@ func ValidateDomains(domains []string) (List, error) {
 	return domainList, nil
 }
 
-// ValidateDomainsStrSlice checks if each domain in the list is valid
-func ValidateDomainsStrSlice(domains []string) ([]string, error) {
+// ValidateDomainsList checks if each domain in the list is valid
+func ValidateDomainsList(domains []string) error {
 	if len(domains) == 0 {
-		return nil, nil
+		return nil
 	}
 	if len(domains) > maxDomains {
-		return nil, fmt.Errorf("domains list exceeds maximum allowed domains: %d", maxDomains)
+		return fmt.Errorf("domains list exceeds maximum allowed domains: %d", maxDomains)
 	}
 
-	domainRegex := regexp.MustCompile(`^(?:\*\.)?(?:(?:xn--)?[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?\.)*(?:xn--)?[a-zA-Z0-9](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?$`)
-
-	var domainList []string
-
 	for _, d := range domains {
 		d := strings.ToLower(d)
-
 		if !domainRegex.MatchString(d) {
-			return domainList, fmt.Errorf("invalid domain format: %s", d)
+			return fmt.Errorf("invalid domain format: %s", d)
 		}
-
-		domainList = append(domainList, d)
 	}
-	return domainList, nil
+	return nil
 }
diff --git a/management/domain/validate_test.go b/management/domain/validate_test.go
index c9c042d9dbf..30efcd9a95f 100644
--- a/management/domain/validate_test.go
+++ b/management/domain/validate_test.go
@@ -97,110 +97,89 @@ func TestValidateDomains(t *testing.T) {
 	}
 }
 
-// TestValidateDomainsStrSlice tests the ValidateDomainsStrSlice function.
-func TestValidateDomainsStrSlice(t *testing.T) {
-	// Generate a slice of valid domains up to maxDomains
+func TestValidateDomainsList(t *testing.T) {
 	validDomains := make([]string, maxDomains)
-	for i := 0; i < maxDomains; i++ {
+	for i := range maxDomains {
 		validDomains[i] = fmt.Sprintf("example%d.com", i)
 	}
 
 	tests := []struct {
-		name     string
-		domains  []string
-		expected []string
-		wantErr  bool
+		name    string
+		domains []string
+		wantErr bool
 	}{
 		{
-			name:     "Empty list",
-			domains:  nil,
-			expected: nil,
-			wantErr:  false,
+			name:    "Empty list",
+			domains: nil,
+			wantErr: false,
 		},
 		{
-			name:     "Single valid ASCII domain",
-			domains:  []string{"sub.ex-ample.com"},
-			expected: []string{"sub.ex-ample.com"},
-			wantErr:  false,
+			name:    "Single valid ASCII domain",
+			domains: []string{"sub.ex-ample.com"},
+			wantErr: false,
 		},
 		{
-			name:     "Underscores in labels",
-			domains:  []string{"_jabber._tcp.gmail.com"},
-			expected: []string{"_jabber._tcp.gmail.com"},
-			wantErr:  false,
+			name:    "Underscores in labels",
+			domains: []string{"_jabber._tcp.gmail.com"},
+			wantErr: false,
 		},
 		{
 			// Unlike ValidateDomains (which converts to punycode),
 			// ValidateDomainsStrSlice will fail on non-ASCII domain chars.
-			name:     "Unicode domain fails (no punycode conversion)",
-			domains:  []string{"münchen.de"},
-			expected: nil,
-			wantErr:  true,
+			name:    "Unicode domain fails (no punycode conversion)",
+			domains: []string{"münchen.de"},
+			wantErr: true,
 		},
 		{
-			name:     "Invalid domain format - leading dash",
-			domains:  []string{"-example.com"},
-			expected: nil,
-			wantErr:  true,
+			name:    "Invalid domain format - leading dash",
+			domains: []string{"-example.com"},
+			wantErr: true,
 		},
 		{
-			name:     "Invalid domain format - trailing dash",
-			domains:  []string{"example-.com"},
-			expected: nil,
-			wantErr:  true,
+			name:    "Invalid domain format - trailing dash",
+			domains: []string{"example-.com"},
+			wantErr: true,
 		},
 		{
-			// The function stops on the first invalid domain and returns an error,
-			// so only the first domain is definitely valid, but the second is invalid.
-			name:     "Multiple domains with a valid one, then invalid",
-			domains:  []string{"google.com", "invalid_domain.com-"},
-			expected: []string{"google.com"},
-			wantErr:  true,
+			name:    "Multiple domains with a valid one, then invalid",
+			domains: []string{"google.com", "invalid_domain.com-"},
+			wantErr: true,
 		},
 		{
-			name:     "Valid wildcard domain",
-			domains:  []string{"*.example.com"},
-			expected: []string{"*.example.com"},
-			wantErr:  false,
+			name:    "Valid wildcard domain",
+			domains: []string{"*.example.com"},
+			wantErr: false,
 		},
 		{
-			name:     "Wildcard with leading dot - invalid",
-			domains:  []string{".*.example.com"},
-			expected: nil,
-			wantErr:  true,
+			name:    "Wildcard with leading dot - invalid",
+			domains: []string{".*.example.com"},
+			wantErr: true,
 		},
 		{
-			name:     "Invalid wildcard with multiple asterisks",
-			domains:  []string{"a.*.example.com"},
-			expected: nil,
-			wantErr:  true,
+			name:    "Invalid wildcard with multiple asterisks",
+			domains: []string{"a.*.example.com"},
+			wantErr: true,
 		},
 		{
-			name:     "Exactly maxDomains items (valid)",
-			domains:  validDomains,
-			expected: validDomains,
-			wantErr:  false,
+			name:    "Exactly maxDomains items (valid)",
+			domains: validDomains,
+			wantErr: false,
 		},
 		{
-			name:     "Exceeds maxDomains items",
-			domains:  append(validDomains, "extra.com"),
-			expected: nil,
-			wantErr:  true,
+			name:    "Exceeds maxDomains items",
+			domains: append(validDomains, "extra.com"),
+			wantErr: true,
 		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			got, err := ValidateDomainsStrSlice(tt.domains)
-			// Check if we got an error where expected
+			err := ValidateDomainsList(tt.domains)
 			if tt.wantErr {
 				assert.Error(t, err)
 			} else {
 				assert.NoError(t, err)
 			}
-
-			// Compare the returned domains to what we expect
-			assert.Equal(t, tt.expected, got)
 		})
 	}
 }
diff --git a/management/server/peer.go b/management/server/peer.go
index 9ff80442eef..8bc6cdb055d 100644
--- a/management/server/peer.go
+++ b/management/server/peer.go
@@ -556,6 +556,10 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s
 			return fmt.Errorf("failed to get free IP: %w", err)
 		}
 
+		if err := domain.ValidateDomainsList(peer.ExtraDNSLabels); err != nil {
+			return status.Errorf(status.InvalidArgument, "invalid extra DNS labels: %v", err)
+		}
+
 		registrationTime := time.Now().UTC()
 		newPeer = &nbpeer.Peer{
 			ID:                          xid.New().String(),
@@ -767,10 +771,11 @@ func (am *DefaultAccountManager) handlePeerLoginNotFound(ctx context.Context, lo
 		// we couldn't find this peer by its public key which can mean that peer hasn't been registered yet.
 		// Try registering it.
 		newPeer := &nbpeer.Peer{
-			Key:      login.WireGuardPubKey,
-			Meta:     login.Meta,
-			SSHKey:   login.SSHKey,
-			Location: nbpeer.Location{ConnectionIP: login.ConnectionIP},
+			Key:            login.WireGuardPubKey,
+			Meta:           login.Meta,
+			SSHKey:         login.SSHKey,
+			Location:       nbpeer.Location{ConnectionIP: login.ConnectionIP},
+			ExtraDNSLabels: login.ExtraDNSLabels,
 		}
 
 		return am.AddPeer(ctx, login.SetupKey, login.UserID, newPeer)
@@ -875,16 +880,6 @@ func (am *DefaultAccountManager) LoginPeer(ctx context.Context, login types.Peer
 			return status.Errorf(status.PreconditionFailed, "couldn't login peer: setup key doesn't allow extra DNS labels")
 		}
 
-		extraLabels, err := domain.ValidateDomainsStrSlice(login.ExtraDNSLabels)
-		if err != nil {
-			return status.Errorf(status.InvalidArgument, "invalid extra DNS labels: %v", err)
-		}
-
-		if !slices.Equal(peer.ExtraDNSLabels, extraLabels) {
-			peer.ExtraDNSLabels = extraLabels
-			shouldStorePeer = true
-		}
-
 		if shouldStorePeer {
 			if err = transaction.SavePeer(ctx, store.LockingStrengthUpdate, accountID, peer); err != nil {
 				return err
diff --git a/management/server/peer_test.go b/management/server/peer_test.go
index 406c3e49eac..9c1de0659a6 100644
--- a/management/server/peer_test.go
+++ b/management/server/peer_test.go
@@ -10,6 +10,7 @@ import (
 	"net/netip"
 	"os"
 	"runtime"
+	"strings"
 	"testing"
 	"time"
 
@@ -1290,15 +1291,21 @@ func Test_RegisterPeerByUser(t *testing.T) {
 		Status:     &nbpeer.PeerStatus{Connected: false, LastSeen: time.Now()},
 		SSHEnabled: false,
 		LastLogin:  util.ToPtr(time.Now()),
+		ExtraDNSLabels: []string{
+			"extraLabel1",
+			"extraLabel2",
+		},
 	}
 
 	addedPeer, _, _, err := am.AddPeer(context.Background(), "", existingUserID, newPeer)
 	require.NoError(t, err)
+	assert.Equal(t, newPeer.ExtraDNSLabels, addedPeer.ExtraDNSLabels)
 
 	peer, err := s.GetPeerByPeerPubKey(context.Background(), store.LockingStrengthShare, addedPeer.Key)
 	require.NoError(t, err)
 	assert.Equal(t, peer.AccountID, existingAccountID)
 	assert.Equal(t, peer.UserID, existingUserID)
+	assert.Equal(t, newPeer.ExtraDNSLabels, peer.ExtraDNSLabels)
 
 	account, err := s.GetAccount(context.Background(), existingAccountID)
 	require.NoError(t, err)
@@ -1339,15 +1346,12 @@ func Test_RegisterPeerBySetupKey(t *testing.T) {
 	assert.NoError(t, err)
 
 	existingAccountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b"
-	existingSetupKeyID := "A2C8E62B-38F5-4553-B31E-DD66C696CEBB"
 
 	_, err = s.GetAccount(context.Background(), existingAccountID)
 	require.NoError(t, err)
 
-	newPeer := &nbpeer.Peer{
-		ID:        xid.New().String(),
+	newPeerTemplate := &nbpeer.Peer{
 		AccountID: existingAccountID,
-		Key:       "newPeerKey",
 		UserID:    "",
 		IP:        net.IP{123, 123, 123, 123},
 		Meta: nbpeer.PeerSystemMeta{
@@ -1358,31 +1362,96 @@ func Test_RegisterPeerBySetupKey(t *testing.T) {
 		DNSLabel:   "newPeer.test",
 		Status:     &nbpeer.PeerStatus{Connected: false, LastSeen: time.Now()},
 		SSHEnabled: false,
+		ExtraDNSLabels: []string{
+			"extraLabel1",
+			"extraLabel2",
+		},
 	}
 
-	addedPeer, _, _, err := am.AddPeer(context.Background(), existingSetupKeyID, "", newPeer)
+	testCases := []struct {
+		name                      string
+		existingSetupKeyID        string
+		expectedGroupIDsInAccount []string
+		expectAddPeerError        bool
+		expectedErrorMsgSubstring string
+	}{
+		{
+			name:                      "Successful registration with setup key allowing extra DNS labels",
+			existingSetupKeyID:        "A2C8E62B-38F5-4553-B31E-DD66C696CEBD",
+			expectAddPeerError:        false,
+			expectedGroupIDsInAccount: []string{"cfefqs706sqkneg59g2g", "cfefqs706sqkneg59g4g"},
+		},
+		{
+			name:                      "Failed registration with setup key not allowing extra DNS labels",
+			existingSetupKeyID:        "A2C8E62B-38F5-4553-B31E-DD66C696CEBB",
+			expectAddPeerError:        true,
+			expectedErrorMsgSubstring: "setup key doesn't allow extra DNS labels",
+		},
+		{
+			name:                      "Absent setup key",
+			existingSetupKeyID:        "AAAAAAAA-38F5-4553-B31E-DD66C696CEBB",
+			expectAddPeerError:        true,
+			expectedErrorMsgSubstring: "failed adding new peer: account not found",
+		},
+	}
 
-	require.NoError(t, err)
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			currentPeer := &nbpeer.Peer{
+				ID:             xid.New().String(),
+				AccountID:      newPeerTemplate.AccountID,
+				Key:            "newPeerKey_" + xid.New().String(),
+				UserID:         newPeerTemplate.UserID,
+				IP:             newPeerTemplate.IP,
+				Meta:           newPeerTemplate.Meta,
+				Name:           newPeerTemplate.Name,
+				DNSLabel:       newPeerTemplate.DNSLabel,
+				Status:         &nbpeer.PeerStatus{Connected: false, LastSeen: time.Now()},
+				SSHEnabled:     newPeerTemplate.SSHEnabled,
+				ExtraDNSLabels: newPeerTemplate.ExtraDNSLabels,
+			}
 
-	peer, err := s.GetPeerByPeerPubKey(context.Background(), store.LockingStrengthShare, newPeer.Key)
-	require.NoError(t, err)
-	assert.Equal(t, peer.AccountID, existingAccountID)
+			addedPeer, _, _, err := am.AddPeer(context.Background(), tc.existingSetupKeyID, "", currentPeer)
 
-	account, err := s.GetAccount(context.Background(), existingAccountID)
-	require.NoError(t, err)
-	assert.Contains(t, account.Peers, addedPeer.ID)
-	assert.Contains(t, account.Groups["cfefqs706sqkneg59g2g"].Peers, addedPeer.ID)
-	assert.Contains(t, account.Groups["cfefqs706sqkneg59g4g"].Peers, addedPeer.ID)
+			if tc.expectAddPeerError {
+				require.Error(t, err, "Expected an error when adding peer with setup key: %s", tc.existingSetupKeyID)
+				assert.Contains(t, err.Error(), tc.expectedErrorMsgSubstring, "Error message mismatch")
+				return
+			}
 
-	assert.Equal(t, uint64(1), account.Network.Serial)
+			require.NoError(t, err, "Expected no error when adding peer with setup key: %s", tc.existingSetupKeyID)
+			assert.NotNil(t, addedPeer, "addedPeer should not be nil on success")
+			assert.Equal(t, currentPeer.ExtraDNSLabels, addedPeer.ExtraDNSLabels, "ExtraDNSLabels mismatch")
 
-	lastUsed, err := time.Parse("2006-01-02T15:04:05Z", "0001-01-01T00:00:00Z")
-	assert.NoError(t, err)
+			peerFromStore, err := s.GetPeerByPeerPubKey(context.Background(), store.LockingStrengthShare, currentPeer.Key)
+			require.NoError(t, err, "Failed to get peer by pub key: %s", currentPeer.Key)
+			assert.Equal(t, existingAccountID, peerFromStore.AccountID, "AccountID mismatch for peer from store")
+			assert.Equal(t, currentPeer.ExtraDNSLabels, peerFromStore.ExtraDNSLabels, "ExtraDNSLabels mismatch for peer from store")
+			assert.Equal(t, addedPeer.ID, peerFromStore.ID, "Peer ID mismatch between addedPeer and peerFromStore")
 
-	hashedKey := sha256.Sum256([]byte(existingSetupKeyID))
-	encodedHashedKey := b64.StdEncoding.EncodeToString(hashedKey[:])
-	assert.NotEqual(t, lastUsed, account.SetupKeys[encodedHashedKey].LastUsed)
-	assert.Equal(t, 1, account.SetupKeys[encodedHashedKey].UsedTimes)
+			account, err := s.GetAccount(context.Background(), existingAccountID)
+			require.NoError(t, err, "Failed to get account: %s", existingAccountID)
+			assert.Contains(t, account.Peers, addedPeer.ID, "Peer ID not found in account.Peers")
+
+			for _, groupID := range tc.expectedGroupIDsInAccount {
+				require.NotNil(t, account.Groups[groupID], "Group %s not found in account", groupID)
+				assert.Contains(t, account.Groups[groupID].Peers, addedPeer.ID, "Peer ID %s not found in group %s", addedPeer.ID, groupID)
+			}
+
+			assert.Equal(t, uint64(1), account.Network.Serial, "Network.Serial mismatch; this assumes specific initial state or increment logic.")
+
+			hashedKey := sha256.Sum256([]byte(tc.existingSetupKeyID))
+			encodedHashedKey := b64.StdEncoding.EncodeToString(hashedKey[:])
+
+			setupKeyData, ok := account.SetupKeys[encodedHashedKey]
+			require.True(t, ok, "Setup key data not found in account.SetupKeys for key ID %s (encoded: %s)", tc.existingSetupKeyID, encodedHashedKey)
+
+			var zeroTime time.Time
+			assert.NotEqual(t, zeroTime, setupKeyData.LastUsed, "Setup key LastUsed time should have been updated and not be zero.")
+
+			assert.Equal(t, 1, setupKeyData.UsedTimes, "Setup key UsedTimes should be 1 after first use.")
+		})
+	}
 
 }
 
@@ -1456,6 +1525,160 @@ func Test_RegisterPeerRollbackOnFailure(t *testing.T) {
 	assert.Equal(t, 0, account.SetupKeys[encodedHashedKey].UsedTimes)
 }
 
+func Test_LoginPeer(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		t.Skip("The SQLite store is not properly supported by Windows yet")
+	}
+
+	s, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "testdata/extended-store.sql", t.TempDir())
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer cleanup()
+
+	eventStore := &activity.InMemoryEventStore{}
+
+	metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
+	assert.NoError(t, err)
+
+	ctrl := gomock.NewController(t)
+	t.Cleanup(ctrl.Finish)
+	settingsMockManager := settings.NewMockManager(ctrl)
+	permissionsManager := permissions.NewManager(s)
+
+	am, err := BuildManager(context.Background(), s, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager)
+	assert.NoError(t, err)
+
+	existingAccountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b"
+	_, err = s.GetAccount(context.Background(), existingAccountID)
+	require.NoError(t, err, "Failed to get existing account, check testdata/extended-store.sql. Account ID: %s", existingAccountID)
+
+	baseMeta := nbpeer.PeerSystemMeta{
+		Hostname: "loginPeerHost",
+		GoOS:     "linux",
+	}
+
+	newPeerTemplate := &nbpeer.Peer{
+		AccountID: existingAccountID,
+		UserID:    "",
+		IP:        net.IP{123, 123, 123, 123},
+		Meta: nbpeer.PeerSystemMeta{
+			Hostname: "newPeer",
+			GoOS:     "linux",
+		},
+		Name:       "newPeerName",
+		DNSLabel:   "newPeer.test",
+		Status:     &nbpeer.PeerStatus{Connected: false, LastSeen: time.Now()},
+		SSHEnabled: false,
+		ExtraDNSLabels: []string{
+			"extraLabel1",
+			"extraLabel2",
+		},
+	}
+
+	testCases := []struct {
+		name                         string
+		setupKey                     string
+		wireGuardPubKey              string
+		expectExtraDNSLabelsMismatch bool
+		extraDNSLabels               []string
+		expectLoginError             bool
+		expectedErrorMsgSubstring    string
+	}{
+		{
+			name:             "Successful login with setup key",
+			setupKey:         "A2C8E62B-38F5-4553-B31E-DD66C696CEBD",
+			expectLoginError: false,
+		},
+		{
+			name:                         "Successful login with setup key with DNS labels mismatch",
+			setupKey:                     "A2C8E62B-38F5-4553-B31E-DD66C696CEBD",
+			expectExtraDNSLabelsMismatch: true,
+			extraDNSLabels:               []string{"anotherLabel1", "anotherLabel2"},
+			expectLoginError:             false,
+		},
+		{
+			name:                         "Failed login with setup key not allowing extra DNS labels",
+			setupKey:                     "A2C8E62B-38F5-4553-B31E-DD66C696CEBB",
+			expectExtraDNSLabelsMismatch: true,
+			extraDNSLabels:               []string{"anotherLabel1", "anotherLabel2"},
+			expectLoginError:             true,
+			expectedErrorMsgSubstring:    "setup key doesn't allow extra DNS labels",
+		},
+	}
+
+	for _, tc := range testCases {
+		currentWireGuardPubKey := "testPubKey_" + xid.New().String()
+
+		t.Run(tc.name, func(t *testing.T) {
+			upperKey := strings.ToUpper(tc.setupKey)
+			hashedKey := sha256.Sum256([]byte(upperKey))
+			encodedHashedKey := b64.StdEncoding.EncodeToString(hashedKey[:])
+			sk, err := s.GetSetupKeyBySecret(context.Background(), store.LockingStrengthUpdate, encodedHashedKey)
+			require.NoError(t, err, "Failed to get setup key %s from storage", tc.setupKey)
+
+			currentPeer := &nbpeer.Peer{
+				ID:         xid.New().String(),
+				AccountID:  newPeerTemplate.AccountID,
+				Key:        currentWireGuardPubKey,
+				UserID:     newPeerTemplate.UserID,
+				IP:         newPeerTemplate.IP,
+				Meta:       newPeerTemplate.Meta,
+				Name:       newPeerTemplate.Name,
+				DNSLabel:   newPeerTemplate.DNSLabel,
+				Status:     &nbpeer.PeerStatus{Connected: false, LastSeen: time.Now()},
+				SSHEnabled: newPeerTemplate.SSHEnabled,
+			}
+			// add peer manually to bypass creation during login stage
+			if sk.AllowExtraDNSLabels {
+				currentPeer.ExtraDNSLabels = newPeerTemplate.ExtraDNSLabels
+			}
+			_, _, _, err = am.AddPeer(context.Background(), tc.setupKey, "", currentPeer)
+			require.NoError(t, err, "Expected no error when adding peer with setup key: %s", tc.setupKey)
+
+			loginInput := types.PeerLogin{
+				WireGuardPubKey: currentWireGuardPubKey,
+				SSHKey:          "test-ssh-key",
+				Meta:            baseMeta,
+				UserID:          "",
+				SetupKey:        tc.setupKey,
+				ConnectionIP:    net.ParseIP("192.0.2.100"),
+			}
+
+			if tc.expectExtraDNSLabelsMismatch {
+				loginInput.ExtraDNSLabels = tc.extraDNSLabels
+			}
+
+			loggedinPeer, networkMap, postureChecks, loginErr := am.LoginPeer(context.Background(), loginInput)
+			if tc.expectLoginError {
+				require.Error(t, loginErr, "Expected an error during LoginPeer with setup key: %s", tc.setupKey)
+				assert.Contains(t, loginErr.Error(), tc.expectedErrorMsgSubstring, "Error message mismatch")
+				assert.Nil(t, loggedinPeer, "LoggedinPeer should be nil on error")
+				assert.Nil(t, networkMap, "NetworkMap should be nil on error")
+				assert.Nil(t, postureChecks, "PostureChecks should be empty or nil on error")
+				return
+			}
+
+			require.NoError(t, loginErr, "Expected no error during LoginPeer with setup key: %s", tc.setupKey)
+			assert.NotNil(t, loggedinPeer, "loggedinPeer should not be nil on success")
+			if tc.expectExtraDNSLabelsMismatch {
+				assert.NotEqual(t, tc.extraDNSLabels, loggedinPeer.ExtraDNSLabels, "ExtraDNSLabels should not match on loggedinPeer")
+				assert.Equal(t, currentPeer.ExtraDNSLabels, loggedinPeer.ExtraDNSLabels, "ExtraDNSLabels mismatch on loggedinPeer")
+			} else {
+				assert.Equal(t, currentPeer.ExtraDNSLabels, loggedinPeer.ExtraDNSLabels, "ExtraDNSLabels mismatch on loggedinPeer")
+			}
+			assert.NotNil(t, networkMap, "networkMap should not be nil on success")
+
+			assert.Equal(t, existingAccountID, loggedinPeer.AccountID, "AccountID mismatch for logged peer")
+
+			peerFromStore, err := s.GetPeerByPeerPubKey(context.Background(), store.LockingStrengthShare, loginInput.WireGuardPubKey)
+			require.NoError(t, err, "Failed to get peer by pub key: %s", loginInput.WireGuardPubKey)
+			assert.Equal(t, existingAccountID, peerFromStore.AccountID, "AccountID mismatch for peer from store")
+			assert.Equal(t, loggedinPeer.ID, peerFromStore.ID, "Peer ID mismatch between loggedinPeer and peerFromStore")
+		})
+	}
+}
+
 func TestPeerAccountPeersUpdate(t *testing.T) {
 	manager, account, peer1, peer2, peer3 := setupNetworkMapTest(t)
 
diff --git a/management/server/testdata/extended-store.sql b/management/server/testdata/extended-store.sql
index 7900dabf5ad..324bf629300 100644
--- a/management/server/testdata/extended-store.sql
+++ b/management/server/testdata/extended-store.sql
@@ -1,5 +1,5 @@
 CREATE TABLE `accounts` (`id` text,`created_by` text,`created_at` datetime,`domain` text,`domain_category` text,`is_domain_primary_account` numeric,`network_identifier` text,`network_net` text,`network_dns` text,`network_serial` integer,`dns_settings_disabled_management_groups` text,`settings_peer_login_expiration_enabled` numeric,`settings_peer_login_expiration` integer,`settings_regular_users_view_blocked` numeric,`settings_groups_propagation_enabled` numeric,`settings_jwt_groups_enabled` numeric,`settings_jwt_groups_claim_name` text,`settings_jwt_allow_groups` text,`settings_extra_peer_approval_enabled` numeric,`settings_extra_integrated_validator_groups` text,PRIMARY KEY (`id`));
-CREATE TABLE `setup_keys` (`id` text,`account_id` text,`key` text,`name` text,`type` text,`created_at` datetime,`expires_at` datetime,`updated_at` datetime,`revoked` numeric,`used_times` integer,`last_used` datetime DEFAULT NULL,`auto_groups` text,`usage_limit` integer,`ephemeral` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_setup_keys_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
+CREATE TABLE `setup_keys` (`id` text,`account_id` text,`key` text,`name` text,`type` text,`created_at` datetime,`expires_at` datetime,`updated_at` datetime,`revoked` numeric,`used_times` integer,`last_used` datetime DEFAULT NULL,`auto_groups` text,`usage_limit` integer,`ephemeral` numeric,`allow_extra_dns_labels` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_setup_keys_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
 CREATE TABLE `peers` (`id` text,`account_id` text,`key` text,`setup_key` text,`ip` text,`meta_hostname` text,`meta_go_os` text,`meta_kernel` text,`meta_core` text,`meta_platform` text,`meta_os` text,`meta_os_version` text,`meta_wt_version` text,`meta_ui_version` text,`meta_kernel_version` text,`meta_network_addresses` text,`meta_system_serial_number` text,`meta_system_product_name` text,`meta_system_manufacturer` text,`meta_environment` text,`meta_files` text,`name` text,`dns_label` text,`peer_status_last_seen` datetime,`peer_status_connected` numeric,`peer_status_login_expired` numeric,`peer_status_requires_approval` numeric,`user_id` text,`ssh_key` text,`ssh_enabled` numeric,`login_expiration_enabled` numeric,`last_login` datetime,`created_at` datetime,`ephemeral` numeric,`location_connection_ip` text,`location_country_code` text,`location_city_name` text,`location_geo_name_id` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_peers_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
 CREATE TABLE `users` (`id` text,`account_id` text,`role` text,`is_service_user` numeric,`non_deletable` numeric,`service_user_name` text,`auto_groups` text,`blocked` numeric,`last_login` datetime DEFAULT NULL,`created_at` datetime,`issued` text DEFAULT "api",`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_users_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
 CREATE TABLE `personal_access_tokens` (`id` text,`user_id` text,`name` text,`hashed_token` text,`expiration_date` datetime,`created_by` text,`created_at` datetime,`last_used` datetime,PRIMARY KEY (`id`),CONSTRAINT `fk_users_pa_ts_g` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`));
@@ -26,8 +26,9 @@ CREATE INDEX `idx_name_server_groups_account_id` ON `name_server_groups`(`accoun
 CREATE INDEX `idx_posture_checks_account_id` ON `posture_checks`(`account_id`);
 
 INSERT INTO accounts VALUES('bf1c8084-ba50-4ce7-9439-34653001fc3b','edafee4e-63fb-11ec-90d6-0242ac120003','2024-10-02 16:01:38.210000+02:00','test.com','private',1,'af1c8024-ha40-4ce2-9418-34653101fc3c','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',0,86400000000000,0,0,0,'',NULL,NULL,NULL);
-INSERT INTO setup_keys VALUES('A2C8E62B-38F5-4553-B31E-DD66C696CEBB','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBB','Default key','reusable','2021-08-19 20:46:20.005936822+02:00','2321-09-18 20:46:20.005936822+02:00','2021-08-19 20:46:20.005936822+02:00',0,0,NULL,'["cfefqs706sqkneg59g2g"]',0,0);
-INSERT INTO setup_keys VALUES('A2C8E62B-38F5-4553-B31E-DD66C696CEBC','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBC','Faulty key with non existing group','reusable','2021-08-19 20:46:20.005936822+02:00','2321-09-18 20:46:20.005936822+02:00','2021-08-19 20:46:20.005936822+02:00',0,0,NULL,'["abcd"]',0,0);
+INSERT INTO setup_keys VALUES('A2C8E62B-38F5-4553-B31E-DD66C696CEBB','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBB','Default key','reusable','2021-08-19 20:46:20.005936822+02:00','2321-09-18 20:46:20.005936822+02:00','2021-08-19 20:46:20.005936822+02:00',0,0,NULL,'["cfefqs706sqkneg59g2g"]',0,0,0);
+INSERT INTO setup_keys VALUES('A2C8E62B-38F5-4553-B31E-DD66C696CEBD','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBD','Default key with extra DNS labels','reusable','2021-08-19 20:46:20.005936822+02:00','2321-09-18 20:46:20.005936822+02:00','2021-08-19 20:46:20.005936822+02:00',0,0,NULL,'["cfefqs706sqkneg59g2g"]',0,0,1);
+INSERT INTO setup_keys VALUES('A2C8E62B-38F5-4553-B31E-DD66C696CEBC','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBC','Faulty key with non existing group','reusable','2021-08-19 20:46:20.005936822+02:00','2321-09-18 20:46:20.005936822+02:00','2021-08-19 20:46:20.005936822+02:00',0,0,NULL,'["abcd"]',0,0,0);
 INSERT INTO users VALUES('edafee4e-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','admin',0,0,'','["cfefqs706sqkneg59g3g"]',0,NULL,'2024-10-02 16:01:38.210678+02:00','api',0,'');
 INSERT INTO users VALUES('f4f6d672-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','user',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.210678+02:00','api',0,'');
 INSERT INTO personal_access_tokens VALUES('9dj38s35-63fb-11ec-90d6-0242ac120003','f4f6d672-63fb-11ec-90d6-0242ac120003','','SoMeHaShEdToKeN','2023-02-27 00:00:00+00:00','user','2023-01-01 00:00:00+00:00','2023-02-01 00:00:00+00:00');


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This pull request fixes a bug where extra DNS labels were not set on a new peer during registration.  
- Fixes missing assignment of extra DNS labels  
- Updates the peer creation logic in the account manager


<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**management/server/peer.go:774**
* Add unit tests for peer registration to ensure that the ExtraDNSLabels field is correctly set on new peer objects. This will help catch any regressions in future changes.
```
ExtraDNSLabels: login.ExtraDNSLabels,
```
**management/server/peer.go:774**
* Verify that the data type of login.ExtraDNSLabels matches the expected type in the nbpeer.Peer struct. If any transformation is needed due to type differences, apply it to ensure compatibility.
```
ExtraDNSLabels: login.ExtraDNSLabels,
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T07:00:10Z
user: login: bcmmbaga
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T14:23:05Z
user: login: bcmmbaga
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T15:25:44Z

Review comments:
diff_hunk: @@ -875,16 +875,6 @@ func (am *DefaultAccountManager) LoginPeer(ctx context.Context, login types.Peer
 			return status.Errorf(status.PreconditionFailed, "couldn't login peer: setup key doesn't allow extra DNS labels")
 		}
 
-		extraLabels, err := domain.ValidateDomainsStrSlice(login.ExtraDNSLabels)
-		if err != nil {
-			return status.Errorf(status.InvalidArgument, "invalid extra DNS labels: %v", err)
-		}
path: management/server/peer.go
user: login: bcmmbaga
body: We should not always trust the extra labels at login. We should validate them
created_at: 2025-05-09T14:10:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -875,16 +875,6 @@ func (am *DefaultAccountManager) LoginPeer(ctx context.Context, login types.Peer
 			return status.Errorf(status.PreconditionFailed, "couldn't login peer: setup key doesn't allow extra DNS labels")
 		}
 
-		extraLabels, err := domain.ValidateDomainsStrSlice(login.ExtraDNSLabels)
-		if err != nil {
-			return status.Errorf(status.InvalidArgument, "invalid extra DNS labels: %v", err)
-		}
-
-		if !slices.Equal(peer.ExtraDNSLabels, extraLabels) {
-			peer.ExtraDNSLabels = extraLabels
-			shouldStorePeer = true
-		}
path: management/server/peer.go
user: login: bcmmbaga
body: Removing this will prevent the extra DNS labels from being updated. Eg. if you run` netbird down` and then `netbird up --extra-dns-labels {new_labels}`, the peer will ignore the new labels and keep the old ones
created_at: 2025-05-09T14:16:09Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-05-09T15:06:02Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=netbirdio_netbird&pullRequest=3798) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=netbirdio_netbird&pullRequest=3798&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=netbirdio_netbird&pullRequest=3798&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=netbirdio_netbird&pullRequest=3798&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=netbirdio_netbird&pullRequest=3798&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=netbirdio_netbird&pullRequest=3798&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=netbirdio_netbird&pullRequest=3798)


author_association: NONE