mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-24 07:39:07 +02:00
[client] Provision the peer identity under the config lock (review item)
Login took the authoritative update-settings and privilege decisions under guardedConfigMu, then released it and called getConfig, which mints the peer's identity and writes the config out. Between that read and that write, a SetConfig holding the same lock could land a change and answer its caller — and then be overwritten by the config the login had already read. The window is narrow: getConfig only writes when the profile has no identity or no file, so in practice a first login racing a settings change on the same profile. It is also narrower than before this branch, where the write happened inside the reader on every read that filled in a default. Provisioning now runs where the decision it belongs to runs: at the end of authorizeAndPrepareLogin, with the lock already held, next to persistLoginOverrides, which writes there too. No lock is taken that was not held before, so the documented guardedConfigMu-then-mutex order is untouched. getConfig keeps its behaviour by calling the same extracted helper; on the login path it now finds the identity already there and writes nothing. The other callers are unchanged, and still provision outside any lock — a concurrent SetConfig is not part of their flow. Reported by cubic on the PR.
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
||||
)
|
||||
|
||||
// The daemon provisions the peer's identity and persists it, because a key that
|
||||
// stayed in memory would come back different on the next start and register a
|
||||
// second peer. Provisioning is idempotent: a profile that already has an
|
||||
// identity keeps the one on disk.
|
||||
func TestProvisionProfileIdentity(t *testing.T) {
|
||||
origDir := profilemanager.DefaultConfigPathDir
|
||||
origPath := profilemanager.DefaultConfigPath
|
||||
t.Cleanup(func() {
|
||||
profilemanager.DefaultConfigPathDir = origDir
|
||||
profilemanager.DefaultConfigPath = origPath
|
||||
})
|
||||
|
||||
dir := t.TempDir()
|
||||
profilemanager.DefaultConfigPathDir = dir
|
||||
profilemanager.DefaultConfigPath = filepath.Join(dir, "default.json")
|
||||
|
||||
activeProf := &profilemanager.ActiveProfileState{ID: "default"}
|
||||
|
||||
t.Run("a profile with no file is provisioned and written", func(t *testing.T) {
|
||||
_, err := os.Stat(profilemanager.DefaultConfigPath)
|
||||
require.True(t, os.IsNotExist(err), "the fixture starts without a config file")
|
||||
|
||||
config, existed, err := provisionProfileIdentity(activeProf)
|
||||
require.NoError(t, err)
|
||||
require.False(t, existed, "the file was reported as pre-existing")
|
||||
require.NotEmpty(t, config.PrivateKey)
|
||||
|
||||
stored, err := profilemanager.GetExistingConfig(profilemanager.DefaultConfigPath)
|
||||
require.NoError(t, err, "provisioning did not write the config out")
|
||||
require.Equal(t, config.PrivateKey, stored.PrivateKey, "the persisted identity is not the one returned")
|
||||
require.NotEmpty(t, stored.SSHKey)
|
||||
})
|
||||
|
||||
t.Run("a second call keeps the identity on disk", func(t *testing.T) {
|
||||
before, err := profilemanager.GetExistingConfig(profilemanager.DefaultConfigPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
config, existed, err := provisionProfileIdentity(activeProf)
|
||||
require.NoError(t, err)
|
||||
require.True(t, existed)
|
||||
require.Equal(t, before.PrivateKey, config.PrivateKey, "provisioning minted a second identity")
|
||||
|
||||
after, err := profilemanager.GetExistingConfig(profilemanager.DefaultConfigPath)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, before.PrivateKey, after.PrivateKey, "provisioning rewrote the stored identity")
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user