From eebafde7842c55ba3d80502250d1295a3a0a080f Mon Sep 17 00:00:00 2001 From: pascal Date: Wed, 22 Jul 2026 23:37:13 +0200 Subject: [PATCH] component types --- .../shared/grpc/components_encoder.go | 25 ++--- .../shared/grpc/components_encoder_test.go | 100 ++++++++--------- .../grpc/components_envelope_response_test.go | 10 +- management/server/groups/manager.go | 9 +- .../http/handlers/peers/peers_handler.go | 21 ++-- .../networks/resources/types/resource.go | 22 ++++ .../server/networks/routers/types/router.go | 31 ++++++ management/server/peer.go | 4 +- management/server/peer/peer.go | 30 +++++ management/server/peer_test.go | 4 +- management/server/posture/nb_version.go | 30 +---- management/server/posture/nb_version_test.go | 65 ----------- management/server/types/account.go | 15 +-- management/server/types/account_components.go | 60 +++++----- .../types/account_private_netmap_test.go | 3 +- management/server/types/account_test.go | 2 +- management/server/types/aliases.go | 29 ++--- .../server}/types/group.go | 36 ++++-- management/server/types/ipv6_endtoend_test.go | 3 +- .../types/networkmap_components_test.go | 2 +- management/server/util/util.go | 31 ------ shared/management/networkmap/decode.go | 60 ++++------ shared/management/networkmap/encode.go | 7 +- shared/management/networkmap/envelope.go | 4 +- shared/management/networkmap/envelope_test.go | 47 ++++---- shared/management/types/component_types.go | 103 ++++++++++++++++++ shared/management/types/firewall_helpers.go | 14 +-- shared/management/types/firewall_rule.go | 5 +- shared/management/types/firewall_rule_test.go | 13 +-- shared/management/types/network.go | 49 +++++++-- .../management/types/network_merge_test.go | 8 +- .../management/types/networkmap_components.go | 79 +++++++------- .../types/networkmap_components_compact.go | 19 ++-- version/version.go | 24 ++++ version/version_test.go | 71 +++++++++++- 35 files changed, 609 insertions(+), 426 deletions(-) rename {shared/management => management/server}/types/group.go (83%) create mode 100644 shared/management/types/component_types.go rename management/server/util/util_test.go => shared/management/types/network_merge_test.go (88%) diff --git a/management/internals/shared/grpc/components_encoder.go b/management/internals/shared/grpc/components_encoder.go index d7b787464..7e43cf478 100644 --- a/management/internals/shared/grpc/components_encoder.go +++ b/management/internals/shared/grpc/components_encoder.go @@ -5,9 +5,6 @@ import ( "strconv" nbdns "github.com/netbirdio/netbird/dns" - resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types" - routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" - nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/types" nbroute "github.com/netbirdio/netbird/route" "github.com/netbirdio/netbird/shared/management/networkmap" @@ -166,7 +163,7 @@ func (e *componentEncoder) indexAllPeers() { } } -func (e *componentEncoder) appendPeer(p *nbpeer.Peer) uint32 { +func (e *componentEncoder) appendPeer(p *types.ComponentPeer) uint32 { if idx, ok := e.peerOrder[p.ID]; ok { return idx } @@ -180,7 +177,7 @@ func (e *componentEncoder) appendPeer(p *nbpeer.Peer) uint32 { // (c.RouterPeers may contain peers not in c.Peers when validation rules drop // them) and returns their wire indexes for the RouterPeerIndexes field. Must // run before any encoder that resolves peer ids via e.peerOrder. -func (e *componentEncoder) indexRouterPeers(routers map[string]*nbpeer.Peer) []uint32 { +func (e *componentEncoder) indexRouterPeers(routers map[string]*types.ComponentPeer) []uint32 { if len(routers) == 0 { return nil } @@ -514,7 +511,7 @@ func encodeCustomZones(zones []nbdns.CustomZone) []*proto.CustomZone { return out } -func (e *componentEncoder) encodeNetworkResources(resources []*resourceTypes.NetworkResource) []*proto.NetworkResourceRaw { +func (e *componentEncoder) encodeNetworkResources(resources []*types.ComponentResource) []*proto.NetworkResourceRaw { if len(resources) == 0 { return nil } @@ -543,7 +540,7 @@ func (e *componentEncoder) encodeNetworkResources(resources []*resourceTypes.Net return out } -func (e *componentEncoder) encodeRoutersMap(routersMap map[string]map[string]*routerTypes.NetworkRouter) map[string]*proto.NetworkRouterList { +func (e *componentEncoder) encodeRoutersMap(routersMap map[string]map[string]*types.ComponentRouter) map[string]*proto.NetworkRouterList { if len(routersMap) == 0 { return nil } @@ -692,20 +689,20 @@ func toAccountNetwork(n *types.Network) *proto.AccountNetwork { return out } -func toPeerCompact(p *nbpeer.Peer) *proto.PeerCompact { +func toPeerCompact(p *types.ComponentPeer) *proto.PeerCompact { pc := &proto.PeerCompact{ WgPubKey: decodeWgKey(p.Key), SshPubKey: []byte(p.SSHKey), DnsLabel: p.DNSLabel, - AgentVersion: p.Meta.WtVersion, - AddedWithSsoLogin: p.UserID != "", + AgentVersion: p.AgentVersion, + AddedWithSsoLogin: p.AddedWithSSOLogin, LoginExpirationEnabled: p.LoginExpirationEnabled, SshEnabled: p.SSHEnabled, - SupportsIpv6: p.SupportsIPv6(), - SupportsSourcePrefixes: p.SupportsSourcePrefixes(), - ServerSshAllowed: p.Meta.Flags.ServerSSHAllowed, + SupportsIpv6: p.SupportsIPv6, + SupportsSourcePrefixes: p.SupportsSourcePrefixes, + ServerSshAllowed: p.ServerSSHAllowed, } - if p.LastLogin != nil { + if !p.LastLogin.IsZero() { pc.LastLoginUnixNano = p.LastLogin.UnixNano() } switch { diff --git a/management/internals/shared/grpc/components_encoder_test.go b/management/internals/shared/grpc/components_encoder_test.go index d82bba362..100ab0948 100644 --- a/management/internals/shared/grpc/components_encoder_test.go +++ b/management/internals/shared/grpc/components_encoder_test.go @@ -15,9 +15,6 @@ import ( goproto "google.golang.org/protobuf/proto" nbdns "github.com/netbirdio/netbird/dns" - resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types" - routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" - nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/types" nbroute "github.com/netbirdio/netbird/route" "github.com/netbirdio/netbird/shared/management/proto" @@ -155,29 +152,28 @@ func envelopesEquivalent(a, b *proto.NetworkMapEnvelope) bool { } func newTestComponents() *types.NetworkMapComponents { - peerA := &nbpeer.Peer{ - ID: "peer-a", - Key: testWgKeyA, - IP: netip.AddrFrom4([4]byte{100, 64, 0, 1}), - DNSLabel: "peera", - SSHKey: "ssh-a", - Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now()}, - Meta: nbpeer.PeerSystemMeta{WtVersion: "0.40.0"}, + peerA := &types.ComponentPeer{ + ID: "peer-a", + Key: testWgKeyA, + IP: netip.AddrFrom4([4]byte{100, 64, 0, 1}), + DNSLabel: "peera", + SSHKey: "ssh-a", + AgentVersion: "0.40.0", } - peerB := &nbpeer.Peer{ - ID: "peer-b", - Key: testWgKeyB, - IP: netip.AddrFrom4([4]byte{100, 64, 0, 2}), - IPv6: netip.AddrFrom16([16]byte{0xfd, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}), - DNSLabel: "peerb", - Meta: nbpeer.PeerSystemMeta{WtVersion: "0.25.0"}, + peerB := &types.ComponentPeer{ + ID: "peer-b", + Key: testWgKeyB, + IP: netip.AddrFrom4([4]byte{100, 64, 0, 2}), + IPv6: netip.AddrFrom16([16]byte{0xfd, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}), + DNSLabel: "peerb", + AgentVersion: "0.25.0", } - peerC := &nbpeer.Peer{ - ID: "peer-c", - Key: testWgKeyC, - IP: netip.AddrFrom4([4]byte{100, 64, 0, 3}), - DNSLabel: "peerc", - Meta: nbpeer.PeerSystemMeta{WtVersion: "0.40.0"}, + peerC := &types.ComponentPeer{ + ID: "peer-c", + Key: testWgKeyC, + IP: netip.AddrFrom4([4]byte{100, 64, 0, 3}), + DNSLabel: "peerc", + AgentVersion: "0.40.0", } return &types.NetworkMapComponents{ @@ -191,12 +187,12 @@ func newTestComponents() *types.NetworkMapComponents { PeerLoginExpirationEnabled: true, PeerLoginExpiration: 2 * time.Hour, }, - Peers: map[string]*nbpeer.Peer{ + Peers: map[string]*types.ComponentPeer{ "peer-a": peerA, "peer-b": peerB, "peer-c": peerC, }, - Groups: map[string]*types.Group{ + Groups: map[string]*types.ComponentGroup{ "group-src": {ID: "group-src", PublicID: "1", Name: "Src", Peers: []string{"peer-a"}}, "group-dst": {ID: "group-dst", PublicID: "2", Name: "Dst", Peers: []string{"peer-b", "peer-c"}}, }, @@ -215,7 +211,7 @@ func newTestComponents() *types.NetworkMapComponents { }}, }, }, - RouterPeers: map[string]*nbpeer.Peer{"peer-c": peerC}, + RouterPeers: map[string]*types.ComponentPeer{"peer-c": peerC}, } } @@ -381,12 +377,12 @@ func TestEncodeNetworkMapEnvelope_MalformedWgKey(t *testing.T) { func TestEncodeNetworkMapEnvelope_IPv6OnlyPeer(t *testing.T) { c := newTestComponents() - v6Only := &nbpeer.Peer{ - ID: "peer-v6", - Key: testWgKeyA, - IPv6: netip.AddrFrom16([16]byte{0xfd, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9}), - DNSLabel: "peerv6", - Meta: nbpeer.PeerSystemMeta{WtVersion: "0.40.0"}, + v6Only := &types.ComponentPeer{ + ID: "peer-v6", + Key: testWgKeyA, + IPv6: netip.AddrFrom16([16]byte{0xfd, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9}), + DNSLabel: "peerv6", + AgentVersion: "0.40.0", } c.Peers["peer-v6"] = v6Only @@ -405,11 +401,11 @@ func TestEncodeNetworkMapEnvelope_IPv6OnlyPeer(t *testing.T) { func TestEncodeNetworkMapEnvelope_PeerWithoutIP(t *testing.T) { c := newTestComponents() - c.Peers["peer-noip"] = &nbpeer.Peer{ - ID: "peer-noip", - Key: testWgKeyA, - DNSLabel: "peernoip", - Meta: nbpeer.PeerSystemMeta{WtVersion: "0.40.0"}, + c.Peers["peer-noip"] = &types.ComponentPeer{ + ID: "peer-noip", + Key: testWgKeyA, + DNSLabel: "peernoip", + AgentVersion: "0.40.0", } full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull() @@ -444,9 +440,9 @@ func TestEncodeNetworkMapEnvelope_EmptyInput(t *testing.T) { func TestEncodeNetworkMapEnvelope_PeerLoginExpirationFields(t *testing.T) { c := newTestComponents() now := time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC) - c.Peers["peer-a"].UserID = "user-1" + c.Peers["peer-a"].AddedWithSSOLogin = true c.Peers["peer-a"].LoginExpirationEnabled = true - c.Peers["peer-a"].LastLogin = &now + c.Peers["peer-a"].LastLogin = now full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull() @@ -557,7 +553,7 @@ func TestEncodeNetworkMapEnvelope_ResourceOnlyPolicyShippedAndIndexed(t *testing } // Resource must appear in components.NetworkResources with a seq id — // encoder uses that to translate the xid map key to uint32. - c.NetworkResources = []*resourceTypes.NetworkResource{ + c.NetworkResources = []*types.ComponentResource{ {ID: "resource-x", PublicID: "77", Name: "res-x", Enabled: true}, } @@ -625,11 +621,11 @@ func TestEncodeNetworkMapEnvelope_PostureFailedPeers(t *testing.T) { func TestEncodeNetworkMapEnvelope_RoutersMap(t *testing.T) { c := newTestComponents() c.NetworkXIDToPublicID = map[string]string{"net-1": "5"} - c.RoutersMap = map[string]map[string]*routerTypes.NetworkRouter{ + c.RoutersMap = map[string]map[string]*types.ComponentRouter{ "net-1": { "peer-c": { - ID: "router-1", PublicID: "200", - Peer: "peer-c", Masquerade: true, Metric: 10, Enabled: true, + PublicID: "200", + Peer: "peer-c", Masquerade: true, Metric: 10, Enabled: true, }, }, } @@ -655,14 +651,14 @@ func TestEncodeNetworkMapEnvelope_RouterPeerNotInComponentsPeers(t *testing.T) { // peer_index reference must still resolve. c := newTestComponents() delete(c.Peers, "peer-c") - routerPeer := &nbpeer.Peer{ + routerPeer := &types.ComponentPeer{ ID: "peer-c", Key: testWgKeyC, IP: netip.AddrFrom4([4]byte{100, 64, 0, 3}), - DNSLabel: "peerc", Meta: nbpeer.PeerSystemMeta{WtVersion: "0.40.0"}, + DNSLabel: "peerc", AgentVersion: "0.40.0", } - c.RouterPeers = map[string]*nbpeer.Peer{"peer-c": routerPeer} + c.RouterPeers = map[string]*types.ComponentPeer{"peer-c": routerPeer} c.NetworkXIDToPublicID = map[string]string{"net-1": "5"} - c.RoutersMap = map[string]map[string]*routerTypes.NetworkRouter{ - "net-1": {"peer-c": {ID: "r-1", PublicID: "1", Peer: "peer-c", Enabled: true}}, + c.RoutersMap = map[string]map[string]*types.ComponentRouter{ + "net-1": {"peer-c": {PublicID: "1", Peer: "peer-c", Enabled: true}}, } full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull() @@ -695,9 +691,9 @@ func TestToProxyPatch_EmptyInputReturnsNil(t *testing.T) { func TestToProxyPatch_PopulatesAllFields(t *testing.T) { nm := &types.NetworkMap{ - Peers: []*nbpeer.Peer{{ + Peers: []*types.ComponentPeer{{ ID: "ext-peer", Key: testWgKeyA, IP: netip.AddrFrom4([4]byte{100, 64, 0, 9}), - DNSLabel: "extpeer", Meta: nbpeer.PeerSystemMeta{WtVersion: "0.40.0"}, + DNSLabel: "extpeer", AgentVersion: "0.40.0", }}, FirewallRules: []*types.FirewallRule{{ PeerIP: "100.64.0.9", Action: "accept", Direction: 0, Protocol: "tcp", @@ -780,6 +776,6 @@ func TestEncodeNetworkMapEnvelope_AccountSettingsAlwaysEmitted(t *testing.T) { func emptyNetworkMapComponents() *types.NetworkMapComponents { return types.EmptyNetworkMapComponents( &types.NetworkMapComponents{ - PeerID: "peer-id", Peers: map[string]*nbpeer.Peer{"peer-id": {}}}, + PeerID: "peer-id", Peers: map[string]*types.ComponentPeer{"peer-id": {}}}, ) } diff --git a/management/internals/shared/grpc/components_envelope_response_test.go b/management/internals/shared/grpc/components_envelope_response_test.go index bf35bb7b9..20f4e6824 100644 --- a/management/internals/shared/grpc/components_envelope_response_test.go +++ b/management/internals/shared/grpc/components_envelope_response_test.go @@ -19,10 +19,10 @@ func TestComputeSSHEnabledForPeer(t *testing.T) { mkComponents := func(rule *types.PolicyRule, sshEnabled bool) (*types.NetworkMapComponents, *nbpeer.Peer) { peer := &nbpeer.Peer{ID: targetPeerID, SSHEnabled: sshEnabled} - group := &types.Group{ID: targetGroupID, Name: "dst", Peers: []string{targetPeerID}} + group := &types.ComponentGroup{ID: targetGroupID, Name: "dst", Peers: []string{targetPeerID}} return &types.NetworkMapComponents{ - Peers: map[string]*nbpeer.Peer{targetPeerID: peer}, - Groups: map[string]*types.Group{targetGroupID: group}, + Peers: map[string]*types.ComponentPeer{targetPeerID: peer.ToComponent()}, + Groups: map[string]*types.ComponentGroup{targetGroupID: group}, Policies: []*types.Policy{{ ID: "p", Enabled: true, @@ -158,8 +158,8 @@ func TestComputeSSHEnabledForPeer(t *testing.T) { func TestComputeSSHEnabledForPeer_TargetMissingFromComponents(t *testing.T) { peer := &nbpeer.Peer{ID: "missing", SSHEnabled: true} c := &types.NetworkMapComponents{ - Peers: map[string]*nbpeer.Peer{}, // target peer NOT present - Groups: map[string]*types.Group{ + Peers: map[string]*types.ComponentPeer{}, // target peer NOT present + Groups: map[string]*types.ComponentGroup{ "g": {ID: "g", Peers: []string{"missing"}}, }, Policies: []*types.Policy{{ diff --git a/management/server/groups/manager.go b/management/server/groups/manager.go index c9a877d6f..6d19b1c35 100644 --- a/management/server/groups/manager.go +++ b/management/server/groups/manager.go @@ -6,6 +6,7 @@ import ( "github.com/netbirdio/netbird/management/server/account" "github.com/netbirdio/netbird/management/server/activity" + resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types" "github.com/netbirdio/netbird/management/server/permissions" "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" @@ -30,6 +31,10 @@ type managerImpl struct { accountManager account.Manager } +func eventMetaResource(group *types.Group, resource *resourceTypes.NetworkResource) map[string]any { + return map[string]any{"name": group.Name, "id": group.ID, "resource_name": resource.Name, "resource_id": resource.ID, "resource_type": resource.Type} +} + type mockManager struct { } @@ -109,7 +114,7 @@ func (m *managerImpl) AddResourceToGroupInTransaction(ctx context.Context, trans } event := func() { - m.accountManager.StoreEvent(ctx, userID, groupID, accountID, activity.ResourceAddedToGroup, group.EventMetaResource(networkResource)) + m.accountManager.StoreEvent(ctx, userID, groupID, accountID, activity.ResourceAddedToGroup, eventMetaResource(group, networkResource)) } return event, nil @@ -133,7 +138,7 @@ func (m *managerImpl) RemoveResourceFromGroupInTransaction(ctx context.Context, } event := func() { - m.accountManager.StoreEvent(ctx, userID, groupID, accountID, activity.ResourceRemovedFromGroup, group.EventMetaResource(networkResource)) + m.accountManager.StoreEvent(ctx, userID, groupID, accountID, activity.ResourceRemovedFromGroup, eventMetaResource(group, networkResource)) } return event, nil diff --git a/management/server/http/handlers/peers/peers_handler.go b/management/server/http/handlers/peers/peers_handler.go index 310f90653..03a37c3ec 100644 --- a/management/server/http/handlers/peers/peers_handler.go +++ b/management/server/http/handlers/peers/peers_handler.go @@ -446,7 +446,7 @@ func (h *Handler) GetAccessiblePeers(w http.ResponseWriter, r *http.Request) { netMap := account.GetPeerNetworkMapFromComponents(ctx, peerID, dns.CustomZone{}, nil, validPeers, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap(), nil, account.GetActiveGroupUsers()) - util.WriteJSONObject(ctx, w, toAccessiblePeers(netMap, dnsDomain)) + util.WriteJSONObject(ctx, w, toAccessiblePeers(netMap, account.Peers, dnsDomain)) } func (h *Handler) CreateTemporaryAccess(w http.ResponseWriter, r *http.Request) { @@ -534,15 +534,20 @@ func (h *Handler) CreateTemporaryAccess(w http.ResponseWriter, r *http.Request) util.WriteJSONObject(r.Context(), w, resp) } -func toAccessiblePeers(netMap *types.NetworkMap, dnsDomain string) []api.AccessiblePeer { +// toAccessiblePeers rehydrates the calculated map's component peers into the +// account's full peer objects, which carry the location/status/meta fields +// the API response needs. +func toAccessiblePeers(netMap *types.NetworkMap, accountPeers map[string]*nbpeer.Peer, dnsDomain string) []api.AccessiblePeer { accessiblePeers := make([]api.AccessiblePeer, 0, len(netMap.Peers)+len(netMap.OfflinePeers)) - for _, p := range netMap.Peers { - accessiblePeers = append(accessiblePeers, peerToAccessiblePeer(p, dnsDomain)) - } - - for _, p := range netMap.OfflinePeers { - accessiblePeers = append(accessiblePeers, peerToAccessiblePeer(p, dnsDomain)) + add := func(peers []*types.ComponentPeer) { + for _, p := range peers { + if peer := accountPeers[p.ID]; peer != nil { + accessiblePeers = append(accessiblePeers, peerToAccessiblePeer(peer, dnsDomain)) + } + } } + add(netMap.Peers) + add(netMap.OfflinePeers) return accessiblePeers } diff --git a/management/server/networks/resources/types/resource.go b/management/server/networks/resources/types/resource.go index 4cf7f7ea3..643f9cdd6 100644 --- a/management/server/networks/resources/types/resource.go +++ b/management/server/networks/resources/types/resource.go @@ -14,6 +14,7 @@ import ( nbDomain "github.com/netbirdio/netbird/shared/management/domain" "github.com/netbirdio/netbird/shared/management/http/api" + sharedTypes "github.com/netbirdio/netbird/shared/management/types" ) type NetworkResourceType string @@ -64,6 +65,27 @@ func NewNetworkResource(accountID, networkID, name, description, address string, }, nil } +// ToComponent converts the resource to its self-contained components +// representation. Returns nil for a nil resource. +func (n *NetworkResource) ToComponent() *sharedTypes.ComponentResource { + if n == nil { + return nil + } + return &sharedTypes.ComponentResource{ + ID: n.ID, + PublicID: n.PublicID, + NetworkID: n.NetworkID, + AccountID: n.AccountID, + Name: n.Name, + Description: n.Description, + Type: sharedTypes.ComponentResourceType(n.Type), + Address: n.Address, + Domain: n.Domain, + Prefix: n.Prefix, + Enabled: n.Enabled, + } +} + func (n *NetworkResource) ToAPIResponse(groups []api.GroupMinimum) *api.NetworkResource { addr := n.Prefix.String() if n.Type == Domain { diff --git a/management/server/networks/routers/types/router.go b/management/server/networks/routers/types/router.go index 189d7f792..b8097cdbb 100644 --- a/management/server/networks/routers/types/router.go +++ b/management/server/networks/routers/types/router.go @@ -7,6 +7,7 @@ import ( "github.com/netbirdio/netbird/management/server/networks/types" "github.com/netbirdio/netbird/shared/management/http/api" + sharedTypes "github.com/netbirdio/netbird/shared/management/types" ) type NetworkRouter struct { @@ -21,6 +22,36 @@ type NetworkRouter struct { Enabled bool } +// ToComponent converts the router to its self-contained components +// representation. Returns nil for a nil router. +func (n *NetworkRouter) ToComponent() *sharedTypes.ComponentRouter { + if n == nil { + return nil + } + return &sharedTypes.ComponentRouter{ + NetworkID: n.NetworkID, + PublicID: n.PublicID, + Peer: n.Peer, + PeerGroups: n.PeerGroups, + Masquerade: n.Masquerade, + Metric: n.Metric, + Enabled: n.Enabled, + } +} + +// ToComponentMap converts a peer-keyed router map to its components +// representation. +func ToComponentMap(routers map[string]*NetworkRouter) map[string]*sharedTypes.ComponentRouter { + if routers == nil { + return nil + } + out := make(map[string]*sharedTypes.ComponentRouter, len(routers)) + for id, r := range routers { + out[id] = r.ToComponent() + } + return out +} + func NewNetworkRouter(accountID string, networkID string, peer string, peerGroups []string, masquerade bool, metric int, enabled bool) (*NetworkRouter, error) { r := &NetworkRouter{ ID: xid.New().String(), diff --git a/management/server/peer.go b/management/server/peer.go index 5f2f5d2a2..589cf9abf 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -405,7 +405,7 @@ func (am *DefaultAccountManager) CreatePeerJob(ctx context.Context, accountID, p return status.NewPeerNotPartOfAccountError() } - meetMinVer, err := posture.MeetsMinVersion(remoteJobsMinVer, p.Meta.WtVersion) + meetMinVer, err := version.MeetsMinVersion(remoteJobsMinVer, p.Meta.WtVersion) if !version.IsDevelopmentVersion(p.Meta.WtVersion) && (!meetMinVer || err != nil) { return status.Errorf(status.PreconditionFailed, "peer version %s does not meet the minimum required version %s for remote jobs", p.Meta.WtVersion, remoteJobsMinVer) } @@ -1588,7 +1588,7 @@ func affectedPeerIDsFromNetworkMap(nmap *types.NetworkMap, selfPeerID string) [] } seen := make(map[string]struct{}, len(nmap.Peers)+len(nmap.OfflinePeers)) ids := make([]string, 0, len(nmap.Peers)+len(nmap.OfflinePeers)) - add := func(peers []*nbpeer.Peer) { + add := func(peers []*types.ComponentPeer) { for _, p := range peers { if p == nil || p.ID == "" || p.ID == selfPeerID { continue diff --git a/management/server/peer/peer.go b/management/server/peer/peer.go index 39022d095..7c4971285 100644 --- a/management/server/peer/peer.go +++ b/management/server/peer/peer.go @@ -13,6 +13,7 @@ import ( "github.com/netbirdio/netbird/management/server/util" "github.com/netbirdio/netbird/shared/management/http/api" + sharedTypes "github.com/netbirdio/netbird/shared/management/types" ) // Peer capability constants mirror the proto enum values. @@ -205,6 +206,35 @@ func (p *Peer) AddedWithSSOLogin() bool { return p.UserID != "" } +// ToComponent converts the peer to its self-contained components +// representation, carrying exactly the subset of peer data that crosses the +// components wire format. Returns nil for a nil peer so callers can convert +// possibly-missing peers without guarding. +func (p *Peer) ToComponent() *sharedTypes.ComponentPeer { + if p == nil { + return nil + } + cp := &sharedTypes.ComponentPeer{ + ID: p.ID, + Key: p.Key, + IP: p.IP, + IPv6: p.IPv6, + DNSLabel: p.DNSLabel, + SSHKey: p.SSHKey, + SSHEnabled: p.SSHEnabled, + ServerSSHAllowed: p.Meta.Flags.ServerSSHAllowed, + AgentVersion: p.Meta.WtVersion, + SupportsSourcePrefixes: p.SupportsSourcePrefixes(), + SupportsIPv6: p.SupportsIPv6(), + LoginExpirationEnabled: p.LoginExpirationEnabled, + AddedWithSSOLogin: p.AddedWithSSOLogin(), + } + if p.LastLogin != nil { + cp.LastLogin = *p.LastLogin + } + return cp +} + // HasCapability reports whether the peer has the given capability. func (p *Peer) HasCapability(capability int32) bool { return slices.Contains(p.Meta.Capabilities, capability) diff --git a/management/server/peer_test.go b/management/server/peer_test.go index d471a1302..a7f8ba695 100644 --- a/management/server/peer_test.go +++ b/management/server/peer_test.go @@ -1092,14 +1092,14 @@ func TestToSyncResponse(t *testing.T) { } networkMap := &types.NetworkMap{ Network: &types.Network{Net: *ipnet, Serial: 1000}, - Peers: []*nbpeer.Peer{{ + Peers: []*types.ComponentPeer{{ IP: netip.MustParseAddr("192.168.1.2"), IPv6: netip.MustParseAddr("fd00::2"), Key: "peer2-key", DNSLabel: "peer2", SSHEnabled: true, SSHKey: "peer2-ssh-key"}}, - OfflinePeers: []*nbpeer.Peer{{ + OfflinePeers: []*types.ComponentPeer{{ IP: netip.MustParseAddr("192.168.1.3"), IPv6: netip.MustParseAddr("fd00::3"), Key: "peer3-key", diff --git a/management/server/posture/nb_version.go b/management/server/posture/nb_version.go index 6e4757021..3cace3b5f 100644 --- a/management/server/posture/nb_version.go +++ b/management/server/posture/nb_version.go @@ -3,11 +3,9 @@ package posture import ( "context" "fmt" - "strings" - - "github.com/hashicorp/go-version" nbpeer "github.com/netbirdio/netbird/management/server/peer" + nbversion "github.com/netbirdio/netbird/version" ) type NBVersionCheck struct { @@ -16,14 +14,8 @@ type NBVersionCheck struct { var _ Check = (*NBVersionCheck)(nil) -// sanitizeVersion removes anything after the pre-release tag (e.g., "-dev", "-alpha", etc.) -func sanitizeVersion(version string) string { - parts := strings.Split(version, "-") - return parts[0] -} - func (n *NBVersionCheck) Check(ctx context.Context, peer nbpeer.Peer) (bool, error) { - meetsMin, err := MeetsMinVersion(n.MinVersion, peer.Meta.WtVersion) + meetsMin, err := nbversion.MeetsMinVersion(n.MinVersion, peer.Meta.WtVersion) if err != nil { return false, err } @@ -48,21 +40,3 @@ func (n *NBVersionCheck) Validate() error { } return nil } - -// MeetsMinVersion checks if the peer's version meets or exceeds the minimum required version -func MeetsMinVersion(minVer, peerVer string) (bool, error) { - peerVer = sanitizeVersion(peerVer) - minVer = sanitizeVersion(minVer) - - peerNBVer, err := version.NewVersion(peerVer) - if err != nil { - return false, err - } - - constraints, err := version.NewConstraint(">= " + minVer) - if err != nil { - return false, err - } - - return constraints.Check(peerNBVer), nil -} diff --git a/management/server/posture/nb_version_test.go b/management/server/posture/nb_version_test.go index d3478afc2..1bf485453 100644 --- a/management/server/posture/nb_version_test.go +++ b/management/server/posture/nb_version_test.go @@ -139,68 +139,3 @@ func TestNBVersionCheck_Validate(t *testing.T) { }) } } - -func TestMeetsMinVersion(t *testing.T) { - tests := []struct { - name string - minVer string - peerVer string - want bool - wantErr bool - }{ - { - name: "Peer version greater than min version", - minVer: "0.26.0", - peerVer: "0.60.1", - want: true, - wantErr: false, - }, - { - name: "Peer version equals min version", - minVer: "1.0.0", - peerVer: "1.0.0", - want: true, - wantErr: false, - }, - { - name: "Peer version less than min version", - minVer: "1.0.0", - peerVer: "0.9.9", - want: false, - wantErr: false, - }, - { - name: "Peer version with pre-release tag greater than min version", - minVer: "1.0.0", - peerVer: "1.0.1-alpha", - want: true, - wantErr: false, - }, - { - name: "Invalid peer version format", - minVer: "1.0.0", - peerVer: "dev", - want: false, - wantErr: true, - }, - { - name: "Invalid min version format", - minVer: "invalid.version", - peerVer: "1.0.0", - want: false, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := MeetsMinVersion(tt.minVer, tt.peerVer) - if tt.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } - assert.Equal(t, tt.want, got) - }) - } -} diff --git a/management/server/types/account.go b/management/server/types/account.go index 05033ae15..588e63a09 100644 --- a/management/server/types/account.go +++ b/management/server/types/account.go @@ -283,8 +283,8 @@ func (a *Account) SynthesizePrivateServiceZones(peerID string) []nbdns.CustomZon // it, adding a single private service would black-hole every // other name under the zone apex. zone = &nbdns.CustomZone{ - Domain: dns.Fqdn(serviceDomainZone), - Records: []nbdns.SimpleRecord{}, + Domain: dns.Fqdn(serviceDomainZone), + Records: []nbdns.SimpleRecord{}, NonAuthoritative: true, SearchDomainDisabled: true, } @@ -1082,6 +1082,7 @@ func (a *Account) connResourcesGenerator(ctx context.Context, targetPeer *nbpeer peersExists := make(map[string]struct{}) rules := make([]*FirewallRule, 0) peers := make([]*nbpeer.Peer, 0) + targetComponent := targetPeer.ToComponent() return func(rule *PolicyRule, groupPeers []*nbpeer.Peer, direction int) { for _, peer := range groupPeers { @@ -1117,10 +1118,10 @@ func (a *Account) connResourcesGenerator(ctx context.Context, targetPeer *nbpeer if len(rule.Ports) == 0 && len(rule.PortRanges) == 0 { rules = append(rules, &fr) } else { - rules = append(rules, ExpandPortsAndRanges(fr, rule, targetPeer)...) + rules = append(rules, ExpandPortsAndRanges(fr, rule, targetComponent)...) } - rules = AppendIPv6FirewallRule(rules, rulesExists, peer, targetPeer, rule, FirewallRuleContext{ + rules = AppendIPv6FirewallRule(rules, rulesExists, peer.ToComponent(), targetComponent, rule, FirewallRuleContext{ Direction: direction, DirStr: strconv.Itoa(direction), ProtocolStr: string(protocol), @@ -1280,7 +1281,7 @@ func (a *Account) getRouteFirewallRules(ctx context.Context, peerID string, poli return fwRules } -func (a *Account) getRulePeers(rule *PolicyRule, postureChecks []string, peerID string, distributionPeers map[string]struct{}, validatedPeersMap map[string]struct{}) []*nbpeer.Peer { +func (a *Account) getRulePeers(rule *PolicyRule, postureChecks []string, peerID string, distributionPeers map[string]struct{}, validatedPeersMap map[string]struct{}) []*ComponentPeer { distPeersWithPolicy := make(map[string]struct{}) for _, id := range rule.Sources { group := a.Groups[id] @@ -1307,13 +1308,13 @@ func (a *Account) getRulePeers(rule *PolicyRule, postureChecks []string, peerID } } - distributionGroupPeers := make([]*nbpeer.Peer, 0, len(distPeersWithPolicy)) + distributionGroupPeers := make([]*ComponentPeer, 0, len(distPeersWithPolicy)) for pID := range distPeersWithPolicy { peer := a.Peers[pID] if peer == nil { continue } - distributionGroupPeers = append(distributionGroupPeers, peer) + distributionGroupPeers = append(distributionGroupPeers, peer.ToComponent()) } return distributionGroupPeers } diff --git a/management/server/types/account_components.go b/management/server/types/account_components.go index 0205a1f55..af27788d8 100644 --- a/management/server/types/account_components.go +++ b/management/server/types/account_components.go @@ -9,9 +9,7 @@ import ( nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/management/internals/modules/zones" - resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types" routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" - nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/telemetry" "github.com/netbirdio/netbird/route" ) @@ -113,7 +111,7 @@ func (a *Account) GetPeerNetworkMapComponents( PeerID: peerID, Network: a.Network.Copy(), // must include the target peer as it's required on the client - Peers: map[string]*nbpeer.Peer{peerID: peer}, + Peers: map[string]*ComponentPeer{peerID: peer.ToComponent()}, }) } @@ -126,7 +124,7 @@ func (a *Account) GetPeerNetworkMapComponents( PeerID: peerID, Network: a.Network.Copy(), // must include the target peer as it's required on the client - Peers: map[string]*nbpeer.Peer{peerID: peer}, + Peers: map[string]*ComponentPeer{peerID: peer.ToComponent()}, }) } @@ -136,10 +134,10 @@ func (a *Account) GetPeerNetworkMapComponents( NameServerGroups: make([]*nbdns.NameServerGroup, 0), CustomZoneDomain: peersCustomZone.Domain, ResourcePoliciesMap: make(map[string][]*Policy), - RoutersMap: make(map[string]map[string]*routerTypes.NetworkRouter), - NetworkResources: make([]*resourceTypes.NetworkResource, 0), + RoutersMap: make(map[string]map[string]*ComponentRouter), + NetworkResources: make([]*ComponentResource, 0), PostureFailedPeers: make(map[string]map[string]struct{}, len(a.PostureChecks)), - RouterPeers: make(map[string]*nbpeer.Peer), + RouterPeers: make(map[string]*ComponentPeer), NetworkXIDToPublicID: make(map[string]string, len(a.Networks)), PostureCheckXIDToPublicID: make(map[string]string, len(a.PostureChecks)), } @@ -174,7 +172,7 @@ func (a *Account) GetPeerNetworkMapComponents( } components.Peers = relevantPeers - components.Groups = relevantGroups + components.Groups = GroupsToComponent(relevantGroups) components.Policies = relevantPolicies components.Routes = relevantRoutes components.AllDNSRecords = filterDNSRecordsByPeers(peersCustomZone.Records, relevantPeers, peer.SupportsIPv6() && peer.IPv6.IsValid()) @@ -223,7 +221,7 @@ func (a *Account) GetPeerNetworkMapComponents( } for _, pID := range a.getPostureValidPeersSaveFailed(peers, policy.SourcePostureChecks, validatedPeersMap, &components.PostureFailedPeers) { if _, exists := components.Peers[pID]; !exists { - components.Peers[pID] = a.GetPeer(pID) + components.Peers[pID] = a.GetPeer(pID).ToComponent() } } } else { @@ -256,14 +254,14 @@ func (a *Account) GetPeerNetworkMapComponents( for _, srcGroupID := range rule.Sources { if g := a.Groups[srcGroupID]; g != nil { if _, exists := components.Groups[srcGroupID]; !exists { - components.Groups[srcGroupID] = g + components.Groups[srcGroupID] = g.ToComponent() } } } for _, dstGroupID := range rule.Destinations { if g := a.Groups[dstGroupID]; g != nil { if _, exists := components.Groups[dstGroupID]; !exists { - components.Groups[dstGroupID] = g + components.Groups[dstGroupID] = g.ToComponent() } } } @@ -278,20 +276,22 @@ func (a *Account) GetPeerNetworkMapComponents( // network in the account — accounts with many tenants/networks // shipped tens of unrelated peers in `peers[]` and `routers_map`. if addSourcePeers { - components.RoutersMap[resource.NetworkID] = networkRoutingPeers + components.RoutersMap[resource.NetworkID] = routerTypes.ToComponentMap(networkRoutingPeers) for peerIDKey := range networkRoutingPeers { if p := a.Peers[peerIDKey]; p != nil { - if _, exists := components.RouterPeers[peerIDKey]; !exists { - components.RouterPeers[peerIDKey] = p + cp := components.RouterPeers[peerIDKey] + if cp == nil { + cp = p.ToComponent() + components.RouterPeers[peerIDKey] = cp } if _, exists := components.Peers[peerIDKey]; !exists { if _, validated := validatedPeersMap[peerIDKey]; validated { - components.Peers[peerIDKey] = p + components.Peers[peerIDKey] = cp } } } } - components.NetworkResources = append(components.NetworkResources, resource) + components.NetworkResources = append(components.NetworkResources, resource.ToComponent()) } } @@ -312,14 +312,14 @@ func (a *Account) getPeersGroupsPoliciesRoutes( peerSSHEnabled bool, validatedPeersMap map[string]struct{}, postureFailedPeers *map[string]map[string]struct{}, -) (map[string]*nbpeer.Peer, map[string]*Group, []*Policy, []*route.Route, sshRequirements) { - relevantPeerIDs := make(map[string]*nbpeer.Peer, len(a.Peers)/4) +) (map[string]*ComponentPeer, map[string]*Group, []*Policy, []*route.Route, sshRequirements) { + relevantPeerIDs := make(map[string]*ComponentPeer, len(a.Peers)/4) relevantGroupIDs := make(map[string]*Group, len(a.Groups)/4) relevantPolicies := make([]*Policy, 0, len(a.Policies)) relevantRoutes := make([]*route.Route, 0, len(a.Routes)) sshReqs := sshRequirements{neededGroupIDs: make(map[string]struct{})} - relevantPeerIDs[peerID] = a.GetPeer(peerID) + relevantPeerIDs[peerID] = a.GetPeer(peerID).ToComponent() peerGroupSet := make(map[string]struct{}, 8) for groupID, group := range a.Groups { @@ -384,7 +384,7 @@ func (a *Account) getPeersGroupsPoliciesRoutes( if r.Peer != "" { if _, ok := validatedPeersMap[r.Peer]; ok { if p := a.GetPeer(r.Peer); p != nil { - relevantPeerIDs[r.Peer] = p + relevantPeerIDs[r.Peer] = p.ToComponent() } } } @@ -401,7 +401,7 @@ func (a *Account) getPeersGroupsPoliciesRoutes( continue } if p := a.GetPeer(pid); p != nil { - relevantPeerIDs[pid] = p + relevantPeerIDs[pid] = p.ToComponent() } } } @@ -458,7 +458,9 @@ func (a *Account) getPeersGroupsPoliciesRoutes( if peerInSources { policyRelevant = true for _, pid := range destinationPeers { - relevantPeerIDs[pid] = a.GetPeer(pid) + if _, exists := relevantPeerIDs[pid]; !exists { + relevantPeerIDs[pid] = a.GetPeer(pid).ToComponent() + } } for _, dstGroupID := range rule.Destinations { relevantGroupIDs[dstGroupID] = a.GetGroup(dstGroupID) @@ -468,7 +470,9 @@ func (a *Account) getPeersGroupsPoliciesRoutes( if peerInDestinations { policyRelevant = true for _, pid := range sourcePeers { - relevantPeerIDs[pid] = a.GetPeer(pid) + if _, exists := relevantPeerIDs[pid]; !exists { + relevantPeerIDs[pid] = a.GetPeer(pid).ToComponent() + } } for _, srcGroupID := range rule.Sources { relevantGroupIDs[srcGroupID] = a.GetGroup(srcGroupID) @@ -624,7 +628,7 @@ func (a *Account) getPostureValidPeersSaveFailed(inputPeers []string, postureChe // that name them. Calculate() tolerates groups with empty Peers (the inner // loops simply iterate zero times), so retaining them is behaviourally a // no-op for the legacy path that consumes the same NetworkMapComponents. -func filterGroupPeers(groups *map[string]*Group, peers map[string]*nbpeer.Peer) { +func filterGroupPeers(groups *map[string]*ComponentGroup, peers map[string]*ComponentPeer) { for groupID, groupInfo := range *groups { filteredPeers := make([]string, 0, len(groupInfo.Peers)) for _, pid := range groupInfo.Peers { @@ -634,14 +638,14 @@ func filterGroupPeers(groups *map[string]*Group, peers map[string]*nbpeer.Peer) } if len(filteredPeers) != len(groupInfo.Peers) { - ng := groupInfo.Copy() + ng := *groupInfo ng.Peers = filteredPeers - (*groups)[groupID] = ng + (*groups)[groupID] = &ng } } } -func filterPostureFailedPeers(postureFailedPeers *map[string]map[string]struct{}, policies []*Policy, resourcePoliciesMap map[string][]*Policy, peers map[string]*nbpeer.Peer) { +func filterPostureFailedPeers(postureFailedPeers *map[string]map[string]struct{}, policies []*Policy, resourcePoliciesMap map[string][]*Policy, peers map[string]*ComponentPeer) { if len(*postureFailedPeers) == 0 { return } @@ -676,7 +680,7 @@ func filterPostureFailedPeers(postureFailedPeers *map[string]map[string]struct{} } } -func filterDNSRecordsByPeers(records []nbdns.SimpleRecord, peers map[string]*nbpeer.Peer, includeIPv6 bool) []nbdns.SimpleRecord { +func filterDNSRecordsByPeers(records []nbdns.SimpleRecord, peers map[string]*ComponentPeer, includeIPv6 bool) []nbdns.SimpleRecord { if len(records) == 0 || len(peers) == 0 { return nil } diff --git a/management/server/types/account_private_netmap_test.go b/management/server/types/account_private_netmap_test.go index dc097ce26..11b3d985a 100644 --- a/management/server/types/account_private_netmap_test.go +++ b/management/server/types/account_private_netmap_test.go @@ -9,7 +9,6 @@ import ( "github.com/stretchr/testify/require" nbdns "github.com/netbirdio/netbird/dns" - nbpeer "github.com/netbirdio/netbird/management/server/peer" ) func TestPrivateService_NetworkMap_UserPeer_AndProxyPeer(t *testing.T) { @@ -49,7 +48,7 @@ func TestPrivateService_NetworkMap_UserPeer_AndProxyPeer(t *testing.T) { }) } -func netmapPeerIDs(peers []*nbpeer.Peer) []string { +func netmapPeerIDs(peers []*ComponentPeer) []string { ids := make([]string, 0, len(peers)) for _, p := range peers { if p == nil { diff --git a/management/server/types/account_test.go b/management/server/types/account_test.go index e5b5708fa..67d9e1c6f 100644 --- a/management/server/types/account_test.go +++ b/management/server/types/account_test.go @@ -666,7 +666,7 @@ func Test_ExpandPortsAndRanges_SSHRuleExpansion(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := ExpandPortsAndRanges(tt.base, tt.rule, tt.peer) + result := ExpandPortsAndRanges(tt.base, tt.rule, tt.peer.ToComponent()) var ports []string for _, fr := range result { diff --git a/management/server/types/aliases.go b/management/server/types/aliases.go index f5837a343..9324cfa1e 100644 --- a/management/server/types/aliases.go +++ b/management/server/types/aliases.go @@ -6,7 +6,6 @@ import ( "net" "net/netip" - nbpeer "github.com/netbirdio/netbird/management/server/peer" nbroute "github.com/netbirdio/netbird/route" sharedtypes "github.com/netbirdio/netbird/shared/management/types" ) @@ -18,9 +17,6 @@ type DNSSettings = sharedtypes.DNSSettings type FirewallRule = sharedtypes.FirewallRule -type Group = sharedtypes.Group -type GroupPeer = sharedtypes.GroupPeer - type Network = sharedtypes.Network type NetworkMap = sharedtypes.NetworkMap type ForwardingRule = sharedtypes.ForwardingRule @@ -42,6 +38,18 @@ type RouteFirewallRule = sharedtypes.RouteFirewallRule type NetworkMapComponents = sharedtypes.NetworkMapComponents +type ComponentPeer = sharedtypes.ComponentPeer +type ComponentGroup = sharedtypes.ComponentGroup +type ComponentRouter = sharedtypes.ComponentRouter +type ComponentResource = sharedtypes.ComponentResource +type ComponentResourceType = sharedtypes.ComponentResourceType + +const ( + ComponentResourceHost = sharedtypes.ComponentResourceHost + ComponentResourceSubnet = sharedtypes.ComponentResourceSubnet + ComponentResourceDomain = sharedtypes.ComponentResourceDomain +) + var EmptyNetworkMapComponents = sharedtypes.EmptyNetworkMapComponents type AccountSettingsInfo = sharedtypes.AccountSettingsInfo @@ -52,12 +60,7 @@ type NetworkMapComponentsCompact = sharedtypes.NetworkMapComponentsCompact type LookupMap = sharedtypes.LookupMap type FirewallRuleContext = sharedtypes.FirewallRuleContext -const ( - GroupIssuedAPI = sharedtypes.GroupIssuedAPI - GroupIssuedJWT = sharedtypes.GroupIssuedJWT - GroupIssuedIntegration = sharedtypes.GroupIssuedIntegration - GroupAllName = sharedtypes.GroupAllName -) +const GroupAllName = sharedtypes.GroupAllName // Function forwarders preserve types.X(...) call sites that previously // resolved to package-local funcs. Plain forwarders (not var aliases) keep @@ -67,11 +70,11 @@ func PolicyRuleImpliesLegacySSH(rule *PolicyRule) bool { return sharedtypes.PolicyRuleImpliesLegacySSH(rule) } -func ExpandPortsAndRanges(base FirewallRule, rule *PolicyRule, peer *nbpeer.Peer) []*FirewallRule { +func ExpandPortsAndRanges(base FirewallRule, rule *PolicyRule, peer *ComponentPeer) []*FirewallRule { return sharedtypes.ExpandPortsAndRanges(base, rule, peer) } -func AppendIPv6FirewallRule(rules []*FirewallRule, rulesExists map[string]struct{}, peer, targetPeer *nbpeer.Peer, rule *PolicyRule, rc FirewallRuleContext) []*FirewallRule { +func AppendIPv6FirewallRule(rules []*FirewallRule, rulesExists map[string]struct{}, peer, targetPeer *ComponentPeer, rule *PolicyRule, rc FirewallRuleContext) []*FirewallRule { return sharedtypes.AppendIPv6FirewallRule(rules, rulesExists, peer, targetPeer, rule, rc) } @@ -79,7 +82,7 @@ func CalculateNetworkMapFromComponents(ctx context.Context, components *NetworkM return sharedtypes.CalculateNetworkMapFromComponents(ctx, components) } -func GenerateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule *PolicyRule, groupPeers []*nbpeer.Peer, direction int, includeIPv6 bool) []*RouteFirewallRule { +func GenerateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule *PolicyRule, groupPeers []*ComponentPeer, direction int, includeIPv6 bool) []*RouteFirewallRule { return sharedtypes.GenerateRouteFirewallRules(ctx, route, rule, groupPeers, direction, includeIPv6) } diff --git a/shared/management/types/group.go b/management/server/types/group.go similarity index 83% rename from shared/management/types/group.go rename to management/server/types/group.go index e6e285e62..a5e196997 100644 --- a/shared/management/types/group.go +++ b/management/server/types/group.go @@ -2,7 +2,6 @@ package types import ( "github.com/netbirdio/netbird/management/server/integration_reference" - "github.com/netbirdio/netbird/management/server/networks/resources/types" ) const ( @@ -68,10 +67,6 @@ func (g *Group) EventMeta() map[string]any { return map[string]any{"name": g.Name} } -func (g *Group) EventMetaResource(resource *types.NetworkResource) map[string]any { - return map[string]any{"name": g.Name, "id": g.ID, "resource_name": resource.Name, "resource_id": resource.ID, "resource_type": resource.Type} -} - func (g *Group) Copy() *Group { group := &Group{ ID: g.ID, @@ -95,14 +90,39 @@ func (g *Group) HasPeers() bool { return len(g.Peers) > 0 } -// GroupAllName is the reserved name of the default group that contains every peer in an account. -const GroupAllName = "All" - // IsGroupAll checks if the group is a default "All" group. func (g *Group) IsGroupAll() bool { return g.Name == GroupAllName } +// ToComponent converts the group to its self-contained components +// representation. The Peers slice is shared, not copied — components are +// treated as immutable snapshots. Returns nil for a nil group. +func (g *Group) ToComponent() *ComponentGroup { + if g == nil { + return nil + } + return &ComponentGroup{ + ID: g.ID, + PublicID: g.PublicID, + Name: g.Name, + Peers: g.Peers, + } +} + +// GroupsToComponent converts an id-keyed group map to its components +// representation, preserving nil entries. +func GroupsToComponent(groups map[string]*Group) map[string]*ComponentGroup { + if groups == nil { + return nil + } + out := make(map[string]*ComponentGroup, len(groups)) + for id, g := range groups { + out[id] = g.ToComponent() + } + return out +} + // AddPeer adds peerID to Peers if not present, returning true if added. func (g *Group) AddPeer(peerID string) bool { if peerID == "" { diff --git a/management/server/types/ipv6_endtoend_test.go b/management/server/types/ipv6_endtoend_test.go index ddd1f649f..d83603abe 100644 --- a/management/server/types/ipv6_endtoend_test.go +++ b/management/server/types/ipv6_endtoend_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/require" nbpeer "github.com/netbirdio/netbird/management/server/peer" + "github.com/netbirdio/netbird/management/server/types" ) func TestNetworkMapComponents_IPv6EndToEnd(t *testing.T) { @@ -104,7 +105,7 @@ func TestNetworkMapComponents_RemotePeerWithoutCapability(t *testing.T) { require.NotNil(t, nm) t.Run("AllowedIPs include remote v6", func(t *testing.T) { - var dst *nbpeer.Peer + var dst *types.ComponentPeer for _, p := range nm.Peers { if p.ID == "peer-dst-1" { dst = p diff --git a/management/server/types/networkmap_components_test.go b/management/server/types/networkmap_components_test.go index 1a99b4511..3f2288f88 100644 --- a/management/server/types/networkmap_components_test.go +++ b/management/server/types/networkmap_components_test.go @@ -49,7 +49,7 @@ func allPeersValidated(account *types.Account, excludePeerIDs ...string) map[str return validated } -func peerIDs(peers []*nbpeer.Peer) []string { +func peerIDs(peers []*types.ComponentPeer) []string { ids := make([]string, len(peers)) for i, p := range peers { ids[i] = p.ID diff --git a/management/server/util/util.go b/management/server/util/util.go index 617484274..d85b55f02 100644 --- a/management/server/util/util.go +++ b/management/server/util/util.go @@ -19,34 +19,3 @@ func Difference(a, b []string) []string { func ToPtr[T any](value T) *T { return &value } - -type comparableObject[T any] interface { - Equal(other T) bool -} - -func MergeUnique[T comparableObject[T]](arr1, arr2 []T) []T { - var result []T - - for _, item := range arr1 { - if !contains(result, item) { - result = append(result, item) - } - } - - for _, item := range arr2 { - if !contains(result, item) { - result = append(result, item) - } - } - - return result -} - -func contains[T comparableObject[T]](slice []T, element T) bool { - for _, item := range slice { - if item.Equal(element) { - return true - } - } - return false -} diff --git a/shared/management/networkmap/decode.go b/shared/management/networkmap/decode.go index c66074b4f..d15117b6e 100644 --- a/shared/management/networkmap/decode.go +++ b/shared/management/networkmap/decode.go @@ -11,9 +11,6 @@ import ( log "github.com/sirupsen/logrus" nbdns "github.com/netbirdio/netbird/dns" - resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types" - routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" - nbpeer "github.com/netbirdio/netbird/management/server/peer" nbroute "github.com/netbirdio/netbird/route" "github.com/netbirdio/netbird/shared/management/domain" "github.com/netbirdio/netbird/shared/management/proto" @@ -38,17 +35,17 @@ func DecodeEnvelope(env *proto.NetworkMapEnvelope) (*types.NetworkMapComponents, Network: decodeAccountNetwork(full.Network), AccountSettings: decodeAccountSettings(full.AccountSettings), CustomZoneDomain: full.CustomZoneDomain, - Peers: make(map[string]*nbpeer.Peer, len(full.Peers)), - Groups: make(map[string]*types.Group, len(full.Groups)), + Peers: make(map[string]*types.ComponentPeer, len(full.Peers)), + Groups: make(map[string]*types.ComponentGroup, len(full.Groups)), Policies: make([]*types.Policy, 0, len(full.Policies)), Routes: make([]*nbroute.Route, 0, len(full.Routes)), NameServerGroups: make([]*nbdns.NameServerGroup, 0, len(full.NameserverGroups)), AllDNSRecords: decodeSimpleRecords(full.AllDnsRecords), AccountZones: decodeCustomZones(full.AccountZones), ResourcePoliciesMap: make(map[string][]*types.Policy), - RoutersMap: make(map[string]map[string]*routerTypes.NetworkRouter), - NetworkResources: make([]*resourceTypes.NetworkResource, 0, len(full.NetworkResources)), - RouterPeers: make(map[string]*nbpeer.Peer), + RoutersMap: make(map[string]map[string]*types.ComponentRouter), + NetworkResources: make([]*types.ComponentResource, 0, len(full.NetworkResources)), + RouterPeers: make(map[string]*types.ComponentPeer), AllowedUserIDs: stringSliceToSet(full.AllowedUserIds), PostureFailedPeers: make(map[string]map[string]struct{}, len(full.PostureFailedPeers)), GroupIDToUserIDs: make(map[string][]string, len(full.GroupIdToUserIds)), @@ -101,7 +98,7 @@ func DecodeEnvelope(env *proto.NetworkMapEnvelope) (*types.NetworkMapComponents, log.WithField("peer idx", idx).Error("unrecognized peer idx during decoding") } } - group := &types.Group{ + group := &types.ComponentGroup{ ID: groupID, PublicID: gc.Id, Peers: peerIDs, @@ -151,7 +148,7 @@ func DecodeEnvelope(env *proto.NetworkMapEnvelope) (*types.NetworkMapComponents, // Phase 7: routers_map (outer key = network seq id, inner key = peer-id // reconstructed from peer_index). Synthesized network id is "net_". for networkID, list := range full.RoutersMap { - inner := make(map[string]*routerTypes.NetworkRouter, len(list.Entries)) + inner := make(map[string]*types.ComponentRouter, len(list.Entries)) for _, entry := range list.Entries { if !entry.PeerIndexSet { continue @@ -161,8 +158,7 @@ func DecodeEnvelope(env *proto.NetworkMapEnvelope) (*types.NetworkMapComponents, continue } peerID := peerIDByIndex[entry.PeerIndex] - inner[peerID] = &routerTypes.NetworkRouter{ - ID: "", + inner[peerID] = &types.ComponentRouter{ NetworkID: networkID, PublicID: entry.Id, Peer: peerID, @@ -264,40 +260,22 @@ func decodeAccountSettings(as *proto.AccountSettingsCompact) *types.AccountSetti } } -func decodePeerCompact(pc *proto.PeerCompact, peerID string) *nbpeer.Peer { - var caps []int32 - if pc.SupportsSourcePrefixes { - caps = append(caps, nbpeer.PeerCapabilitySourcePrefixes) - } - if pc.SupportsIpv6 { - caps = append(caps, nbpeer.PeerCapabilityIPv6Overlay) - } - peer := &nbpeer.Peer{ +func decodePeerCompact(pc *proto.PeerCompact, peerID string) *types.ComponentPeer { + peer := &types.ComponentPeer{ ID: peerID, Key: peerID, SSHKey: string(pc.SshPubKey), SSHEnabled: pc.SshEnabled, DNSLabel: pc.DnsLabel, LoginExpirationEnabled: pc.LoginExpirationEnabled, - Meta: nbpeer.PeerSystemMeta{ - WtVersion: pc.AgentVersion, - Capabilities: caps, - Flags: nbpeer.Flags{ - ServerSSHAllowed: pc.ServerSshAllowed, - }, - }, - } - if pc.AddedWithSsoLogin { - // Set a non-empty UserID so (*Peer).AddedWithSSOLogin() returns true. - // The original UserID isn't on the wire; the value is intentionally - // visibly synthetic so any future consumer that mistakes UserID for a - // real account user xid won't silently match (or worse, write the - // sentinel into a downstream record). - peer.UserID = "" + AgentVersion: pc.AgentVersion, + SupportsSourcePrefixes: pc.SupportsSourcePrefixes, + SupportsIPv6: pc.SupportsIpv6, + ServerSSHAllowed: pc.ServerSshAllowed, + AddedWithSSOLogin: pc.AddedWithSsoLogin, } if pc.LastLoginUnixNano != 0 { - t := time.Unix(0, pc.LastLoginUnixNano) - peer.LastLogin = &t + peer.LastLogin = time.Unix(0, pc.LastLoginUnixNano) } switch len(pc.Ip) { case 4: @@ -424,14 +402,14 @@ func decodeNameServerGroupRaw(nsg *proto.NameServerGroupRaw) *nbdns.NameServerGr return out } -func decodeNetworkResource(nr *proto.NetworkResourceRaw) *resourceTypes.NetworkResource { - out := &resourceTypes.NetworkResource{ +func decodeNetworkResource(nr *proto.NetworkResourceRaw) *types.ComponentResource { + out := &types.ComponentResource{ ID: nr.Id, PublicID: nr.Id, NetworkID: nr.NetworkSeq, Name: nr.Name, Description: nr.Description, - Type: resourceTypes.NetworkResourceType(nr.Type), + Type: types.ComponentResourceType(nr.Type), Address: nr.Address, Domain: nr.DomainValue, Enabled: nr.Enabled, diff --git a/shared/management/networkmap/encode.go b/shared/management/networkmap/encode.go index e808480ea..ccde32faf 100644 --- a/shared/management/networkmap/encode.go +++ b/shared/management/networkmap/encode.go @@ -20,10 +20,9 @@ import ( nbdns "github.com/netbirdio/netbird/dns" "net/netip" - nbpeer "github.com/netbirdio/netbird/management/server/peer" - "github.com/netbirdio/netbird/shared/management/types" nbroute "github.com/netbirdio/netbird/route" "github.com/netbirdio/netbird/shared/management/proto" + "github.com/netbirdio/netbird/shared/management/types" "github.com/netbirdio/netbird/shared/netiputil" "github.com/netbirdio/netbird/shared/sshauth" ) @@ -274,7 +273,7 @@ func ToProtocolDNSConfig(update nbdns.Config, cache DNSConfigCache, forwardPort // AppendRemotePeerConfig appends typed peers as proto.RemotePeerConfig // entries to dst and returns the result. -func AppendRemotePeerConfig(dst []*proto.RemotePeerConfig, peers []*nbpeer.Peer, dnsName string, includeIPv6 bool) []*proto.RemotePeerConfig { +func AppendRemotePeerConfig(dst []*proto.RemotePeerConfig, peers []*types.ComponentPeer, dnsName string, includeIPv6 bool) []*proto.RemotePeerConfig { for _, rPeer := range peers { allowedIPs := []string{rPeer.IP.String() + "/32"} if includeIPv6 && rPeer.IPv6.IsValid() { @@ -285,7 +284,7 @@ func AppendRemotePeerConfig(dst []*proto.RemotePeerConfig, peers []*nbpeer.Peer, AllowedIps: allowedIPs, SshConfig: &proto.SSHConfig{SshPubKey: []byte(rPeer.SSHKey)}, Fqdn: rPeer.FQDN(dnsName), - AgentVersion: rPeer.Meta.WtVersion, + AgentVersion: rPeer.AgentVersion, }) } return dst diff --git a/shared/management/networkmap/envelope.go b/shared/management/networkmap/envelope.go index 3f045a9eb..a928c5059 100644 --- a/shared/management/networkmap/envelope.go +++ b/shared/management/networkmap/envelope.go @@ -54,8 +54,8 @@ func EnvelopeToNetworkMap(ctx context.Context, env *proto.NetworkMapEnvelope, lo } components.PeerID = canonicalKey - includeIPv6 := localPeer.SupportsIPv6() && localPeer.IPv6.IsValid() - useSourcePrefixes := localPeer.SupportsSourcePrefixes() + includeIPv6 := localPeer.SupportsIPv6 && localPeer.IPv6.IsValid() + useSourcePrefixes := localPeer.SupportsSourcePrefixes typedNM := components.Calculate(ctx) diff --git a/shared/management/networkmap/envelope_test.go b/shared/management/networkmap/envelope_test.go index 11a5335be..a81478aff 100644 --- a/shared/management/networkmap/envelope_test.go +++ b/shared/management/networkmap/envelope_test.go @@ -13,7 +13,6 @@ import ( goproto "google.golang.org/protobuf/proto" mgmtgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc" - nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/types" nbnetworkmap "github.com/netbirdio/netbird/shared/management/networkmap" "github.com/netbirdio/netbird/shared/management/proto" @@ -144,14 +143,14 @@ func TestDecodeEnvelope_MalformedWgKeyPeerSkipped(t *testing.T) { func TestEnvelopeRoundTrip_AllGroupShortCircuitParity(t *testing.T) { ctx := context.Background() - peers := map[string]*nbpeer.Peer{} + peers := map[string]*types.ComponentPeer{} for i, id := range []string{"peer-T", "peer-S", "peer-ALL", "peer-O"} { - peers[id] = &nbpeer.Peer{ - ID: id, - Key: randomWgKey(t), - IP: netip.AddrFrom4([4]byte{100, 64, 0, byte(i + 1)}), - DNSLabel: id, - Meta: nbpeer.PeerSystemMeta{WtVersion: "0.40.0"}, + peers[id] = &types.ComponentPeer{ + ID: id, + Key: randomWgKey(t), + IP: netip.AddrFrom4([4]byte{100, 64, 0, byte(i + 1)}), + DNSLabel: id, + AgentVersion: "0.40.0", } } @@ -165,7 +164,7 @@ func TestEnvelopeRoundTrip_AllGroupShortCircuitParity(t *testing.T) { AccountSettings: &types.AccountSettingsInfo{}, DNSSettings: &types.DNSSettings{}, Peers: peers, - Groups: map[string]*types.Group{ + Groups: map[string]*types.ComponentGroup{ "g-src": {ID: "g-src", PublicID: "1", Name: "staff", Peers: []string{"peer-T", "peer-S"}}, "g-all": {ID: "g-all", PublicID: "2", Name: "All", Peers: []string{"peer-ALL"}}, "g-two": {ID: "g-two", PublicID: "3", Name: "second", Peers: []string{"peer-T", "peer-O"}}, @@ -232,22 +231,22 @@ func buildSmokeComponents(t *testing.T) (*types.NetworkMapComponents, string) { peerAKey := randomWgKey(t) peerBKey := randomWgKey(t) - peerA := &nbpeer.Peer{ - ID: "peer-A", - Key: peerAKey, - IP: netip.AddrFrom4([4]byte{100, 64, 0, 1}), - DNSLabel: "peerA", - Meta: nbpeer.PeerSystemMeta{WtVersion: "0.40.0"}, + peerA := &types.ComponentPeer{ + ID: "peer-A", + Key: peerAKey, + IP: netip.AddrFrom4([4]byte{100, 64, 0, 1}), + DNSLabel: "peerA", + AgentVersion: "0.40.0", } - peerB := &nbpeer.Peer{ - ID: "peer-B", - Key: peerBKey, - IP: netip.AddrFrom4([4]byte{100, 64, 0, 2}), - DNSLabel: "peerB", - Meta: nbpeer.PeerSystemMeta{WtVersion: "0.40.0"}, + peerB := &types.ComponentPeer{ + ID: "peer-B", + Key: peerBKey, + IP: netip.AddrFrom4([4]byte{100, 64, 0, 2}), + DNSLabel: "peerB", + AgentVersion: "0.40.0", } - group := &types.Group{ + group := &types.ComponentGroup{ ID: "group-all", PublicID: "1", Name: "All", Peers: []string{"peer-A", "peer-B"}, } @@ -274,11 +273,11 @@ func buildSmokeComponents(t *testing.T) (*types.NetworkMapComponents, string) { }, AccountSettings: &types.AccountSettingsInfo{}, DNSSettings: &types.DNSSettings{}, - Peers: map[string]*nbpeer.Peer{ + Peers: map[string]*types.ComponentPeer{ "peer-A": peerA, "peer-B": peerB, }, - Groups: map[string]*types.Group{ + Groups: map[string]*types.ComponentGroup{ "group-all": group, }, Policies: []*types.Policy{policy}, diff --git a/shared/management/types/component_types.go b/shared/management/types/component_types.go new file mode 100644 index 000000000..a511097b1 --- /dev/null +++ b/shared/management/types/component_types.go @@ -0,0 +1,103 @@ +package types + +import ( + "net/netip" + "time" +) + +// ComponentPeer is the self-contained peer representation used by +// NetworkMapComponents and the calculated NetworkMap. It carries exactly the +// subset of peer data that crosses the components wire format, so the shared +// calculation layer stays independent of the management server's domain +// types. +type ComponentPeer struct { + ID string + Key string + IP netip.Addr + IPv6 netip.Addr + DNSLabel string + SSHKey string + SSHEnabled bool + ServerSSHAllowed bool + AgentVersion string + SupportsSourcePrefixes bool + SupportsIPv6 bool + LoginExpirationEnabled bool + AddedWithSSOLogin bool + LastLogin time.Time +} + +// FQDN returns the peer's FQDN combined of the peer's DNS label and the system's DNS domain. +func (p *ComponentPeer) FQDN(dnsDomain string) string { + if dnsDomain == "" { + return "" + } + return p.DNSLabel + "." + dnsDomain +} + +// LoginExpired indicates whether the peer's login has expired, mirroring the +// server-side peer semantics: only SSO-added peers with login expiration +// enabled can expire. +func (p *ComponentPeer) LoginExpired(expiresIn time.Duration) (bool, time.Duration) { + if !p.AddedWithSSOLogin || !p.LoginExpirationEnabled { + return false, 0 + } + timeLeft := time.Until(p.LastLogin.Add(expiresIn)) + return timeLeft <= 0, timeLeft +} + +// GroupAllName is the reserved name of the default group that contains every peer in an account. +const GroupAllName = "All" + +// ComponentGroup is the self-contained group representation used by +// NetworkMapComponents: just the membership view the network-map calculation +// needs, without the server's storage fields. +type ComponentGroup struct { + ID string + PublicID string + Name string + Peers []string +} + +// IsGroupAll checks if the group is a default "All" group. +func (g *ComponentGroup) IsGroupAll() bool { + return g.Name == GroupAllName +} + +// ComponentRouter is the self-contained network-router representation used by +// NetworkMapComponents. +type ComponentRouter struct { + NetworkID string + PublicID string + Peer string + PeerGroups []string + Masquerade bool + Metric int + Enabled bool +} + +// ComponentResourceType mirrors the network-resource type enum on the +// components wire format. +type ComponentResourceType string + +const ( + ComponentResourceHost ComponentResourceType = "host" + ComponentResourceSubnet ComponentResourceType = "subnet" + ComponentResourceDomain ComponentResourceType = "domain" +) + +// ComponentResource is the self-contained network-resource representation +// used by NetworkMapComponents. +type ComponentResource struct { + ID string + PublicID string + NetworkID string + AccountID string + Name string + Description string + Type ComponentResourceType + Address string + Domain string + Prefix netip.Prefix + Enabled bool +} diff --git a/shared/management/types/firewall_helpers.go b/shared/management/types/firewall_helpers.go index dd174abe4..6e43af33e 100644 --- a/shared/management/types/firewall_helpers.go +++ b/shared/management/types/firewall_helpers.go @@ -3,8 +3,6 @@ package types import ( "strconv" - nbpeer "github.com/netbirdio/netbird/management/server/peer" - "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/version" ) @@ -48,8 +46,8 @@ func portsIncludesSSH(ports []string) bool { } // ExpandPortsAndRanges expands Ports and PortRanges of a rule into individual firewall rules. -func ExpandPortsAndRanges(base FirewallRule, rule *PolicyRule, peer *nbpeer.Peer) []*FirewallRule { - features := peerSupportedFirewallFeatures(peer.Meta.WtVersion) +func ExpandPortsAndRanges(base FirewallRule, rule *PolicyRule, peer *ComponentPeer) []*FirewallRule { + features := peerSupportedFirewallFeatures(peer.AgentVersion) var expanded []*FirewallRule @@ -106,8 +104,8 @@ func isPortInRule(portString string, portInt uint16, rule *FirewallRule) bool { return rule.Port == portString || (rule.PortRange.Start <= portInt && portInt <= rule.PortRange.End) } -func shouldCheckRulesForNativeSSH(supportsNative bool, rule *PolicyRule, peer *nbpeer.Peer) bool { - return supportsNative && peer.SSHEnabled && peer.Meta.Flags.ServerSSHAllowed && rule.Protocol == PolicyRuleProtocolTCP +func shouldCheckRulesForNativeSSH(supportsNative bool, rule *PolicyRule, peer *ComponentPeer) bool { + return supportsNative && peer.SSHEnabled && peer.ServerSSHAllowed && rule.Protocol == PolicyRuleProtocolTCP } func peerSupportedFirewallFeatures(peerVer string) supportedFeatures { @@ -117,13 +115,13 @@ func peerSupportedFirewallFeatures(peerVer string) supportedFeatures { var features supportedFeatures - meetMinVer, err := posture.MeetsMinVersion(firewallRuleMinNativeSSHVer, peerVer) + meetMinVer, err := version.MeetsMinVersion(firewallRuleMinNativeSSHVer, peerVer) features.nativeSSH = err == nil && meetMinVer if features.nativeSSH { features.portRanges = true } else { - meetMinVer, err = posture.MeetsMinVersion(firewallRuleMinPortRangesVer, peerVer) + meetMinVer, err = version.MeetsMinVersion(firewallRuleMinPortRangesVer, peerVer) features.portRanges = err == nil && meetMinVer } diff --git a/shared/management/types/firewall_rule.go b/shared/management/types/firewall_rule.go index 87dcfe307..67cb581a2 100644 --- a/shared/management/types/firewall_rule.go +++ b/shared/management/types/firewall_rule.go @@ -9,7 +9,6 @@ import ( log "github.com/sirupsen/logrus" - nbpeer "github.com/netbirdio/netbird/management/server/peer" nbroute "github.com/netbirdio/netbird/route" ) @@ -51,7 +50,7 @@ func (r *FirewallRule) Equal(other *FirewallRule) bool { // For static routes, source ranges match the destination family (v4 or v6). // For dynamic routes (domain-based), separate v4 and v6 rules are generated // so the routing peer's forwarding chain allows both address families. -func GenerateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule *PolicyRule, groupPeers []*nbpeer.Peer, direction int, includeIPv6 bool) []*RouteFirewallRule { +func GenerateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule *PolicyRule, groupPeers []*ComponentPeer, direction int, includeIPv6 bool) []*RouteFirewallRule { rulesExists := make(map[string]struct{}) rules := make([]*RouteFirewallRule, 0) @@ -107,7 +106,7 @@ func GenerateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule } // splitPeerSourcesByFamily separates peer IPs into v4 (/32) and v6 (/128) source ranges. -func splitPeerSourcesByFamily(groupPeers []*nbpeer.Peer) (v4, v6 []string) { +func splitPeerSourcesByFamily(groupPeers []*ComponentPeer) (v4, v6 []string) { v4 = make([]string, 0, len(groupPeers)) v6 = make([]string, 0, len(groupPeers)) for _, peer := range groupPeers { diff --git a/shared/management/types/firewall_rule_test.go b/shared/management/types/firewall_rule_test.go index 9de4ca04a..c21cfa2df 100644 --- a/shared/management/types/firewall_rule_test.go +++ b/shared/management/types/firewall_rule_test.go @@ -8,13 +8,12 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/route" "github.com/netbirdio/netbird/shared/management/domain" ) func TestSplitPeerSourcesByFamily(t *testing.T) { - peers := []*nbpeer.Peer{ + peers := []*ComponentPeer{ { IP: netip.MustParseAddr("100.64.0.1"), IPv6: netip.MustParseAddr("fd00::1"), @@ -36,7 +35,7 @@ func TestSplitPeerSourcesByFamily(t *testing.T) { } func TestGenerateRouteFirewallRules_V4Route(t *testing.T) { - peers := []*nbpeer.Peer{ + peers := []*ComponentPeer{ { IP: netip.MustParseAddr("100.64.0.1"), IPv6: netip.MustParseAddr("fd00::1"), @@ -65,7 +64,7 @@ func TestGenerateRouteFirewallRules_V4Route(t *testing.T) { } func TestGenerateRouteFirewallRules_V6Route(t *testing.T) { - peers := []*nbpeer.Peer{ + peers := []*ComponentPeer{ { IP: netip.MustParseAddr("100.64.0.1"), IPv6: netip.MustParseAddr("fd00::1"), @@ -93,7 +92,7 @@ func TestGenerateRouteFirewallRules_V6Route(t *testing.T) { } func TestGenerateRouteFirewallRules_DynamicRoute_DualStack(t *testing.T) { - peers := []*nbpeer.Peer{ + peers := []*ComponentPeer{ { IP: netip.MustParseAddr("100.64.0.1"), IPv6: netip.MustParseAddr("fd00::1"), @@ -126,7 +125,7 @@ func TestGenerateRouteFirewallRules_DynamicRoute_DualStack(t *testing.T) { } func TestGenerateRouteFirewallRules_DynamicRoute_NoV6Peers(t *testing.T) { - peers := []*nbpeer.Peer{ + peers := []*ComponentPeer{ {IP: netip.MustParseAddr("100.64.0.1")}, {IP: netip.MustParseAddr("100.64.0.2")}, } @@ -150,7 +149,7 @@ func TestGenerateRouteFirewallRules_DynamicRoute_NoV6Peers(t *testing.T) { } func TestGenerateRouteFirewallRules_IncludeIPv6False(t *testing.T) { - peers := []*nbpeer.Peer{ + peers := []*ComponentPeer{ { IP: netip.MustParseAddr("100.64.0.1"), IPv6: netip.MustParseAddr("fd00::1"), diff --git a/shared/management/types/network.go b/shared/management/types/network.go index fe67bfd97..72a5cc5b3 100644 --- a/shared/management/types/network.go +++ b/shared/management/types/network.go @@ -15,8 +15,6 @@ import ( "golang.org/x/exp/maps" nbdns "github.com/netbirdio/netbird/dns" - nbpeer "github.com/netbirdio/netbird/management/server/peer" - "github.com/netbirdio/netbird/management/server/util" "github.com/netbirdio/netbird/route" "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/shared/management/status" @@ -39,11 +37,11 @@ const ( ) type NetworkMap struct { - Peers []*nbpeer.Peer + Peers []*ComponentPeer Network *Network Routes []*route.Route DNSConfig nbdns.Config - OfflinePeers []*nbpeer.Peer + OfflinePeers []*ComponentPeer FirewallRules []*FirewallRule RoutesFirewallRules []*RouteFirewallRule ForwardingRules []*ForwardingRule @@ -53,15 +51,46 @@ type NetworkMap struct { func (nm *NetworkMap) Merge(other *NetworkMap) { nm.Peers = mergeUniquePeersByID(nm.Peers, other.Peers) - nm.Routes = util.MergeUnique(nm.Routes, other.Routes) + nm.Routes = mergeUnique(nm.Routes, other.Routes) nm.OfflinePeers = mergeUniquePeersByID(nm.OfflinePeers, other.OfflinePeers) - nm.FirewallRules = util.MergeUnique(nm.FirewallRules, other.FirewallRules) - nm.RoutesFirewallRules = util.MergeUnique(nm.RoutesFirewallRules, other.RoutesFirewallRules) - nm.ForwardingRules = util.MergeUnique(nm.ForwardingRules, other.ForwardingRules) + nm.FirewallRules = mergeUnique(nm.FirewallRules, other.FirewallRules) + nm.RoutesFirewallRules = mergeUnique(nm.RoutesFirewallRules, other.RoutesFirewallRules) + nm.ForwardingRules = mergeUnique(nm.ForwardingRules, other.ForwardingRules) } -func mergeUniquePeersByID(peers1, peers2 []*nbpeer.Peer) []*nbpeer.Peer { - result := make(map[string]*nbpeer.Peer) +type comparableObject[T any] interface { + Equal(other T) bool +} + +func mergeUnique[T comparableObject[T]](arr1, arr2 []T) []T { + var result []T + + for _, item := range arr1 { + if !containsEqual(result, item) { + result = append(result, item) + } + } + + for _, item := range arr2 { + if !containsEqual(result, item) { + result = append(result, item) + } + } + + return result +} + +func containsEqual[T comparableObject[T]](slice []T, element T) bool { + for _, item := range slice { + if item.Equal(element) { + return true + } + } + return false +} + +func mergeUniquePeersByID(peers1, peers2 []*ComponentPeer) []*ComponentPeer { + result := make(map[string]*ComponentPeer) for _, peer := range peers1 { result[peer.ID] = peer } diff --git a/management/server/util/util_test.go b/shared/management/types/network_merge_test.go similarity index 88% rename from management/server/util/util_test.go rename to shared/management/types/network_merge_test.go index 5c928b369..a7ef24c1e 100644 --- a/management/server/util/util_test.go +++ b/shared/management/types/network_merge_test.go @@ -1,4 +1,4 @@ -package util +package types import ( "testing" @@ -17,7 +17,7 @@ func (t testObject) Equal(other testObject) bool { func Test_MergeUniqueArraysWithoutDuplicates(t *testing.T) { arr1 := []testObject{{value: 1}, {value: 2}} arr2 := []testObject{{value: 2}, {value: 3}} - result := MergeUnique(arr1, arr2) + result := mergeUnique(arr1, arr2) assert.Len(t, result, 3) assert.Contains(t, result, testObject{value: 1}) assert.Contains(t, result, testObject{value: 2}) @@ -27,14 +27,14 @@ func Test_MergeUniqueArraysWithoutDuplicates(t *testing.T) { func Test_MergeUniqueHandlesEmptyArrays(t *testing.T) { arr1 := []testObject{} arr2 := []testObject{} - result := MergeUnique(arr1, arr2) + result := mergeUnique(arr1, arr2) assert.Empty(t, result) } func Test_MergeUniqueHandlesOneEmptyArray(t *testing.T) { arr1 := []testObject{{value: 1}, {value: 2}} arr2 := []testObject{} - result := MergeUnique(arr1, arr2) + result := mergeUnique(arr1, arr2) assert.Len(t, result, 2) assert.Contains(t, result, testObject{value: 1}) assert.Contains(t, result, testObject{value: 2}) diff --git a/shared/management/types/networkmap_components.go b/shared/management/types/networkmap_components.go index fdb70f2f7..a708e99e1 100644 --- a/shared/management/types/networkmap_components.go +++ b/shared/management/types/networkmap_components.go @@ -12,9 +12,6 @@ import ( "github.com/netbirdio/netbird/client/ssh/auth" nbdns "github.com/netbirdio/netbird/dns" - resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types" - routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" - nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/route" "github.com/netbirdio/netbird/shared/management/domain" ) @@ -27,22 +24,22 @@ type NetworkMapComponents struct { DNSSettings *DNSSettings CustomZoneDomain string - Peers map[string]*nbpeer.Peer - Groups map[string]*Group + Peers map[string]*ComponentPeer + Groups map[string]*ComponentGroup Policies []*Policy Routes []*route.Route NameServerGroups []*nbdns.NameServerGroup AllDNSRecords []nbdns.SimpleRecord AccountZones []nbdns.CustomZone ResourcePoliciesMap map[string][]*Policy - RoutersMap map[string]map[string]*routerTypes.NetworkRouter - NetworkResources []*resourceTypes.NetworkResource + RoutersMap map[string]map[string]*ComponentRouter + NetworkResources []*ComponentResource GroupIDToUserIDs map[string][]string AllowedUserIDs map[string]struct{} PostureFailedPeers map[string]map[string]struct{} - RouterPeers map[string]*nbpeer.Peer + RouterPeers map[string]*ComponentPeer // NetworkXIDToPublicID maps Network.ID (xid) → PublicID. // Consumed by the envelope encoder to @@ -78,15 +75,15 @@ func EmptyNetworkMapComponents(nm *NetworkMapComponents) *NetworkMapComponents { return nm } -func (c *NetworkMapComponents) GetPeerInfo(peerID string) *nbpeer.Peer { +func (c *NetworkMapComponents) GetPeerInfo(peerID string) *ComponentPeer { return c.Peers[peerID] } -func (c *NetworkMapComponents) GetRouterPeerInfo(peerID string) *nbpeer.Peer { +func (c *NetworkMapComponents) GetRouterPeerInfo(peerID string) *ComponentPeer { return c.RouterPeers[peerID] } -func (c *NetworkMapComponents) GetGroupInfo(groupID string) *Group { +func (c *NetworkMapComponents) GetGroupInfo(groupID string) *ComponentGroup { return c.Groups[groupID] } @@ -142,7 +139,7 @@ func (c *NetworkMapComponents) Calculate(ctx context.Context) *NetworkMap { includeIPv6 := false if p := c.Peers[targetPeerID]; p != nil { - includeIPv6 = p.SupportsIPv6() && p.IPv6.IsValid() + includeIPv6 = p.SupportsIPv6 && p.IPv6.IsValid() } routesUpdate := filterAndExpandRoutes(c.getRoutesToSync(targetPeerID, peersToConnect, peerGroups), includeIPv6) routesFirewallRules := c.getPeerRoutesFirewallRules(ctx, targetPeerID, includeIPv6) @@ -200,7 +197,7 @@ func (c *NetworkMapComponents) IsEmpty() bool { return c.empty } -func (c *NetworkMapComponents) getPeerConnectionResources(targetPeerID string) ([]*nbpeer.Peer, []*FirewallRule, map[string]map[string]struct{}, bool) { +func (c *NetworkMapComponents) getPeerConnectionResources(targetPeerID string) ([]*ComponentPeer, []*FirewallRule, map[string]map[string]struct{}, bool) { targetPeer := c.GetPeerInfo(targetPeerID) if targetPeer == nil { return nil, nil, nil, false @@ -220,7 +217,7 @@ func (c *NetworkMapComponents) getPeerConnectionResources(targetPeerID string) ( continue } - var sourcePeers, destinationPeers []*nbpeer.Peer + var sourcePeers, destinationPeers []*ComponentPeer var peerInSources, peerInDestinations bool if rule.SourceResource.Type == ResourceTypePeer && rule.SourceResource.ID != "" { @@ -303,13 +300,13 @@ func (c *NetworkMapComponents) getAllowedUserIDs() map[string]struct{} { return make(map[string]struct{}) } -func (c *NetworkMapComponents) connResourcesGenerator(targetPeer *nbpeer.Peer) (func(*PolicyRule, []*nbpeer.Peer, int), func() ([]*nbpeer.Peer, []*FirewallRule)) { +func (c *NetworkMapComponents) connResourcesGenerator(targetPeer *ComponentPeer) (func(*PolicyRule, []*ComponentPeer, int), func() ([]*ComponentPeer, []*FirewallRule)) { rulesExists := make(map[string]struct{}) peersExists := make(map[string]struct{}) rules := make([]*FirewallRule, 0) - peers := make([]*nbpeer.Peer, 0) + peers := make([]*ComponentPeer, 0) - return func(rule *PolicyRule, groupPeers []*nbpeer.Peer, direction int) { + return func(rule *PolicyRule, groupPeers []*ComponentPeer, direction int) { protocol := rule.Protocol if protocol == PolicyRuleProtocolNetbirdSSH { protocol = PolicyRuleProtocolTCP @@ -361,15 +358,15 @@ func (c *NetworkMapComponents) connResourcesGenerator(targetPeer *nbpeer.Peer) ( PortsJoined: portsJoined, }) } - }, func() ([]*nbpeer.Peer, []*FirewallRule) { + }, func() ([]*ComponentPeer, []*FirewallRule) { return peers, rules } } -func (c *NetworkMapComponents) getAllPeersFromGroups(groups []string, peerID string, sourcePostureChecksIDs []string) ([]*nbpeer.Peer, bool) { +func (c *NetworkMapComponents) getAllPeersFromGroups(groups []string, peerID string, sourcePostureChecksIDs []string) ([]*ComponentPeer, bool) { peerInGroups := false uniquePeerIDs := c.getUniquePeerIDsFromGroupsIDs(groups) - filteredPeers := make([]*nbpeer.Peer, 0, len(uniquePeerIDs)) + filteredPeers := make([]*ComponentPeer, 0, len(uniquePeerIDs)) for _, p := range uniquePeerIDs { peerInfo := c.GetPeerInfo(p) @@ -421,22 +418,22 @@ func (c *NetworkMapComponents) getUniquePeerIDsFromGroupsIDs(groups []string) [] return ids } -func (c *NetworkMapComponents) getPeerFromResource(resource Resource, peerID string) ([]*nbpeer.Peer, bool) { +func (c *NetworkMapComponents) getPeerFromResource(resource Resource, peerID string) ([]*ComponentPeer, bool) { if resource.ID == peerID { - return []*nbpeer.Peer{}, true + return []*ComponentPeer{}, true } peerInfo := c.GetPeerInfo(resource.ID) if peerInfo == nil { - return []*nbpeer.Peer{}, false + return []*ComponentPeer{}, false } - return []*nbpeer.Peer{peerInfo}, false + return []*ComponentPeer{peerInfo}, false } -func (c *NetworkMapComponents) filterPeersByLoginExpiration(aclPeers []*nbpeer.Peer) ([]*nbpeer.Peer, []*nbpeer.Peer) { - peersToConnect := make([]*nbpeer.Peer, 0, len(aclPeers)) - var expiredPeers []*nbpeer.Peer +func (c *NetworkMapComponents) filterPeersByLoginExpiration(aclPeers []*ComponentPeer) ([]*ComponentPeer, []*ComponentPeer) { + peersToConnect := make([]*ComponentPeer, 0, len(aclPeers)) + var expiredPeers []*ComponentPeer for _, p := range aclPeers { expired, _ := p.LoginExpired(c.AccountSettings.PeerLoginExpiration) @@ -518,7 +515,7 @@ func filterAndExpandRoutes(routes []*route.Route, includeIPv6 bool) []*route.Rou return filtered } -func (c *NetworkMapComponents) getRoutesToSync(peerID string, aclPeers []*nbpeer.Peer, peerGroups LookupMap) []*route.Route { +func (c *NetworkMapComponents) getRoutesToSync(peerID string, aclPeers []*ComponentPeer, peerGroups LookupMap) []*route.Route { routes, peerDisabledRoutes := c.getRoutingPeerRoutes(peerID) peerRoutesMembership := make(LookupMap) for _, r := range append(routes, peerDisabledRoutes...) { @@ -732,7 +729,7 @@ func (c *NetworkMapComponents) getRouteFirewallRules(ctx context.Context, peerID return fwRules } -func (c *NetworkMapComponents) getRulePeers(rule *PolicyRule, postureChecks []string, peerID string, distributionPeers map[string]struct{}) []*nbpeer.Peer { +func (c *NetworkMapComponents) getRulePeers(rule *PolicyRule, postureChecks []string, peerID string, distributionPeers map[string]struct{}) []*ComponentPeer { distPeersWithPolicy := make(map[string]struct{}) for _, id := range rule.Sources { group := c.GetGroupInfo(id) @@ -759,7 +756,7 @@ func (c *NetworkMapComponents) getRulePeers(rule *PolicyRule, postureChecks []st } } - distributionGroupPeers := make([]*nbpeer.Peer, 0, len(distPeersWithPolicy)) + distributionGroupPeers := make([]*ComponentPeer, 0, len(distPeersWithPolicy)) for pID := range distPeersWithPolicy { peerInfo := c.GetPeerInfo(pID) if peerInfo == nil { @@ -799,8 +796,8 @@ func (c *NetworkMapComponents) getNetworkResourcesRoutesToSync(peerID string) (b func (c *NetworkMapComponents) processResourcePolicies( peerID string, - resource *resourceTypes.NetworkResource, - networkRoutingPeers map[string]*routerTypes.NetworkRouter, + resource *ComponentResource, + networkRoutingPeers map[string]*ComponentRouter, addSourcePeers bool, allSourcePeers map[string]struct{}, ) []*route.Route { @@ -833,7 +830,7 @@ func (c *NetworkMapComponents) getResourcePolicyPeers(policy *Policy) []string { return c.getUniquePeerIDsFromGroupsIDs(policy.SourceGroups()) } -func (c *NetworkMapComponents) getNetworkResourcesRoutes(resource *resourceTypes.NetworkResource, peerID string, router *routerTypes.NetworkRouter) []*route.Route { +func (c *NetworkMapComponents) getNetworkResourcesRoutes(resource *ComponentResource, peerID string, router *ComponentRouter) []*route.Route { resourceAppliedPolicies := c.ResourcePoliciesMap[resource.ID] var routes []*route.Route @@ -847,7 +844,7 @@ func (c *NetworkMapComponents) getNetworkResourcesRoutes(resource *resourceTypes return routes } -func (c *NetworkMapComponents) networkResourceToRoute(resource *resourceTypes.NetworkResource, peer *nbpeer.Peer, router *routerTypes.NetworkRouter) *route.Route { +func (c *NetworkMapComponents) networkResourceToRoute(resource *ComponentResource, peer *ComponentPeer, router *ComponentRouter) *route.Route { r := &route.Route{ ID: route.ID(resource.ID + ":" + peer.ID), AccountID: resource.AccountID, @@ -861,7 +858,7 @@ func (c *NetworkMapComponents) networkResourceToRoute(resource *resourceTypes.Ne Description: resource.Description, } - if resource.Type == resourceTypes.Host || resource.Type == resourceTypes.Subnet { + if resource.Type == ComponentResourceHost || resource.Type == ComponentResourceSubnet { r.Network = resource.Prefix r.NetworkType = route.IPv4Network @@ -870,7 +867,7 @@ func (c *NetworkMapComponents) networkResourceToRoute(resource *resourceTypes.Ne } } - if resource.Type == resourceTypes.Domain { + if resource.Type == ComponentResourceDomain { domainList, err := domain.FromStringList([]string{resource.Domain}) if err == nil { r.Domains = domainList @@ -948,11 +945,11 @@ func (c *NetworkMapComponents) getPoliciesSourcePeers(policies []*Policy) map[st func (c *NetworkMapComponents) addNetworksRoutingPeers( networkResourcesRoutes []*route.Route, peerID string, - peersToConnect []*nbpeer.Peer, - expiredPeers []*nbpeer.Peer, + peersToConnect []*ComponentPeer, + expiredPeers []*ComponentPeer, isRouter bool, sourcePeers map[string]struct{}, -) []*nbpeer.Peer { +) []*ComponentPeer { networkRoutesPeers := make(map[string]struct{}, len(networkResourcesRoutes)) for _, r := range networkResourcesRoutes { @@ -1002,8 +999,8 @@ type FirewallRuleContext struct { PortsJoined string } -func AppendIPv6FirewallRule(rules []*FirewallRule, rulesExists map[string]struct{}, peer, targetPeer *nbpeer.Peer, rule *PolicyRule, rc FirewallRuleContext) []*FirewallRule { - if !peer.IPv6.IsValid() || !targetPeer.SupportsIPv6() || !targetPeer.IPv6.IsValid() { +func AppendIPv6FirewallRule(rules []*FirewallRule, rulesExists map[string]struct{}, peer, targetPeer *ComponentPeer, rule *PolicyRule, rc FirewallRuleContext) []*FirewallRule { + if !peer.IPv6.IsValid() || !targetPeer.SupportsIPv6 || !targetPeer.IPv6.IsValid() { return rules } diff --git a/shared/management/types/networkmap_components_compact.go b/shared/management/types/networkmap_components_compact.go index b60f8bdb1..a1f53690d 100644 --- a/shared/management/types/networkmap_components_compact.go +++ b/shared/management/types/networkmap_components_compact.go @@ -2,9 +2,6 @@ package types import ( nbdns "github.com/netbirdio/netbird/dns" - resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types" - routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" - nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/route" ) @@ -21,7 +18,7 @@ type NetworkMapComponentsCompact struct { DNSSettings *DNSSettings CustomZoneDomain string - AllPeers []*nbpeer.Peer + AllPeers []*ComponentPeer PeerIndexes []int RouterPeerIndexes []int @@ -34,8 +31,8 @@ type NetworkMapComponentsCompact struct { AllDNSRecords []nbdns.SimpleRecord AccountZones []nbdns.CustomZone - RoutersMap map[string]map[string]*routerTypes.NetworkRouter - NetworkResources []*resourceTypes.NetworkResource + RoutersMap map[string]map[string]*ComponentRouter + NetworkResources []*ComponentResource GroupIDToUserIDs map[string][]string AllowedUserIDs map[string]struct{} @@ -44,7 +41,7 @@ type NetworkMapComponentsCompact struct { func (c *NetworkMapComponents) ToCompact() *NetworkMapComponentsCompact { peerToIndex := make(map[string]int) - var allPeers []*nbpeer.Peer + var allPeers []*ComponentPeer for id, peer := range c.Peers { if _, exists := peerToIndex[id]; !exists { @@ -150,7 +147,7 @@ func (c *NetworkMapComponents) ToCompact() *NetworkMapComponentsCompact { } func (c *NetworkMapComponentsCompact) ToFull() *NetworkMapComponents { - peers := make(map[string]*nbpeer.Peer, len(c.PeerIndexes)) + peers := make(map[string]*ComponentPeer, len(c.PeerIndexes)) for _, idx := range c.PeerIndexes { if idx >= 0 && idx < len(c.AllPeers) { peer := c.AllPeers[idx] @@ -158,7 +155,7 @@ func (c *NetworkMapComponentsCompact) ToFull() *NetworkMapComponents { } } - routerPeers := make(map[string]*nbpeer.Peer, len(c.RouterPeerIndexes)) + routerPeers := make(map[string]*ComponentPeer, len(c.RouterPeerIndexes)) for _, idx := range c.RouterPeerIndexes { if idx >= 0 && idx < len(c.AllPeers) { peer := c.AllPeers[idx] @@ -166,7 +163,7 @@ func (c *NetworkMapComponentsCompact) ToFull() *NetworkMapComponents { } } - groups := make(map[string]*Group, len(c.Groups)) + groups := make(map[string]*ComponentGroup, len(c.Groups)) for id, gc := range c.Groups { peerIDs := make([]string, 0, len(gc.PeerIndexes)) for _, idx := range gc.PeerIndexes { @@ -174,7 +171,7 @@ func (c *NetworkMapComponentsCompact) ToFull() *NetworkMapComponents { peerIDs = append(peerIDs, c.AllPeers[idx].ID) } } - groups[id] = &Group{ + groups[id] = &ComponentGroup{ ID: id, Name: gc.Name, Peers: peerIDs, diff --git a/version/version.go b/version/version.go index 074305bd6..b92e5ac7e 100644 --- a/version/version.go +++ b/version/version.go @@ -71,6 +71,30 @@ func NetbirdCommit() string { return revision } +// sanitizeVersion removes anything after the pre-release tag (e.g., "-dev", "-alpha", etc.) +func sanitizeVersion(version string) string { + parts := strings.Split(version, "-") + return parts[0] +} + +// MeetsMinVersion checks if the peer's version meets or exceeds the minimum required version +func MeetsMinVersion(minVer, peerVer string) (bool, error) { + peerVer = sanitizeVersion(peerVer) + minVer = sanitizeVersion(minVer) + + peerNBVer, err := v.NewVersion(peerVer) + if err != nil { + return false, err + } + + constraints, err := v.NewConstraint(">= " + minVer) + if err != nil { + return false, err + } + + return constraints.Check(peerNBVer), nil +} + // IsDevelopmentVersion reports whether the given version string identifies // a non-release / development build. It is the single source of truth for // "is this a dev build" checks across the codebase; use it instead of diff --git a/version/version_test.go b/version/version_test.go index cdba6b804..f05bcbd87 100644 --- a/version/version_test.go +++ b/version/version_test.go @@ -1,6 +1,10 @@ package version -import "testing" +import ( + "testing" + + "github.com/stretchr/testify/assert" +) func TestIsDevelopmentVersion(t *testing.T) { tests := []struct { @@ -26,3 +30,68 @@ func TestIsDevelopmentVersion(t *testing.T) { }) } } + +func TestMeetsMinVersion(t *testing.T) { + tests := []struct { + name string + minVer string + peerVer string + want bool + wantErr bool + }{ + { + name: "Peer version greater than min version", + minVer: "0.26.0", + peerVer: "0.60.1", + want: true, + wantErr: false, + }, + { + name: "Peer version equals min version", + minVer: "1.0.0", + peerVer: "1.0.0", + want: true, + wantErr: false, + }, + { + name: "Peer version less than min version", + minVer: "1.0.0", + peerVer: "0.9.9", + want: false, + wantErr: false, + }, + { + name: "Peer version with pre-release tag greater than min version", + minVer: "1.0.0", + peerVer: "1.0.1-alpha", + want: true, + wantErr: false, + }, + { + name: "Invalid peer version format", + minVer: "1.0.0", + peerVer: "dev", + want: false, + wantErr: true, + }, + { + name: "Invalid min version format", + minVer: "invalid.version", + peerVer: "1.0.0", + want: false, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := MeetsMinVersion(tt.minVer, tt.peerVer) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tt.want, got) + }) + } +}