From fd55d93994bc3fccc46dc54ecb7d8a5dc0c40896 Mon Sep 17 00:00:00 2001 From: mlsmaycon Date: Mon, 8 Jun 2026 17:56:26 +0200 Subject: [PATCH] [management] Add version gate to stop sending deprecated RemotePeers field don't send top-level remote peers on peers in the v0.29.3 or newer --- .../internals/shared/grpc/conversion.go | 29 +++++++++++++-- .../internals/shared/grpc/conversion_test.go | 36 +++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/management/internals/shared/grpc/conversion.go b/management/internals/shared/grpc/conversion.go index b4a0d8b28..d7a00f451 100644 --- a/management/internals/shared/grpc/conversion.go +++ b/management/internals/shared/grpc/conversion.go @@ -8,6 +8,8 @@ import ( "strings" "time" + "github.com/hashicorp/go-version" + nbversion "github.com/netbirdio/netbird/version" log "github.com/sirupsen/logrus" goproto "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" @@ -155,7 +157,11 @@ func ToSyncResponse(ctx context.Context, config *nbconfig.Config, httpConfig *nb remotePeers := make([]*proto.RemotePeerConfig, 0, len(networkMap.Peers)+len(networkMap.OfflinePeers)) remotePeers = appendRemotePeerConfig(remotePeers, networkMap.Peers, dnsName, includeIPv6) - response.RemotePeers = remotePeers + + if !shouldSkipSendingDeprecatedRemotePeers(peer.Meta.WtVersion) { + response.RemotePeers = remotePeers + } + response.NetworkMap.RemotePeers = remotePeers response.RemotePeersIsEmpty = len(remotePeers) == 0 response.NetworkMap.RemotePeersIsEmpty = response.RemotePeersIsEmpty @@ -246,6 +252,26 @@ func buildAuthorizedUsersProto(ctx context.Context, authorizedUsers map[string]m return hashedUsers, machineUsers } +const deprecatedRemotePeersVersion = "0.29.3" + +func shouldSkipSendingDeprecatedRemotePeers(peerVersion string) bool { + if nbversion.IsDevelopmentVersion(peerVersion) { + return true + } + + peerNBVersion, err := version.NewVersion(peerVersion) + if err != nil { + return false + } + + constraints, err := version.NewConstraint(">= " + deprecatedRemotePeersVersion) + if err != nil { + return false + } + + return constraints.Check(peerNBVersion) +} + func appendRemotePeerConfig(dst []*proto.RemotePeerConfig, peers []*nbpeer.Peer, dnsName string, includeIPv6 bool) []*proto.RemotePeerConfig { for _, rPeer := range peers { allowedIPs := []string{rPeer.IP.String() + "/32"} @@ -363,7 +389,6 @@ func toProtocolFirewallRules(rules []*types.FirewallRule, includeIPv6, useSource return result } - // populateSourcePrefixes sets SourcePrefixes on fwRule and returns any // additional rules needed (e.g. a v6 wildcard clone when the peer IP is unspecified). func populateSourcePrefixes(fwRule *proto.FirewallRule, rule *types.FirewallRule, includeIPv6 bool) []*proto.FirewallRule { diff --git a/management/internals/shared/grpc/conversion_test.go b/management/internals/shared/grpc/conversion_test.go index 5efb24319..01a67e4fa 100644 --- a/management/internals/shared/grpc/conversion_test.go +++ b/management/internals/shared/grpc/conversion_test.go @@ -202,6 +202,42 @@ func TestBuildJWTConfig_Audiences(t *testing.T) { } } +// TestShouldSkipSendingDeprecatedRemotePeers covers the version gate that +// stops populating the deprecated top-level SyncResponse.RemotePeers field for +// peers new enough to read RemotePeers off the NetworkMap. Development builds +// are treated as latest and skip the field. The gate otherwise fails safe: a +// release version older than the boundary, or one that can't be parsed (empty, +// garbage, prereleases of the boundary) still receives the deprecated field so +// older/unknown clients keep working. +func TestShouldSkipSendingDeprecatedRemotePeers(t *testing.T) { + tests := []struct { + name string + peerVersion string + wantSkip bool + }{ + {"exact boundary skips", "0.29.3", true}, + {"newer patch skips", "0.29.4", true}, + {"newer minor skips", "0.30.0", true}, + {"newer major skips", "1.0.0", true}, + {"v-prefixed newer skips", "v0.30.0", true}, + {"development build skips", "development", true}, + {"development build with commit skips", "development-abc123def456-dirty", true}, + {"older patch keeps field", "0.29.2", false}, + {"older minor keeps field", "0.28.0", false}, + {"prerelease of boundary keeps field", "0.29.3-SNAPSHOT", false}, + {"tagged dev prerelease keeps field", "v0.31.1-dev", false}, + {"empty version keeps field", "", false}, + {"garbage version keeps field", "not-a-version", false}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := shouldSkipSendingDeprecatedRemotePeers(tc.peerVersion) + assert.Equal(t, tc.wantSkip, got, "skip decision for peer version %q", tc.peerVersion) + }) + } +} + // TestEncodeSessionExpiresAt pins the wire encoding the client's // applySessionDeadline depends on: //