diff --git a/management/internals/shared/grpc/conversion.go b/management/internals/shared/grpc/conversion.go index f9ce43796..bdb4c8cf4 100644 --- a/management/internals/shared/grpc/conversion.go +++ b/management/internals/shared/grpc/conversion.go @@ -47,6 +47,10 @@ func init() { precomputedDeprecatedRemotePeersConstraint = constraint } +// toNetbirdConfig converts the server configuration to the wire representation. It returns +// nil when no server config is set (the fan-out network-map path) because clients treat any +// non-nil config as authoritative: a config without a relay section is interpreted as relay +// disabled and wipes the clients' relay URLs. func toNetbirdConfig(config *nbconfig.Config, turnCredentials *Token, relayToken *Token, extraSettings *types.ExtraSettings, settings *types.Settings) *proto.NetbirdConfig { if config == nil { return nil diff --git a/management/internals/shared/grpc/conversion_test.go b/management/internals/shared/grpc/conversion_test.go index 01a67e4fa..c81bef25c 100644 --- a/management/internals/shared/grpc/conversion_test.go +++ b/management/internals/shared/grpc/conversion_test.go @@ -8,11 +8,13 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/management/internals/controllers/network_map" "github.com/netbirdio/netbird/management/internals/controllers/network_map/controller/cache" nbconfig "github.com/netbirdio/netbird/management/internals/server/config" + "github.com/netbirdio/netbird/management/server/types" ) func TestToProtocolDNSConfigWithCache(t *testing.T) { @@ -263,3 +265,39 @@ func TestEncodeSessionExpiresAt(t *testing.T) { assert.True(t, got.AsTime().Equal(deadline)) }) } + +// TestToNetbirdConfig_RelayInvariant guards against the v0.74.0 relay-wipe regression. +// Clients treat any non-nil NetbirdConfig as authoritative and interpret a missing relay +// section as relay disabled, wiping their relay URLs. toNetbirdConfig must therefore +// return nil when no server config is set (the fan-out network-map path) instead of a +// partial config, and a result built from a relay-enabled config must carry the relay +// section. +func TestToNetbirdConfig_RelayInvariant(t *testing.T) { + settings := &types.Settings{MetricsPushEnabled: true} + + t.Run("nil server config returns nil config", func(t *testing.T) { + nbCfg := toNetbirdConfig(nil, nil, nil, nil, settings) + assert.Nil(t, nbCfg, "fan-out updates must not carry a partial NetbirdConfig even when settings are present") + }) + + t.Run("relay-enabled config carries relay section", func(t *testing.T) { + cfg := &nbconfig.Config{ + Stuns: []*nbconfig.Host{{Proto: nbconfig.UDP, URI: "stun:stun.example.com:3478"}}, + TURNConfig: &nbconfig.TURNConfig{ + Turns: []*nbconfig.Host{{Proto: nbconfig.UDP, URI: "turn:turn.example.com:3478", Username: "user", Password: "pass"}}, + }, + Relay: &nbconfig.Relay{Addresses: []string{"rels://relay.example.com:443"}}, + Signal: &nbconfig.Host{Proto: nbconfig.HTTP, URI: "signal.example.com:10000"}, + } + relayToken := &Token{Payload: "token-payload", Signature: "token-signature"} + + nbCfg := toNetbirdConfig(cfg, nil, relayToken, nil, settings) + require.NotNil(t, nbCfg) + require.NotNil(t, nbCfg.Relay, "non-nil NetbirdConfig must include the relay section") + assert.Equal(t, cfg.Relay.Addresses, nbCfg.Relay.Urls, "relay URLs should match the server config") + assert.Equal(t, relayToken.Payload, nbCfg.Relay.TokenPayload, "relay token payload should be set") + assert.Equal(t, relayToken.Signature, nbCfg.Relay.TokenSignature, "relay token signature should be set") + require.NotNil(t, nbCfg.Metrics) + assert.True(t, nbCfg.Metrics.Enabled, "metrics flag should carry the settings value") + }) +} diff --git a/management/server/peer_test.go b/management/server/peer_test.go index 6c243c4c7..d471a1302 100644 --- a/management/server/peer_test.go +++ b/management/server/peer_test.go @@ -1048,11 +1048,7 @@ func testUpdateAccountPeers(t *testing.T) { for _, channel := range peerChannels { update := <-channel - 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.Nil(t, update.Update.NetbirdConfig, "fan-out updates must not carry a NetbirdConfig; clients treat a config without relay as relay disabled and wipe their relay URLs") assert.Equal(t, tc.peers, len(update.Update.NetworkMap.RemotePeers)) assert.Equal(t, tc.peers*2, len(update.Update.NetworkMap.FirewallRules)) }