From f548a74c7c5b7cde2d5cdc658737b1eedff3e90c Mon Sep 17 00:00:00 2001 From: dmitri-netbird Date: Thu, 22 Oct 2026 20:27:53 +0200 Subject: [PATCH] [management] Compensate for a possible nil peer when returning "empty" network map (#8285) * we somehow can get a nil peer when returning empty components; this fix compensates for the issue, but doesn't fix the root cause (which is unknown atm) Signed-off-by: Dmitri Dolguikh * fix test Signed-off-by: Dmitri Dolguikh --------- Signed-off-by: Dmitri Dolguikh --- .../controllers/network_map/controller/controller.go | 2 +- management/internals/shared/grpc/components_encoder.go | 5 +++-- management/internals/shared/grpc/components_encoder_test.go | 3 +++ .../internals/shared/grpc/components_envelope_response.go | 1 + shared/management/networkmap/envelope_test.go | 1 + 5 files changed, 9 insertions(+), 3 deletions(-) diff --git a/management/internals/controllers/network_map/controller/controller.go b/management/internals/controllers/network_map/controller/controller.go index 9b8471c1b..08d018e5f 100644 --- a/management/internals/controllers/network_map/controller/controller.go +++ b/management/internals/controllers/network_map/controller/controller.go @@ -933,7 +933,7 @@ func (c *Controller) GetValidatedPeerWithComponents(ctx context.Context, isRequi } // it's possible that the peer gets deleted between the call to "sendInitialSync()" and here, bail out in this case - if _, ok := account.Peers[peer.ID]; !ok { + if p, _ := account.Peers[peer.ID]; p == nil { return nil, nil, nil, 0, fmt.Errorf("peer '%s' no longer exists", peer.ID) } diff --git a/management/internals/shared/grpc/components_encoder.go b/management/internals/shared/grpc/components_encoder.go index a4516be52..4763177ab 100644 --- a/management/internals/shared/grpc/components_encoder.go +++ b/management/internals/shared/grpc/components_encoder.go @@ -20,6 +20,7 @@ const wgKeyRawLen = 32 // computed alongside the components in the network_map controller and reused // from the legacy proto path) and the dns_domain string. type ComponentsEnvelopeInput struct { + Peer *nmdata.Peer // only used when encoding "empty" component maps Components *types.NetworkMapComponents PeerConfig *proto.PeerConfig DNSDomain string @@ -59,8 +60,8 @@ func EncodeNetworkMapEnvelope(in ComponentsEnvelopeInput) *proto.NetworkMapEnvel Serial: networkSerial(c.Network), Network: toAccountNetwork(c.Network), PeerConfig: in.PeerConfig, - // components.Peers always contains the target peer - Peers: []*proto.PeerCompact{toPeerCompact(c.Peers[c.PeerID])}, + // components.Peers may not contain the target peer + Peers: []*proto.PeerCompact{toPeerCompact(in.Peer)}, DnsDomain: in.DNSDomain, DnsForwarderPort: in.DNSForwarderPort, UserIdClaim: in.UserIDClaim, diff --git a/management/internals/shared/grpc/components_encoder_test.go b/management/internals/shared/grpc/components_encoder_test.go index a6421af11..a9e9cd939 100644 --- a/management/internals/shared/grpc/components_encoder_test.go +++ b/management/internals/shared/grpc/components_encoder_test.go @@ -717,6 +717,7 @@ func TestEncodeNetworkMapEnvelope_NilComponentsGracefulDegrade(t *testing.T) { // nil Components → minimal envelope, no crash. Matches the legacy // behaviour for missing/unvalidated peers. env := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{ + Peer: &nmdata.Peer{ID: "missing-peer", SSHKey: "missing-peer-key"}, Components: emptyNetworkMapComponents(), DNSDomain: "netbird.cloud", }) @@ -731,6 +732,8 @@ func TestEncodeNetworkMapEnvelope_NilComponentsGracefulDegrade(t *testing.T) { require.NotNil(t, full.Network, "client runs Calculate() over the envelope and dereferences Network unconditionally; a nil here would crash the receiver") assert.Equal(t, "net-empty", full.Network.Identifier) assert.Equal(t, uint64(9), full.Serial) + assert.Len(t, env.GetFull().Peers, 1) + assert.Equal(t, string(env.GetFull().Peers[0].SshPubKey), "missing-peer-key") } func TestEncodeNetworkMapEnvelope_AccountSettingsAlwaysEmitted(t *testing.T) { diff --git a/management/internals/shared/grpc/components_envelope_response.go b/management/internals/shared/grpc/components_envelope_response.go index cbf9bb477..40ac7a0c7 100644 --- a/management/internals/shared/grpc/components_envelope_response.go +++ b/management/internals/shared/grpc/components_envelope_response.go @@ -55,6 +55,7 @@ func ToComponentSyncResponse( } envelope := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{ + Peer: peer, Components: components, PeerConfig: peerConfig, DNSDomain: dnsName, diff --git a/shared/management/networkmap/envelope_test.go b/shared/management/networkmap/envelope_test.go index 98333a3c8..fe71016b8 100644 --- a/shared/management/networkmap/envelope_test.go +++ b/shared/management/networkmap/envelope_test.go @@ -244,6 +244,7 @@ func TestEnvelopeToNetworkMap_EmptyComponents(t *testing.T) { }) envelope := mgmtgrpc.EncodeNetworkMapEnvelope(mgmtgrpc.ComponentsEnvelopeInput{ + Peer: &nmdata.Peer{ID: "peer-A", Key: localPeerKey, IP: netip.AddrFrom4([4]byte{100, 64, 0, 1})}, Components: c, DNSDomain: "netbird.cloud", })