From 3ce261369d3642b5722df0de3e1710f2c8395799 Mon Sep 17 00:00:00 2001 From: bcmmbaga Date: Tue, 25 Aug 2026 01:55:36 +0300 Subject: [PATCH] Guard network map component encoding when the target peer is absent --- .../shared/grpc/components_encoder.go | 21 +++++++-- .../shared/grpc/components_encoder_test.go | 47 +++++++++++++++++-- management/server/types/account_components.go | 11 +++-- 3 files changed, 66 insertions(+), 13 deletions(-) diff --git a/management/internals/shared/grpc/components_encoder.go b/management/internals/shared/grpc/components_encoder.go index e1a5cae48..9711ca663 100644 --- a/management/internals/shared/grpc/components_encoder.go +++ b/management/internals/shared/grpc/components_encoder.go @@ -58,14 +58,22 @@ func EncodeNetworkMapEnvelope(in ComponentsEnvelopeInput) *proto.NetworkMapEnvel // Match legacy missing-peer minimum: a NetworkMap with only Network // populated. The receiver gets enough to bootstrap (Network // identifier, dns_domain, account_settings) and the peer itself. + // + // components.Peers carries the target peer unless the peer vanished + // from the account mid-sync, in which case there is nothing to send + // for it and the slice stays empty rather than holding a nil entry. + var peers []*proto.PeerCompact + if target := toPeerCompact(c.Peers[c.PeerID]); target != nil { + peers = []*proto.PeerCompact{target} + } + return &proto.NetworkMapEnvelope{ Payload: &proto.NetworkMapEnvelope_Full{ Full: &proto.NetworkMapComponentsFull{ - 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])}, + Serial: networkSerial(c.Network), + Network: toAccountNetwork(c.Network), + PeerConfig: in.PeerConfig, + Peers: peers, DnsDomain: in.DNSDomain, DnsForwarderPort: in.DNSForwarderPort, UserIdClaim: in.UserIDClaim, @@ -692,6 +700,9 @@ func toAccountNetwork(n *types.Network) *proto.AccountNetwork { } func toPeerCompact(p *types.ComponentPeer) *proto.PeerCompact { + if p == nil { + return nil + } pc := &proto.PeerCompact{ WgPubKey: decodeWgKey(p.Key), SshPubKey: []byte(p.SSHKey), diff --git a/management/internals/shared/grpc/components_encoder_test.go b/management/internals/shared/grpc/components_encoder_test.go index f7df82f2f..6369c5597 100644 --- a/management/internals/shared/grpc/components_encoder_test.go +++ b/management/internals/shared/grpc/components_encoder_test.go @@ -20,9 +20,11 @@ import ( "github.com/netbirdio/netbird/shared/management/proto" ) -const testWgKeyA = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopq=" -const testWgKeyB = "BBCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopq=" -const testWgKeyC = "CBCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopq=" +const ( + testWgKeyA = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopq=" + testWgKeyB = "BBCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopq=" + testWgKeyC = "CBCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopq=" +) // canonicalize rewrites a NetworkMapComponentsFull in place into a canonical // form: peers reordered by wg_pub_key, with the rest of the message rewritten @@ -367,7 +369,7 @@ func TestEncodeNetworkMapEnvelope_MalformedWgKey(t *testing.T) { require.Len(t, full.Peers, 3) - var byLabel = map[string]*proto.PeerCompact{} + byLabel := map[string]*proto.PeerCompact{} for _, p := range full.Peers { byLabel[p.DnsLabel] = p } @@ -788,3 +790,40 @@ func emptyNetworkMapComponents() *types.NetworkMapComponents { }, ) } + +func TestEncodeNetworkMapEnvelope_EmptyComponentsMissingTargetPeer(t *testing.T) { + for name, peers := range map[string]map[string]*types.ComponentPeer{ + "absent entry": {}, + "nil entry": {"peer-id": nil}, + } { + t.Run(name, func(t *testing.T) { + c := types.EmptyNetworkMapComponents(&types.NetworkMapComponents{ + PeerID: "peer-id", + Peers: peers, + Network: &types.Network{ + Identifier: "net-empty", + Net: net.IPNet{IP: net.IP{100, 64, 0, 0}, Mask: net.CIDRMask(10, 32)}, + Serial: 9, + }, + }) + + full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{ + Components: c, + DNSDomain: "netbird.cloud", + }).GetFull() + + require.NotNil(t, full) + assert.Empty(t, full.Peers, "a missing target peer must not become a nil entry on the wire") + require.NotNil(t, full.Network, "the account Network floor still applies") + assert.Equal(t, "net-empty", full.Network.Identifier) + assert.Equal(t, uint64(9), full.Serial) + require.NotNil(t, full.AccountSettings) + + wire, err := goproto.Marshal(&proto.NetworkMapEnvelope{ + Payload: &proto.NetworkMapEnvelope_Full{Full: full}, + }) + require.NoError(t, err, "envelope must stay marshalable") + assert.NotEmpty(t, wire) + }) + } +} diff --git a/management/server/types/account_components.go b/management/server/types/account_components.go index 624a778fe..5e3ffae58 100644 --- a/management/server/types/account_components.go +++ b/management/server/types/account_components.go @@ -109,11 +109,13 @@ func (a *Account) GetPeerNetworkMapComponents( // TODO (dmitri) maybe consider using invariants? if peer == nil { log.WithField("peer id", peerID).Error("NetworkMapComponents are computed for a peer missing from the account") + // The peer is gone from the account, so there is nothing to carry + // for it. The client resolves its own identity from PeerConfig and + // tolerates a peers list without it. return EmptyNetworkMapComponents(&NetworkMapComponents{ PeerID: peerID, Network: a.Network.Copy(), - // must include the target peer as it's required on the client - Peers: map[string]*ComponentPeer{peerID: peer.ToComponent()}, + Peers: map[string]*ComponentPeer{}, }) } @@ -506,7 +508,8 @@ func (a *Account) getPeersGroupsPoliciesRoutes( } func (a *Account) getPeersFromGroups(ctx context.Context, groups []string, peerID string, sourcePostureChecksIDs []string, - validatedPeersMap map[string]struct{}, postureFailedPeers *map[string]map[string]struct{}) ([]string, bool) { + validatedPeersMap map[string]struct{}, postureFailedPeers *map[string]map[string]struct{}, +) ([]string, bool) { peerInGroups := false var filteredPeerIDs []string var seenPeerIds map[string]struct{} @@ -705,7 +708,7 @@ func peerPassesPostureChecks(ctx context.Context, checks []posture.Check, peer * return true } -func (a *Account) getPostureValidPeersSaveFailed(inputPeers []string, postureChecksIDs []string, validatedPeersMap map[string]struct{}, postureFailedPeers *map[string]map[string]struct{}) []string { +func (a *Account) getPostureValidPeersSaveFailed(inputPeers, postureChecksIDs []string, validatedPeersMap map[string]struct{}, postureFailedPeers *map[string]map[string]struct{}) []string { var dest []string for _, peerID := range inputPeers { if _, validated := validatedPeersMap[peerID]; !validated {