Files
netbird/client/internal/profilemanager/config_probe_test.go
T
riccardom 319f47718a [client] Keep the mTLS pair off the gate's dry run (review item)
WouldChange runs the real apply() against a throwaway copy, and apply() loads
the client mTLS certificate and key from disk whenever the config names them.
So every gated SetConfig and Login read the pair — twice per request, once for
the normalization pass and once for the verdict — including requests that were
about to be refused or that changed nothing, and logged an error per request
when the files were missing. The gate used to be presence-based and never
called apply(), so this was new work on a request path.

The loaded pair feeds the connection and never the comparison: nothing in
apply() reads it back, and it does not move the `updated` verdict. A config
built only to be compared against now says so, and apply() skips the load for
it.

Reported by cubic on the PR.
2026-09-23 12:32:41 +02:00

97 lines
3.2 KiB
Go

package profilemanager
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/require"
)
// writeCertPair writes a throwaway certificate and key, so apply() has
// something real to load rather than a missing file it would only log about.
func writeCertPair(t *testing.T) (certPath, keyPath string) {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "probe-test"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
}
der, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
require.NoError(t, err)
keyDER, err := x509.MarshalECPrivateKey(key)
require.NoError(t, err)
dir := t.TempDir()
certPath = filepath.Join(dir, "client.crt")
keyPath = filepath.Join(dir, "client.key")
require.NoError(t, os.WriteFile(certPath, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), 0o600))
require.NoError(t, os.WriteFile(keyPath, pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}), 0o600))
return certPath, keyPath
}
// The dry run behind the update-settings gate must not read the mTLS pair off
// disk. The loaded pair feeds the connection, never the comparison, and the
// gate runs it on every SetConfig and Login — twice per request — including the
// ones it refuses.
func TestProbeDoesNotLoadTheCertificatePair(t *testing.T) {
certPath, keyPath := writeCertPair(t)
t.Run("a real apply loads it", func(t *testing.T) {
config := newConfigSkeleton()
config.ClientCertPath, config.ClientCertKeyPath = certPath, keyPath
_, err := config.apply(ConfigInput{})
require.NoError(t, err)
require.NotNil(t, config.ClientCertKeyPair, "the connection would have no client certificate")
})
t.Run("a probe does not", func(t *testing.T) {
config := newConfigSkeleton()
config.ClientCertPath, config.ClientCertKeyPath = certPath, keyPath
config.probing = true
_, err := config.apply(ConfigInput{})
require.NoError(t, err)
require.Nil(t, config.ClientCertKeyPair, "the dry run read the certificate off disk")
})
// And the verdict is the same either way, which is the only thing the gate
// asks of the probe.
t.Run("the verdict is unaffected", func(t *testing.T) {
path := filepath.Join(t.TempDir(), "mtls.json")
_, err := UpdateOrCreateConfig(ConfigInput{
ConfigPath: path,
ManagementURL: DefaultManagementURL,
ClientCertPath: certPath,
ClientCertKeyPath: keyPath,
})
require.NoError(t, err)
stored, err := GetExistingConfig(path)
require.NoError(t, err)
changed, err := stored.WouldChange(ConfigInput{ClientCertPath: certPath, ClientCertKeyPath: keyPath})
require.NoError(t, err)
require.False(t, changed, "restating the stored certificate paths is not a change")
changed, err = stored.WouldChange(ConfigInput{ClientCertPath: filepath.Join(t.TempDir(), "other.crt")})
require.NoError(t, err)
require.True(t, changed, "a different certificate path is a change")
})
}