mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-13 08:59:08 +02:00
[management,client] Carry the debug upload destination on PeerConfig
A destination set from the dashboard never reached a connected peer. The peer fan-out that follows a settings change builds its sync with a nil server config, and toNetbirdConfig returns nil for that, so no NetbirdConfig went out at all and the peer kept whatever it had. The new URL only arrived when the peer reconnected, and until then its bundles kept going to NetBird's upload service — which is the disclosure this PR exists to close. PeerConfig is the message the fan-out already fills from the account settings, alongside the other knobs an operator flips at runtime: lazy connection, MTU, routing-peer DNS resolution, auto-update. The destination belongs with them. Sending a partial NetbirdConfig from the fan-out instead would be worse: clients treat a non-nil one as authoritative for relay and would drop their relay URLs. Start now reads the destination off the PeerConfig it was given in the previous commit, for the same reason it used to read it off NetbirdConfig: without it, a bundle collected between login and the first sync sees no destination, and the first seconds after a start are exactly when someone collects a bundle about a start that went wrong. DebugConfig is removed rather than deprecated. It was added earlier in this branch and has never been in a release, so no deployed client reads it.
This commit is contained in:
+18
-18
@@ -622,12 +622,13 @@ func (e *Engine) Start(netbirdConfig *mgmProto.NetbirdConfig, peerConfig *mgmPro
|
||||
log.Warnf("failed to populate DNS cache: %v", err)
|
||||
}
|
||||
|
||||
// The login response carries the same NetbirdConfig a sync does, but Start
|
||||
// does not run it through updateNetbirdConfig. Without this, a bundle
|
||||
// requested between login and the first sync sees no published destination
|
||||
// and falls back to the service NetBird runs, even where the deployment
|
||||
// configured its own.
|
||||
e.handleDebugUploadUpdate(netbirdConfig.GetDebug())
|
||||
// The login response carries the same PeerConfig a sync does, but Start does
|
||||
// not run it through updateNetworkMap. Without this, a bundle requested
|
||||
// between login and the first sync sees no published destination and falls
|
||||
// back to the service NetBird runs, even where the deployment configured its
|
||||
// own — and the first seconds after a start are exactly when someone collects
|
||||
// a bundle about a start that went wrong.
|
||||
e.handleDebugUploadUpdate(peerConfig)
|
||||
|
||||
e.routeManager = routemanager.NewManager(routemanager.ManagerConfig{
|
||||
Context: e.ctx,
|
||||
@@ -1171,8 +1172,6 @@ func (e *Engine) updateNetbirdConfig(wCfg *mgmProto.NetbirdConfig) error {
|
||||
|
||||
e.handleMetricsUpdate(wCfg.GetMetrics())
|
||||
|
||||
e.handleDebugUploadUpdate(wCfg.GetDebug())
|
||||
|
||||
if err := e.PopulateNetbirdConfig(wCfg, nil); err != nil {
|
||||
log.Warnf("Failed to update DNS server config: %v", err)
|
||||
}
|
||||
@@ -1251,20 +1250,19 @@ func (e *Engine) handleMetricsUpdate(config *mgmProto.MetricsConfig) {
|
||||
}
|
||||
|
||||
// handleDebugUploadUpdate records the debug-bundle destination the management
|
||||
// server published.
|
||||
// server published for this peer.
|
||||
//
|
||||
// A nil DebugConfig carries no information and is left alone: the partial
|
||||
// updates that refresh TURN and relay credentials ship a NetbirdConfig holding
|
||||
// only those fields, and treating their absent Debug as "no destination" would
|
||||
// silently drop the operator's choice on every credential refresh. An operator
|
||||
// clearing the destination is an empty UploadUrl on a full config, which does
|
||||
// reach the store below.
|
||||
func (e *Engine) handleDebugUploadUpdate(config *mgmProto.DebugConfig) {
|
||||
if config == nil {
|
||||
// A nil PeerConfig carries no information and is left alone: a partial update
|
||||
// ships a SyncResponse without one, and reading that absence as "no
|
||||
// destination" would silently drop the operator's choice. An operator clearing
|
||||
// the destination sends a PeerConfig with an empty URL, which does reach the
|
||||
// store below.
|
||||
func (e *Engine) handleDebugUploadUpdate(peerConfig *mgmProto.PeerConfig) {
|
||||
if peerConfig == nil {
|
||||
return
|
||||
}
|
||||
|
||||
url := config.GetUploadUrl()
|
||||
url := peerConfig.GetDebugBundleUploadUrl()
|
||||
e.debugUploadURL.Store(&url)
|
||||
}
|
||||
|
||||
@@ -1635,6 +1633,8 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error {
|
||||
if err := e.connMgr.UpdatedRemoteFeatureFlag(e.ctx, peerConfig.GetLazyConnectionEnabled()); err != nil {
|
||||
log.Errorf("failed to update lazy connection feature flag: %v", err)
|
||||
}
|
||||
|
||||
e.handleDebugUploadUpdate(peerConfig)
|
||||
}
|
||||
|
||||
if e.firewall != nil {
|
||||
|
||||
@@ -47,18 +47,17 @@ func TestEngineDebugUploadURL(t *testing.T) {
|
||||
e.handleDebugUploadUpdate(nil)
|
||||
assert.Empty(t, e.DebugUploadURL(), "a management server predating the field publishes none")
|
||||
|
||||
e.handleDebugUploadUpdate(&mgmProto.DebugConfig{UploadUrl: "https://upload.example.com/upload-url"})
|
||||
e.handleDebugUploadUpdate(&mgmProto.PeerConfig{DebugBundleUploadUrl: "https://upload.example.com/upload-url"})
|
||||
assert.Equal(t, "https://upload.example.com/upload-url", e.DebugUploadURL())
|
||||
|
||||
// The partial updates that refresh TURN and relay credentials carry a
|
||||
// NetbirdConfig with no Debug at all. Treating that as "no destination"
|
||||
// would drop the operator's choice on every credential refresh.
|
||||
// A partial update ships a SyncResponse with no PeerConfig at all. Treating
|
||||
// that as "no destination" would drop the operator's choice.
|
||||
e.handleDebugUploadUpdate(nil)
|
||||
assert.Equal(t, "https://upload.example.com/upload-url", e.DebugUploadURL(),
|
||||
"a partial config update must not clear the published destination")
|
||||
"a partial update must not clear the published destination")
|
||||
|
||||
// An operator that removes the destination sends an empty UploadUrl on a
|
||||
// full config, and that does reach the peer.
|
||||
e.handleDebugUploadUpdate(&mgmProto.DebugConfig{})
|
||||
// An operator that removes the destination sends a PeerConfig with an empty
|
||||
// URL, and that does reach the peer.
|
||||
e.handleDebugUploadUpdate(&mgmProto.PeerConfig{})
|
||||
assert.Empty(t, e.DebugUploadURL())
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ func ToComponentSyncResponse(
|
||||
// TODO (dmitri) consider using invariants?
|
||||
//
|
||||
enableSSH := computeSSHEnabledForPeer(components, peer)
|
||||
peerConfig := toPeerConfig(peer, components.Network, dnsName, settings, httpConfig, deviceFlowConfig, enableSSH, components.ForceRoutingPeerDNSResolution)
|
||||
peerConfig := toPeerConfig(peer, components.Network, dnsName, settings, httpConfig, deviceFlowConfig, enableSSH, components.ForceRoutingPeerDNSResolution, debugUploadDefault)
|
||||
|
||||
includeIPv6 := peer.SupportsIPv6() && peer.IPv6.IsValid()
|
||||
useSourcePrefixes := peer.SupportsSourcePrefixes()
|
||||
@@ -76,7 +76,7 @@ func ToComponentSyncResponse(
|
||||
Version: int32(sharedgrpc.ComponentNetworkMap),
|
||||
}
|
||||
|
||||
nbConfig := toNetbirdConfig(config, turnCredentials, relayCredentials, extraSettings, settings, debugUploadDefault)
|
||||
nbConfig := toNetbirdConfig(config, turnCredentials, relayCredentials, extraSettings, settings)
|
||||
resp.NetbirdConfig = integrationsConfig.ExtendNetBirdConfig(peer.ID, peerGroups, nbConfig, extraSettings)
|
||||
|
||||
// settings == nil → field stays nil → "no info in this snapshot", client
|
||||
|
||||
@@ -46,7 +46,7 @@ func init() {
|
||||
// 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 *nmdata.AccountSettingsInfo, debugUploadDefault string) *proto.NetbirdConfig {
|
||||
func toNetbirdConfig(config *nbconfig.Config, turnCredentials *Token, relayToken *Token, extraSettings *types.ExtraSettings, settings *nmdata.AccountSettingsInfo) *proto.NetbirdConfig {
|
||||
if config == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -115,19 +115,19 @@ func toNetbirdConfig(config *nbconfig.Config, turnCredentials *Token, relayToken
|
||||
}
|
||||
}
|
||||
|
||||
// Always sent, empty included: this is a full config, so the peer can tell an
|
||||
// operator clearing the destination from the partial updates that carry only
|
||||
// TURN or relay credentials and say nothing about it.
|
||||
nbConfig.Debug = &proto.DebugConfig{UploadUrl: resolveDebugUploadURL(settings, debugUploadDefault)}
|
||||
|
||||
return nbConfig
|
||||
}
|
||||
|
||||
// resolveDebugUploadURL picks the debug-bundle destination a peer is told to
|
||||
// resolveDebugUploadURL picks the debug-bundle destination this peer is told to
|
||||
// use. The account setting wins; deploymentDefault is the server-config value a
|
||||
// self-hosted install sets once for every account. Both are https-validated
|
||||
// where they are written, and neither set means the peer falls back to the
|
||||
// service NetBird runs.
|
||||
//
|
||||
// The deployment default is passed in rather than read from the server config
|
||||
// here because the peer fan-out builds its sync with a nil server config: left
|
||||
// to that, a settings change would publish an empty destination and silently
|
||||
// move the deployment's bundles back to NetBird's service.
|
||||
func resolveDebugUploadURL(settings *nmdata.AccountSettingsInfo, deploymentDefault string) string {
|
||||
if settings != nil && settings.DebugBundleUploadURL != "" {
|
||||
return settings.DebugBundleUploadURL
|
||||
@@ -135,7 +135,7 @@ func resolveDebugUploadURL(settings *nmdata.AccountSettingsInfo, deploymentDefau
|
||||
return deploymentDefault
|
||||
}
|
||||
|
||||
func toPeerConfig(peer *nmdata.Peer, network *nmdata.Network, dnsName string, settings *nmdata.AccountSettingsInfo, httpConfig *nbconfig.HttpServerConfig, deviceFlowConfig *nbconfig.DeviceAuthorizationFlow, enableSSH bool, forceRoutingPeerDNS bool) *proto.PeerConfig {
|
||||
func toPeerConfig(peer *nmdata.Peer, network *nmdata.Network, dnsName string, settings *nmdata.AccountSettingsInfo, httpConfig *nbconfig.HttpServerConfig, deviceFlowConfig *nbconfig.DeviceAuthorizationFlow, enableSSH bool, forceRoutingPeerDNS bool, debugUploadDefault string) *proto.PeerConfig {
|
||||
netmask, _ := network.Net.Mask.Size()
|
||||
fqdn := peer.FQDN(dnsName)
|
||||
|
||||
@@ -157,6 +157,7 @@ func toPeerConfig(peer *nmdata.Peer, network *nmdata.Network, dnsName string, se
|
||||
Version: settings.AutoUpdateVersion,
|
||||
AlwaysUpdate: settings.AutoUpdateAlways,
|
||||
},
|
||||
DebugBundleUploadUrl: resolveDebugUploadURL(settings, debugUploadDefault),
|
||||
}
|
||||
|
||||
if peer.SupportsIPv6() && peer.IPv6.IsValid() && network.NetV6.IP != nil {
|
||||
@@ -179,17 +180,17 @@ func ToSyncResponse(ctx context.Context, config *nbconfig.Config, httpConfig *nb
|
||||
localIsProxy := peer.ProxyMeta.Embedded
|
||||
|
||||
response := &proto.SyncResponse{
|
||||
PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, settings, httpConfig, deviceFlowConfig, networkMap.EnableSSH, networkMap.ForceRoutingPeerDNSResolution),
|
||||
PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, settings, httpConfig, deviceFlowConfig, networkMap.EnableSSH, networkMap.ForceRoutingPeerDNSResolution, debugUploadDefault),
|
||||
NetworkMap: &proto.NetworkMap{
|
||||
Serial: networkMap.Network.CurrentSerial(),
|
||||
Routes: networkmap.ToProtocolRoutes(networkMap.Routes),
|
||||
DNSConfig: networkmap.ToProtocolDNSConfig(networkMap.DNSConfig, dnsCache, dnsFwdPort),
|
||||
PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, settings, httpConfig, deviceFlowConfig, networkMap.EnableSSH, networkMap.ForceRoutingPeerDNSResolution),
|
||||
PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, settings, httpConfig, deviceFlowConfig, networkMap.EnableSSH, networkMap.ForceRoutingPeerDNSResolution, debugUploadDefault),
|
||||
},
|
||||
Checks: toProtocolChecks(ctx, checks),
|
||||
}
|
||||
|
||||
nbConfig := toNetbirdConfig(config, turnCredentials, relayCredentials, extraSettings, settings, debugUploadDefault)
|
||||
nbConfig := toNetbirdConfig(config, turnCredentials, relayCredentials, extraSettings, settings)
|
||||
extendedConfig := integrationsConfig.ExtendNetBirdConfig(peer.ID, peerGroups, nbConfig, extraSettings)
|
||||
response.NetbirdConfig = extendedConfig
|
||||
|
||||
|
||||
@@ -278,7 +278,7 @@ 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, types.TwinAccountSettings(settings), "")
|
||||
nbCfg := toNetbirdConfig(nil, nil, nil, nil, types.TwinAccountSettings(settings))
|
||||
assert.Nil(t, nbCfg, "fan-out updates must not carry a partial NetbirdConfig even when settings are present")
|
||||
})
|
||||
|
||||
@@ -293,7 +293,7 @@ func TestToNetbirdConfig_RelayInvariant(t *testing.T) {
|
||||
}
|
||||
relayToken := &Token{Payload: "token-payload", Signature: "token-signature"}
|
||||
|
||||
nbCfg := toNetbirdConfig(cfg, nil, relayToken, nil, types.TwinAccountSettings(settings), "")
|
||||
nbCfg := toNetbirdConfig(cfg, nil, relayToken, nil, types.TwinAccountSettings(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")
|
||||
@@ -329,9 +329,56 @@ func TestToPeerConfig_RoutingPeerDNSResolution(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
settings := &types.Settings{RoutingPeerDNSResolutionEnabled: tt.globalFlag}
|
||||
cfg := toPeerConfig(types.TwinPeer(newPeer(tt.embedded)), types.TwinNetwork(network), "netbird.selfhosted", types.TwinAccountSettings(settings), nil, nil, false, tt.forceParam)
|
||||
cfg := toPeerConfig(types.TwinPeer(newPeer(tt.embedded)), types.TwinNetwork(network), "netbird.selfhosted", types.TwinAccountSettings(settings), nil, nil, false, tt.forceParam, "")
|
||||
assert.Equal(t, tt.wantEnabled, cfg.RoutingPeerDnsResolutionEnabled,
|
||||
"RoutingPeerDnsResolutionEnabled should reflect global || embedded || forced")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The debug-bundle destination rides PeerConfig because the peer fan-out that
|
||||
// follows a settings change builds its sync with a nil server config. The
|
||||
// deployment-wide default therefore has to reach toPeerConfig as a value: left
|
||||
// to be read off the server config, a fan-out would publish an empty
|
||||
// destination and move the deployment's bundles back to NetBird's service.
|
||||
func TestToPeerConfigDebugUploadURL(t *testing.T) {
|
||||
network := &types.Network{Net: net.IPNet{IP: net.IPv4(100, 0, 0, 0), Mask: net.CIDRMask(8, 32)}}
|
||||
peer := &nbpeer.Peer{IP: netip.MustParseAddr("100.0.0.1")}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
accountSetting string
|
||||
deploymentDefault string
|
||||
want string
|
||||
}{
|
||||
{name: "neither set leaves the peer on the NetBird service"},
|
||||
{
|
||||
name: "the account setting is published",
|
||||
accountSetting: "https://account.example.com/upload-url",
|
||||
want: "https://account.example.com/upload-url",
|
||||
},
|
||||
{
|
||||
// What the fan-out sends: no server config in hand, so this is the
|
||||
// value the controller passes in.
|
||||
name: "the deployment default is published when the account names none",
|
||||
deploymentDefault: "https://deployment.example.com/upload-url",
|
||||
want: "https://deployment.example.com/upload-url",
|
||||
},
|
||||
{
|
||||
name: "the account setting wins over the deployment default",
|
||||
accountSetting: "https://account.example.com/upload-url",
|
||||
deploymentDefault: "https://deployment.example.com/upload-url",
|
||||
want: "https://account.example.com/upload-url",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
settings := &types.Settings{DebugBundleUploadURL: tt.accountSetting}
|
||||
cfg := toPeerConfig(types.TwinPeer(peer), types.TwinNetwork(network), "netbird.selfhosted",
|
||||
types.TwinAccountSettings(settings), nil, nil, false, false, tt.deploymentDefault)
|
||||
assert.Equal(t, tt.want, cfg.GetDebugBundleUploadUrl(),
|
||||
"the peer must be told the destination its operator configured")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -828,8 +828,8 @@ func (s *Server) prepareLoginResponse(ctx context.Context, peer *nbpeer.Peer, ne
|
||||
|
||||
// if peer has reached this point then it has logged in
|
||||
loginResp := &proto.LoginResponse{
|
||||
NetbirdConfig: toNetbirdConfig(s.config, nil, relayToken, nil, types.TwinAccountSettings(settings), s.config.DebugUpload.URL),
|
||||
PeerConfig: toPeerConfig(types.TwinPeer(peer), types.TwinNetwork(network), s.networkMapController.GetDNSDomain(settings), types.TwinAccountSettings(settings), s.config.HttpConfig, s.config.DeviceAuthorizationFlow, enableSSH, false),
|
||||
NetbirdConfig: toNetbirdConfig(s.config, nil, relayToken, nil, types.TwinAccountSettings(settings)),
|
||||
PeerConfig: toPeerConfig(types.TwinPeer(peer), types.TwinNetwork(network), s.networkMapController.GetDNSDomain(settings), types.TwinAccountSettings(settings), s.config.HttpConfig, s.config.DeviceAuthorizationFlow, enableSSH, false, s.config.DebugUpload.URL),
|
||||
Checks: toProtocolChecks(ctx, postureChecks),
|
||||
}
|
||||
|
||||
|
||||
+1281
-1343
File diff suppressed because it is too large
Load Diff
@@ -337,8 +337,6 @@ message NetbirdConfig {
|
||||
FlowConfig flow = 5;
|
||||
|
||||
MetricsConfig metrics = 6;
|
||||
|
||||
DebugConfig debug = 7;
|
||||
}
|
||||
|
||||
// HostConfig describes connection properties of some server (e.g. STUN, Signal, Management)
|
||||
@@ -381,18 +379,6 @@ message MetricsConfig {
|
||||
bool enabled = 1;
|
||||
}
|
||||
|
||||
// DebugConfig carries the deployment-wide debug settings the operator of this
|
||||
// management server publishes to its peers.
|
||||
message DebugConfig {
|
||||
// upload_url is the debug-bundle upload service this peer's account uses,
|
||||
// taken from the account settings or, failing that, from the management
|
||||
// server config. Setting it keeps a deployment's bundles — which carry peer
|
||||
// logs, routes, DNS and firewall state — inside infrastructure the operator
|
||||
// controls. An empty value means no destination is published, and the peer
|
||||
// uploads to the service NetBird runs.
|
||||
string upload_url = 1;
|
||||
}
|
||||
|
||||
// JWTConfig represents JWT authentication configuration for validating tokens.
|
||||
message JWTConfig {
|
||||
string issuer = 1;
|
||||
@@ -437,6 +423,18 @@ message PeerConfig {
|
||||
|
||||
// IPv6 overlay address as compact bytes: 16 bytes IP + 1 byte prefix length.
|
||||
bytes address_v6 = 9;
|
||||
|
||||
// debugBundleUploadUrl is the debug-bundle upload service this peer's account
|
||||
// uses, taken from the account settings or, failing that, from the management
|
||||
// server config. Setting it keeps a deployment's bundles — which carry peer
|
||||
// logs, routes, DNS and firewall state — inside infrastructure the operator
|
||||
// controls. An empty value means no destination is published, and the peer
|
||||
// uploads to the service NetBird runs.
|
||||
//
|
||||
// It rides PeerConfig rather than NetbirdConfig because the peer fan-out that
|
||||
// follows a settings change builds its sync with a nil server config, so a
|
||||
// NetbirdConfig field only reaches a peer when that peer reconnects.
|
||||
string debugBundleUploadUrl = 10;
|
||||
}
|
||||
|
||||
message AutoUpdateSettings {
|
||||
|
||||
Reference in New Issue
Block a user